"""The department checklists and the Q.C. checks, ported from the old checklistFor(), qcChecksFor() and qcMissing(). Weld departments get the welding list, the others the plain list. The drawing, rev and WPS are filled into the text. The lists live in the settings rows `checklist_templates` (migration 0006) and `qc_check_items` (migration 0007); the defaults here are the fallback. The keys are the old keys, so a checklist stored as {key: {done, at, by, stamp}} reads the same. """ from __future__ import annotations import logging import re from collections.abc import Iterable, Mapping from typing import Any log = logging.getLogger(__name__) NO_REV = "-" NDE_NONE = ("", "none", "none required") NDE_OK = ("acceptable", "accept", "accepted") REJECTS = ("reject", "rejected") def _item(group: str, key: str, text: str) -> dict[str, str]: return {"group": group, "key": key, "text": text} DWG_TEXT = "Drawing {drawing} Rev {rev} matches the packet" # The defaults. The settings row `checklist_templates` holds the same lists (migration 0006); these are the fallback # when that row is missing or does not parse. DEFAULT_TEMPLATES: dict[str, list[dict[str, str]]] = { "weld": [ _item("Before welding", "dwg", DWG_TEXT), _item("Before welding", "heat", "Heat numbers on material match the Materials tab"), _item("Before welding", "wps", "WPS reviewed: {wps}"), _item("Before welding", "fit", "Fit-up and dimensions checked against the drawing"), _item("Before welding", "pre", "Preheat and interpass temperature per WPS"), _item("After welding", "vt", "Visual inspection of all welds acceptable"), _item("After welding", "stamp", "Welds stamped with welder ID"), _item("After welding", "clean", "Slag and spatter removed"), _item("After welding", "qty", "Completed quantity matches each work order"), ], "plain": [ _item("Before starting", "dwg", DWG_TEXT), _item("Before starting", "prev", "Previous department's work complete and released"), _item("Before starting", "mat", "Material and parts on hand match the packet"), _item("When done", "work", "Work completed per drawing and procedure"), _item("When done", "insp", "Self-inspection done; no damage to finished surfaces"), _item("When done", "qty", "Completed quantity matches each work order"), ], } SETTINGS_KEY = "checklist_templates" RX_KEY = re.compile(r"^[a-z][a-z0-9_]{0,29}$") RX_PLACEHOLDER = re.compile(r"\{(drawing|rev|wps)\}") TEXT_MAX = 200 ITEMS_MAX = 40 def _valid_list(value: Any) -> list[dict[str, str]] | None: """A template list from settings, or None when it does not have the right shape.""" if not isinstance(value, list) or not value or len(value) > ITEMS_MAX: return None out: list[dict[str, str]] = [] seen: set[str] = set() for it in value: if not isinstance(it, Mapping): return None group, key, text = it.get("group"), it.get("key"), it.get("text") if not (isinstance(group, str) and isinstance(key, str) and isinstance(text, str)): return None if not RX_KEY.match(key) or key in seen or not group.strip() or not text.strip(): return None seen.add(key) out.append(_item(group.strip()[:TEXT_MAX], key, text.strip()[:TEXT_MAX])) return out def parse_templates(value: Any) -> dict[str, list[dict[str, str]]] | None: """The `checklist_templates` settings value, checked. None when it is malformed.""" if not isinstance(value, Mapping): return None weld, plain = _valid_list(value.get("weld")), _valid_list(value.get("plain")) if weld is None or plain is None: return None return {"weld": weld, "plain": plain} def load_templates(conn: Any) -> dict[str, list[dict[str, str]]]: """The department checklists from settings, else the defaults in code (with a warning when the row is bad).""" row = conn.execute("SELECT value FROM settings WHERE key = %s", (SETTINGS_KEY,)).fetchone() if row is None: return DEFAULT_TEMPLATES parsed = parse_templates(row[0]) if parsed is None: log.warning("settings %s does not parse; using the default checklists", SETTINGS_KEY) return DEFAULT_TEMPLATES return parsed def _fill(text: str, drawing: str | None, rev: str | None, wps: str | None) -> str: values = {"drawing": drawing or "", "rev": rev or NO_REV, "wps": wps or "per drawing"} return " ".join(RX_PLACEHOLDER.sub(lambda m: values[m.group(1)], text).split()) def checklist_for( weld: bool, drawing: str | None, rev: str | None, wps: str | None, templates: Mapping[str, list[dict[str, str]]] | None = None, ) -> list[dict[str, str]]: """The stage checklist for a department, as a flat list of {group, key, text} in display order.""" t = templates or DEFAULT_TEMPLATES items = t.get("weld" if weld else "plain") or DEFAULT_TEMPLATES["weld" if weld else "plain"] return [_item(i["group"], i["key"], _fill(i["text"], drawing, rev, wps)) for i in items] QC_SETTINGS_KEY = "qc_check_items" RX_QC_PLACEHOLDER = re.compile(r"\{(drawing|rev|wps|stamp)\}") G_BOOK, G_SPOOL = "Data book package", "Physical spool" # The Q.C. check items, with {wps}, {stamp}, {drawing} and {rev} placeholders. The settings row `qc_check_items` # (migration 0007) holds the same list; this is the fallback when that row is missing or does not parse. QC_DEFAULT_ITEMS: list[dict[str, str]] = [ _item(G_BOOK, "db_heats", "Heat number recorded for every BOM item"), _item(G_BOOK, "db_mtr", "MTRs on file and match the heat numbers"), _item(G_BOOK, "db_weldmap", "Weld map complete: every weld identified"), _item(G_BOOK, "db_wps", "Welds made to {wps}; welder {stamp}qualified"), _item(G_BOOK, "db_ck", "Stall checklist and weld record complete"), _item(G_SPOOL, "ph_dim", "Dimensions verified against {drawing} Rev {rev}"), _item(G_SPOOL, "ph_heat", "Heat numbers on material match the heat map"), _item(G_SPOOL, "ph_stamp", "Welds stamped with welder ID"), _item(G_SPOOL, "ph_clean", "Clean: no slag, spatter or arc strikes"), _item(G_SPOOL, "ph_qty", "Quantity matches the work orders"), ] def parse_qc_items(value: Any) -> list[dict[str, str]] | None: """The `qc_check_items` settings value ({"groups": [{"group", "items": [{"key", "text"}]}]}) as a flat list of {group, key, text}. None when it is malformed.""" if not isinstance(value, Mapping) or not isinstance(value.get("groups"), list): return None flat: list[dict[str, Any]] = [] for g in value["groups"]: if not isinstance(g, Mapping) or not isinstance(g.get("items"), list): return None flat += [{**i, "group": g.get("group")} if isinstance(i, Mapping) else i for i in g["items"]] return _valid_list(flat) def load_qc_items(conn: Any) -> list[dict[str, str]]: """The Q.C. check items from settings, else the defaults in code (with a warning when the row is bad).""" row = conn.execute("SELECT value FROM settings WHERE key = %s", (QC_SETTINGS_KEY,)).fetchone() if row is None: return QC_DEFAULT_ITEMS parsed = parse_qc_items(row[0]) if parsed is None: log.warning("settings %s does not parse; using the default Q.C. items", QC_SETTINGS_KEY) return QC_DEFAULT_ITEMS return parsed def qc_fill(text: str, drawing: str | None, rev: str | None, wps: str | None, welder_stamp: str | None) -> str: values = { "drawing": drawing or "the drawing", "rev": rev or NO_REV, "wps": wps or "the WPS", "stamp": f"[{welder_stamp}] " if welder_stamp else "", } return " ".join(RX_QC_PLACEHOLDER.sub(lambda m: values[m.group(1)], text).split()) def qc_items_for( drawing: str | None, rev: str | None, wps: str | None, welder_stamp: str | None, items: Iterable[Mapping[str, str]] | None = None, ) -> list[dict[str, str]]: """The Q.C. inspection items, as a flat list of {group, key, text} with the packet's values filled in.""" src = list(items) if items is not None else QC_DEFAULT_ITEMS return [_item(i["group"], i["key"], qc_fill(i["text"], drawing, rev, wps, welder_stamp)) for i in src] def checklist_keys(weld: bool, templates: Mapping[str, list[dict[str, str]]] | None = None) -> list[str]: return [i["key"] for i in checklist_for(weld, None, None, None, templates)] def qc_keys(items: Iterable[Mapping[str, str]] | None = None) -> list[str]: return [i["key"] for i in (items if items is not None else QC_DEFAULT_ITEMS)] def is_done(value: Any) -> bool: """A checklist or Q.C. check entry: `true`, or `{"done": true, ...}`.""" if value is True: return True return isinstance(value, Mapping) and value.get("done") is True def checklist_complete( checklist: Mapping[str, Any] | None, weld: bool, templates: Mapping[str, list[dict[str, str]]] | None = None ) -> bool: """Every key of the department checklist is done. An empty list counts as complete.""" ck = checklist or {} return all(is_done(ck.get(k)) for k in checklist_keys(weld, templates)) def _plural(n: int, word: str) -> str: return f"{n} {word}{'s' if n > 1 else ''}" def qc_missing( packet: Mapping[str, Any], qc: Mapping[str, Any] | None, welds: Iterable[Mapping[str, Any]], items: Iterable[Mapping[str, str]] | None = None, ) -> list[str]: """What Q.C. still has to record before an accept. The old qcMissing(). `packet` is unused today (the item list does not depend on it for the keys) but kept for the old signature. `qc` is the open inspection row (checks, weld_exam, nde_method, nde_result, inspector) or None. `welds` are the live weld rows (weld_no). `items` are the loaded Q.C. items (load_qc_items); None means the defaults. """ del packet q = qc or {} checks = q.get("checks") or {} missing: list[str] = [] open_items = sum(1 for k in qc_keys(items) if not is_done(checks.get(k))) if open_items: missing.append(_plural(open_items, "inspection item")) exam = q.get("weld_exam") or {} results = [] for w in welds: entry = exam.get(str(w.get("weld_no"))) result = entry.get("result") if isinstance(entry, Mapping) else None results.append(str(result).lower() if result else "") no_result = sum(1 for r in results if not r) if no_result: missing.append(_plural(no_result, "weld") + " without a visual result") if any(r in REJECTS for r in results): missing.append("a rejected weld (reject the packet instead)") method = str(q.get("nde_method") or "").strip().lower() if method not in NDE_NONE and str(q.get("nde_result") or "").strip().lower() not in NDE_OK: missing.append("NDE result") if not str(q.get("inspector") or "").strip(): missing.append("inspector name") return missing