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
+21 -7
View File
@@ -3,7 +3,7 @@
Order of checks, exactly:
1. removed packet: nothing passes, Admin included.
2. status: the rule's from statuses and the status conditions (RETURN_TO_CLERICAL needs wh_skipped,
SET_VCLOSED needs routed_to databook). An unknown status fails here.
SET_VCLOSED needs a hand packet routed to the data book). An unknown status fails here.
3. change-order hold, Admin included.
4. system-only rules refuse people; the system principal passes only system-only rules.
5. admin bypass of who and custody, with an IT reason (SET_DATABOOK_AT needs none).
@@ -40,6 +40,7 @@ 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_FEED_CLOSES = "The VISUAL feed marks this job closed when it sees every work order closed."
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,
@@ -60,11 +61,15 @@ _NO_PACKET = PacketState(
)
# Preconditions with their own refusal code (the API maps reason_required to 400 and ncr_required to 422).
PRECONDITION_CODES: dict[str, str] = {"reason_given": "reason_required", "ncr_given": "ncr_required"}
@dataclass(frozen=True)
class Decision:
ok: bool
code: str = "" # removed | wrong_status | co_hold | filed | it_reason_required | forbidden | precondition
# | reason_required | system_only
# | reason_required | ncr_required | system_only
why: str = "" # the message for the user
admin_bypass: bool = False # an admin passed who and custody only through the bypass (an IT edit)
signature: str | None = None # the rule's signature meaning, when one is required
@@ -78,15 +83,22 @@ STATUS_CONDITIONS: dict[Action, tuple[Callable[[PacketState], bool], str]] = {
}
def _status_condition(state: PacketState, action: Action) -> str | None:
# C5: the feed alone closes a VISUAL packet in VISUAL; Clerical confirms a hand packet only.
if action is Action.SET_VCLOSED and state.source_kind == "visual":
return MSG_FEED_CLOSES
cond = STATUS_CONDITIONS.get(action)
if cond is not None and not cond[0](state):
return cond[1]
return None
def _wrong_status(state: PacketState, action: Action, rule: Rule) -> str | None:
if state.status not in STATUSES:
return MSG_UNKNOWN_STATUS
if rule.from_status and state.status not in rule.from_status:
return f"This step isn't open while the packet is {STATUS_LABELS[state.status]}."
cond = STATUS_CONDITIONS.get(action)
if cond is not None and not cond[0](state):
return cond[1]
return None
return _status_condition(state, action)
def _forbidden_why(action: Action, rule: Rule, state: PacketState, extra: Mapping[str, Any]) -> str:
@@ -155,7 +167,9 @@ def _evaluate(
if read:
blockers.append(failed)
continue
code = "reason_required" if rule.reason_required and name == "reason_given" else "precondition"
code = PRECONDITION_CODES.get(name, "precondition")
if name == "reason_given" and not rule.reason_required:
code = "precondition"
return Decision(False, code, failed, admin_bypass=bypass)
if blockers:
code = "co_hold" if rule.co_hold and state.co_hold else "precondition"