Phase 6: Q.C. inspection, project folders, packet documents and the digital ITP
ci / frontend (push) Successful in 37s
ci / server (push) Successful in 9m18s
ci / image (push) Successful in 35s
ci / e2e (push) Successful in 2m17s

The Q.C. screen (KPIs, boards, project folders), the Q.C. tab on the packet
page (check items from the qc_check_items settings row, visual exam per weld,
NDE, hardness with the NACE label, notes), accept with a route and a
signature gated by qc_missing_empty, reject with a reason or an NCR number
closing the inspection into qc_history, Q.C. folder documents per project and
unit with superseding revisions, packet documents as a closed set with
take-out reasons, the digital ITP per unit from the frozen template (line
initials with CAS, unsign, N/A, customer codes, rejects with NCR, block
signatures, sign-off with the PDF hook, admin reopen, the ITP log), hold-point
warnings by department page group, the scoped policy check for unit and
project writes, migration 0007, tests, Playwright scenarios and docs.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-10-17 20:08:53 -06:00
co-authored by Claude Fable 5.1
parent 8bb1f7ae4d
commit 00531cb329
64 changed files with 11715 additions and 260 deletions
+143 -24
View File
@@ -43,6 +43,10 @@ SEAL_EXCLUDE = (
"target_hrs",
"labor",
"heat_src",
"qc_last",
"qc_folder_docs",
"itp",
"itp_hold",
)
@@ -242,6 +246,7 @@ class Children:
dept_names: dict[str, str] = field(default_factory=dict)
dept_labels: dict[str, str] = field(default_factory=dict)
templates: dict[str, list[dict[str, str]]] = field(default_factory=dict)
qc_items: list[dict[str, str]] = field(default_factory=list)
@property
def open_stage(self) -> dict[str, Any] | None:
@@ -264,6 +269,7 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
ch.dept_names[code] = name
ch.dept_labels[code] = dept_label(code, name, kind)
ch.templates = checklists.load_templates(conn)
ch.qc_items = checklists.load_qc_items(conn)
ch.pages = rows(conn, "SELECT * FROM packet_pages WHERE packet_id = %s ORDER BY n", (pid,))
ch.bom = rows(conn, "SELECT * FROM bom_lines WHERE packet_id = %s ORDER BY item", (pid,))
shorts = rows(conn, "SELECT * FROM bom_shorts WHERE packet_id = %s AND closed_at IS NULL", (pid,))
@@ -311,8 +317,12 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
ch.co_required.setdefault(r["co_id"], []).append(r["dept_code"])
for r in rows(conn, "SELECT * FROM co_verifications WHERE co_id = ANY(%s) ORDER BY at", (co_ids,)):
ch.co_verified.setdefault(r["co_id"], []).append(r)
# Every document, the ones taken out of the data book too (a removed row is a record).
ch.docs = rows(
conn, "SELECT * FROM packet_docs WHERE packet_id = %s AND removed_at IS NULL ORDER BY added_at, id", (pid,)
conn,
"SELECT d.*, a.original_name AS file_name, a.size AS file_size FROM packet_docs d"
" JOIN assets a ON a.id = d.asset_id WHERE d.packet_id = %s ORDER BY d.added_at, d.id",
(pid,),
)
ch.seal = one(conn, "SELECT * FROM packet_seals WHERE packet_id = %s ORDER BY id DESC LIMIT 1", (pid,))
return ch
@@ -370,7 +380,7 @@ def state_from(row: Mapping[str, Any], ch: Children) -> policy.PacketState:
checklist_complete=stage is not None
and checklists.checklist_complete(stage["checklist"], weld, ch.templates or None),
filler_set=stage is not None and bool((stage["filler"] or "").strip()),
qc_missing=tuple(checklists.qc_missing(row, ch.qc, ch.welds)),
qc_missing=tuple(checklists.qc_missing(row, ch.qc, ch.welds, ch.qc_items or None)),
has_qc_sent=ch.qc is not None,
)
@@ -485,7 +495,10 @@ def _stage(
return out
def _qc(q: Mapping[str, Any], people: People) -> dict[str, Any]:
RX_NACE = re.compile(r"NACE", re.IGNORECASE)
def _qc(q: Mapping[str, Any], people: People, row: Mapping[str, Any], missing: list[str]) -> dict[str, Any]:
keys = (
"id",
"sent_at",
@@ -504,16 +517,57 @@ def _qc(q: Mapping[str, Any], people: People) -> dict[str, Any]:
"result",
"result_at",
"result_note",
"ncr",
"row_version",
)
out = {k: q[k] for k in keys}
out = {k: q.get(k) for k in keys}
out["sent_by"] = people.ref(q["sent_by"])
out["result_by"] = people.ref(q["result_by"])
out["checks"] = q["checks"] or {}
out["weld_exam"] = q["weld_exam"] or {}
out["checks"] = _checklist(q["checks"], people) # entries gain by_name
out["weld_exam"] = _checklist(q["weld_exam"], people)
out["nace_label"] = bool(RX_NACE.search(str(row.get("wps") or "")))
out["missing"] = missing
return out
def _entry_ids(value: Any) -> list[Any]:
return [as_uuid(v.get("by")) for v in (value or {}).values() if isinstance(v, Mapping)]
def qc_history_row(h: Mapping[str, Any], people: People) -> dict[str, Any]:
"""A closed inspection: the qc_history row with the parts of its snapshot the screens show. `weld_exam` keeps
each weld's result and note so the screens can show the defect next to a rejected weld."""
from .qc import rejected_welds
snap = h.get("snapshot")
if not isinstance(snap, Mapping):
snap = {}
raw = snap.get("weld_exam")
exam: Mapping[str, Any] = raw if isinstance(raw, Mapping) else {}
return {
"id": h["id"],
"at": h["at"],
"result": h["result"],
"inspector": h["inspector"],
"note": h["note"],
"dept_code": h["dept_code"],
"ncr": snap.get("ncr"),
"rejected_welds": rejected_welds(exam),
"weld_exam": {
str(k): {"result": v.get("result"), "note": v.get("note")}
for k, v in exam.items()
if isinstance(v, Mapping)
},
"by": people.ref(as_uuid(snap.get("result_by"))),
"location": snap.get("location"),
"sent_at": snap.get("sent_at"),
}
# Keys phase 6 added to qc_history rows. Seals made before phase 6 do not cover them, so the seal leaves them out.
SEAL_QC_HISTORY = ("ncr", "rejected_welds", "weld_exam", "by", "location", "sent_at")
def _who_ids(row: Mapping[str, Any], ch: Children) -> list[Any]:
ids: list[Any] = [row["welder_id"], row["for_sup_id"], row["vclosed_by"], row["removed_by"]]
ids += [w["welder_id"] for w in ch.welds] + [w["by"] for w in ch.welds]
@@ -527,7 +581,12 @@ def _who_ids(row: Mapping[str, Any], ch: Children) -> list[Any]:
ids.append(ch.wh["by"])
if ch.qc:
ids += [ch.qc["sent_by"], ch.qc["result_by"]]
ids += [d["added_by"] for d in ch.docs]
ids += _entry_ids(ch.qc["checks"]) + _entry_ids(ch.qc["weld_exam"])
for h in ch.qc_history:
snap = h.get("snapshot")
if isinstance(snap, Mapping):
ids.append(as_uuid(snap.get("result_by")))
ids += [d["added_by"] for d in ch.docs] + [d["removed_by"] for d in ch.docs]
for c in ch.change_orders:
ids += [c["issued_by"], c["approved_by"], c["rejected_by"]]
for vs in ch.co_verified.values():
@@ -723,30 +782,18 @@ def build(
"auto": ch.wh["auto"],
}
)
out["qc"] = _qc(ch.qc, people) if ch.qc else None
out["qc_history"] = [
{k: h[k] for k in ("id", "at", "result", "inspector", "note", "dept_code")} for h in ch.qc_history
]
out["qc"] = _qc(ch.qc, people, row, list(state.qc_missing)) if ch.qc else None
out["qc_history"] = [qc_history_row(h, people) for h in ch.qc_history]
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"] = [
{
"id": d["id"],
"asset_id": str(d["asset_id"]),
"kind": d["kind"],
"title": d["title"],
"added_at": d["added_at"],
"added_by": people.ref(d["added_by"]),
"url": f"/_blob/{d['asset_id']}",
}
for d in ch.docs
]
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"]}
)
out["checklist_items"] = checklists.checklist_for(weld, row["drawing"], row["rev"], row["wps"], ch.templates)
out["qc_items"] = checklists.qc_items_for(row["drawing"], row["rev"], row["wps"], row["welder_stamp"])
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)
@@ -754,6 +801,7 @@ def build(
from . import shop # shop builds on this module
out["heat_src"] = shop.heat_src(conn, row, ch)
out.update(qc_extras(conn, row))
if pp is not None:
allowed, blocked = policy.can(pp, state)
out["can"] = [str(a) for a in allowed]
@@ -786,6 +834,9 @@ def _seal_strip(out: dict[str, Any]) -> None:
for r in out.get(key) or []:
for n in names:
r.pop(n, None)
for h in out.get("qc_history") or []:
for n in SEAL_QC_HISTORY:
h.pop(n, None)
for co in out.get("change_orders") or []:
for v in (co.get("verified") or {}).values():
v.pop("signed", None)
@@ -803,6 +854,74 @@ def _seal_strip(out: dict[str, Any]) -> None:
v.pop("by_name", None)
def _doc(d: Mapping[str, Any], people: People, *, seal: bool) -> dict[str, Any]:
"""A packet document. Seal mode keeps the phase 2 keys, plus the note and the take-out when they are set, so a
seal made before phase 6 still verifies for a packet whose documents were never taken out."""
from .qc import DOC_KINDS
removed = (
None
if d["removed_at"] is None
else {"at": d["removed_at"], "by": people.ref(d["removed_by"]), "reason": d["removed_reason"]}
)
out: dict[str, Any] = {
"id": d["id"],
"asset_id": str(d["asset_id"]),
"kind": d["kind"],
"title": d["title"],
"added_at": d["added_at"],
"added_by": people.ref(d["added_by"]),
"url": f"/_blob/{d['asset_id']}",
}
if seal:
if d.get("note"):
out["note"] = d["note"]
if removed is not None:
out["removed"] = removed
return out
out["kind_label"] = DOC_KINDS.get(str(d["kind"]), str(d["kind"]))
out["note"] = d.get("note")
out["file_name"] = d.get("file_name")
out["size"] = d.get("file_size")
out["removed"] = removed
return out
def qc_items_view(row: Mapping[str, Any], ch: Children) -> list[dict[str, Any]]:
"""The Q.C. items with the old qcTab hints: heats and MTRs over the BOM lines, the welds on the map, and the open
(or last) stage's checklist progress with its filler."""
items = checklists.qc_items_for(row["drawing"], row["rev"], row["wps"], row["welder_stamp"], ch.qc_items or None)
nb = len(ch.bom)
heats = sum(1 for b in ch.bom if str(b.get("heat") or "").strip() or b.get("na"))
mtrs = sum(1 for b in ch.bom if b.get("mtr_asset_id"))
stage = ch.open_stage or (ch.stages[-1] if ch.stages else None)
keys = checklists.checklist_keys(bool(ch.dept.get("weld")), ch.templates or None)
ck = (stage or {}).get("checklist") or {}
done = sum(1 for k in keys if checklists.is_done(ck.get(k)))
filler = str((stage or {}).get("filler") or "").strip()
hints = {
"db_heats": f"{heats}/{nb} recorded",
"db_mtr": f"{mtrs}/{nb} MTRs attached",
"db_weldmap": f"{len(ch.welds)} welds on map",
"db_ck": f"checklist {done}/{len(keys)}" + (f" · filler {filler}" if filler else ""),
}
return [{**i, "hint": hints.get(i["key"])} for i in items]
def qc_extras(conn: Any, row: Mapping[str, Any]) -> dict[str, Any]:
"""The unit's Q.C. folder documents, its ITP summary and the packet's ITP hold warning. Outside the seal."""
from . import itp, qcdocs
unit_id = int(row["unit_id"])
r = conn.execute("SELECT project_id FROM units WHERE id = %s", (unit_id,)).fetchone()
found = itp.unit_itps(conn, [unit_id]).get(unit_id)
return {
"qc_folder_docs": qcdocs.packet_folder_docs(conn, unit_id, int(r[0]) if r else None),
"itp": itp.summary(found[0], found[1]) if found else None,
"itp_hold": itp.holds_for_packet(found[0], found[1], row["dept_code"]) if found else None,
}
def target_hrs(work_orders: Iterable[Mapping[str, Any]]) -> float:
"""The packet's target hours: the sum of its work orders' target hours (0 when none)."""
total = 0.0