147 lines
6.5 KiB
Python
147 lines
6.5 KiB
Python
|
|
"""
|
||
|
|
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()
|