Phase 2: core domain, policy engine, packet API, Clerical screen
Migration 0003: projects, aliases, renumbers, units, packets and every child table, change orders, assets, documents, ITPs, station limits, department weld flag. Policy package: Principal, PacketState, Action rules, custody, preconditions, CO hold, order of checks, can/blocked/holder on reads. docs/POLICY.md with the tightenings table for Seth. Write pipeline: FOR UPDATE, CAS on status and dept, If-Match row versions, server-assigned numbers, field CAS, version bump, packet log, audit, NOTIFY, signatures, IT reasons, filed lock and HMAC seal. API: /api/packets (header, transitions, work orders, pages, docs, move, remove, BOM, shorts, welds, heat tags, heat table, stage, checklist, clock, warehouse stage, Q.C., change orders, log, seal), /api/projects, /api/units, /api/assets, /_blob. Frontend: Clerical home, projects and units, packet page with pdf.js and QR codes, hand draft, heat map, send to warehouse, department or Q.C. Tests: policy matrix, API, concurrency, schema, projects; Playwright Clerical draft scenario. Verified on docker-1 with the CI stack and the Samba DC. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""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 keys are the old keys, so a checklist stored as {key: {done, at, by, stamp}} reads the same.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import Any
|
||||
|
||||
NO_REV = "-"
|
||||
NDE_NONE = ("", "none", "none required")
|
||||
NDE_OK = ("acceptable", "accept", "accepted")
|
||||
REJECTS = ("reject", "rejected")
|
||||
|
||||
|
||||
def _item(group: str, key: str, text: str) -> dict[str, str]:
|
||||
return {"group": group, "key": key, "text": text}
|
||||
|
||||
|
||||
def checklist_for(weld: bool, drawing: str | None, rev: str | None, wps: str | None) -> list[dict[str, str]]:
|
||||
"""The stage checklist for a department, as a flat list of {group, key, text} in display order."""
|
||||
dwg = f"Drawing {drawing or ''} Rev {rev or NO_REV} matches the packet".replace(" ", " ")
|
||||
if not weld:
|
||||
g1, g2 = "Before starting", "When done"
|
||||
return [
|
||||
_item(g1, "dwg", dwg),
|
||||
_item(g1, "prev", "Previous department's work complete and released"),
|
||||
_item(g1, "mat", "Material and parts on hand match the packet"),
|
||||
_item(g2, "work", "Work completed per drawing and procedure"),
|
||||
_item(g2, "insp", "Self-inspection done; no damage to finished surfaces"),
|
||||
_item(g2, "qty", "Completed quantity matches each work order"),
|
||||
]
|
||||
g1, g2 = "Before welding", "After welding"
|
||||
return [
|
||||
_item(g1, "dwg", dwg),
|
||||
_item(g1, "heat", "Heat numbers on material match the Materials tab"),
|
||||
_item(g1, "wps", f"WPS reviewed: {wps or 'per drawing'}"),
|
||||
_item(g1, "fit", "Fit-up and dimensions checked against the drawing"),
|
||||
_item(g1, "pre", "Preheat and interpass temperature per WPS"),
|
||||
_item(g2, "vt", "Visual inspection of all welds acceptable"),
|
||||
_item(g2, "stamp", "Welds stamped with welder ID"),
|
||||
_item(g2, "clean", "Slag and spatter removed"),
|
||||
_item(g2, "qty", "Completed quantity matches each work order"),
|
||||
]
|
||||
|
||||
|
||||
def qc_items_for(
|
||||
drawing: str | None, rev: str | None, wps: str | None, welder_stamp: str | 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"),
|
||||
]
|
||||
|
||||
|
||||
def checklist_keys(weld: bool) -> list[str]:
|
||||
return [i["key"] for i in checklist_for(weld, None, None, None)]
|
||||
|
||||
|
||||
def qc_keys() -> list[str]:
|
||||
return [i["key"] for i in qc_items_for(None, None, None, None)]
|
||||
|
||||
|
||||
def is_done(value: Any) -> bool:
|
||||
"""A checklist or Q.C. check entry: `true`, or `{"done": true, ...}`."""
|
||||
if value is True:
|
||||
return True
|
||||
return isinstance(value, Mapping) and value.get("done") is True
|
||||
|
||||
|
||||
def checklist_complete(checklist: Mapping[str, Any] | None, weld: bool) -> bool:
|
||||
"""Every key of the department checklist is done. An empty list counts as complete."""
|
||||
ck = checklist or {}
|
||||
return all(is_done(ck.get(k)) for k in checklist_keys(weld))
|
||||
|
||||
|
||||
def _plural(n: int, word: str) -> str:
|
||||
return f"{n} {word}{'s' if n > 1 else ''}"
|
||||
|
||||
|
||||
def qc_missing(
|
||||
packet: Mapping[str, Any], qc: Mapping[str, Any] | None, welds: Iterable[Mapping[str, Any]]
|
||||
) -> 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).
|
||||
"""
|
||||
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)))
|
||||
if open_items:
|
||||
missing.append(_plural(open_items, "inspection item"))
|
||||
exam = q.get("weld_exam") or {}
|
||||
results = []
|
||||
for w in welds:
|
||||
entry = exam.get(str(w.get("weld_no")))
|
||||
result = entry.get("result") if isinstance(entry, Mapping) else None
|
||||
results.append(str(result).lower() if result else "")
|
||||
no_result = sum(1 for r in results if not r)
|
||||
if no_result:
|
||||
missing.append(_plural(no_result, "weld") + " without a visual result")
|
||||
if any(r in REJECTS for r in results):
|
||||
missing.append("a rejected weld (reject the packet instead)")
|
||||
method = str(q.get("nde_method") or "").strip().lower()
|
||||
if method not in NDE_NONE and str(q.get("nde_result") or "").strip().lower() not in NDE_OK:
|
||||
missing.append("NDE result")
|
||||
if not str(q.get("inspector") or "").strip():
|
||||
missing.append("inspector name")
|
||||
return missing
|
||||
Reference in New Issue
Block a user