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
+62
View File
@@ -10,6 +10,9 @@ Order of checks, exactly:
6. filed lock: only SET_DATABOOK_AT and ADMIN_REOPEN, Admin included.
7. who and custody.
8. preconditions in rule order, never bypassed.
`check_scoped` decides the actions on a project or unit (Q.C. folder documents, the ITP) the same way without a
packet: system, admin bypass, who, then the input preconditions. There is no status, CO hold, filed lock or custody.
"""
from __future__ import annotations
@@ -37,6 +40,24 @@ MSG_FILED = "This packet is filed and locked. IT can reopen it if a correction i
MSG_FORBIDDEN_STEP = "Your access doesn't allow this step."
MSG_NOT_WH_SKIPPED = "This packet came through the warehouse. Send it back to the warehouse instead."
MSG_NOT_DATABOOK = "Only packets routed to the data book are closed in VISUAL here."
MSG_FORBIDDEN_SCOPED = "Your access doesn't allow this."
# The packet view check_scoped hands the who predicates. Only predicates that ignore the packet (CL, QC, QC_SUP,
# CL_OR_QC and the like) can pass with it; HOLDER and the department predicates fail.
_NO_PACKET = PacketState(
id=0,
status="",
dept="",
station_id=None,
welder_id=None,
wh_skipped=False,
routed_to=None,
vclosed=False,
removed=False,
source_kind="hand",
weld_dept=False,
co_hold=False,
)
@dataclass(frozen=True)
@@ -181,3 +202,44 @@ def can(
elif d.reasons:
blocked[action] = list(d.reasons)
return allowed, blocked
def check_scoped(
principal: Principal,
action: Action,
*,
extra: Mapping[str, Any] | None = None,
it_reason: str | None = None,
assume_it_reason: bool = False,
) -> Decision:
"""Decide an action on a project or unit (no packet): the steps of check() that do not need one.
System principals fail `forbidden` (a system-only rule fails `system_only` for people). An admin the rule does not
allow passes with an IT reason of at least 8 characters (`admin_bypass`), else `it_reason_required`. Then who
(`forbidden`), then the input preconditions in rule order (`reason_given` fails `reason_required`). Custody is not
checked: nobody holds a project or unit. `assume_it_reason` is the read mode for an admin (what the screen offers).
"""
rule = RULES[action]
ex = extra or EMPTY
if rule.system_only and principal.system is None:
return Decision(False, "system_only", MSG_SYSTEM_ONLY)
if principal.system is not None:
return Decision(False, "forbidden", MSG_SYSTEM_FORBIDDEN)
who_ok = allows(rule.who, principal, _NO_PACKET, ex)
bypass = False
if not who_ok and principal.admin:
if assume_it_reason or reason_ok(it_reason) is not None:
bypass = True
else:
return Decision(False, "it_reason_required", MSG_IT_REASON)
if not who_ok and not bypass:
return Decision(False, "forbidden", MSG_FORBIDDEN_SCOPED)
for name in rule.preconditions:
if name not in INPUT_ONLY:
continue # a state precondition needs a packet; no scoped rule has one
failed = PRECONDITIONS[name](_NO_PACKET, ex)
if failed is None:
continue
code = "reason_required" if rule.reason_required and name == "reason_given" else "precondition"
return Decision(False, code, failed, admin_bypass=bypass)
return Decision(True, admin_bypass=bypass, signature=rule.signature)