- /fedex: batch create, document upload/replace/delete, save shipment, imported list
- /api/orders, /api/invoices, /api/pb-file, /api/pb/refresh-token, /api/stock-codes
- /api/payments/bundle: generate + download-all remittance bundles
- shared app/components/DocCell.tsx and app/lib/{invoice-docs,pb-fetch,pb-token}.ts
- recon: invoice reconciliation reports and PowerShell fix-plan scripts
- .gitignore: exclude one-off scripts/_* debugging artefacts, recon/pdfs+txts, data backups
123 lines
5 KiB
Python
123 lines
5 KiB
Python
"""
|
|
Create the 3 JEI_fedex_* collections in PocketBase.
|
|
|
|
Collections:
|
|
JEI_fedex_import_batches - one per import batch (date, vat, duties, status)
|
|
JEI_fedex_shipments - links invoice <-> batch with waybill_no + fedex_ref_no
|
|
JEI_fedex_batch_documents - documents (Waybill/SAD500/SAD507/BillOfEntry) per batch
|
|
|
|
Idempotent: skips a collection if it already exists.
|
|
|
|
Run: python scripts/create_fedex_collections.py
|
|
"""
|
|
import os, sys, json, urllib.request
|
|
from urllib.error import HTTPError
|
|
|
|
PB_URL = os.environ.get("PB_URL", "https://pb.agent.apphub.co.za")
|
|
PB_TOKEN = None
|
|
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("PB_TOKEN missing")
|
|
H = {"Authorization": PB_TOKEN, "Content-Type": "application/json"}
|
|
|
|
SUPERUSER_RULES = {"listRule": "", "viewRule": "", "createRule": "",
|
|
"updateRule": "", "deleteRule": ""}
|
|
|
|
|
|
def pb_get(path):
|
|
r = urllib.request.Request(PB_URL + path, headers=H, method="GET")
|
|
with urllib.request.urlopen(r, timeout=30) as resp:
|
|
return json.loads(resp.read())
|
|
|
|
|
|
def pb_post(path, body):
|
|
data = json.dumps(body).encode("utf-8")
|
|
r = urllib.request.Request(PB_URL + path, data=data, headers=H, method="POST")
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=30) as resp:
|
|
return json.loads(resp.read()), None
|
|
except HTTPError as e:
|
|
return None, json.loads(e.read())
|
|
|
|
|
|
def find_col(name):
|
|
items = pb_get("/api/collections?perPage=200")["items"]
|
|
return next((c for c in items if c["name"] == name), None)
|
|
|
|
|
|
def ensure(name, factory):
|
|
existing = find_col(name)
|
|
if existing:
|
|
print(f"EXISTS: {name}")
|
|
return existing
|
|
result, err = pb_post("/api/collections", factory())
|
|
if err:
|
|
sys.exit(f"ERROR creating {name}: {err}")
|
|
print(f"CREATED: {name} (id={result['id']})")
|
|
return result
|
|
|
|
|
|
def batches_schema():
|
|
# NOTE: PB v0.39.8 quirk — select options go at field root, NOT in `options`
|
|
return {
|
|
"name": "JEI_fedex_import_batches",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "import_date", "type": "date", "required": True, "options": {"min": "", "max": ""}},
|
|
{"name": "vat_paid", "type": "number", "required": False, "options": {"min": 0, "max": None, "noDecimal": False}},
|
|
{"name": "duties_paid", "type": "number", "required": False, "options": {"min": 0, "max": None, "noDecimal": False}},
|
|
{"name": "status", "type": "select", "required": True, "maxSelect": 1, "values": ["pending", "imported", "cancelled"]},
|
|
{"name": "notes", "type": "text", "required": False, "options": {"min": 0, "max": 2000, "pattern": ""}},
|
|
],
|
|
**SUPERUSER_RULES,
|
|
}
|
|
|
|
|
|
def shipments_schema(invoices_id, batches_id):
|
|
# PB v0.39.8 quirk: relation fields need collectionId/cascadeDelete at root,
|
|
# not inside options
|
|
return {
|
|
"name": "JEI_fedex_shipments",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "invoice", "type": "relation", "required": True, "collectionId": invoices_id, "cascadeDelete": False, "minSelect": 1, "maxSelect": 1},
|
|
{"name": "batch", "type": "relation", "required": True, "collectionId": batches_id, "cascadeDelete": True, "minSelect": 1, "maxSelect": 1},
|
|
{"name": "waybill_no", "type": "text", "required": False, "options": {"min": 0, "max": 100, "pattern": ""}},
|
|
{"name": "fedex_ref_no", "type": "text", "required": False, "options": {"min": 0, "max": 100, "pattern": ""}},
|
|
{"name": "notes", "type": "text", "required": False, "options": {"min": 0, "max": 1000, "pattern": ""}},
|
|
],
|
|
**SUPERUSER_RULES,
|
|
}
|
|
|
|
|
|
def documents_schema(batches_id):
|
|
return {
|
|
"name": "JEI_fedex_batch_documents",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "batch", "type": "relation", "required": True, "collectionId": batches_id, "cascadeDelete": True, "minSelect": 1, "maxSelect": 1},
|
|
{"name": "doc_type", "type": "select", "required": True, "maxSelect": 1, "values": ["waybill", "sad500", "sad507", "bill_of_entry"]},
|
|
{"name": "file", "type": "file", "required": False, "options": {"maxSelect": 1, "maxSize": 0, "mimeTypes": ["application/pdf", "image/png", "image/jpeg"], "thumbs": []}},
|
|
{"name": "notes", "type": "text", "required": False, "options": {"min": 0, "max": 500, "pattern": ""}},
|
|
],
|
|
**SUPERUSER_RULES,
|
|
}
|
|
|
|
|
|
def main():
|
|
print(f"PB_URL: {PB_URL}")
|
|
inv = find_col("JEI_invoices")
|
|
if not inv:
|
|
sys.exit("JEI_invoices not found - run scripts/create_collections.py first")
|
|
b = ensure("JEI_fedex_import_batches", batches_schema)
|
|
s = ensure("JEI_fedex_shipments", lambda: shipments_schema(inv["id"], b["id"]))
|
|
d = ensure("JEI_fedex_batch_documents", lambda: documents_schema(b["id"]))
|
|
print("DONE: 3 JEI_fedex_* collections ready.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|