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
+44
View File
@@ -91,6 +91,11 @@ class Action(StrEnum):
CO_REJECT = "co_reject"
CO_HOLD_TOGGLE = "co_hold_toggle"
CO_VERIFY = "co_verify"
QC_DOC_ADD = "qc_doc_add"
ITP_START = "itp_start"
ITP_LINE_SIGN = "itp_line_sign"
ITP_LINE_UNSIGN = "itp_line_unsign"
ITP_BLOCK_UNSIGN = "itp_block_unsign"
ITP_EDIT_LINE = "itp_edit_line"
ITP_BLOCK_SIGN = "itp_block_sign"
ITP_SIGN_OFF = "itp_sign_off"
@@ -270,6 +275,12 @@ RULES: dict[Action, Rule] = {
),
A.CO_HOLD_TOGGLE: Rule(Who.CL, ANY, None),
A.CO_VERIFY: Rule(Who.CO_VERIFIER, ANY, None, signature="Verify a change order for the department"),
# Q.C. folder documents and the ITP belong to a project or a unit, not a packet: engine.check_scoped decides them.
A.QC_DOC_ADD: Rule(Who.CL_OR_QC, ANY, None),
A.ITP_START: Rule(Who.QC, ANY, None),
A.ITP_LINE_SIGN: Rule(Who.QC, ANY, None),
A.ITP_LINE_UNSIGN: Rule(Who.QC, ANY, None),
A.ITP_BLOCK_UNSIGN: Rule(Who.QC, ANY, None),
A.ITP_EDIT_LINE: Rule(Who.QC, ANY, None),
A.ITP_BLOCK_SIGN: Rule(Who.QC, ANY, None, signature="Sign an ITP block"),
A.ITP_SIGN_OFF: Rule(Who.QC, ANY, None, signature="Sign off the ITP"),
@@ -313,8 +324,41 @@ EDIT_WHAT: dict[Action, str] = {
A.CO_REJECT: "change orders",
A.CO_HOLD_TOGGLE: "change orders",
A.CO_VERIFY: "change orders",
A.QC_DOC_ADD: "the Q.C. folder",
A.ITP_START: "the ITP",
A.ITP_LINE_SIGN: "the ITP",
A.ITP_LINE_UNSIGN: "the ITP",
A.ITP_BLOCK_UNSIGN: "the ITP",
A.ITP_EDIT_LINE: "the ITP",
A.ITP_BLOCK_SIGN: "the ITP",
A.ITP_SIGN_OFF: "the ITP",
A.ITP_REOPEN: "the ITP",
}
# The actions on a project or unit rather than a packet (check them with engine.check_scoped).
SCOPED: frozenset[Action] = frozenset(
{
A.QC_DOC_ADD,
A.ITP_START,
A.ITP_LINE_SIGN,
A.ITP_LINE_UNSIGN,
A.ITP_EDIT_LINE,
A.ITP_BLOCK_SIGN,
A.ITP_BLOCK_UNSIGN,
A.ITP_SIGN_OFF,
A.ITP_REOPEN,
}
)
# Their labels (the audit and log texts name the event more exactly).
SCOPED_LABELS: dict[Action, str] = {
A.QC_DOC_ADD: "Q.C. document added",
A.ITP_START: "ITP started",
A.ITP_LINE_SIGN: "ITP line approved",
A.ITP_LINE_UNSIGN: "ITP line initial removed",
A.ITP_BLOCK_UNSIGN: "ITP block signature removed",
A.ITP_EDIT_LINE: "ITP changed",
A.ITP_BLOCK_SIGN: "ITP block signed",
A.ITP_SIGN_OFF: "ITP signed off",
A.ITP_REOPEN: "ITP reopened",
}