Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
- 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:
@@ -2,7 +2,7 @@
|
||||
|
||||
Inside one transaction: lock the packet row (SELECT ... FOR UPDATE), build the policy's PacketState, check the
|
||||
caller's expected step (409 stale_step), check the packet version when If-Match is given (409 version_conflict),
|
||||
ask the policy (403 or 400 with the decision's code), take the signature when the rule needs one, hand the caller a
|
||||
ask the policy (403, 400 or 422 with the decision's code), take the signature when the rule needs one, hand the caller a
|
||||
`Write`, then bump the version, write the packet_log row, the transition row and its side effects, the audit row and
|
||||
the NOTIFY. The policy decides; a handler never does.
|
||||
|
||||
@@ -38,7 +38,7 @@ from ..auth.signatures import Signature
|
||||
from ..policy.engine import MSG_FILED
|
||||
from ..settings import SecretFileError
|
||||
from ..text import LIMITS, clean_opt, reason_ok
|
||||
from . import packets
|
||||
from . import checklists, packets
|
||||
from . import qc as qc_dom
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -52,14 +52,6 @@ MSG_FIELD = "Someone else changed this since you opened it."
|
||||
MSG_DATABOOK = "The data book could not be built: {reason}. Nothing was filed."
|
||||
|
||||
|
||||
def open_wos_text(wos: Sequence[str]) -> str:
|
||||
"""The FILE confirm question: "VISUAL still shows 2 work orders open on this packet: L489052/1, L489052/2. File
|
||||
anyway?" """
|
||||
n = len(wos)
|
||||
what = "1 work order" if n == 1 else f"{n} work orders"
|
||||
return f"VISUAL still shows {what} open on this packet: {', '.join(wos)}. File anyway?"
|
||||
|
||||
|
||||
# Codes that answer 400 (the UI asks for input and retries); every other refusal answers 403.
|
||||
ASK_CODES = frozenset({"it_reason_required", "reason_required"})
|
||||
|
||||
@@ -202,7 +194,7 @@ def refuse(decision: policy.Decision) -> None:
|
||||
"""Raise the API error for a failed policy decision."""
|
||||
if decision.ok:
|
||||
return
|
||||
status = 400 if decision.code in ASK_CODES else 403
|
||||
status = 400 if decision.code in ASK_CODES else 422 if decision.code == "ncr_required" else 403
|
||||
raise ApiError(status, decision.code, decision.why)
|
||||
|
||||
|
||||
@@ -348,11 +340,6 @@ def packet_write(
|
||||
bypass = bypass or dd.admin_bypass
|
||||
if if_match is not None and int(if_match) != int(row["version"]):
|
||||
raise ApiError(409, "version_conflict", MSG_VERSION, extra={"current": current_of(row)})
|
||||
if action is A.FILE and extra.get("confirm_open_wos") is not True:
|
||||
# Before the signature: a cancelled confirm costs no attempt and writes nothing.
|
||||
wos = packets.packet_open_wos(conn, row)
|
||||
if wos:
|
||||
raise ApiError(409, "open_work_orders", open_wos_text(wos), extra={"work_orders": wos})
|
||||
w = Write(
|
||||
conn=conn,
|
||||
principal=p,
|
||||
@@ -555,6 +542,13 @@ def _set_result(w: Write, insp: Mapping[str, Any] | None, result: str, note: str
|
||||
)
|
||||
|
||||
|
||||
def _set_ncr(w: Write, insp: Mapping[str, Any] | None) -> None:
|
||||
"""The NCR number the step carries (a reject, or the accept after a reject without one), on the inspection."""
|
||||
ncr = w.extra.get("ncr") or None
|
||||
if insp is not None and (ncr or w.action is A.QC_REJECT):
|
||||
w.conn.execute("UPDATE qc_inspections SET ncr = %s WHERE id = %s", (ncr, insp["id"]))
|
||||
|
||||
|
||||
def _set_packet(w: Write, assignments: str, params: Sequence[Any] = ()) -> None:
|
||||
w.conn.execute(f"UPDATE packets SET {assignments} WHERE id = %s", (*params, w.id)) # noqa: S608 (fixed fragments)
|
||||
|
||||
@@ -685,6 +679,8 @@ def apply_transition(w: Write) -> None:
|
||||
assert to_status is not None
|
||||
ex = w.extra
|
||||
reason = _reason(w)
|
||||
if a is A.QC_REJECT and reason and ex.get("ncr"):
|
||||
reason = f"{ex['ncr']} · {reason}" # the supervisor's return note leads with the NCR number
|
||||
from_dept = w.from_dept
|
||||
to_dept = from_dept
|
||||
if rule.dept_change != "no" and isinstance(ex.get("dept"), str) and ex.get("dept"):
|
||||
@@ -751,6 +747,8 @@ def apply_transition(w: Write) -> None:
|
||||
"clock_running_since = coalesce(clock_running_since, now()),"
|
||||
" clock_started_at = coalesce(clock_started_at, now())",
|
||||
)
|
||||
# change C8: the stage keeps the checklist it started with
|
||||
checklists.freeze_on_start(w.conn, w.id)
|
||||
elif a is A.SUBMIT:
|
||||
_set_packet(w, "return_note = NULL")
|
||||
_stage_update(w, CLOCK_STOP)
|
||||
@@ -776,6 +774,7 @@ def apply_transition(w: Write) -> None:
|
||||
elif a in (A.ROUTE_FROM_REVIEW, A.QC_ACCEPT_AND_ROUTE):
|
||||
insp = _open_inspection(w)
|
||||
if a is A.QC_ACCEPT_AND_ROUTE:
|
||||
_set_ncr(w, insp)
|
||||
insp = _set_result(w, insp, "accepted", _note(w))
|
||||
_close_stage(w, qc_snapshot=insp)
|
||||
_close_inspection(w, insp, from_dept)
|
||||
@@ -784,15 +783,16 @@ def apply_transition(w: Write) -> None:
|
||||
elif a is A.QC_REJECT:
|
||||
# The reject closes the inspection into qc_history at once; the next SEND_TO_QC opens a fresh one.
|
||||
insp = _open_inspection(w)
|
||||
if insp is not None:
|
||||
w.conn.execute("UPDATE qc_inspections SET ncr = %s WHERE id = %s", (ex.get("ncr") or None, insp["id"]))
|
||||
_set_ncr(w, insp)
|
||||
insp = _set_result(w, insp, "rejected", reason)
|
||||
_close_inspection(w, insp, from_dept)
|
||||
_set_packet(w, "return_note = %s", (reason,))
|
||||
if not packets.one(w.conn, "SELECT id FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL", (w.id,)):
|
||||
_open_stage(w, from_dept) # a draft sent straight to Q.C. has no stage yet
|
||||
elif a is A.QC_ACCEPT:
|
||||
insp = _set_result(w, _open_inspection(w), "accepted", _note(w))
|
||||
insp = _open_inspection(w)
|
||||
_set_ncr(w, insp)
|
||||
insp = _set_result(w, insp, "accepted", _note(w))
|
||||
_close_inspection(w, insp, from_dept)
|
||||
_close_stage(w, qc_snapshot=insp)
|
||||
_set_packet(w, "routed_to = %s::route_kind", (str(ex["routed_to"]),))
|
||||
@@ -838,20 +838,16 @@ def _file(w: Write) -> None:
|
||||
_set_packet(w, "databook_at = now()")
|
||||
row = packets.load_row(w.conn, w.id)
|
||||
assert row is not None
|
||||
wos = packets.packet_open_wos(w.conn, row) if w.extra.get("confirm_open_wos") is True else []
|
||||
content = packets.seal_content(w.conn, row)
|
||||
digest = seal_hash(key, content)
|
||||
kid = seal_key_id(key)
|
||||
seal_id, sealed_at = w.conn.execute(
|
||||
"INSERT INTO packet_seals (packet_id, hash, key_id, sealed_by, signature_id, open_wos)"
|
||||
" VALUES (%s, %s, %s, %s, %s, %s) RETURNING id, sealed_at",
|
||||
(w.id, digest, kid, w.user_id, w.signature.id if w.signature else None, wos or None),
|
||||
"INSERT INTO packet_seals (packet_id, hash, key_id, sealed_by, signature_id)"
|
||||
" VALUES (%s, %s, %s, %s, %s) RETURNING id, sealed_at",
|
||||
(w.id, digest, kid, w.user_id, w.signature.id if w.signature else None),
|
||||
).fetchone()
|
||||
pages = _file_databook(w, row, {"hash": digest.hex(), "key_id": kid, "sealed_at": sealed_at}, int(seal_id))
|
||||
w.log_also("Data book filed", f"{pages} pages")
|
||||
if wos:
|
||||
w.note = " · ".join(x for x in (_note(w), "Filed with work orders open in VISUAL: " + ", ".join(wos)) if x)
|
||||
w.detail["open_wos"] = wos
|
||||
w.detail["seal_id"] = int(seal_id)
|
||||
w.detail["databook_pages"] = pages
|
||||
|
||||
|
||||
Reference in New Issue
Block a user