#!/usr/bin/env python3 """ Creates the DN_BATCHES and DN_BATCH_NOTES collections, and adds the batch back-references to DELIVERY_NOTES. A batch is the uploaded file. Notes found inside it live in DN_BATCH_NOTES as candidates until they pass every check — only then is a DELIVERY_NOTES row created. That separation is the point: before it, delivery_notes was both the inbox and the ledger, so a half-read document sat inside the figures used to pay suppliers. Idempotent — existing collections and fields are left alone. """ import sys from pb_env import PB, http, superuser_token def get_collection(token, name): code, resp = http("GET", f"/api/collections/{name}", token=token) return (resp if code == 200 else None) def collection_id(token, name): coll = get_collection(token, name) if not coll: sys.exit(f"collection {name} not found — cannot resolve its id") return coll["id"] def create_collection(token, payload): existing = get_collection(token, payload["name"]) if existing: print(f" • {payload['name']} already exists — skipping create") return existing["id"] code, resp = http("POST", "/api/collections", payload, token) if code != 200: sys.exit(f"CREATE {payload['name']} failed: {code} {resp}") print(f" ✓ created {payload['name']}") return resp["id"] def add_fields(token, name, wanted): coll = get_collection(token, name) if not coll: sys.exit(f"collection {name} not found") fields = list(coll.get("fields", [])) existing = {f.get("name") for f in fields} additions = [w for w in wanted if w["name"] not in existing] if not additions: print(f" • {name}: no new fields needed") return code, resp = http("PATCH", f"/api/collections/{coll['id']}", {"fields": fields + additions}, token) if code != 200: sys.exit(f"PATCH {name} failed: {code} {resp}") for a in additions: print(f" ✓ {name}: added {a['name']}") def main(): token = superuser_token() print(f"\n→ {PB}") sites_id = collection_id(token, "sites") pos_id = collection_id(token, "purchase_orders") dns_id = collection_id(token, "delivery_notes") batches_id = create_collection(token, { "name": "DN_BATCHES", "type": "base", "fields": [ # PB system fields — required so `sort=created` / `sort=-created` # queries work (the tick route and the inbox page both sort on # these). PB manages their values internally once the field # names match. {"name": "created", "type": "date", "required": False, "onCreate": True}, {"name": "updated", "type": "date", "required": False, "onCreate": True, "onUpdate": True}, {"name": "file", "type": "file", "required": False, "maxSelect": 1, "maxSize": 20971520}, {"name": "file_hash", "type": "text", "required": False}, {"name": "site", "type": "relation", "required": True, "collectionId": sites_id, "maxSelect": 1, "cascadeDelete": False}, {"name": "source", "type": "select", "required": False, "maxSelect": 1, "values": ["web", "mobile", "email"]}, {"name": "uploaded_by", "type": "text", "required": False}, {"name": "status", "type": "select", "required": False, "maxSelect": 1, "values": ["inbox", "analysing", "needs_attention", "processed"]}, {"name": "page_count", "type": "number", "required": False}, {"name": "notes_detected", "type": "number", "required": False}, {"name": "notes_promoted", "type": "number", "required": False}, {"name": "segmentation", "type": "json", "required": False, "maxSize": 2000000}, {"name": "checks", "type": "json", "required": False, "maxSize": 2000000}, {"name": "attempts", "type": "number", "required": False}, {"name": "claimed_at", "type": "date", "required": False}, {"name": "last_error", "type": "text", "required": False}, ], }) create_collection(token, { "name": "DN_BATCH_NOTES", "type": "base", "fields": [ {"name": "created", "type": "date", "required": False, "onCreate": True}, {"name": "updated", "type": "date", "required": False, "onCreate": True, "onUpdate": True}, {"name": "batch", "type": "relation", "required": True, "collectionId": batches_id, "maxSelect": 1, "cascadeDelete": True}, {"name": "note_index", "type": "number", "required": False}, {"name": "page_from", "type": "number", "required": False}, {"name": "page_to", "type": "number", "required": False}, {"name": "page_file", "type": "file", "required": False, "maxSelect": 1, "maxSize": 20971520}, {"name": "delivery_note_number", "type": "text", "required": False}, {"name": "po_number_raw", "type": "text", "required": False}, {"name": "po_number_normalised", "type": "text", "required": False}, {"name": "purchase_order", "type": "relation", "required": False, "collectionId": pos_id, "maxSelect": 1, "cascadeDelete": False}, {"name": "gross_mass", "type": "number", "required": False}, {"name": "tare_mass", "type": "number", "required": False}, {"name": "nett_mass", "type": "number", "required": False}, {"name": "quantity", "type": "number", "required": False}, {"name": "unit", "type": "text", "required": False}, {"name": "items", "type": "json", "required": False, "maxSize": 2000000}, {"name": "checks", "type": "json", "required": False, "maxSize": 2000000}, {"name": "status", "type": "select", "required": False, "maxSelect": 1, "values": ["candidate", "blocked", "promoted"]}, {"name": "delivery_note", "type": "relation", "required": False, "collectionId": dns_id, "maxSelect": 1, "cascadeDelete": False}, ], }) add_fields(token, "delivery_notes", [ {"name": "batch", "type": "relation", "required": False, "collectionId": batches_id, "maxSelect": 1, "cascadeDelete": False}, {"name": "page_from", "type": "number", "required": False}, {"name": "page_to", "type": "number", "required": False}, ]) # Repair the standard system date fields on both new collections # if they were created before this script knew to include them. The # `create_collection` path only fires for from-scratch creates; # add_fields is the path that heals an existing live collection. add_fields(token, "dn_batches", [ {"name": "created", "type": "date", "required": False, "onCreate": True}, {"name": "updated", "type": "date", "required": False, "onCreate": True, "onUpdate": True}, ]) add_fields(token, "dn_batch_notes", [ {"name": "created", "type": "date", "required": False, "onCreate": True}, {"name": "updated", "type": "date", "required": False, "onCreate": True, "onUpdate": True}, ]) if __name__ == "__main__": main() print("\nDone.")