""" Create the three JEI_* collections in PocketBase v0.39.8. NOTE on PB v0.39.8 API quirk discovered 2026-09-17: - Field options for select go at the FIELD ROOT, not nested inside `options`. e.g. { name: "foo", type: "select", maxSelect: 1, values: [...] } NOT { name: "foo", type: "select", options: { maxSelect: 1, values: [...] } } - This is opposite to the docs for some earlier PB versions. Idempotent: skips a collection if it already exists by name. Run: python scripts/create_collections.py """ import os import sys import json import urllib.request from urllib.error import HTTPError PB_URL = os.environ.get("PB_URL", "https://pb.agent.apphub.co.za") PB_TOKEN = os.environ.get("PB_TOKEN", "") if not PB_TOKEN: env_path = os.path.join(os.path.dirname(__file__), "..", ".env.local") if os.path.exists(env_path): for line in open(env_path): if line.startswith("PB_TOKEN="): PB_TOKEN = line.strip().split("=", 1)[1] if not PB_TOKEN: sys.exit("ERROR: PB_TOKEN not set") HEADERS = {"Authorization": PB_TOKEN, "Content-Type": "application/json"} def pb_get(path): req = urllib.request.Request(PB_URL + path, headers=HEADERS, method="GET") with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read()) def pb_post(path, body): req = urllib.request.Request(PB_URL + path, data=json.dumps(body).encode("utf-8"), headers=HEADERS, method="POST") try: with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read()), None except HTTPError as e: return None, json.loads(e.read()) SUPERUSER_RULES = {"listRule": "", "viewRule": "", "createRule": "", "updateRule": "", "deleteRule": ""} def invoices_schema(): return { "name": "JEI_invoices", "type": "base", "fields": [ {"name": "invoice_no", "type": "text", "required": True, "min": 1, "max": 50, "pattern": ""}, {"name": "invoice_date", "type": "date", "required": True, "min": "", "max": ""}, # Select options go at the field root (PB v0.39.8 quirk) {"name": "document_type", "type": "select", "required": True, "maxSelect": 1, "values": ["invoice", "proforma", "quote"]}, {"name": "customer_order_no", "type": "text", "required": False, "min": 0, "max": 100, "pattern": ""}, {"name": "buyer_name_variant", "type": "select", "required": True, "maxSelect": 1, "values": ["t/a JEI South Africa", "JEI South Africa", "JEI South Africa Importers"]}, {"name": "supplier_name", "type": "text", "required": True, "min": 0, "max": 200, "pattern": ""}, {"name": "account_ref", "type": "text", "required": False, "min": 0, "max": 50, "pattern": ""}, {"name": "total_net", "type": "number", "required": True, "min": 0, "max": None, "noDecimal": False}, {"name": "carriage_net", "type": "number", "required": False, "min": 0, "max": None, "noDecimal": False}, {"name": "invoice_total", "type": "number", "required": True, "min": 0, "max": None, "noDecimal": False}, {"name": "deliver_to_name", "type": "text", "required": False, "min": 0, "max": 200, "pattern": ""}, {"name": "deliver_to_address", "type": "text", "required": False, "min": 0, "max": 1000, "pattern": ""}, {"name": "source_filename", "type": "text", "required": False, "min": 0, "max": 500, "pattern": ""}, {"name": "notes", "type": "text", "required": False, "min": 0, "max": 5000, "pattern": ""}, {"name": "pdf_file", "type": "file", "required": False, "maxSelect": 1, "maxSize": 0, "mimeTypes": ["application/pdf"], "thumbs": []}, ], **SUPERUSER_RULES, } def stock_codes_schema(): return { "name": "JEI_stock_codes", "type": "base", "fields": [ {"name": "code", "type": "text", "required": True, "min": 1, "max": 100, "pattern": ""}, {"name": "description_goods", "type": "text", "required": True, "min": 1, "max": 500, "pattern": ""}, {"name": "first_seen_date", "type": "date", "required": False, "min": "", "max": ""}, {"name": "last_seen_date", "type": "date", "required": False, "min": "", "max": ""}, ], **SUPERUSER_RULES, } def invoice_lines_schema(invoices_id, stock_codes_id): return { "name": "JEI_invoice_lines", "type": "base", "fields": [ # relation: collectionId goes at field root in PB v0.39.8 {"name": "invoice", "type": "relation", "required": True, "collectionId": invoices_id, "cascadeDelete": True, "minSelect": 1, "maxSelect": 1}, {"name": "line_no", "type": "number", "required": True, "min": 0, "max": None, "noDecimal": True}, {"name": "stock_code", "type": "relation", "required": True, "collectionId": stock_codes_id, "cascadeDelete": True, "minSelect": 1, "maxSelect": 1}, {"name": "quantity", "type": "number", "required": True, "min": 0, "max": None, "noDecimal": False}, {"name": "unit_price", "type": "number", "required": True, "min": 0, "max": None, "noDecimal": False}, {"name": "net_amount", "type": "number", "required": True, "min": 0, "max": None, "noDecimal": False}, {"name": "commodity_code", "type": "text", "required": False, "min": 0, "max": 50, "pattern": ""}, {"name": "country_of_origin", "type": "text", "required": False, "min": 0, "max": 50, "pattern": ""}, ], **SUPERUSER_RULES, } def find_collection(name): collections = pb_get("/api/collections?perPage=200")["items"] for c in collections: if c["name"] == name: return c return None def ensure_collection(name, factory): existing = find_collection(name) if existing: print(f"EXISTS: {name} (id={existing['id']}, fields={len(existing.get('fields') or [])})") return existing body = factory() result, err = pb_post("/api/collections", body) if err: sys.exit(f"ERROR creating {name}: {err}") print(f"CREATED: {name} (id={result['id']}, fields={len(result.get('fields') or [])})") return result def main(): print(f"PB_URL: {PB_URL}") inv = ensure_collection("JEI_invoices", invoices_schema) sc = ensure_collection("JEI_stock_codes", stock_codes_schema) def lines_factory(): return invoice_lines_schema(inv["id"], sc["id"]) ensure_collection("JEI_invoice_lines", lines_factory) print("DONE: 3 JEI_* collections ready.") if __name__ == "__main__": main()