"""Q.C.: the home board, the accept and reject log texts, and the packet document kinds. The home board ports the old homeQC: packets waiting for inspection, the latest accepted, and the rejected ones back with their supervisor, plus the project folders. The texts port the old qcAccept and qcReject handlers. """ from __future__ import annotations from collections.abc import Mapping from typing import Any from .. import policy from ..notify import raise_event from ..text import LIMITS from . import itp, packets, qcdocs, search A = policy.Action # Packet document kinds (the old PDOC_TYPES): kind -> label. DOC_KINDS: dict[str, str] = { "nde": "NDE report (RT / UT / MT / PT)", "hydro": "Hydrotest report", "dimensional": "Dimensional report", "pmi": "PMI report", "coating": "Coating / paint report", "coc": "Certificate of conformance", "customer": "Customer inspection report", "ncr": "NCR", "other": "Other", } DOC_MIMES = qcdocs.DOC_MIMES DOC_NOTE_MAX = LIMITS["doc_note"] NDE_METHODS: tuple[str, ...] = ("None required", "RT", "UT", "MT", "PT") NDE_NONE = "None required" NDE_RESULTS: tuple[str, ...] = ("", "Acceptable", "Rejectable") # The accept routes (the old ROUTES). The en dash is the old text. ROUTE_TEXTS = {"databook": "Clerical – Data Book", "shipping": "Shipping"} ACCEPTED_ON_BOARD = 8 # ---------------------------------------------------------------- texts def weld_label(no: Any) -> str: return f"W{no}" def rejected_welds(exam: Mapping[str, Any] | None) -> list[str]: """The weld ids with a reject result, sorted by number: ["W1", "W3"].""" nos: list[int] = [] for k, v in (exam or {}).items(): if isinstance(v, Mapping) and str(v.get("result") or "").lower() == "reject": try: nos.append(int(k)) except (TypeError, ValueError): continue return [weld_label(n) for n in sorted(nos)] def inspector_text(insp: Mapping[str, Any] | None) -> str: q = insp or {} who = str(q.get("inspector") or "").strip() stamp = str(q.get("stamp") or "").strip() return f"{who} [{stamp}]" if stamp else who def _notify_reject(w: Any, why: str) -> None: """qc_reject for the packet's department, in the write's transaction (notify-send mails it).""" pkt = w.packet dept = str(w.from_dept or pkt.get("dept_code") or "") or None what = str(pkt.get("title") or pkt.get("drawing") or "") who = (w.principal.user or {}).get("display_name") or "" raise_event( w.conn, "qc_reject", subject=f"packet:{w.id}", title=f"Q.C. rejected packet {w.id}" + (f" ({what})" if what else "") + (f", dept {dept}" if dept else ""), body="\n".join(x for x in (f"Rejected by {who}." if who else "", why) if x), detail={"packet_id": w.id, "dept_code": dept, "by": str(w.user_id)}, dept_code=dept, ) def qc_texts(w: Any, a: policy.Action, to_dept: str) -> None: """The packet_log label and note of a Q.C. accept or reject. Called before the side effects close the inspection. `w` is the pipeline Write; its extra holds `reason_text` and `ncr` for a reject. """ insp = packets.one(w.conn, "SELECT * FROM qc_inspections WHERE packet_id = %s AND closed_at IS NULL", (w.id,)) note = str(w.extra.get("note") or "").strip() or None if a is A.QC_REJECT: exam = (insp or {}).get("weld_exam") or {} parts = [str(w.extra.get("ncr") or ""), str(w.extra.get("reason_text") or "")] welds = rejected_welds(exam) if welds: parts.append("welds " + ", ".join(welds)) w.log("Q.C. rejected → supervisor", " · ".join(x for x in parts if x) or None) _notify_reject(w, " · ".join(x for x in parts if x)) return if a is A.QC_ACCEPT_AND_ROUTE: labels = getattr(w.children, "dept_labels", None) or packets.dept_labels(w.conn) text = " · ".join(x for x in (inspector_text(insp), f"from {labels.get(w.from_dept, w.from_dept)}", note) if x) w.log(f"Q.C. accepted → {labels.get(to_dept, to_dept)}", text) return if a is A.QC_ACCEPT: n = len(w.children.welds) if w.children is not None else 0 parts = [inspector_text(insp), f"{n} welds VT accepted"] method = str((insp or {}).get("nde_method") or "").strip() if method and method != NDE_NONE: parts.append(f"{method} {(insp or {}).get('nde_result') or ''}".strip()) if note: parts.append(note) route = ROUTE_TEXTS.get(str(w.extra.get("routed_to")), str(w.extra.get("routed_to"))) w.log(f"Q.C. accepted → {route}", " · ".join(x for x in parts if x)) # ---------------------------------------------------------------- the home board def _last_results(conn: Any, packet_ids: list[int]) -> dict[int, dict[str, Any]]: if not packet_ids: return {} rows = packets.rows( conn, "SELECT DISTINCT ON (packet_id) * FROM qc_history WHERE packet_id = ANY(%s) ORDER BY packet_id, id DESC", (packet_ids,), ) return {int(r["packet_id"]): r for r in rows} def _scope_where(project_id: int | None, unit_id: int | None) -> tuple[str, list[Any]]: """Fixed SQL fragments over the packets alias p, and their parameters.""" where, params = "", [] if unit_id is not None: where += " AND p.unit_id = %s" params.append(unit_id) if project_id is not None: where += " AND p.unit_id IN (SELECT id FROM units WHERE project_id = %s)" params.append(project_id) return where, params LAST_SQL = ( "SELECT p.id FROM packets p JOIN LATERAL (SELECT h.result, h.at, h.id FROM qc_history h" " WHERE h.packet_id = p.id ORDER BY h.id DESC LIMIT 1) h ON true" " WHERE p.removed_at IS NULL AND p.status::text = ANY(%s) AND h.result = %s" ) WAITING_SQL = "SELECT count(*) FROM packets p WHERE p.removed_at IS NULL AND p.status = 'qc'" BOARD_LIMIT = 5000 COUNT_SQL = ( "SELECT count(DISTINCT l.packet_id) FROM packet_log l JOIN packets p ON p.id = l.packet_id" " WHERE l.at > now() - interval '7 days' AND l.action LIKE %s AND p.removed_at IS NULL" ) def _ids_with_last(conn: Any, statuses: tuple[str, ...], result: str, where: str, params: list[Any]) -> list[int]: """Packets in the statuses whose newest qc_history result is `result`, newest result first.""" sql = LAST_SQL + where + " ORDER BY h.id DESC" # where: fixed fragments from _scope_where rows = conn.execute(sql, (list(statuses), result, *params)).fetchall() return [int(r[0]) for r in rows] def _count_7d(conn: Any, prefix: str, where: str, params: list[Any]) -> int: row = conn.execute(COUNT_SQL + where, (prefix + "%", *params)).fetchone() return int(row[0] or 0) def home( conn: Any, pp: policy.Principal, *, project_id: int | None = None, unit_id: int | None = None, filters: Mapping[str, Any] | None = None, ) -> dict[str, Any]: """GET /api/qc/home: the KPIs, the three boards and the active project folders. `filters` (the find bar's) narrow the cards of the three boards; the KPIs follow the project and unit only.""" scope: dict[str, Any] = {"project_id": project_id, "unit_id": unit_id} narrow = search.board_filters(filters or {}) narrow.update(scope) ready = sorted( packets.list_cards(conn, pp, {**narrow, "status": "qc", "limit": BOARD_LIMIT}), key=lambda c: (c["status_at"], c["id"]), ) where, params = _scope_where(project_id, unit_id) acc_all = _ids_with_last(conn, ("complete", "filed"), "accepted", where, params) if search.narrowing(narrow) and acc_all: # The newest accepted packets that match the find bar (a light pass, no `can`), then the board's eight. hits = { c["id"] for c in packets.list_cards(conn, pp, {**narrow, "ids": acc_all, "lite": True, "limit": BOARD_LIMIT}) } acc_all = [i for i in acc_all if i in hits] acc_ids = acc_all[:ACCEPTED_ON_BOARD] rej_ids = _ids_with_last(conn, ("review",), "rejected", where, params) by_id: dict[Any, dict[str, Any]] = {} if acc_ids or rej_ids: by_id = { c["id"]: c for c in packets.list_cards(conn, pp, {**narrow, "ids": acc_ids + rej_ids, "limit": BOARD_LIMIT}) } accepted = [by_id[i] for i in acc_ids if i in by_id] rejected = [by_id[i] for i in rej_ids if i in by_id] _add_qc(conn, ready + accepted + rejected) waiting = len(ready) if search.narrowing(filters or {}): row = conn.execute(WAITING_SQL + where, params).fetchone() # where: fixed fragments from _scope_where waiting = int(row[0] or 0) return { "kpis": { "waiting": waiting, "accepted_7d": _count_7d(conn, "Q.C. accepted", where, params), "rejected_7d": _count_7d(conn, "Q.C. rejected", where, params), }, "columns": {"ready": ready, "accepted": accepted, "rejected": rejected}, "folders": qcdocs.folders(conn, "active"), "facets": search.facets(conn, project_id=project_id, unit_id=unit_id), } def _add_qc(conn: Any, cards: list[dict[str, Any]]) -> None: """Each card gains `qc` (the open inspection's location and sent time, or the last result) and itp_hold_count.""" if not cards: return ids = [int(c["id"]) for c in cards] open_rows = { int(r["packet_id"]): r for r in packets.rows( conn, "SELECT packet_id, location, sent_at, inspector, result FROM qc_inspections" " WHERE packet_id = ANY(%s) AND closed_at IS NULL", (ids,), ) } last = _last_results(conn, ids) units = itp.unit_itps(conn, [int(c["unit"]["id"]) for c in cards]) for c in cards: pid = int(c["id"]) o = open_rows.get(pid) if o is not None: c["qc"] = {k: o[k] for k in ("location", "sent_at", "inspector", "result")} elif pid in last: h = last[pid] snap = h.get("snapshot") or {} c["qc"] = { "location": snap.get("location"), "sent_at": snap.get("sent_at"), "inspector": h.get("inspector"), "result": h.get("result"), } else: c["qc"] = None u = units.get(int(c["unit"]["id"])) hold = itp.holds_for_packet(u[0], u[1], c["dept"]["code"]) if u else None c["itp_hold_count"] = hold["count"] if hold else 0