Files
PacketStation/server/packetstation/domain/packets.py
T
spencermandClaude Fable 5.1 37d9a0cb50 Phase 2: core domain, policy engine, packet API, Clerical screen
Migration 0003: projects, aliases, renumbers, units, packets and every child table,
change orders, assets, documents, ITPs, station limits, department weld flag.
Policy package: Principal, PacketState, Action rules, custody, preconditions,
CO hold, order of checks, can/blocked/holder on reads. docs/POLICY.md with the
tightenings table for Seth. Write pipeline: FOR UPDATE, CAS on status and dept,
If-Match row versions, server-assigned numbers, field CAS, version bump, packet
log, audit, NOTIFY, signatures, IT reasons, filed lock and HMAC seal.
API: /api/packets (header, transitions, work orders, pages, docs, move, remove,
BOM, shorts, welds, heat tags, heat table, stage, checklist, clock, warehouse
stage, Q.C., change orders, log, seal), /api/projects, /api/units, /api/assets,
/_blob. Frontend: Clerical home, projects and units, packet page with pdf.js
and QR codes, hand draft, heat map, send to warehouse, department or Q.C.
Tests: policy matrix, API, concurrency, schema, projects; Playwright Clerical
draft scenario. Verified on docker-1 with the CI stack and the Samba DC.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-16 09:31:45 -06:00

788 lines
31 KiB
Python

"""The packet read model, the policy's PacketState, and the board cards.
`read_packet` is the full answer of GET /api/packets/{id} and of every packet mutation. `packet_state` builds the
policy's view from the same child rows, so a write and a read decide from the same facts. `list_cards` serves the
boards from header columns, v_open_cos and the open short count only.
"""
from __future__ import annotations
import uuid
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field
from typing import Any
from psycopg.rows import dict_row
from .. import policy
from ..api import deps
from ..api.errors import ApiError
from . import checklists
HAND_FLAG = "Made by hand - check it and send it on"
DRAWING_NEEDED_FLAG = "Drawing needed - looking it up in the vault"
DRAWING_STATES_ATTACHED = ("attached", "hand")
# Keys left out of the sealed content: they change without the packet changing (who can do what, the counter,
# the seal itself, the data book export date) or come from code and AD rather than the packet (checklist texts,
# the flag, the unit and project names, display names).
SEAL_EXCLUDE = (
"can",
"blocked",
"holder",
"version",
"log",
"seal",
"databook_at",
"checklist_items",
"qc_items",
"flag",
)
# ---------------------------------------------------------------- small helpers
def rows(conn: Any, query: str, params: Any = ()) -> list[dict[str, Any]]:
with conn.cursor(row_factory=dict_row) as cur:
return list(cur.execute(query, params).fetchall())
def one(conn: Any, query: str, params: Any = ()) -> dict[str, Any] | None:
with conn.cursor(row_factory=dict_row) as cur:
r = cur.execute(query, params).fetchone()
return dict(r) if r is not None else None
def uid_text(value: Any) -> str | None:
return None if value is None else str(value)
def load_row(conn: Any, packet_id: int, *, lock: bool = False) -> dict[str, Any] | None:
"""The packets row, or None. With lock=True it is taken FOR UPDATE (inside a transaction)."""
sql = "SELECT *, status::text AS status, source_kind::text AS source_kind, routed_to::text AS routed_to,"
sql += " drawing_state::text AS drawing_state FROM packets WHERE id = %s"
if lock:
sql += " FOR UPDATE"
return one(conn, sql, (packet_id,))
def flag_of(status: str, source_kind: str, flags: Iterable[str] | None, drawing_state: str | None = None) -> str | None:
"""The draft flag shown on cards and the packet page. The old packetFlag(), phase 2 part."""
if status != "draft":
return None
if source_kind == "hand":
return HAND_FLAG
if "drawing_needed" in (flags or ()):
return DRAWING_NEEDED_FLAG
return None
# SQL for the same rule, for the list filter.
FLAG_SQL = "(p.status = 'draft' AND (p.source_kind = 'hand' OR 'drawing_needed' = ANY(p.flags)))"
def policy_principal_for(conn: Any, p: deps.Principal) -> policy.Principal:
"""The policy Principal for a session: its roles, the user's station limits and the session kind."""
limits = [r[0] for r in conn.execute("SELECT station_id FROM user_station_limits WHERE user_id = %s", (p.user_id,))]
return policy.principal_from_roles(str(p.user_id), p.roles, station_limits=limits, session_kind=p.kind)
def co_hold_why(open_cos: Iterable[Mapping[str, Any]], dept_names: Mapping[str, str]) -> str:
parts: list[str] = []
for co in open_cos:
if str(co.get("review")) == "pending":
parts.append(f"Change order {co['co_no']} is on hold.")
else:
names = ", ".join(dept_names.get(c, c) for c in co.get("pending_depts") or [])
parts.append(f"Change order {co['co_no']} is waiting for verification by {names}.")
return " ".join(parts)
def dept_names(conn: Any) -> dict[str, str]:
return {r[0]: r[1] for r in conn.execute("SELECT code, name FROM departments")}
# ---------------------------------------------------------------- the child rows
@dataclass
class Children:
dept: dict[str, Any]
station: dict[str, Any] | None
work_orders: list[dict[str, Any]] = field(default_factory=list)
pages: list[dict[str, Any]] = field(default_factory=list)
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
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)
stages: list[dict[str, Any]] = field(default_factory=list)
wh: dict[str, Any] | None = None
qc: dict[str, Any] | None = None
qc_history: list[dict[str, Any]] = field(default_factory=list)
change_orders: list[dict[str, Any]] = field(default_factory=list)
co_items: dict[int, list[dict[str, Any]]] = field(default_factory=dict)
co_required: dict[int, list[str]] = field(default_factory=dict)
co_verified: dict[int, list[dict[str, Any]]] = field(default_factory=dict)
open_cos: list[dict[str, Any]] = field(default_factory=list)
docs: list[dict[str, Any]] = field(default_factory=list)
seal: dict[str, Any] | None = None
dept_names: dict[str, str] = field(default_factory=dict)
@property
def open_stage(self) -> dict[str, Any] | None:
return next((s for s in self.stages if s["closed_at"] is None), None)
def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Children:
"""Every child row the read model and the policy need. full=False skips what only the read model shows."""
pid = row["id"]
dept = one(conn, "SELECT code, name, weld FROM departments WHERE code = %s", (row["dept_code"],)) or {
"code": row["dept_code"],
"name": row["dept_code"],
"weld": False,
}
station = (
one(conn, "SELECT id, name FROM stations WHERE id = %s", (row["station_id"],)) if row["station_id"] else None
)
ch = Children(dept=dept, station=station)
ch.dept_names = dept_names(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,))
shorts = rows(conn, "SELECT * FROM bom_shorts WHERE packet_id = %s AND closed_at IS NULL", (pid,))
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,))
ch.qc = one(conn, "SELECT * FROM qc_inspections WHERE packet_id = %s AND closed_at IS NULL", (pid,))
ch.open_cos = rows(
conn,
"SELECT co_id, co_no, review::text AS review, hold, pending_depts FROM v_open_cos WHERE packet_id = %s"
" ORDER BY co_no",
(pid,),
)
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.receipts.setdefault(r["short_id"], []).append(r)
ch.heat_tags = rows(
conn, "SELECT * FROM heat_tags WHERE packet_id = %s AND removed_at IS NULL ORDER BY tag_no", (pid,)
)
ch.heat_table = rows(conn, "SELECT * FROM heat_table_placements WHERE packet_id = %s ORDER BY page", (pid,))
ch.wh = one(
conn, "SELECT *, stage::text AS stage FROM packet_wh WHERE packet_id = %s AND closed_at IS NULL", (pid,)
)
ch.qc_history = rows(conn, "SELECT * FROM qc_history WHERE packet_id = %s ORDER BY id", (pid,))
ch.change_orders = rows(
conn,
"SELECT *, origin::text AS origin, review::text AS review FROM change_orders WHERE packet_id = %s"
" ORDER BY co_no",
(pid,),
)
co_ids = [c["id"] for c in ch.change_orders]
if co_ids:
for r in rows(conn, "SELECT * FROM co_items WHERE co_id = ANY(%s) ORDER BY id", (co_ids,)):
ch.co_items.setdefault(r["co_id"], []).append(r)
for r in rows(
conn, "SELECT co_id, dept_code FROM co_required_depts WHERE co_id = ANY(%s) ORDER BY dept_code", (co_ids,)
):
ch.co_required.setdefault(r["co_id"], []).append(r["dept_code"])
for r in rows(conn, "SELECT * FROM co_verifications WHERE co_id = ANY(%s) ORDER BY at", (co_ids,)):
ch.co_verified.setdefault(r["co_id"], []).append(r)
ch.docs = rows(
conn, "SELECT * FROM packet_docs WHERE packet_id = %s AND removed_at IS NULL ORDER BY added_at, id", (pid,)
)
ch.seal = one(conn, "SELECT * FROM packet_seals WHERE packet_id = %s ORDER BY id DESC LIMIT 1", (pid,))
return ch
def state_from(row: Mapping[str, Any], ch: Children) -> policy.PacketState:
weld = bool(ch.dept.get("weld"))
live_bom = [b for b in ch.bom if not b["na"]]
stage = ch.open_stage
return policy.PacketState(
id=int(row["id"]),
status=str(row["status"]),
dept=str(row["dept_code"]),
station_id=row["station_id"],
welder_id=uid_text(row["welder_id"]),
wh_skipped=bool(row["wh_skipped"]),
routed_to=row["routed_to"],
vclosed=row["vclosed_at"] is not None,
removed=row["removed_at"] is not None,
source_kind=str(row["source_kind"]),
weld_dept=weld,
co_hold=bool(ch.open_cos),
co_hold_why=co_hold_why(ch.open_cos, ch.dept_names),
open_shorts=len(ch.shorts),
drawing_attached=any(pg["type"] == "drawing" and pg["void_at"] is None for pg in ch.pages),
heat_map=row["heat_map"],
bom_total=len(live_bom),
bom_ready=sum(1 for b in live_bom if b["pulled"] or b["id"] in ch.shorts),
welds=len(ch.welds),
checklist_complete=stage is not None and checklists.checklist_complete(stage["checklist"], weld),
filler_set=stage is not None and bool((stage["filler"] or "").strip()),
qc_missing=tuple(checklists.qc_missing(row, ch.qc, ch.welds)),
has_qc_sent=ch.qc is not None,
)
def packet_state(conn: Any, row: Mapping[str, Any]) -> policy.PacketState:
"""The policy's view of a packet row, from its child rows."""
return state_from(row, load_children(conn, row, full=False))
def holder_text(state: policy.PacketState, ch: Children) -> str:
return policy.holder_name(state, ch.dept.get("name"), (ch.station or {}).get("name"))
# ---------------------------------------------------------------- shaping
class People:
"""Display names for who references. In seal mode a reference is the id only."""
def __init__(self, conn: Any, ids: Iterable[Any], *, seal: bool = False):
self.seal = seal
want = sorted({str(i) for i in ids if i is not None})
self.names: dict[str, str] = {}
if want and not seal:
for r in conn.execute(
"SELECT id, display_name FROM users WHERE id = ANY(%s)", ([uuid.UUID(i) for i in want],)
):
self.names[str(r[0])] = r[1]
def ref(self, uid: Any) -> dict[str, Any] | None:
if uid is None:
return None
if self.seal:
return {"id": str(uid)}
return {"id": str(uid), "display_name": self.names.get(str(uid))}
def _station_ref(conn: Any, station_id: Any, cache: dict[Any, dict[str, Any] | None]) -> dict[str, Any] | None:
if station_id is None:
return None
if station_id not in cache:
cache[station_id] = one(conn, "SELECT id, name FROM stations WHERE id = %s", (station_id,))
return cache[station_id]
def _stage(conn: Any, s: Mapping[str, Any], people: People, cache: dict[Any, Any], *, closed: bool) -> dict[str, Any]:
welder = people.ref(s["welder_id"])
if welder is not None:
welder["stamp"] = s["welder_stamp"]
out = {
"id": s["id"],
"dept_code": s["dept_code"],
"station": _station_ref(conn, s["station_id"], cache),
"welder": welder,
"checklist": s["checklist"] or {},
"filler": s["filler"],
"filler_lot": s["filler_lot"],
"preheat": s["preheat"],
"notes": s["notes"],
"clock": {
"elapsed_ms": int(s["clock_elapsed_ms"] or 0),
"running_since": s["clock_running_since"],
"started_at": s["clock_started_at"],
},
"opened_at": s["opened_at"],
"row_version": s["row_version"],
}
if closed:
out["closed_at"] = s["closed_at"]
return out
def _qc(q: Mapping[str, Any], people: People) -> dict[str, Any]:
keys = (
"id",
"sent_at",
"book",
"spool",
"location",
"sent_note",
"nde_method",
"nde_result",
"nde_report",
"hardness",
"nace",
"inspector",
"stamp",
"notes",
"result",
"result_at",
"result_note",
"row_version",
)
out = {k: q[k] for k in keys}
out["sent_by"] = people.ref(q["sent_by"])
out["result_by"] = people.ref(q["result_by"])
out["checks"] = q["checks"] or {}
out["weld_exam"] = q["weld_exam"] or {}
return out
def _who_ids(row: Mapping[str, Any], ch: Children) -> list[Any]:
ids: list[Any] = [row["welder_id"], row["for_sup_id"], row["vclosed_by"], row["removed_by"]]
ids += [w["welder_id"] for w in ch.welds]
ids += [s["welder_id"] for s in ch.stages]
if ch.wh:
ids.append(ch.wh["by"])
if ch.qc:
ids += [ch.qc["sent_by"], ch.qc["result_by"]]
ids += [d["added_by"] for d in ch.docs]
for c in ch.change_orders:
ids += [c["issued_by"], c["approved_by"], c["rejected_by"]]
for vs in ch.co_verified.values():
ids += [v["by"] for v in vs]
for rs in ch.receipts.values():
ids += [r["by"] for r in rs]
return ids
def _unit(conn: Any, unit_id: int, *, seal: bool) -> dict[str, Any]:
if seal:
return {"id": unit_id}
u = one(
conn,
"SELECT u.id, u.lot, u.name, u.number, u.due_effective, p.id AS project_id, p.number AS project_number,"
" p.name AS project_name, p.customer, p.po, p.status::text AS project_status"
" FROM v_units u JOIN projects p ON p.id = u.project_id WHERE u.id = %s",
(unit_id,),
)
if u is None:
return {"id": unit_id, "number": None, "lot": None, "name": None, "project": None, "due_effective": None}
return {
"id": u["id"],
"number": u["number"],
"lot": u["lot"],
"name": u["name"],
"project": {
"id": u["project_id"],
"number": u["project_number"],
"name": u["project_name"],
"customer": u["customer"],
"po": u["po"],
"status": u["project_status"],
},
"due_effective": u["due_effective"],
}
def build(
conn: Any,
row: Mapping[str, Any],
pp: policy.Principal | None,
*,
include_children: bool = True,
seal: bool = False,
) -> dict[str, Any]:
"""The read model of one loaded packets row. pp None (seal mode) leaves out can, blocked and holder."""
ch = load_children(conn, row, full=True)
state = state_from(row, ch)
people = People(conn, _who_ids(row, ch), seal=seal)
cache: dict[Any, Any] = {}
welder = people.ref(row["welder_id"])
if welder is not None:
welder["stamp"] = row["welder_stamp"]
weld = bool(ch.dept.get("weld"))
out: dict[str, Any] = {
"id": row["id"],
"version": row["version"],
"unit": _unit(conn, row["unit_id"], seal=seal),
"dept": {"code": ch.dept["code"], "name": ch.dept["name"], "weld": weld},
"op": row["op"],
"title": row["title"],
"customer": row["customer"],
"drawing": row["drawing"],
"rev": row["rev"],
"wps": row["wps"],
"status": row["status"],
"status_at": row["status_at"],
"station": ch.station,
"welder": welder,
"for_sup": people.ref(row["for_sup_id"]),
"heat_map": row["heat_map"],
"wh_skipped": row["wh_skipped"],
"wh_sent_at": row["wh_sent_at"],
"routed_to": row["routed_to"],
"return_note": row["return_note"],
"vclosed": None
if row["vclosed_at"] is None
else {"at": row["vclosed_at"], "by": people.ref(row["vclosed_by"])},
"databook_at": row["databook_at"],
"source_kind": row["source_kind"],
"source": row["source"] or {},
"drawing_state": row["drawing_state"],
"flags": list(row["flags"] or []),
"visual": row["visual"] or {},
"removed": None
if row["removed_at"] is None
else {"at": row["removed_at"], "by": people.ref(row["removed_by"]), "reason": row["removed_reason"]},
"open_cos": [c["co_no"] for c in ch.open_cos],
"open_shorts": len(ch.shorts),
}
if include_children:
out["work_orders"] = [
{
k: w[k]
for k in (
"id",
"wo_base",
"lot",
"split",
"sub",
"seq",
"qty",
"target_hrs",
"op",
"description",
"visual_status",
"gone_at",
"row_version",
)
}
for w in ch.work_orders
]
out["pages"] = [
{
"n": pg["n"],
"type": pg["type"],
"asset_id": uid_text(pg["asset_id"]),
"pn": pg["pn"],
"dwg": pg["dwg"],
"rev": pg["rev"],
"void": None
if pg["void_at"] is None
else {"co_id": pg["void_co_id"], "at": pg["void_at"], "new_n": pg["void_new_n"]},
"url": f"/_blob/{pg['asset_id']}" if pg["asset_id"] else None,
}
for pg in ch.pages
]
out["bom"] = [_bom(b, ch, people) for b in ch.bom]
out["welds"] = [_weld(w, people) for w in ch.welds]
out["heat_tags"] = [
{k: t[k] for k in ("tag_no", "page", "item", "fx", "fy", "row_version")} for t in ch.heat_tags
]
out["heat_table"] = [{k: h[k] for k in ("page", "x", "y", "s")} for h in ch.heat_table]
st = ch.open_stage
out["stage"] = _stage(conn, st, people, cache, closed=False) if st else None
out["stages"] = [_stage(conn, s, people, cache, closed=True) for s in ch.stages if s["closed_at"] is not None]
out["wh"] = (
None
if ch.wh is None
else {
"stage": ch.wh["stage"],
"by": people.ref(ch.wh["by"]),
"at": ch.wh["at"],
"reason": ch.wh["reason"],
"note": ch.wh["note"],
"auto": ch.wh["auto"],
}
)
out["qc"] = _qc(ch.qc, people) if ch.qc else None
out["qc_history"] = [
{k: h[k] for k in ("id", "at", "result", "inspector", "note", "dept_code")} for h in ch.qc_history
]
out["change_orders"] = [_co(c, ch, people) for c in ch.change_orders]
out["docs"] = [
{
"id": d["id"],
"asset_id": str(d["asset_id"]),
"kind": d["kind"],
"title": d["title"],
"added_at": d["added_at"],
"added_by": people.ref(d["added_by"]),
"url": f"/_blob/{d['asset_id']}",
}
for d in ch.docs
]
out["seal"] = (
None
if ch.seal is None
else {"sealed_at": ch.seal["sealed_at"], "key_id": ch.seal["key_id"], "broken_at": ch.seal["broken_at"]}
)
out["checklist_items"] = checklists.checklist_for(weld, row["drawing"], row["rev"], row["wps"])
out["qc_items"] = checklists.qc_items_for(row["drawing"], row["rev"], row["wps"], row["welder_stamp"])
out["flag"] = flag_of(row["status"], row["source_kind"], row["flags"], row["drawing_state"])
if pp is not None:
allowed, blocked = policy.can(pp, state)
out["can"] = [str(a) for a in allowed]
out["blocked"] = {str(a): r for a, r in blocked.items()}
out["holder"] = holder_text(state, ch)
if seal:
for k in SEAL_EXCLUDE:
out.pop(k, None)
return out
def _bom(b: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]:
keys = (
"id",
"item",
"qty",
"part_id",
"description",
"dwg_ref",
"cl_item",
"length",
"material",
"heat",
"na",
"issued",
"loc",
"pulled",
"src",
"row_version",
)
out = {k: b[k] for k in keys}
out["mtr_asset_id"] = uid_text(b["mtr_asset_id"])
s = ch.shorts.get(b["id"])
out["short"] = (
None
if s is None
else {
"id": s["id"],
"qty": s["qty"],
"qty_open": s["qty_open"],
"reason": s["reason"],
"ref": s["ref"],
"eta": s["eta"],
"note": s["note"],
"opened_at": s["opened_at"],
"receipts": [
{
"id": r["id"],
"qty": r["qty"],
"heat": r["heat"],
"note": r["note"],
"at": r["at"],
"by": people.ref(r["by"]),
}
for r in ch.receipts.get(s["id"], [])
],
"row_version": s["row_version"],
}
)
return out
def _weld(w: Mapping[str, Any], people: People) -> dict[str, Any]:
keys = ("weld_no", "page", "fx", "fy", "wo_base", "piece", "type", "size", "process", "stamp", "at", "row_version")
out = {k: w[k] for k in keys}
out["items"] = list(w["items"] or [])
out["welder"] = people.ref(w["welder_id"])
return out
def _co(c: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]:
keys = (
"id",
"co_no",
"origin",
"review",
"minor",
"hold",
"ecn",
"reason",
"issued_at",
"approved_at",
"rejected_at",
"rejected_reason",
"row_version",
)
out = {k: c[k] for k in keys}
out["issued_by"] = people.ref(c["issued_by"])
out["approved_by"] = people.ref(c["approved_by"])
out["rejected_by"] = people.ref(c["rejected_by"])
out["items"] = [
{k: i[k] for k in ("old_page", "old_dwg", "old_rev", "new_page", "new_dwg", "new_rev", "added")}
for i in ch.co_items.get(c["id"], [])
]
out["required"] = ch.co_required.get(c["id"], [])
out["verified"] = {
v["dept_code"]: {"by": people.ref(v["by"]), "at": v["at"], "note": v["note"]}
for v in ch.co_verified.get(c["id"], [])
}
return out
def read_packet(
conn: Any, packet_id: int, policy_principal: policy.Principal | None, *, include_children: bool = True
) -> dict[str, Any]:
"""The read model, or 404 not_found. A removed packet reads fine, with `removed` set."""
row = load_row(conn, packet_id)
if row is None:
raise ApiError(404, "not_found", "No such packet.")
return build(conn, row, policy_principal, include_children=include_children)
def seal_content(conn: Any, row: Mapping[str, Any]) -> dict[str, Any]:
"""What a filing seal covers: the read model without the volatile and derived keys (SEAL_EXCLUDE)."""
return build(conn, row, None, include_children=True, seal=True)
# ---------------------------------------------------------------- board cards
CARD_SQL = """
SELECT p.id, p.version, p.unit_id, u.lot, pr.number || u.lot AS unit_number, pr.id AS project_id,
pr.number AS project_number, pr.name AS project_name, p.dept_code, d.name AS dept_name, d.weld,
p.op, p.title, p.customer, p.drawing, p.rev, p.status::text AS status, p.status_at, p.station_id,
st.name AS station_name, p.welder_id, wu.display_name AS welder_name, p.welder_stamp,
p.routed_to::text AS routed_to, p.vclosed_at, p.vclosed_by, vu.display_name AS vclosed_name,
p.source_kind::text AS source_kind, p.flags, p.drawing_state::text AS drawing_state, p.wh_skipped,
p.heat_map, p.removed_at, coalesce(u.due, pr.due) AS due_effective, p.updated_at,
(SELECT w.stage::text FROM packet_wh w WHERE w.packet_id = p.id AND w.closed_at IS NULL) AS wh_stage,
(SELECT count(*) FROM bom_shorts s WHERE s.packet_id = p.id AND s.closed_at IS NULL) AS open_shorts,
(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), '[]')
FROM v_open_cos c WHERE c.packet_id = p.id) AS open_co_rows
FROM packets p
JOIN units u ON u.id = p.unit_id
JOIN projects pr ON pr.id = u.project_id
JOIN departments d ON d.code = p.dept_code
LEFT JOIN stations st ON st.id = p.station_id
LEFT JOIN users wu ON wu.id = p.welder_id
LEFT JOIN users vu ON vu.id = p.vclosed_by
"""
FLAG_ON = ("1", "true", "yes", "any", "flagged")
FLAG_OFF = ("0", "false", "no", "none")
def _like(word: str) -> str:
return "%" + word.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
def card_state(r: Mapping[str, Any], names: Mapping[str, str]) -> policy.PacketState:
"""PacketState from header columns and counts. Child-row facts the board does not load pass (the packet
page decides them): checklist, welds, filler, BOM pulled, Q.C. items."""
cos = r["open_co_rows"] or []
return policy.PacketState(
id=int(r["id"]),
status=str(r["status"]),
dept=str(r["dept_code"]),
station_id=r["station_id"],
welder_id=uid_text(r["welder_id"]),
wh_skipped=bool(r["wh_skipped"]),
routed_to=r["routed_to"],
vclosed=r["vclosed_at"] is not None,
removed=r["removed_at"] is not None,
source_kind=str(r["source_kind"]),
weld_dept=bool(r["weld"]),
co_hold=bool(cos),
co_hold_why=co_hold_why(cos, names),
open_shorts=int(r["open_shorts"] or 0),
drawing_attached=r["drawing_state"] in DRAWING_STATES_ATTACHED,
heat_map=r["heat_map"],
bom_total=0,
bom_ready=0,
welds=1,
checklist_complete=True,
filler_set=True,
qc_missing=(),
has_qc_sent=r["status"] == "qc",
)
def list_cards(conn: Any, policy_principal: policy.Principal, filters: Mapping[str, Any]) -> list[dict[str, Any]]:
"""The board cards. Filters: status (comma list), dept, unit_id, project_id, q, flag, limit, removed."""
where: list[str] = []
params: list[Any] = []
if not filters.get("removed"):
where.append("p.removed_at IS NULL")
status = [s.strip() for s in str(filters.get("status") or "").split(",") if s.strip()]
if status:
where.append("p.status::text = ANY(%s)")
params.append(status)
if filters.get("dept"):
where.append("p.dept_code = %s")
params.append(str(filters["dept"]))
if filters.get("unit_id") is not None:
where.append("p.unit_id = %s")
params.append(int(filters["unit_id"]))
if filters.get("project_id") is not None:
where.append("u.project_id = %s")
params.append(int(filters["project_id"]))
flag = str(filters.get("flag") or "").strip().lower()
if flag in FLAG_ON:
where.append(FLAG_SQL)
elif flag in FLAG_OFF:
where.append("NOT " + FLAG_SQL)
elif flag == "hand":
where.append("(p.status = 'draft' AND p.source_kind = 'hand')")
for word in str(filters.get("q") or "").split()[:8]:
pat = _like(word)
where.append(
"(pr.number ILIKE %s OR pr.name ILIKE %s OR (pr.number || u.lot) ILIKE %s OR u.lot ILIKE %s"
" OR p.drawing ILIKE %s OR p.customer ILIKE %s OR pr.customer ILIKE %s OR pr.po ILIKE %s"
" OR p.title ILIKE %s OR EXISTS (SELECT 1 FROM packet_work_orders wo WHERE wo.packet_id = p.id"
" AND wo.wo_base ILIKE %s))"
)
params += [pat] * 10
sql = CARD_SQL + (" WHERE " + " AND ".join(where) if where else "")
sql += " ORDER BY p.updated_at DESC, p.id DESC LIMIT %s"
params.append(int(filters.get("limit") or 500))
names = dept_names(conn)
transitions = {str(a) for a in policy.TRANSITIONS}
out: list[dict[str, Any]] = []
for r in rows(conn, sql, params):
state = card_state(r, names)
allowed, blocked = policy.can(policy_principal, state)
welder = (
None
if r["welder_id"] is None
else {"id": str(r["welder_id"]), "display_name": r["welder_name"], "stamp": r["welder_stamp"]}
)
out.append(
{
"id": r["id"],
"version": r["version"],
"unit": {"id": r["unit_id"], "number": r["unit_number"]},
"project": {"id": r["project_id"], "number": r["project_number"], "name": r["project_name"]},
"dept": {"code": r["dept_code"], "name": r["dept_name"]},
"op": r["op"],
"title": r["title"],
"customer": r["customer"],
"drawing": r["drawing"],
"rev": r["rev"],
"status": r["status"],
"status_at": r["status_at"],
"station": None if r["station_id"] is None else {"id": r["station_id"], "name": r["station_name"]},
"welder": welder,
"routed_to": r["routed_to"],
"vclosed": None
if r["vclosed_at"] is None
else {
"at": r["vclosed_at"],
"by": None
if r["vclosed_by"] is None
else {"id": str(r["vclosed_by"]), "display_name": r["vclosed_name"]},
},
"source_kind": r["source_kind"],
"flag": flag_of(r["status"], r["source_kind"], r["flags"], r["drawing_state"]),
"open_cos": [c["co_no"] for c in r["open_co_rows"] or []],
"open_shorts": int(r["open_shorts"] or 0),
"wh_stage": r["wh_stage"],
"due_effective": r["due_effective"],
"updated_at": r["updated_at"],
"holder": policy.holder_name(state, r["dept_name"], r["station_name"]),
"can": [str(a) for a in allowed if str(a) in transitions],
"blocked": {str(a): v for a, v in blocked.items() if str(a) in transitions},
"wo_count": int(r["wo_count"] or 0),
"removed": r["removed_at"] is not None,
}
)
return out