"""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 re 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 = "Made from VISUAL - drawing not attached yet" 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", "visual_state", "server_now", "target_hrs", "labor", "heat_src", "qc_last", "qc_folder_docs", "itp", "itp_hold", "seals", "vclose_check", "prints", ) # ---------------------------------------------------------------- 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,)) # Flags the drawing job owns; a hand drawing or an approved rev clears them. DRAWING_FLAGS = ("drawing_needed", "rev_mismatch", "not_released", "not_found", "lookup_failed", "bad_rev") # drawing_state values that mean the drawing still needs attention. DRAWING_STATES_FLAGGED = ("needed", "rev_mismatch", "not_released", "not_found", "lookup_failed") HAND_UPLOAD_FLAGS = ("hand_drawing", "hand_bom") # Flag kinds in display priority (the first one present gives the text). FLAG_PRIORITY = ( "hand_drawing", "hand_bom", "rev_mismatch", "not_released", "not_found", "lookup_failed", "bad_rev", "drawing_needed", "no_drawing", "wo_gone", "cancelled", ) FLAG_TEXTS = { "not_found": "Drawing not in M-Files - check the drawing number in VISUAL", "bad_rev": "VISUAL rev is not a number - drawing not attached", "lookup_failed": "M-Files lookup failed", "drawing_needed": DRAWING_NEEDED_FLAG, "no_drawing": "No drawing number in VISUAL - add it there or upload the drawing by hand", "wo_gone": "Work order no longer in VISUAL", "cancelled": "Work order cancelled in VISUAL", } # drawing_state -> flag kind, for a visual draft whose flags list does not name the problem. STATE_KINDS = { "needed": "drawing_needed", "rev_mismatch": "rev_mismatch", "not_released": "not_released", "not_found": "not_found", "lookup_failed": "lookup_failed", } RX_FLAG_KIND = re.compile(r"^[a-z_]{1,30}$") def _appr_note(visual: Mapping[str, Any] | None) -> str: appr = (visual or {}).get("appr") if isinstance(visual, Mapping) else None if isinstance(appr, Mapping): note = appr.get("note") if isinstance(note, str) and note.strip(): return note.strip() return "" def flag_of( status: str, source_kind: str, flags: Iterable[str] | None, drawing_state: str | None = None, visual: Mapping[str, Any] | None = None, ) -> str | None: """The draft flag shown on cards and the packet page: the old packetFlag(), handFlag() and revMismatch() texts. Only drafts carry a flag. A hand draft keeps the phase 2 text. A VISUAL draft shows its most important flag kind. """ if status != "draft": return None if source_kind == "hand": return HAND_FLAG kinds = list(flags or ()) if not kinds and drawing_state in STATE_KINDS: kinds = [STATE_KINDS[drawing_state]] if not kinds: return None dwg, bom = "hand_drawing" in kinds, "hand_bom" in kinds if dwg or bom: what = "Drawing and BOM" if dwg and bom else "Drawing" if dwg else "BOM" return f"{what} uploaded by hand - check it and send it on" note = _appr_note(visual) for kind in FLAG_PRIORITY: if kind not in kinds: continue if kind == "rev_mismatch": return note or "Rev mismatch: VISUAL and M-Files show different revs" if kind == "not_released": return note or "Not released in M-Files yet" if kind in ("lookup_failed", "bad_rev"): # The drawing job writes the whole text ("M-Files lookup failed: ...", "VISUAL rev A is not a number; # M-Files has rev 3 - not attached"). return note or FLAG_TEXTS[kind] return FLAG_TEXTS[kind] return "Flagged: " + ", ".join(k.replace("_", " ") for k in kinds) # SQL for the same rule, for the list filter. FLAG_SQL = "(p.status = 'draft' AND (p.source_kind = 'hand' OR cardinality(p.flags) > 0))" # A pending change order from the feed (VISUAL or M-Files): the "Changes to approve" board. CO_PENDING_SQL = ( "EXISTS (SELECT 1 FROM change_orders c WHERE c.packet_id = p.id AND c.review = 'pending'" " AND c.origin IN ('visual', 'drawing'))" ) 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_label(co_no: Any) -> str: """The change order id shown everywhere: CO-2.""" return f"CO-{co_no}" def co_hold_why(open_cos: Iterable[Mapping[str, Any]], labels: Mapping[str, str]) -> str: """The CO hold text, one sentence per open change order. `labels` maps a department code to its display name (dept_labels: "03 Weld Parts", "Warehouse").""" parts: list[str] = [] for co in open_cos: label = co_label(co["co_no"]) if str(co.get("review")) == "pending": parts.append( f"{label}: Clerical is reviewing a change from VISUAL. Held until Clerical approves or rejects it." ) else: names = ", ".join(labels.get(c, c) for c in co.get("pending_depts") or []) parts.append(f"Change order {label} must be verified first 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")} def dept_label(code: str, name: str | None, kind: str | None) -> str: """A department in texts: a shop department as code and name ("03 Weld Parts"), a support one by name.""" if kind == "shop": return f"{code} {name or ''}".strip() return name or code def dept_labels(conn: Any) -> dict[str, str]: return {r[0]: dept_label(r[0], r[1], r[2]) for r in conn.execute("SELECT code, name, kind::text 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) dept_labels: dict[str, str] = field(default_factory=dict) templates: dict[str, list[dict[str, str]]] = field(default_factory=dict) qc_items: list[dict[str, str]] = field(default_factory=list) @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) 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,)) 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) # Every document, the ones taken out of the data book too (a removed row is a record). ch.docs = rows( conn, "SELECT d.*, a.original_name AS file_name, a.size AS file_size FROM packet_docs d" " JOIN assets a ON a.id = d.asset_id WHERE d.packet_id = %s ORDER BY d.added_at, d.id", (pid,), ) ch.seal = one(conn, "SELECT * FROM packet_seals WHERE packet_id = %s ORDER BY id DESC LIMIT 1", (pid,)) return ch 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" def needs_heat(line: Mapping[str, Any]) -> bool: """The old needsHeat for a pulled line: not N/A, not a sub-assembly, pieces issued and no heat.""" if line.get("na") or is_sub(line) or not line.get("pulled"): return False if str(line.get("heat") or "").strip(): return False try: return float(line.get("issued") or 0) > 0 except (TypeError, ValueError): return False def dept_welds(row: Mapping[str, Any], ch: Children) -> int: """The live welds of the packet's current department. A weld row without a department (written outside the API) counts for the current one. Welds an earlier department logged are carried, not counted (the old missing()).""" dept = str(row["dept_code"]) return sum(1 for w in ch.welds if w.get("dept_code") in (None, dept)) 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_labels), 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), bom_missing_heat=sum(1 for b in live_bom if needs_heat(b)), welds=dept_welds(row, ch), checklist_complete=stage is not None and checklists.checklist_complete(stage["checklist"], weld, ch.templates or None), 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, ) 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 name(self, uid: Any) -> str | None: return None if uid is None else self.names.get(str(uid)) def as_uuid(value: Any) -> uuid.UUID | None: """A uuid from stored text (a checklist entry's `by`), or None when it is not one.""" if value is None: return None try: return uuid.UUID(str(value)) except ValueError: return None 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 clock_of(s: Mapping[str, Any], now: Any) -> dict[str, Any]: """A stage clock. elapsed_ms is live: the stored time plus the running span up to the database now. Only the server writes the clock columns; the client adds the time since its fetch for the ticking display only.""" stored = int(s["clock_elapsed_ms"] or 0) since = s["clock_running_since"] running = since is not None and s.get("closed_at") is None live = stored if running and now is not None: live += max(0, int((now - since).total_seconds() * 1000)) return { "elapsed_ms": live, "stored_ms": stored, "running": running, "running_since": since, "started_at": s["clock_started_at"], } def _checklist(value: Any, people: People) -> dict[str, Any]: out: dict[str, Any] = {} for k, v in (value or {}).items(): if isinstance(v, Mapping): entry = dict(v) if entry.get("by") is not None: entry["by_name"] = people.name(as_uuid(entry["by"])) out[k] = entry else: out[k] = v return out def _stage( conn: Any, s: Mapping[str, Any], people: People, cache: dict[Any, Any], *, closed: bool, now: Any = None ) -> 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": _checklist(s["checklist"], people), "filler": s["filler"], "filler_lot": s["filler_lot"], "preheat": s["preheat"], "notes": s["notes"], "clock": clock_of(s, now), "opened_at": s["opened_at"], "row_version": s["row_version"], } if closed: out["closed_at"] = s["closed_at"] return out RX_NACE = re.compile(r"NACE", re.IGNORECASE) def _qc(q: Mapping[str, Any], people: People, row: Mapping[str, Any], missing: list[str]) -> 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", "ncr", "row_version", ) out = {k: q.get(k) for k in keys} out["sent_by"] = people.ref(q["sent_by"]) out["result_by"] = people.ref(q["result_by"]) out["checks"] = _checklist(q["checks"], people) # entries gain by_name out["weld_exam"] = _checklist(q["weld_exam"], people) out["nace_label"] = bool(RX_NACE.search(str(row.get("wps") or ""))) out["missing"] = missing return out def _entry_ids(value: Any) -> list[Any]: return [as_uuid(v.get("by")) for v in (value or {}).values() if isinstance(v, Mapping)] def qc_history_row(h: Mapping[str, Any], people: People) -> dict[str, Any]: """A closed inspection: the qc_history row with the parts of its snapshot the screens show. `weld_exam` keeps each weld's result and note so the screens can show the defect next to a rejected weld.""" from .qc import rejected_welds snap = h.get("snapshot") if not isinstance(snap, Mapping): snap = {} raw = snap.get("weld_exam") exam: Mapping[str, Any] = raw if isinstance(raw, Mapping) else {} return { "id": h["id"], "at": h["at"], "result": h["result"], "inspector": h["inspector"], "note": h["note"], "dept_code": h["dept_code"], "ncr": snap.get("ncr"), "rejected_welds": rejected_welds(exam), "weld_exam": { str(k): {"result": v.get("result"), "note": v.get("note")} for k, v in exam.items() if isinstance(v, Mapping) }, "by": people.ref(as_uuid(snap.get("result_by"))), "location": snap.get("location"), "sent_at": snap.get("sent_at"), } # Keys phase 6 added to qc_history rows. Seals made before phase 6 do not cover them, so the seal leaves them out. SEAL_QC_HISTORY = ("ncr", "rejected_welds", "weld_exam", "by", "location", "sent_at") 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] + [w["by"] for w in ch.welds] ids += [s["welder_id"] for s in ch.stages] for s in ch.stages: for v in (s["checklist"] or {}).values(): if isinstance(v, Mapping): ids.append(as_uuid(v.get("by"))) ids += [h["by"] for h in ch.heat_table] + [pg["void_by"] for pg in ch.pages] if ch.wh: ids.append(ch.wh["by"]) if ch.qc: ids += [ch.qc["sent_by"], ch.qc["result_by"]] ids += _entry_ids(ch.qc["checks"]) + _entry_ids(ch.qc["weld_exam"]) for h in ch.qc_history: snap = h.get("snapshot") if isinstance(snap, Mapping): ids.append(as_uuid(snap.get("result_by"))) ids += [d["added_by"] for d in ch.docs] + [d["removed_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 _visual(conn: Any, row: Mapping[str, Any], *, seal: bool) -> dict[str, Any]: """The packet's visual jsonb. On a VISUAL draft (outside seal mode) it gains `staged_rev`: the M-Files copy the drawing job staged for this drawing ({rev, name}), or None. It changes without the packet changing, so the seal leaves it out.""" out = dict(row["visual"] or {}) if seal or row["source_kind"] != "visual" or row["status"] != "draft": return out out["staged_rev"] = staged_rev(conn, row["drawing"]) return out def staged_rev(conn: Any, drawing: Any) -> dict[str, Any] | None: lk = staged_lookup(conn, drawing) if lk is None: return None return {"rev": lk["rev"], "name": lk["name"]} def staged_lookup(conn: Any, drawing: Any) -> dict[str, Any] | None: """The drawing_lookups row for a drawing number when it holds a fetched copy (asset and rev set).""" dwg = str(drawing or "").strip() if not dwg: return None return one( conn, "SELECT * FROM drawing_lookups WHERE dwg = %s AND asset_id IS NOT NULL AND rev IS NOT NULL", (dwg,), ) def visual_state(conn: Any, row: Mapping[str, Any]) -> dict[str, Any] | None: """What the feed last observed for a VISUAL packet: leg statuses, ticket hours, the chosen order, and the closed, cancelled and gone times. None for a hand packet.""" if row["source_kind"] != "visual": return None vs = one( conn, "SELECT legs, ticket_hours, order_ref, observed_at FROM packet_visual_state WHERE packet_id = %s", (row["id"],), ) return { "legs": (vs or {}).get("legs") or [], "ticket_hours": (vs or {}).get("ticket_hours"), "order_ref": (vs or {}).get("order_ref"), "observed_at": (vs or {}).get("observed_at"), "closed_at": row.get("visual_closed_at"), "cancelled_at": row.get("visual_cancelled_at"), "wo_gone_at": row.get("wo_gone_at"), } # Leg status codes the feed records that mean the work order is no longer open in VISUAL: closed and cancelled. LEG_DONE = ("C", "X") def open_wos(vs: Mapping[str, Any] | None) -> list[str]: """The work orders the feed last saw open in VISUAL, as "wo_base/lot" text, in leg order without repeats. A leg is open when the feed recorded a status for it and that status is not closed (C) or cancelled (X). A leg with no recorded status is left out: the feed has not seen it. The IDs stay text exactly as stored.""" out: list[str] = [] for leg in (vs or {}).get("legs") or []: if not isinstance(leg, Mapping): continue st = str(leg.get("st") or leg.get("status") or "").strip().upper() if not st or st in LEG_DONE: continue parts = str(leg.get("key") or "").split("/") if len(parts) >= 4: base, lot = "/".join(parts[:-3]), parts[-3] else: base, lot = parts[0], (parts[1] if len(parts) > 1 else "") if not base: continue text = f"{base}/{lot}" if lot else base if text not in out: out.append(text) return out def vclose_check(row: Mapping[str, Any], vs: Mapping[str, Any] | None) -> dict[str, Any] | None: """What the feed says about closing the job in VISUAL, next to the Clerical confirm. None for a hand packet. `agrees` is true when the Clerical confirm (vclosed) and the feed's closed time are both set or both unset.""" if vs is None: return None closed_at = vs.get("closed_at") return { "observed_at": vs.get("observed_at"), "closed_at": closed_at, "open_wos": open_wos(vs), "agrees": (row.get("vclosed_at") is not None) == (closed_at is not None), } def packet_open_wos(conn: Any, row: Mapping[str, Any]) -> list[str]: """The open work orders of `vclose_check` for one packets row (empty for a hand packet).""" return open_wos(visual_state(conn, row)) def seal_count(conn: Any, packet_id: int) -> int: r = conn.execute("SELECT count(*) FROM packet_seals WHERE packet_id = %s", (packet_id,)).fetchone() return int(r[0]) if r else 0 def seal_view(conn: Any, s: Mapping[str, Any] | None) -> dict[str, Any] | None: """The newest seal row, broken or not, with who sealed it and the data book filed with it.""" if s is None: return None people = People(conn, [s["sealed_by"], s["broken_by"]]) book: dict[str, Any] | None = None aid = s.get("databook_asset_id") if aid is not None: a = one(conn, "SELECT id, size, original_name FROM assets WHERE id = %s", (aid,)) book = { "asset_id": str(aid), "url": f"/_blob/{aid}", "download_url": f"/_blob/{aid}?download=1", "pages": s.get("databook_pages"), "size": None if a is None else a["size"], "file_name": None if a is None else a["original_name"], } wos = s.get("open_wos") return { "id": s["id"], "sealed_at": s["sealed_at"], "sealed_by": people.ref(s["sealed_by"]), "key_id": s["key_id"], "signed": s["signature_id"] is not None, "broken_at": s["broken_at"], "broken_by": people.ref(s["broken_by"]), "broken_reason": s["broken_reason"], "open_wos": None if wos is None else list(wos), "databook": book, } 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] = {} now = None if seal else conn.execute("SELECT now()").fetchone()[0] 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": _visual(conn, row, seal=seal), "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 not seal: out["server_now"] = now out["target_hrs"] = target_hrs(ch.work_orders) out["labor"] = labor_of(conn, row) 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", ) } | {k: w.get(k) for k in ("dwg", "rev", "partspecs")} for w in ch.work_orders ] out["pages"] = [_page(pg, ch, people) for pg in ch.pages] out["bom"] = [_bom(b, ch, people) for b in ch.bom] 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] out["heat_table"] = [ {k: h[k] for k in ("page", "x", "y", "s", "carried_from", "at")} | {"by": people.ref(h["by"])} for h in ch.heat_table ] out["stage"] = _stage(conn, st, people, cache, closed=False, now=now) if st else None out["stages"] = [ _stage(conn, s, people, cache, closed=True, now=now) 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, row, list(state.qc_missing)) if ch.qc else None out["qc_history"] = [qc_history_row(h, people) for h in ch.qc_history] out["qc_last"] = out["qc_history"][-1] if out["qc_history"] else None out["change_orders"] = [_co(c, ch, people) for c in ch.change_orders] out["docs"] = [_doc(d, people, seal=seal) for d in ch.docs] if not seal: out["seal"] = seal_view(conn, ch.seal) out["seals"] = seal_count(conn, row["id"]) if ch.seal is not None else 0 out["checklist_items"] = checklists.checklist_for(weld, row["drawing"], row["rev"], row["wps"], ch.templates) out["qc_items"] = qc_items_view(row, ch) out["flag"] = flag_of(row["status"], row["source_kind"], row["flags"], row["drawing_state"], row["visual"]) if not seal: out["visual_state"] = visual_state(conn, row) out["vclose_check"] = vclose_check(row, out["visual_state"]) if include_children: from . import shop # shop builds on this module out["heat_src"] = shop.heat_src(conn, row, ch) out.update(qc_extras(conn, row)) 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) # `sub` joined the BOM lines in phase 4; seals made before that do not cover it. for b in out.get("bom") or []: b.pop("sub", None) _seal_strip(out) return out # Keys phase 5 added inside the child rows. Seals made before phase 5 do not cover them, and they are derived # (carried, labels, pending lists) or volatile (the live clock), so the seal leaves them out. SEAL_PHASE5 = { "welds": ("bfx", "bfy", "dept_code", "stage_id", "carried", "by"), "heat_tags": ("bfx", "bfy", "carried", "carried_from"), "heat_table": ("carried_from", "by", "at"), "change_orders": ("label", "pending_depts", "open"), } SEAL_CLOCK = ("stored_ms", "running") SEAL_VOID = ("co_no", "by", "new") def _seal_strip(out: dict[str, Any]) -> None: for key, names in SEAL_PHASE5.items(): for r in out.get(key) or []: for n in names: r.pop(n, None) for h in out.get("qc_history") or []: for n in SEAL_QC_HISTORY: h.pop(n, None) for co in out.get("change_orders") or []: for v in (co.get("verified") or {}).values(): v.pop("signed", None) for pg in out.get("pages") or []: if pg.get("void"): for n in SEAL_VOID: pg["void"].pop(n, None) for st in [out.get("stage"), *(out.get("stages") or [])]: if not st: continue for n in SEAL_CLOCK: st["clock"].pop(n, None) for v in (st.get("checklist") or {}).values(): if isinstance(v, dict): v.pop("by_name", None) def _doc(d: Mapping[str, Any], people: People, *, seal: bool) -> dict[str, Any]: """A packet document. Seal mode keeps the phase 2 keys, plus the note and the take-out when they are set, so a seal made before phase 6 still verifies for a packet whose documents were never taken out.""" from .qc import DOC_KINDS removed = ( None if d["removed_at"] is None else {"at": d["removed_at"], "by": people.ref(d["removed_by"]), "reason": d["removed_reason"]} ) out: dict[str, Any] = { "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']}", } if seal: if d.get("note"): out["note"] = d["note"] if removed is not None: out["removed"] = removed return out out["kind_label"] = DOC_KINDS.get(str(d["kind"]), str(d["kind"])) out["note"] = d.get("note") out["file_name"] = d.get("file_name") out["size"] = d.get("file_size") out["removed"] = removed return out def qc_items_view(row: Mapping[str, Any], ch: Children) -> list[dict[str, Any]]: """The Q.C. items with the old qcTab hints: heats and MTRs over the BOM lines, the welds on the map, and the open (or last) stage's checklist progress with its filler.""" items = checklists.qc_items_for(row["drawing"], row["rev"], row["wps"], row["welder_stamp"], ch.qc_items or None) nb = len(ch.bom) heats = sum(1 for b in ch.bom if str(b.get("heat") or "").strip() or b.get("na")) mtrs = sum(1 for b in ch.bom if b.get("mtr_asset_id")) stage = ch.open_stage or (ch.stages[-1] if ch.stages else None) keys = checklists.checklist_keys(bool(ch.dept.get("weld")), ch.templates or None) ck = (stage or {}).get("checklist") or {} done = sum(1 for k in keys if checklists.is_done(ck.get(k))) filler = str((stage or {}).get("filler") or "").strip() hints = { "db_heats": f"{heats}/{nb} recorded", "db_mtr": f"{mtrs}/{nb} MTRs attached", "db_weldmap": f"{len(ch.welds)} welds on map", "db_ck": f"checklist {done}/{len(keys)}" + (f" ยท filler {filler}" if filler else ""), } return [{**i, "hint": hints.get(i["key"])} for i in items] def qc_extras(conn: Any, row: Mapping[str, Any]) -> dict[str, Any]: """The unit's Q.C. folder documents, its ITP summary and the packet's ITP hold warning. Outside the seal.""" from . import itp, qcdocs unit_id = int(row["unit_id"]) r = conn.execute("SELECT project_id FROM units WHERE id = %s", (unit_id,)).fetchone() found = itp.unit_itps(conn, [unit_id]).get(unit_id) return { "qc_folder_docs": qcdocs.packet_folder_docs(conn, unit_id, int(r[0]) if r else None), "itp": itp.summary(found[0], found[1]) if found else None, "itp_hold": itp.holds_for_packet(found[0], found[1], row["dept_code"]) if found else None, } def target_hrs(work_orders: Iterable[Mapping[str, Any]]) -> float: """The packet's target hours: the sum of its work orders' target hours (0 when none).""" total = 0.0 for w in work_orders: try: total += float(w.get("target_hrs") or 0) except (TypeError, ValueError): continue return round(total, 3) LABOR_SQL = """ SELECT l.wo_base, l.lot, l.split, l.sub, l.seq, l.hours, l.tickets, l.last_ticket, l.observed_at FROM packet_work_orders w JOIN visual_labor l ON l.wo_base = w.wo_base AND l.lot = w.lot AND l.split = w.split AND l.sub = w.sub AND l.seq = w.seq WHERE w.packet_id = %s ORDER BY w.sort, w.id """ def labor_of(conn: Any, row: Mapping[str, Any]) -> dict[str, Any] | None: """VISUAL's labor ticket hours on the packet's work orders, or None for a hand packet or when nothing matches. The station timer stays the clock of record; these hours are shown beside it.""" if row["source_kind"] != "visual": return None found = rows(conn, LABOR_SQL, (row["id"],)) if not found: return None per = [ { "wo_base": r["wo_base"], "lot": r["lot"], "split": r["split"], "sub": r["sub"], "seq": r["seq"], "hours": float(r["hours"] or 0), "tickets": int(r["tickets"] or 0), "last_ticket": r["last_ticket"], } for r in found ] return { "hours": round(sum(x["hours"] for x in per), 3), "tickets": sum(x["tickets"] for x in per), "per_wo": per, "observed_at": max(r["observed_at"] for r in found), } def _page(pg: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]: void: dict[str, Any] | None = None if pg["void_at"] is not None: co_no = next((c["co_no"] for c in ch.change_orders if c["id"] == pg["void_co_id"]), None) new = next((x for x in ch.pages if pg["void_new_n"] is not None and x["n"] == pg["void_new_n"]), None) void = { "co_id": pg["void_co_id"], "at": pg["void_at"], "new_n": pg["void_new_n"], "co_no": co_no, "by": people.ref(pg["void_by"]), "new": None if new is None else {"n": new["n"], "dwg": new["dwg"], "rev": new["rev"]}, } return { "n": pg["n"], "type": pg["type"], "asset_id": uid_text(pg["asset_id"]), "pn": pg["pn"], "dwg": pg["dwg"], "rev": pg["rev"], "void": void, "url": f"/_blob/{pg['asset_id']}" if pg["asset_id"] else None, } def _heat_tag(t: Mapping[str, Any], open_stage: Mapping[str, Any] | None) -> dict[str, Any]: out = {k: t[k] for k in ("tag_no", "page", "item", "fx", "fy", "bfx", "bfy", "carried_from", "row_version")} out["carried"] = open_stage is not None and t["at"] is not None and t["at"] < open_stage["opened_at"] 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", "sub", "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 WELD_KEYS = ( "weld_no", "page", "fx", "fy", "bfx", "bfy", "wo_base", "piece", "type", "size", "process", "stamp", "dept_code", "stage_id", "at", "row_version", ) def _weld(w: Mapping[str, Any], people: People, open_stage: Mapping[str, Any] | None = None) -> dict[str, Any]: out = {k: w[k] for k in WELD_KEYS} out["items"] = list(w["items"] or []) out["welder"] = people.ref(w["welder_id"]) out["by"] = people.ref(w["by"]) open_id = open_stage["id"] if open_stage else None out["carried"] = w["stage_id"] is not None and w["stage_id"] != open_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", "vis", "waiting", "staged", "target_fp", ) out = {k: c.get(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"], []) ] required = ch.co_required.get(c["id"], []) verified: dict[str, Any] = { v["dept_code"]: { "by": people.ref(v["by"]), "at": v["at"], "note": v["note"], "signed": v["signature_id"] is not None, } for v in ch.co_verified.get(c["id"], []) } out["required"] = required out["verified"] = verified out["label"] = co_label(c["co_no"]) out["pending_depts"] = [d for d in required if d not in verified] out["open"] = (c["review"] == "approved" and bool(out["pending_depts"])) or ( c["review"] == "pending" and bool(c["hold"]) ) 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, p.visual, p.visual_closed_at, p.visual_cancelled_at, p.wo_gone_at, EXISTS (SELECT 1 FROM change_orders c WHERE c.packet_id = p.id AND c.review = 'pending' AND c.origin IN ('visual', 'drawing')) AS co_pending, pw.stage::text AS wh_stage, pw.by AS wh_by, whu.display_name AS wh_by_name, pw.at AS wh_at, 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, (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 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 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 WHERE s.bom_line_id = b.id AND s.closed_at IS NULL))) AS bom_pulled, count(*) FILTER (WHERE NOT b.na AND upper(btrim(coalesce(b.sub, ''))) <> 'Y' AND b.pulled AND coalesce(b.issued, 0) > 0 AND btrim(coalesce(b.heat, '')) = '') AS bom_missing_heat FROM bom_lines b WHERE b.packet_id = p.id ) bc ON true """ 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 wh_card(r: Mapping[str, Any]) -> dict[str, Any] | None: """The current warehouse stage row of a card, in the read model's `wh` shape.""" if r["wh_stage"] is None: return None return { "stage": r["wh_stage"], "by": None if r["wh_by"] is None else {"id": str(r["wh_by"]), "display_name": r["wh_by_name"]}, "at": r["wh_at"], "reason": r["wh_reason"], "note": r["wh_note"], "auto": bool(r["wh_auto"]), } def card_where(filters: Mapping[str, Any]) -> tuple[list[str], list[Any]]: """The WHERE terms and parameters of the card filters, over CARD_SQL's aliases (p, u, pr).""" where: list[str] = [] params: list[Any] = [] if filters.get("ids") is not None: where.append("p.id = ANY(%s)") params.append([int(i) for i in filters["ids"]]) 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": # Made by hand, or a VISUAL draft with a drawing or BOM uploaded by hand. where.append("(p.status = 'draft' AND (p.source_kind = 'hand' OR p.flags && %s::text[]))") params.append(list(HAND_UPLOAD_FLAGS)) elif flag in ("hand_upload", "hand_uploads"): where.append("(p.status = 'draft' AND p.flags && %s::text[])") params.append(list(HAND_UPLOAD_FLAGS)) elif flag and RX_FLAG_KIND.match(flag): # One flag kind on the Flagged board (drafts). A visual draft whose flags are empty but whose drawing state # still names the problem counts too, the same way flag_of reads it. states = [st for st, k in STATE_KINDS.items() if k == flag] where.append( "(p.status = 'draft' AND (%s = ANY(p.flags) OR (cardinality(p.flags) = 0 AND p.source_kind = 'visual'" " AND p.drawing_state::text = ANY(%s))))" ) params += [flag, states] co = str(filters.get("co") or "").strip().lower() if co == "pending": where.append(CO_PENDING_SQL) 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 return where, params 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 (1|0|hand|hand_upload| ), co (pending), ids (packet ids), limit, removed.""" where, params = card_where(filters) 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_labels(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"], r["visual"]), "flag_kinds": list(r["flags"] or []), "co_pending": bool(r["co_pending"]), "visual_closed": r["visual_closed_at"] is not None, "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), "wh_stage": r["wh_stage"], "wh": wh_card(r), "wh_sent_at": r["wh_sent_at"], "visual_rel": r["visual_rel"], "bom_total": int(r["bom_total"] or 0), "bom_pulled": int(r["bom_pulled"] or 0), "bom_missing_heat": int(r["bom_missing_heat"] or 0), "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