Phase 6: Q.C. inspection, project folders, packet documents and the digital ITP
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:
@@ -1,7 +1,8 @@
|
||||
"""The department checklists and the Q.C. checks, ported from the old checklistFor(), qcChecksFor() and qcMissing().
|
||||
|
||||
Weld departments get the welding list, the others the plain list. The drawing, rev and WPS are filled into the text.
|
||||
The lists live in the settings row `checklist_templates` (migration 0006); the defaults here are the fallback.
|
||||
The lists live in the settings rows `checklist_templates` (migration 0006) and `qc_check_items` (migration 0007); the
|
||||
defaults here are the fallback.
|
||||
The keys are the old keys, so a checklist stored as {key: {done, at, by, stamp}} reads the same.
|
||||
"""
|
||||
|
||||
@@ -115,32 +116,79 @@ def checklist_for(
|
||||
return [_item(i["group"], i["key"], _fill(i["text"], drawing, rev, wps)) for i in items]
|
||||
|
||||
|
||||
QC_SETTINGS_KEY = "qc_check_items"
|
||||
RX_QC_PLACEHOLDER = re.compile(r"\{(drawing|rev|wps|stamp)\}")
|
||||
G_BOOK, G_SPOOL = "Data book package", "Physical spool"
|
||||
|
||||
# The Q.C. check items, with {wps}, {stamp}, {drawing} and {rev} placeholders. The settings row `qc_check_items`
|
||||
# (migration 0007) holds the same list; this is the fallback when that row is missing or does not parse.
|
||||
QC_DEFAULT_ITEMS: list[dict[str, str]] = [
|
||||
_item(G_BOOK, "db_heats", "Heat number recorded for every BOM item"),
|
||||
_item(G_BOOK, "db_mtr", "MTRs on file and match the heat numbers"),
|
||||
_item(G_BOOK, "db_weldmap", "Weld map complete: every weld identified"),
|
||||
_item(G_BOOK, "db_wps", "Welds made to {wps}; welder {stamp}qualified"),
|
||||
_item(G_BOOK, "db_ck", "Stall checklist and weld record complete"),
|
||||
_item(G_SPOOL, "ph_dim", "Dimensions verified against {drawing} Rev {rev}"),
|
||||
_item(G_SPOOL, "ph_heat", "Heat numbers on material match the heat map"),
|
||||
_item(G_SPOOL, "ph_stamp", "Welds stamped with welder ID"),
|
||||
_item(G_SPOOL, "ph_clean", "Clean: no slag, spatter or arc strikes"),
|
||||
_item(G_SPOOL, "ph_qty", "Quantity matches the work orders"),
|
||||
]
|
||||
|
||||
|
||||
def parse_qc_items(value: Any) -> list[dict[str, str]] | None:
|
||||
"""The `qc_check_items` settings value ({"groups": [{"group", "items": [{"key", "text"}]}]}) as a flat list of
|
||||
{group, key, text}. None when it is malformed."""
|
||||
if not isinstance(value, Mapping) or not isinstance(value.get("groups"), list):
|
||||
return None
|
||||
flat: list[dict[str, Any]] = []
|
||||
for g in value["groups"]:
|
||||
if not isinstance(g, Mapping) or not isinstance(g.get("items"), list):
|
||||
return None
|
||||
flat += [{**i, "group": g.get("group")} if isinstance(i, Mapping) else i for i in g["items"]]
|
||||
return _valid_list(flat)
|
||||
|
||||
|
||||
def load_qc_items(conn: Any) -> list[dict[str, str]]:
|
||||
"""The Q.C. check items from settings, else the defaults in code (with a warning when the row is bad)."""
|
||||
row = conn.execute("SELECT value FROM settings WHERE key = %s", (QC_SETTINGS_KEY,)).fetchone()
|
||||
if row is None:
|
||||
return QC_DEFAULT_ITEMS
|
||||
parsed = parse_qc_items(row[0])
|
||||
if parsed is None:
|
||||
log.warning("settings %s does not parse; using the default Q.C. items", QC_SETTINGS_KEY)
|
||||
return QC_DEFAULT_ITEMS
|
||||
return parsed
|
||||
|
||||
|
||||
def qc_fill(text: str, drawing: str | None, rev: str | None, wps: str | None, welder_stamp: str | None) -> str:
|
||||
values = {
|
||||
"drawing": drawing or "the drawing",
|
||||
"rev": rev or NO_REV,
|
||||
"wps": wps or "the WPS",
|
||||
"stamp": f"[{welder_stamp}] " if welder_stamp else "",
|
||||
}
|
||||
return " ".join(RX_QC_PLACEHOLDER.sub(lambda m: values[m.group(1)], text).split())
|
||||
|
||||
|
||||
def qc_items_for(
|
||||
drawing: str | None, rev: str | None, wps: str | None, welder_stamp: str | None
|
||||
drawing: str | None,
|
||||
rev: str | None,
|
||||
wps: str | None,
|
||||
welder_stamp: str | None,
|
||||
items: Iterable[Mapping[str, str]] | None = None,
|
||||
) -> list[dict[str, str]]:
|
||||
"""The Q.C. inspection items, as a flat list of {group, key, text}."""
|
||||
g1, g2 = "Data book package", "Physical spool"
|
||||
stamp = f"[{welder_stamp}] " if welder_stamp else ""
|
||||
return [
|
||||
_item(g1, "db_heats", "Heat number recorded for every BOM item"),
|
||||
_item(g1, "db_mtr", "MTRs on file and match the heat numbers"),
|
||||
_item(g1, "db_weldmap", "Weld map complete: every weld identified"),
|
||||
_item(g1, "db_wps", f"Welds made to {wps or 'the WPS'}; welder {stamp}qualified"),
|
||||
_item(g1, "db_ck", "Stall checklist and weld record complete"),
|
||||
_item(g2, "ph_dim", f"Dimensions verified against {drawing or 'the drawing'} Rev {rev or NO_REV}"),
|
||||
_item(g2, "ph_heat", "Heat numbers on material match the heat map"),
|
||||
_item(g2, "ph_stamp", "Welds stamped with welder ID"),
|
||||
_item(g2, "ph_clean", "Clean: no slag, spatter or arc strikes"),
|
||||
_item(g2, "ph_qty", "Quantity matches the work orders"),
|
||||
]
|
||||
"""The Q.C. inspection items, as a flat list of {group, key, text} with the packet's values filled in."""
|
||||
src = list(items) if items is not None else QC_DEFAULT_ITEMS
|
||||
return [_item(i["group"], i["key"], qc_fill(i["text"], drawing, rev, wps, welder_stamp)) for i in src]
|
||||
|
||||
|
||||
def checklist_keys(weld: bool, templates: Mapping[str, list[dict[str, str]]] | None = None) -> list[str]:
|
||||
return [i["key"] for i in checklist_for(weld, None, None, None, templates)]
|
||||
|
||||
|
||||
def qc_keys() -> list[str]:
|
||||
return [i["key"] for i in qc_items_for(None, None, None, None)]
|
||||
def qc_keys(items: Iterable[Mapping[str, str]] | None = None) -> list[str]:
|
||||
return [i["key"] for i in (items if items is not None else QC_DEFAULT_ITEMS)]
|
||||
|
||||
|
||||
def is_done(value: Any) -> bool:
|
||||
@@ -163,19 +211,23 @@ def _plural(n: int, word: str) -> str:
|
||||
|
||||
|
||||
def qc_missing(
|
||||
packet: Mapping[str, Any], qc: Mapping[str, Any] | None, welds: Iterable[Mapping[str, Any]]
|
||||
packet: Mapping[str, Any],
|
||||
qc: Mapping[str, Any] | None,
|
||||
welds: Iterable[Mapping[str, Any]],
|
||||
items: Iterable[Mapping[str, str]] | None = None,
|
||||
) -> list[str]:
|
||||
"""What Q.C. still has to record before an accept. The old qcMissing().
|
||||
|
||||
`packet` is unused today (the item list does not depend on it for the keys) but kept for the old signature.
|
||||
`qc` is the open inspection row (checks, weld_exam, nde_method, nde_result, inspector) or None.
|
||||
`welds` are the live weld rows (weld_no).
|
||||
`welds` are the live weld rows (weld_no). `items` are the loaded Q.C. items (load_qc_items); None means the
|
||||
defaults.
|
||||
"""
|
||||
del packet
|
||||
q = qc or {}
|
||||
checks = q.get("checks") or {}
|
||||
missing: list[str] = []
|
||||
open_items = sum(1 for k in qc_keys() if not is_done(checks.get(k)))
|
||||
open_items = sum(1 for k in qc_keys(items) if not is_done(checks.get(k)))
|
||||
if open_items:
|
||||
missing.append(_plural(open_items, "inspection item"))
|
||||
exam = q.get("weld_exam") or {}
|
||||
|
||||
Reference in New Issue
Block a user