#!/usr/bin/env python3 """ Adds dn_batches.uploaded_at and backfills it. PocketBase's own `created` is an empty string on every dn_batches record — the collection was created without autodate — so the board's "-created" sort is a no-op and the tick's "oldest first" means nothing. The rest of this codebase already works around the same quirk (delivery_notes sorts by date_received), so an explicit field is the consistent fix. Backfill, in priority order: 1. The earliest date_received of the delivery_notes linked to the batch. That is the document's own date and the most trustworthy source. 2. A date parsed from the stored filename. These are scanner-generated ("cam_scanner_2026_09_10_15", "20260910_144808"), so they are unambiguous — but they are the SCAN date, not the upload date. Good enough to order a board by; not to be treated as provenance. 3. Nothing. Left empty rather than invented. Batches uploaded after this ships get uploaded_at set at upload time, so this only ever runs against the historical set. Idempotent. Dry-run by default; pass --apply to write. """ import re import sys from pb_env import PB, http, superuser_token # A scanner filename date: 20260910, 2026_09_10, 2026-09-10. FILENAME_DATE = re.compile(r"(20\d{2})[_-]?(\d{2})[_-]?(\d{2})") def date_from_filename(filename: str) -> str | None: m = FILENAME_DATE.search(filename or "") if not m: return None year, month, day = (int(g) for g in m.groups()) # Reject a match that is really an invoice or order number. if not (2024 <= year <= 2027 and 1 <= month <= 12 and 1 <= day <= 31): return None return f"{year:04d}-{month:02d}-{day:02d} 00:00:00.000Z" def get_all(token, coll, fields=None, filt=None): out, page = [], 1 while True: q = f"?page={page}&perPage=200" if fields: q += f"&fields={fields}" if filt: q += f"&filter={filt}" code, resp = http("GET", f"/api/collections/{coll}/records{q}", token=token) if code != 200: sys.exit(f"GET {coll} failed: {code} {resp}") out.extend(resp["items"]) if page >= resp["totalPages"]: return out page += 1 def main(apply: bool): token = superuser_token() print(f"\n→ dn_batches on {PB} ({'APPLY' if apply else 'DRY RUN'})") code, coll = http("GET", "/api/collections/dn_batches", token=token) if code != 200: sys.exit(f"GET collection failed: {code} {coll}") # Read the live field list and APPEND. PocketBase replaces the whole # array on PATCH, so building it from anything but a fresh read risks # dropping columns. fields = list(coll.get("fields", [])) if any(f.get("name") == "uploaded_at" for f in fields): print(" • uploaded_at already exists") elif not apply: print(f" would add uploaded_at (preserving {len(fields)} existing fields)") else: code, resp = http( "PATCH", f"/api/collections/{coll['id']}", {"fields": fields + [{"name": "uploaded_at", "type": "date", "required": False}]}, token, ) if code != 200: sys.exit(f"PATCH failed: {code} {resp}") print(f" ✓ added uploaded_at (kept {len(fields)} existing fields)") # Source 1: the earliest date_received among the batch's delivery notes. earliest: dict[str, str] = {} for d in get_all(token, "delivery_notes", "batch,date_received"): batch = d.get("batch") received = (d.get("date_received") or "").strip() if not batch or not received: continue if batch not in earliest or received < earliest[batch]: earliest[batch] = received batches = get_all(token, "dn_batches", "id,uploaded_at,file") counts = {"delivery_note": 0, "filename": 0, "left_empty": 0, "already_set": 0} for b in batches: if (b.get("uploaded_at") or "").strip(): counts["already_set"] += 1 continue value = earliest.get(b["id"]) source = "delivery_note" if not value: value = date_from_filename(b.get("file") or "") source = "filename" if not value: counts["left_empty"] += 1 print(f" {'left empty':14s} {'-':24s} {(b.get('file') or '')[:40]}") continue counts[source] += 1 print(f" {source:14s} {value[:10]:24s} {(b.get('file') or '')[:40]}") if apply: code, resp = http( "PATCH", f"/api/collections/dn_batches/records/{b['id']}", {"uploaded_at": value}, token, ) if code != 200: sys.exit(f"PATCH {b['id']} failed: {code} {resp}") print( f"\n from delivery notes: {counts['delivery_note']}" f" from filename: {counts['filename']}" f" left empty: {counts['left_empty']}" f" already set: {counts['already_set']}" ) if not apply: print("\n DRY RUN — nothing written. Re-run with --apply.") if __name__ == "__main__": main("--apply" in sys.argv) print("\nDone.")