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

- 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 16:36:49 -06:00
co-authored by Claude Opus 5.5
parent 150c507a31
commit 70a97398f1
101 changed files with 3871 additions and 1098 deletions
+92 -7
View File
@@ -232,6 +232,12 @@ class Children:
bom: list[dict[str, Any]] = field(default_factory=list)
shorts: dict[int, dict[str, Any]] = field(default_factory=dict) # bom_line_id -> open short
receipts: dict[int, list[dict[str, Any]]] = field(default_factory=dict) # short id -> receipts
all_shorts: list[dict[str, Any]] = field(default_factory=list) # every short, open or closed, with its item
approvals: dict[int, dict[str, Any]] = field(default_factory=dict) # receipt id -> supervisor approval (C7)
receipts_unapproved: int = 0 # receipts with no supervisor approval (C7)
ncr_required: bool = False # a Q.C. reject since the last accept has no NCR number (C2)
itp_open: bool = False # the unit has an ITP that is not complete (C3)
visual_open_wos: list[str] = field(default_factory=list) # what the feed last saw open in VISUAL (C5)
welds: list[dict[str, Any]] = field(default_factory=list)
heat_tags: list[dict[str, Any]] = field(default_factory=list)
heat_table: list[dict[str, Any]] = field(default_factory=list)
@@ -271,7 +277,6 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
for code, name, kind in conn.execute("SELECT code, name, kind::text FROM departments"):
ch.dept_names[code] = name
ch.dept_labels[code] = dept_label(code, name, kind)
ch.templates = checklists.load_templates(conn)
ch.qc_items = checklists.load_qc_items(conn)
ch.pages = rows(conn, "SELECT * FROM packet_pages WHERE packet_id = %s ORDER BY n", (pid,))
ch.bom = rows(conn, "SELECT * FROM bom_lines WHERE packet_id = %s ORDER BY item", (pid,))
@@ -279,6 +284,8 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
ch.shorts = {s["bom_line_id"]: s for s in shorts}
ch.welds = rows(conn, "SELECT * FROM welds WHERE packet_id = %s AND removed_at IS NULL ORDER BY weld_no", (pid,))
ch.stages = rows(conn, "SELECT * FROM packet_stages WHERE packet_id = %s ORDER BY id", (pid,))
# Change C8: the stage's checklist frozen at START, else the department's live list
ch.templates = checklists.templates_for_packet(conn, row["dept_code"], ch.stages)
ch.qc = one(conn, "SELECT * FROM qc_inspections WHERE packet_id = %s AND closed_at IS NULL", (pid,))
ch.open_cos = rows(
conn,
@@ -286,16 +293,22 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
" ORDER BY co_no",
(pid,),
)
_load_close_facts(conn, row, ch)
if not full:
return ch
ch.work_orders = rows(conn, "SELECT * FROM packet_work_orders WHERE packet_id = %s ORDER BY sort, id", (pid,))
if shorts:
for r in rows(
conn,
"SELECT * FROM short_receipts WHERE short_id = ANY(%s) ORDER BY id",
([s["id"] for s in shorts],),
):
ch.all_shorts = rows(
conn,
"SELECT s.*, b.item FROM bom_shorts s JOIN bom_lines b ON b.id = s.bom_line_id WHERE s.packet_id = %s"
" ORDER BY s.id",
(pid,),
)
if ch.all_shorts:
for r in rows(conn, "SELECT * FROM short_receipts WHERE packet_id = %s ORDER BY id", (pid,)):
ch.receipts.setdefault(r["short_id"], []).append(r)
ch.approvals = {
int(r["receipt_id"]): r for r in rows(conn, "SELECT * FROM short_approvals WHERE packet_id = %s", (pid,))
}
ch.heat_tags = rows(
conn, "SELECT * FROM heat_tags WHERE packet_id = %s AND removed_at IS NULL ORDER BY tag_no", (pid,)
)
@@ -331,6 +344,31 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
return ch
# A Q.C. reject after the packet's last accept that has no NCR number (C2).
NCR_REQUIRED_SQL = """
SELECT EXISTS (
SELECT 1 FROM qc_history h
WHERE h.packet_id = %(p)s AND h.result = 'rejected' AND btrim(coalesce(h.snapshot->>'ncr', '')) = ''
AND h.id > coalesce((SELECT max(a.id) FROM qc_history a WHERE a.packet_id = %(p)s AND a.result = 'accepted'), 0)
)
"""
RECEIPTS_UNAPPROVED_SQL = (
"SELECT count(*) FROM short_receipts r WHERE r.packet_id = %s"
" AND NOT EXISTS (SELECT 1 FROM short_approvals a WHERE a.receipt_id = r.id)"
)
def _load_close_facts(conn: Any, row: Mapping[str, Any], ch: Children) -> None:
"""The policy facts of C2, C3, C5 and C7: unapproved short receipts, the NCR an accept needs, the unit's open ITP
and the work orders the feed last saw open."""
pid = row["id"]
ch.receipts_unapproved = int(conn.execute(RECEIPTS_UNAPPROVED_SQL, (pid,)).fetchone()[0])
ch.ncr_required = bool(conn.execute(NCR_REQUIRED_SQL, {"p": pid}).fetchone()[0])
itp_sql = "SELECT 1 FROM itps WHERE unit_id = %s AND status <> 'complete'"
ch.itp_open = conn.execute(itp_sql, (row["unit_id"],)).fetchone() is not None
ch.visual_open_wos = open_wos(visual_state(conn, row))
def is_sub(line: Mapping[str, Any]) -> bool:
"""A sub-assembly line: made on its own packet, so its heats are recorded there."""
return str(line.get("sub") or "").strip().upper() == "Y"
@@ -385,6 +423,10 @@ def state_from(row: Mapping[str, Any], ch: Children) -> policy.PacketState:
filler_set=stage is not None and bool((stage["filler"] or "").strip()),
qc_missing=tuple(checklists.qc_missing(row, ch.qc, ch.welds, ch.qc_items or None)),
has_qc_sent=ch.qc is not None,
shorts_unapproved=len(ch.shorts) + ch.receipts_unapproved,
itp_open=ch.itp_open,
ncr_required=ch.ncr_required,
visual_open_wos=tuple(ch.visual_open_wos),
)
@@ -596,6 +638,7 @@ def _who_ids(row: Mapping[str, Any], ch: Children) -> list[Any]:
ids += [v["by"] for v in vs]
for rs in ch.receipts.values():
ids += [r["by"] for r in rs]
ids += [s["opened_by"] for s in ch.all_shorts] + [a["by"] for a in ch.approvals.values()]
return ids
@@ -823,6 +866,8 @@ def build(
out["server_now"] = now
out["target_hrs"] = target_hrs(ch.work_orders)
out["labor"] = labor_of(conn, row)
out["shorts_unapproved"] = state.shorts_unapproved
out["ncr_required"] = state.ncr_required
if include_children:
out["work_orders"] = [
{
@@ -848,6 +893,12 @@ def build(
]
out["pages"] = [_page(pg, ch, people) for pg in ch.pages]
out["bom"] = [_bom(b, ch, people) for b in ch.bom]
if not seal:
# C7: who approved each receipt. Not in the seal: seals made before C7 do not cover it.
for b in out["bom"]:
for r in (b["short"] or {}).get("receipts") or []:
r["approval"] = _approval(ch.approvals.get(int(r["id"])), people)
out["shorts"] = [_short(sh, ch, people) for sh in ch.all_shorts]
st = ch.open_stage
out["welds"] = [_weld(w, people, st) for w in ch.welds]
out["heat_tags"] = [_heat_tag(t, st) for t in ch.heat_tags]
@@ -1143,6 +1194,32 @@ def _bom(b: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]:
return out
def _approval(a: Mapping[str, Any] | None, people: People) -> dict[str, Any] | None:
if a is None:
return None
return {"at": a["at"], "by": people.ref(a["by"]), "note": a["note"], "dept_code": a["dept_code"]}
def _short(s: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]:
"""One short, open or closed, with each receipt: who received it and who approved it (C7)."""
keys = ("id", "item", "qty", "qty_open", "reason", "ref", "eta", "note", "opened_at", "closed_at")
out = {k: s[k] for k in keys}
out["opened_by"] = people.ref(s["opened_by"])
out["receipts"] = [
{
"id": r["id"],
"qty": r["qty"],
"heat": r["heat"],
"note": r["note"],
"at": r["at"],
"by": people.ref(r["by"]),
"approval": _approval(ch.approvals.get(int(r["id"])), people),
}
for r in ch.receipts.get(s["id"], [])
]
return out
WELD_KEYS = (
"weld_no",
"page",
@@ -1254,6 +1331,7 @@ SELECT p.id, p.version, p.unit_id, u.lot, pr.number || u.lot AS unit_number, pr.
pw.reason AS wh_reason, pw.note AS wh_note, pw.auto AS wh_auto, p.wh_sent_at, p.visual_rel,
bc.bom_total, bc.bom_pulled, bc.bom_missing_heat,
(SELECT count(*) FROM bom_shorts s WHERE s.packet_id = p.id AND s.closed_at IS NULL) AS open_shorts,
coalesce(ur.n, 0) AS receipts_unapproved, coalesce(itp.status::text <> 'complete', false) AS itp_open,
(SELECT count(*) FROM packet_work_orders wo WHERE wo.packet_id = p.id) AS wo_count,
(SELECT coalesce(json_agg(json_build_object('co_no', c.co_no, 'review', c.review,
'pending_depts', c.pending_depts) ORDER BY c.co_no), '[]')
@@ -1267,6 +1345,10 @@ LEFT JOIN users wu ON wu.id = p.welder_id
LEFT JOIN users vu ON vu.id = p.vclosed_by
LEFT JOIN packet_wh pw ON pw.packet_id = p.id AND pw.closed_at IS NULL
LEFT JOIN users whu ON whu.id = pw.by
LEFT JOIN itps itp ON itp.unit_id = p.unit_id
LEFT JOIN (SELECT r.packet_id, count(*) AS n FROM short_receipts r
WHERE NOT EXISTS (SELECT 1 FROM short_approvals a WHERE a.receipt_id = r.id)
GROUP BY r.packet_id) ur ON ur.packet_id = p.id
LEFT JOIN LATERAL (
SELECT count(*) FILTER (WHERE NOT b.na) AS bom_total,
count(*) FILTER (WHERE NOT b.na AND (b.pulled OR EXISTS (SELECT 1 FROM bom_shorts s
@@ -1306,6 +1388,8 @@ def card_state(r: Mapping[str, Any], names: Mapping[str, str]) -> policy.PacketS
filler_set=True,
qc_missing=(),
has_qc_sent=r["status"] == "qc",
shorts_unapproved=int(r["open_shorts"] or 0) + int(r["receipts_unapproved"] or 0),
itp_open=bool(r["itp_open"]),
)
@@ -1407,6 +1491,7 @@ def list_cards(conn: Any, policy_principal: policy.Principal, filters: Mapping[s
"drawing_state": r["drawing_state"],
"open_cos": [c["co_no"] for c in r["open_co_rows"] or []],
"open_shorts": int(r["open_shorts"] or 0),
"shorts_unapproved": int(r["open_shorts"] or 0) + int(r["receipts_unapproved"] or 0),
"wh_stage": r["wh_stage"],
"wh": wh_card(r),
"wh_sent_at": r["wh_sent_at"],