Phase 7: close, file, PDFs and prints
ci / frontend (push) Successful in 40s
ci / server (push) Successful in 10m56s
ci / image (push) Successful in 48s
ci / e2e (push) Successful in 2m44s

Clerical close and file: the VISUAL observed state beside the close confirm, a 409 with a second confirm when the
feed still shows work orders open, filing with a signature and the HMAC seal, the data book built on the server
and stored on the seal in the filing transaction, admin reopen keeping the broken seal with its data book.

Server PDFs with reportlab, pypdf and pypdfium2: the manufacturing data book page for page after the old build
(cover, index, drawings with VOID stamps, traceability and heat map rasters, weld maps and welding records, Q.C.
inspections, Q.C. and test documents, MTRs, record history), the job traveler with work order QR codes, the ITP
PDF filed automatically at sign-off, every print kind with the PRINTED COPY stamp, the controlled-copy stamp
signed with the seal key, PDF metadata, the traceability CSV with the formula guard, and the append-only
print_log with an audit row for every print and export (migration 0008).

Frontend: the close and file panel, the print menu and its entry points, the printed copies list on the History
tab, the ITP PDF buttons. Tests: server (close, file, reopen, seal, the builders, prints, CSV, stamps), frontend,
and the Playwright close and print scenarios. Docs updated; SECURITY closes M2, S6, S7 and S8.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-17 23:49:14 -06:00
co-authored by Claude Fable 5.1
parent 00531cb329
commit 904437153d
59 changed files with 8226 additions and 136 deletions
+93 -5
View File
@@ -47,6 +47,9 @@ SEAL_EXCLUDE = (
"qc_folder_docs",
"itp",
"itp_hold",
"seals",
"vclose_check",
"prints",
)
@@ -676,6 +679,92 @@ def visual_state(conn: Any, row: Mapping[str, Any]) -> dict[str, Any] | None:
}
# Leg status codes the feed records that mean the work order is no longer open in VISUAL: closed and cancelled.
LEG_DONE = ("C", "X")
def open_wos(vs: Mapping[str, Any] | None) -> list[str]:
"""The work orders the feed last saw open in VISUAL, as "wo_base/lot" text, in leg order without repeats.
A leg is open when the feed recorded a status for it and that status is not closed (C) or cancelled (X). A leg
with no recorded status is left out: the feed has not seen it. The IDs stay text exactly as stored."""
out: list[str] = []
for leg in (vs or {}).get("legs") or []:
if not isinstance(leg, Mapping):
continue
st = str(leg.get("st") or leg.get("status") or "").strip().upper()
if not st or st in LEG_DONE:
continue
parts = str(leg.get("key") or "").split("/")
if len(parts) >= 4:
base, lot = "/".join(parts[:-3]), parts[-3]
else:
base, lot = parts[0], (parts[1] if len(parts) > 1 else "")
if not base:
continue
text = f"{base}/{lot}" if lot else base
if text not in out:
out.append(text)
return out
def vclose_check(row: Mapping[str, Any], vs: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""What the feed says about closing the job in VISUAL, next to the Clerical confirm. None for a hand packet.
`agrees` is true when the Clerical confirm (vclosed) and the feed's closed time are both set or both unset."""
if vs is None:
return None
closed_at = vs.get("closed_at")
return {
"observed_at": vs.get("observed_at"),
"closed_at": closed_at,
"open_wos": open_wos(vs),
"agrees": (row.get("vclosed_at") is not None) == (closed_at is not None),
}
def packet_open_wos(conn: Any, row: Mapping[str, Any]) -> list[str]:
"""The open work orders of `vclose_check` for one packets row (empty for a hand packet)."""
return open_wos(visual_state(conn, row))
def seal_count(conn: Any, packet_id: int) -> int:
r = conn.execute("SELECT count(*) FROM packet_seals WHERE packet_id = %s", (packet_id,)).fetchone()
return int(r[0]) if r else 0
def seal_view(conn: Any, s: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""The newest seal row, broken or not, with who sealed it and the data book filed with it."""
if s is None:
return None
people = People(conn, [s["sealed_by"], s["broken_by"]])
book: dict[str, Any] | None = None
aid = s.get("databook_asset_id")
if aid is not None:
a = one(conn, "SELECT id, size, original_name FROM assets WHERE id = %s", (aid,))
book = {
"asset_id": str(aid),
"url": f"/_blob/{aid}",
"download_url": f"/_blob/{aid}?download=1",
"pages": s.get("databook_pages"),
"size": None if a is None else a["size"],
"file_name": None if a is None else a["original_name"],
}
wos = s.get("open_wos")
return {
"id": s["id"],
"sealed_at": s["sealed_at"],
"sealed_by": people.ref(s["sealed_by"]),
"key_id": s["key_id"],
"signed": s["signature_id"] is not None,
"broken_at": s["broken_at"],
"broken_by": people.ref(s["broken_by"]),
"broken_reason": s["broken_reason"],
"open_wos": None if wos is None else list(wos),
"databook": book,
}
def build(
conn: Any,
row: Mapping[str, Any],
@@ -787,16 +876,15 @@ def build(
out["qc_last"] = out["qc_history"][-1] if out["qc_history"] else None
out["change_orders"] = [_co(c, ch, people) for c in ch.change_orders]
out["docs"] = [_doc(d, people, seal=seal) for d in ch.docs]
out["seal"] = (
None
if ch.seal is None
else {"sealed_at": ch.seal["sealed_at"], "key_id": ch.seal["key_id"], "broken_at": ch.seal["broken_at"]}
)
if not seal:
out["seal"] = seal_view(conn, ch.seal)
out["seals"] = seal_count(conn, row["id"]) if ch.seal is not None else 0
out["checklist_items"] = checklists.checklist_for(weld, row["drawing"], row["rev"], row["wps"], ch.templates)
out["qc_items"] = qc_items_view(row, ch)
out["flag"] = flag_of(row["status"], row["source_kind"], row["flags"], row["drawing_state"], row["visual"])
if not seal:
out["visual_state"] = visual_state(conn, row)
out["vclose_check"] = vclose_check(row, out["visual_state"])
if include_children:
from . import shop # shop builds on this module