2026-09-17 11:49:47 +00:00
|
|
|
#!/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": [
|
2026-09-17 13:15:27 +00:00
|
|
|
# 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},
|
2026-09-17 11:49:47 +00:00
|
|
|
{"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": [
|
2026-09-17 13:15:27 +00:00
|
|
|
{"name": "created", "type": "date", "required": False, "onCreate": True},
|
|
|
|
|
{"name": "updated", "type": "date", "required": False, "onCreate": True, "onUpdate": True},
|
2026-09-17 11:49:47 +00:00
|
|
|
{"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},
|
|
|
|
|
])
|
|
|
|
|
|
2026-09-17 13:17:17 +00:00
|
|
|
# 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},
|
|
|
|
|
])
|
|
|
|
|
|
2026-09-17 11:49:47 +00:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|
|
|
|
|
print("\nDone.")
|