"""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