Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
ci / frontend (pull_request) Successful in 57s
ci / server (pull_request) Successful in 12m33s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 5m4s

- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 13:33:51 -06:00
co-authored by Claude Opus 5.5
parent 1ae6819289
commit 1cd6688247
101 changed files with 3871 additions and 1098 deletions
+87 -2
View File
@@ -376,7 +376,12 @@ PRECONDITION_CASES: list[tuple[Action, str, str, dict[str, Any], dict[str, Any],
(A.QC_ACCEPT, "qc", "qc", {"qc_missing": ("weld exam",)}, {}, "qc_missing_empty"),
(A.QC_ACCEPT, "qc", "qc", {}, {"routed_to": "scrap"}, "route_given"),
(A.QC_ACCEPT_AND_ROUTE, "qc", "qc", {}, {"dept": "WH"}, "target_dept_given"),
(A.FILE, "complete", "clerical", {"vclosed": False}, {}, "vclosed_if_databook"),
(A.FILE, "complete", "clerical", {"vclosed": False}, {}, "closed_in_visual"),
(A.FILE, "complete", "clerical", {"itp_open": True}, {}, "itp_complete"),
(A.FILE, "complete", "clerical", {"shorts_unapproved": 1}, {}, "shorts_approved"),
(A.QC_ACCEPT, "qc", "qc", {"shorts_unapproved": 2}, {}, "shorts_approved"),
(A.QC_ACCEPT, "qc", "qc", {"ncr_required": True}, {}, "ncr_given"),
(A.QC_ACCEPT_AND_ROUTE, "qc", "qc", {"ncr_required": True}, {"ncr": "NCR-26-1"}, "ncr_given"),
(A.WH_SEND_BACK, "warehouse", "warehouse", {}, {"reason": "short"}, "reason_given"),
(A.QC_REJECT, "qc", "qc", {}, {"reason": None}, "reason_given"),
(A.REMOVE, "draft", "clerical", {}, {"reason": " x "}, "reason_given"),
@@ -396,7 +401,7 @@ def test_each_precondition_fails_alone(
x = {**FULL_EXTRA, **extra}
assert check(ROLES[role], state(status), action, extra=FULL_EXTRA).ok
d = check(ROLES[role], s, action, extra=x)
want = "reason_required" if name == "reason_given" else "precondition"
want = {"reason_given": "reason_required", "ncr_given": "ncr_required"}.get(name, "precondition")
assert (d.ok, d.code) == (False, want)
if name == "qc_missing_empty":
assert d.why == "Q.C. still needs: weld exam."
@@ -423,6 +428,76 @@ def test_vclosed_only_needed_for_the_data_book_route() -> None:
assert check(ROLES["clerical"], state("complete", routed_to="shipping", vclosed=False), A.FILE).ok
@pytest.mark.parametrize("route", ["databook", "shipping"])
def test_visual_packet_files_only_once_the_feed_saw_it_closed(route: str) -> None:
"""C5: a VISUAL packet waits for the feed on both routes; open work orders are named."""
clerk = ROLES["clerical"]
vis = state("complete", source_kind="visual", routed_to=route)
assert check(clerk, vis, A.FILE).ok
d = check(clerk, dataclasses.replace(vis, vclosed=False), A.FILE)
assert (d.code, d.why) == ("precondition", "Filing waits until the VISUAL feed sees the job closed.")
opened = dataclasses.replace(vis, visual_open_wos=("L670001/001", "L670002/7A"))
d = check(clerk, opened, A.FILE)
assert d.code == "precondition"
assert d.why == (
"VISUAL still shows work orders open on this packet: L670001/001, L670002/7A."
" Filing waits until VISUAL closes them."
)
assert can(clerk, opened)[1][A.FILE] == [d.why]
# never bypassed
assert check(ROLES["admin"], opened, A.FILE, it_reason=IT).code == "precondition"
def test_file_blocked_shows_every_close_reason() -> None:
s = state("complete", vclosed=False, itp_open=True, shorts_unapproved=1)
allowed, blocked = can(ROLES["clerical"], s)
assert A.FILE not in allowed
assert blocked[A.FILE] == [MESSAGES["closed_in_visual"], MESSAGES["itp_complete"], MESSAGES["shorts_approved"]]
assert MESSAGES["itp_complete"] == "Complete the unit's ITP first. Q.C. signs it off on the ITP page."
assert MESSAGES["shorts_approved"] == "Every short must be received and approved by the supervisor first."
def test_ncr_given_is_input_and_skipped_by_can() -> None:
"""C2: after a reject with no NCR, the accept carries a well-formed NCR. can() does not block on it."""
s = state("qc", ncr_required=True)
allowed, blocked = can(ROLES["qc"], s)
assert {A.QC_ACCEPT, A.QC_ACCEPT_AND_ROUTE} <= set(allowed) and A.QC_ACCEPT not in blocked
d = check(ROLES["qc"], s, A.QC_ACCEPT, extra=FULL_EXTRA)
assert (d.ok, d.code, d.why) == (False, "ncr_required", "Enter the NCR number of the reject first.")
assert check(ROLES["qc"], s, A.QC_ACCEPT, extra={**FULL_EXTRA, "ncr": "NCR-2026-0021"}).ok
assert check(ROLES["qc"], s, A.QC_ACCEPT_AND_ROUTE, extra={**FULL_EXTRA, "ncr": "ncr-2026-0021"}).ok
# not needed when no reject is waiting for one
assert check(ROLES["qc"], state("qc"), A.QC_ACCEPT, extra=FULL_EXTRA).ok
def test_shorts_block_the_accept_but_not_the_accept_and_route() -> None:
s = state("qc", shorts_unapproved=1)
_, blocked = can(ROLES["qc"], s)
assert blocked[A.QC_ACCEPT] == [MESSAGES["shorts_approved"]]
assert A.QC_ACCEPT_AND_ROUTE not in blocked
assert check(ROLES["qc"], s, A.QC_ACCEPT_AND_ROUTE, extra=FULL_EXTRA).ok
@pytest.mark.parametrize("status", STATUSES)
def test_short_approve_is_the_department_supervisor(status: str) -> None:
"""C7: the supervisor of the packet's department (a lead is not enough), any status but filed; Admin through
the bypass with an IT reason. A signature is required."""
s = state(status)
d = check(ROLES["sup03"], s, A.SHORT_APPROVE)
if status == "filed":
assert d.code == "filed"
return
assert d.ok and d.signature == "Approve a received short"
assert A.SHORT_APPROVE in can(ROLES["sup03"], s)[0]
for role in ("lead03", "worker03", "welder", "warehouse_sup", "qc_sup", "clerical_sup", "pm", "worker06"):
d = check(ROLES[role], s, A.SHORT_APPROVE)
assert (d.code, d.why) == ("forbidden", "Your access doesn't allow this step."), role
assert check(P(["shop:06:supervisor"]), s, A.SHORT_APPROVE).code == "forbidden"
assert check(ROLES["admin"], s, A.SHORT_APPROVE).code == "it_reason_required"
a = check(ROLES["admin"], s, A.SHORT_APPROVE, it_reason=IT)
assert a.ok and a.admin_bypass
def test_co_staged_if_dwg_change() -> None:
from packetstation.policy.preconditions import co_staged_if_dwg_change
@@ -468,6 +543,16 @@ def test_set_vclosed_needs_the_data_book_route() -> None:
assert check(ROLES["qc"], state("complete"), A.SET_VCLOSED).code == "forbidden"
def test_set_vclosed_is_for_a_hand_packet_only() -> None:
"""C5: the feed closes a VISUAL packet; Clerical confirms a hand packet only."""
for route in ("databook", "shipping"):
d = check(ROLES["clerical"], state("complete", source_kind="visual", routed_to=route), A.SET_VCLOSED)
assert (d.code, d.why) == (
"wrong_status",
"The VISUAL feed marks this job closed when it sees every work order closed.",
)
@pytest.mark.parametrize("status", STATUSES)
def test_set_databook_at_is_clerical_or_qc(status: str) -> None:
s = state(status)