2026-09-17 18:53:23 +00:00
|
|
|
#!/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.
|
|
|
|
|
|
2026-09-17 19:25:33 +00:00
|
|
|
Backfill, in priority order:
|
2026-09-17 18:53:23 +00:00
|
|
|
|
2026-09-17 19:25:33 +00:00
|
|
|
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.
|
2026-09-17 18:53:23 +00:00
|
|
|
"""
|
2026-09-17 19:25:33 +00:00
|
|
|
import re
|
2026-09-17 18:53:23 +00:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
from pb_env import PB, http, superuser_token
|
|
|
|
|
|
2026-09-17 19:25:33 +00:00
|
|
|
# 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"
|
|
|
|
|
|
2026-09-17 18:53:23 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-09-17 19:25:33 +00:00
|
|
|
def main(apply: bool):
|
2026-09-17 18:53:23 +00:00
|
|
|
token = superuser_token()
|
2026-09-17 19:25:33 +00:00
|
|
|
print(f"\n→ dn_batches on {PB} ({'APPLY' if apply else 'DRY RUN'})")
|
2026-09-17 18:53:23 +00:00
|
|
|
|
|
|
|
|
code, coll = http("GET", "/api/collections/dn_batches", token=token)
|
|
|
|
|
if code != 200:
|
|
|
|
|
sys.exit(f"GET collection failed: {code} {coll}")
|
2026-09-17 19:25:33 +00:00
|
|
|
# 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.
|
2026-09-17 18:53:23 +00:00
|
|
|
fields = list(coll.get("fields", []))
|
|
|
|
|
if any(f.get("name") == "uploaded_at" for f in fields):
|
|
|
|
|
print(" • uploaded_at already exists")
|
2026-09-17 19:25:33 +00:00
|
|
|
elif not apply:
|
|
|
|
|
print(f" would add uploaded_at (preserving {len(fields)} existing fields)")
|
2026-09-17 18:53:23 +00:00
|
|
|
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}")
|
2026-09-17 19:25:33 +00:00
|
|
|
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:
|
2026-09-17 18:53:23 +00:00
|
|
|
continue
|
2026-09-17 19:25:33 +00:00
|
|
|
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}
|
2026-09-17 18:53:23 +00:00
|
|
|
|
|
|
|
|
for b in batches:
|
|
|
|
|
if (b.get("uploaded_at") or "").strip():
|
2026-09-17 19:25:33 +00:00
|
|
|
counts["already_set"] += 1
|
2026-09-17 18:53:23 +00:00
|
|
|
continue
|
2026-09-17 19:25:33 +00:00
|
|
|
|
|
|
|
|
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]}")
|
2026-09-17 18:53:23 +00:00
|
|
|
continue
|
|
|
|
|
|
2026-09-17 19:25:33 +00:00
|
|
|
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.")
|
2026-09-17 18:53:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-09-17 19:25:33 +00:00
|
|
|
main("--apply" in sys.argv)
|
2026-09-17 18:53:23 +00:00
|
|
|
print("\nDone.")
|