Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.
Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -39,6 +39,10 @@ SEAL_EXCLUDE = (
|
||||
"qc_items",
|
||||
"flag",
|
||||
"visual_state",
|
||||
"server_now",
|
||||
"target_hrs",
|
||||
"labor",
|
||||
"heat_src",
|
||||
)
|
||||
|
||||
|
||||
@@ -173,14 +177,24 @@ def policy_principal_for(conn: Any, p: deps.Principal) -> policy.Principal:
|
||||
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:
|
||||
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"Change order {co['co_no']} is on hold.")
|
||||
parts.append(
|
||||
f"{label}: Clerical is reviewing a change from VISUAL. Held until Clerical approves or rejects it."
|
||||
)
|
||||
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}.")
|
||||
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)
|
||||
|
||||
|
||||
@@ -188,6 +202,17 @@ 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
|
||||
|
||||
|
||||
@@ -215,6 +240,8 @@ class Children:
|
||||
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)
|
||||
|
||||
@property
|
||||
def open_stage(self) -> dict[str, Any] | None:
|
||||
@@ -233,7 +260,10 @@ def load_children(conn: Any, row: Mapping[str, Any], *, full: bool = True) -> Ch
|
||||
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)
|
||||
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.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,))
|
||||
@@ -305,6 +335,13 @@ def needs_heat(line: Mapping[str, Any]) -> bool:
|
||||
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"]]
|
||||
@@ -322,15 +359,16 @@ def state_from(row: Mapping[str, Any], ch: Children) -> policy.PacketState:
|
||||
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),
|
||||
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=len(ch.welds),
|
||||
checklist_complete=stage is not None and checklists.checklist_complete(stage["checklist"], weld),
|
||||
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)),
|
||||
has_qc_sent=ch.qc is not None,
|
||||
@@ -369,6 +407,19 @@ class People:
|
||||
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:
|
||||
@@ -378,7 +429,40 @@ def _station_ref(conn: Any, station_id: Any, cache: dict[Any, dict[str, Any] | N
|
||||
return cache[station_id]
|
||||
|
||||
|
||||
def _stage(conn: Any, s: Mapping[str, Any], people: People, cache: dict[Any, Any], *, closed: bool) -> dict[str, Any]:
|
||||
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"]
|
||||
@@ -387,16 +471,12 @@ def _stage(conn: Any, s: Mapping[str, Any], people: People, cache: dict[Any, Any
|
||||
"dept_code": s["dept_code"],
|
||||
"station": _station_ref(conn, s["station_id"], cache),
|
||||
"welder": welder,
|
||||
"checklist": s["checklist"] or {},
|
||||
"checklist": _checklist(s["checklist"], people),
|
||||
"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"],
|
||||
},
|
||||
"clock": clock_of(s, now),
|
||||
"opened_at": s["opened_at"],
|
||||
"row_version": s["row_version"],
|
||||
}
|
||||
@@ -436,8 +516,13 @@ def _qc(q: Mapping[str, Any], people: People) -> dict[str, Any]:
|
||||
|
||||
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 += [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:
|
||||
@@ -545,6 +630,7 @@ def build(
|
||||
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"]
|
||||
@@ -585,6 +671,10 @@ def build(
|
||||
"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"] = [
|
||||
{
|
||||
@@ -608,30 +698,19 @@ def build(
|
||||
| {k: w.get(k) for k in ("dwg", "rev", "partspecs")}
|
||||
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["pages"] = [_page(pg, ch, people) 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["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
|
||||
@@ -666,11 +745,15 @@ def build(
|
||||
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["checklist_items"] = checklists.checklist_for(weld, row["drawing"], row["rev"], row["wps"], ch.templates)
|
||||
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"], row["visual"])
|
||||
if not seal:
|
||||
out["visual_state"] = visual_state(conn, row)
|
||||
if include_children:
|
||||
from . import shop # shop builds on this module
|
||||
|
||||
out["heat_src"] = shop.heat_src(conn, row, ch)
|
||||
if pp is not None:
|
||||
allowed, blocked = policy.can(pp, state)
|
||||
out["can"] = [str(a) for a in allowed]
|
||||
@@ -682,6 +765,122 @@ def build(
|
||||
# `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 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 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
|
||||
|
||||
|
||||
@@ -737,11 +936,33 @@ def _bom(b: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]:
|
||||
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}
|
||||
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
|
||||
|
||||
|
||||
@@ -773,11 +994,23 @@ def _co(c: Mapping[str, Any], ch: Children, people: People) -> dict[str, Any]:
|
||||
{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"]}
|
||||
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
|
||||
|
||||
|
||||
@@ -955,7 +1188,7 @@ def list_cards(conn: Any, policy_principal: policy.Principal, filters: Mapping[s
|
||||
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)
|
||||
names = dept_labels(conn)
|
||||
transitions = {str(a) for a in policy.TRANSITIONS}
|
||||
out: list[dict[str, Any]] = []
|
||||
for r in rows(conn, sql, params):
|
||||
|
||||
Reference in New Issue
Block a user