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:
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user