MEGAINFRA-WEBAPP/scripts/pb-bootstrap-schema.py

341 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Bootstrap PocketBase schema for MEGA INFRASTRUCTURE.
PocketBase v0.39 JSON shape: type-specific options (values, maxSelect,
mimeTypes, collectionId, cascadeDelete, min, max) live at the field
top level — NOT nested under "options".
"""
import json
import sys
import urllib.request
import urllib.error
PB = "https://pb.mega.infra.apphub.co.za"
EMAIL = "admin@mega.co.za"
PASSWORD = "12345678"
def http(method, path, body=None, token=None):
url = f"{PB}{path}"
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method=method)
if body is not None:
req.add_header("Content-Type", "application/json")
if token:
req.add_header("Authorization", token)
try:
with urllib.request.urlopen(req) as r:
txt = r.read().decode()
return r.status, json.loads(txt) if txt else None
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read().decode() or "{}")
code, auth = http("POST", "/api/collections/_superusers/auth-with-password",
{"identity": EMAIL, "password": PASSWORD})
if code != 200:
sys.exit(f"AUTH FAILED: {code} {auth}")
TOKEN = auth["token"]
print(f"auth ok, superuser id={auth['record']['id']}\n")
LOCKED = { # superuser-only access
"listRule": None, "viewRule": None,
"createRule": None, "updateRule": None, "deleteRule": None,
}
def field(name, type_, **kw):
"""Build a field. Extra kwargs (required, values, maxSelect, etc.)
go at the field top level."""
f = {"name": name, "type": type_}
f.update(kw)
return f
DEFS = {}
DEFS["data_import_batches"] = {
"name": "DATA_IMPORT_BATCHES", "type": "base",
"fields": [
field("batch_type", "select", required=True,
values=["timesheets", "fuel_logs", "purchase_orders", "delivery_notes", "plant_hours"], maxSelect=1),
field("original_file", "file", required=False,
maxSelect=1, maxSize=20971520,
mimeTypes=["text/csv", "text/plain", "application/vnd.ms-excel", "application/csv"]),
field("date_uploaded", "date", required=False),
field("uploaded_by", "text", required=False),
],
"indexes": [],
}
DEFS["sites"] = {
"name": "SITES", "type": "base",
"fields": [
field("project_code", "text", required=True, min=1, max=64),
field("name", "text", required=False),
field("duration", "text", required=False),
field("contract_value", "number", required=False),
field("work_hours", "number", required=False),
field("hours_per_week", "number", required=False),
field("wages_hours_per_day", "number", required=False),
field("job_start_date", "date", required=False),
],
"indexes": [
"CREATE UNIQUE INDEX `idx_sites_project_code` ON `sites` (`project_code`)",
],
}
DEFS["employees"] = {
"name": "EMPLOYEES", "type": "base",
"fields": [
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("name", "text", required=False),
field("employee_no", "text", required=False),
field("position", "text", required=False),
field("rate_per_hour", "number", required=False),
field("hours_per_day_standard", "number", required=False),
field("worker_type", "select", required=False,
values=["permanent", "casual"], maxSelect=1),
],
"indexes": [],
}
DEFS["purchase_orders"] = {
"name": "PURCHASE_ORDERS", "type": "base",
"fields": [
field("purchase_order_number", "text", required=True, min=1, max=128),
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("supplier", "text", required=False),
field("required_by_date", "date", required=False),
field("ordered_by", "text", required=False),
field("items_count", "number", required=False),
field("total_value", "number", required=False),
field("purchase_order_file", "file", required=False,
maxSelect=1, maxSize=20971520,
mimeTypes=["application/pdf", "image/jpeg", "image/png", "image/webp"]),
field("raw_ocr_data", "json", required=False),
field("is_verified", "bool", required=False),
],
"indexes": [
"CREATE UNIQUE INDEX `idx_po_number` ON `purchase_orders` (`purchase_order_number`)",
],
}
DEFS["purchase_order_items"] = {
"name": "PURCHASE_ORDER_ITEMS", "type": "base",
"fields": [
field("purchase_order", "relation", required=False,
collectionId="__PURCHASE_ORDERS_ID__", cascadeDelete=True, maxSelect=1),
field("material_code", "text", required=False),
field("cost_code", "text", required=False),
field("description", "text", required=False),
field("qty", "number", required=False),
field("qty_outstanding", "number", required=False),
field("unit", "text", required=False),
field("rate", "number", required=False),
field("disc_percent", "number", required=False),
field("vat_percent", "number", required=False),
field("amount", "number", required=False),
],
"indexes": [],
}
DEFS["fixed_costs"] = {
"name": "FIXED_COSTS", "type": "base",
"fields": [
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("date_logged", "date", required=False),
field("description", "text", required=False),
field("cost", "number", required=False),
field("notes", "text", required=False),
field("is_recurring", "bool", required=False, default=True),
],
"indexes": [],
}
DEFS["plant"] = {
"name": "PLANT", "type": "base",
"fields": [
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("description", "text", required=False),
field("standard_work_hours", "text", required=False),
field("cost_per_hour", "number", required=False),
],
"indexes": [],
}
DEFS["vehicles"] = {
"name": "VEHICLES", "type": "base",
"fields": [
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("description", "text", required=False),
field("standard_work_hours", "text", required=False),
field("fuel_limits", "text", required=False),
field("unit_type", "select", required=True,
values=["fuel", "plant"], maxSelect=1, default="fuel"),
field("attention_dismissed_at", "date", required=False),
],
"indexes": [],
}
DEFS["timesheets"] = {
"name": "TIMESHEETS", "type": "base",
"fields": [
field("employee", "relation", required=False,
collectionId="__EMPLOYEES_ID__", cascadeDelete=False, maxSelect=1),
field("batch_reference", "relation", required=False,
collectionId="__DATA_IMPORT_BATCHES_ID__", cascadeDelete=False, maxSelect=1),
field("date_logged", "date", required=False),
field("hours_worked", "number", required=False),
],
"indexes": [],
}
DEFS["fuel_logs"] = {
"name": "FUEL_LOGS", "type": "base",
"fields": [
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("vehicle", "relation", required=False,
collectionId="__VEHICLES_ID__", cascadeDelete=False, maxSelect=1),
field("batch_reference", "relation", required=False,
collectionId="__DATA_IMPORT_BATCHES_ID__", cascadeDelete=False, maxSelect=1),
field("date_logged", "date", required=False),
field("litres", "number", required=False),
field("odo_reading", "number", required=False),
],
"indexes": [],
}
DEFS["plant_hours_log"] = {
"name": "PLANT_HOURS_LOG", "type": "base",
"fields": [
field("date_logged", "date", required=True),
field("vehicle_qr", "text", required=True),
field("vehicle", "relation", required=True,
collectionId="__VEHICLES_ID__", cascadeDelete=False, maxSelect=1, minSelect=1),
field("site", "relation", required=True,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1, minSelect=1),
field("project_code", "text", required=True),
field("start_date", "date", required=True),
field("end_date", "date", required=True),
field("start_time", "text", required=False),
field("end_time", "text", required=False),
field("start_hours", "number", required=True),
field("end_hours", "number", required=True),
field("hours", "number", required=True),
field("notes", "text", required=False),
field("hour_meter_budget", "number", required=True),
field("cost_per_hour", "number", required=True),
field("value", "number", required=True),
field("over_budget", "bool", required=True),
field("hours_mismatch", "bool", required=True),
field("vehicle_unit_type_at_import", "select", required=True,
values=["fuel", "plant"], maxSelect=1),
field("batch_reference", "relation", required=False,
collectionId="__DATA_IMPORT_BATCHES_ID__", cascadeDelete=False, maxSelect=1, minSelect=0),
],
"indexes": [
"CREATE UNIQUE INDEX `idx_plant_hours_log_qr_start` ON `plant_hours_log` (`vehicle_qr`, `start_date`)",
],
}
DEFS["delivery_notes"] = {
"name": "DELIVERY_NOTES", "type": "base",
"fields": [
field("delivery_note_number", "text", required=False),
field("project_code", "text", required=False),
field("site", "relation", required=False,
collectionId="__SITES_ID__", cascadeDelete=False, maxSelect=1),
field("purchase_order", "relation", required=False,
collectionId="__PURCHASE_ORDERS_ID__", cascadeDelete=False, maxSelect=1),
field("date_received", "date", required=False),
field("received_by", "text", required=False),
field("slip_image", "file", required=False,
maxSelect=1, maxSize=20971520,
mimeTypes=["image/jpeg", "image/png", "image/webp", "application/pdf"]),
field("raw_ocr_data", "json", required=False),
field("is_verified", "bool", required=False),
field("processing_status", "select", required=False,
values=["pending", "ocr-complete", "auto-verified",
"needs-review", "manual-verified", "failed"], maxSelect=1),
field("ocr_confidence", "number", required=False),
field("ocr_completed_at", "date", required=False),
field("received_by_name", "text", required=False),
],
"indexes": [],
}
DEFS["received_items"] = {
"name": "RECEIVED_ITEMS", "type": "base",
"fields": [
field("delivery_note", "relation", required=False,
collectionId="__DELIVERY_NOTES_ID__", cascadeDelete=True, maxSelect=1),
field("purchase_order_item", "relation", required=False,
collectionId="__PURCHASE_ORDER_ITEMS_ID__", cascadeDelete=False, maxSelect=1),
field("qty_received", "number", required=False),
field("condition_notes", "text", required=False),
],
"indexes": [],
}
ORDER = [
"data_import_batches",
"sites",
"employees",
"purchase_orders",
"purchase_order_items",
"fixed_costs",
"plant",
"vehicles",
"timesheets",
"fuel_logs",
"plant_hours_log",
"delivery_notes",
"received_items",
]
ids = {}
for key in ORDER:
body = dict(DEFS[key])
body.update(LOCKED)
for f in body["fields"]:
if f.get("type") == "relation":
cid = f.get("collectionId", "")
# placeholders use collection table name in upper, e.g. __SITES_ID__
for other_key, other_id in ids.items():
collection_name_upper = DEFS[other_key]["name"] # e.g. "SITES"
cid = cid.replace(f"__{collection_name_upper}_ID__", other_id)
if "__" in cid:
sys.exit(f"unresolved placeholder in {key}.{f['name']}: {cid}")
f["collectionId"] = cid
code, resp = http("POST", "/api/collections", body, TOKEN)
if code == 200:
ids[key] = resp["id"]
print(f" ✓ {body['name']:25s} → {resp['id']}")
elif code == 400:
code2, existing = http("GET", f"/api/collections/{body['name']}", token=TOKEN)
if code2 == 200:
ids[key] = existing["id"]
print(f" • {body['name']:25s} → {existing['id']} (already existed)")
else:
print(f" ✗ {body['name']:25s} FAILED: {resp}", file=sys.stderr)
sys.exit(1)
else:
print(f" ✗ {body['name']:25s} FAILED: {code} {resp}", file=sys.stderr)
sys.exit(1)
print(f"\nDone. {len(ids)} collections on {PB}.")