"""The traceability CSV: the old exportCsv, row for row, from the packet read model. Every cell goes through `csv_cell`, the formula guard: a value that starts with =, +, -, @, a tab or a CR gets a leading apostrophe, so a spreadsheet shows it as text and never runs it. Lines end in CRLF. UTF-8, no BOM. """ from __future__ import annotations from collections.abc import Iterable, Mapping, Sequence from datetime import date, datetime from typing import Any from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from ..domain import packets as packets_dom from ..domain.pipeline import qty_text FORMULA_START = ("=", "+", "-", "@", "\t", "\r") TITLE = "PESCO weld & heat traceability record" BOM_HEAD = ( "BOM item", "Qty", "Issued", "Location", "Part ID", "Description", "Length", "Material", "Heat no.", "MTR on file", "Open short qty", "Short reason", "Short received", ) WELD_HEAD = ( "Weld", "Work order", "Piece", "Joins items", "Heats joined", "Type", "Size", "Process", "WPS", "Stamp", "Logged", ) CHECK_HEAD = ("Checklist item", "Done", "Stamp", "When") def csv_cell(value: Any) -> str: """One CSV cell with the formula guard and RFC 4180 quoting.""" text = "" if value is None else str(value) if text.startswith(FORMULA_START): text = "'" + text if any(c in text for c in ('"', ",", "\n", "\r")): text = '"' + text.replace('"', '""') + '"' return text def to_csv(rows: Iterable[Sequence[Any]]) -> str: return "\r\n".join(",".join(csv_cell(v) for v in r) for r in rows) def _zone(tz: str | None) -> ZoneInfo: try: return ZoneInfo(tz or "America/Denver") except (ZoneInfoNotFoundError, ValueError): return ZoneInfo("America/Denver") def _when(value: Any, tz: str | None) -> datetime | date | None: if value is None or value == "": return None if isinstance(value, datetime): return value.astimezone(_zone(tz)) if value.tzinfo else value if isinstance(value, date): return value try: d = datetime.fromisoformat(str(value).replace("Z", "+00:00")) except ValueError: return None return d.astimezone(_zone(tz)) if d.tzinfo else d def fmt_dt(value: Any, tz: str | None) -> str: """M/D/YYYY h:mm AM in the site time zone; empty for no value.""" d = _when(value, tz) if d is None: return "" if not isinstance(d, datetime): return f"{d.month}/{d.day}/{d.year}" hour = d.hour % 12 or 12 return f"{d.month}/{d.day}/{d.year} {hour}:{d.minute:02d} {'AM' if d.hour < 12 else 'PM'}" def fmt_d(value: Any, tz: str | None) -> str: """M/D/YYYY in the site time zone; empty for no value.""" d = _when(value, tz) if d is None: return "" return f"{d.month}/{d.day}/{d.year}" def _num(value: Any) -> str: return "" if value is None else qty_text(value) def _done(entry: Any) -> bool: return entry is True or (isinstance(entry, Mapping) and entry.get("done") is True) def _entry(entry: Any) -> Mapping[str, Any]: return entry if isinstance(entry, Mapping) else {} def _shorts(conn: Any, packet_id: int) -> dict[int, dict[str, Any]]: """The newest short per BOM line, open or closed, with its receipts (a closed short still has its record).""" out: dict[int, dict[str, Any]] = {} for s in packets_dom.rows(conn, "SELECT * FROM bom_shorts WHERE packet_id = %s ORDER BY id", (packet_id,)): out[int(s["bom_line_id"])] = {**s, "receipts": []} ids = {int(s["id"]): line for line, s in out.items()} if ids: for r in packets_dom.rows( conn, "SELECT * FROM short_receipts WHERE short_id = ANY(%s) ORDER BY id", (list(ids),) ): out[ids[int(r["short_id"])]]["receipts"].append(r) return out def _stage(read: Mapping[str, Any]) -> Mapping[str, Any]: """The open stage, else the last closed one, else nothing.""" if read.get("stage"): return read["stage"] stages = read.get("stages") or [] return stages[-1] if stages else {} def _inspection(conn: Any, read: Mapping[str, Any]) -> Mapping[str, Any] | None: """The open inspection, else the newest closed one (its snapshot carries every field).""" if read.get("qc"): return read["qc"] r = packets_dom.one( conn, "SELECT result, inspector, at, snapshot FROM qc_history WHERE packet_id = %s ORDER BY id DESC LIMIT 1", (read["id"],), ) if r is None: return None snap = r["snapshot"] if isinstance(r["snapshot"], Mapping) else {} return { **snap, "result": r["result"] or snap.get("result"), "inspector": r["inspector"] or snap.get("inspector"), "result_at": snap.get("result_at") or r["at"], } def rows_for(conn: Any, read: Mapping[str, Any], tz: str | None) -> list[list[Any]]: """Every CSV row for one packet read model.""" unit = read.get("unit") or {} project = (unit.get("project") or {}).get("number") or "" lot = unit.get("lot") or unit.get("number") or "" stage = _stage(read) welder = stage.get("welder") or {} station = stage.get("station") or {} out: list[list[Any]] = [ [TITLE], ["Project", project, "Lot", lot, "Operation", read.get("op")], [ "Drawing", read.get("drawing"), "Rev", read.get("rev"), "Title", read.get("title"), "Customer", read.get("customer"), ], [ "WPS", read.get("wps"), "Station", station.get("name"), "Welder", welder.get("display_name"), "Stamp", welder.get("stamp"), ], ["Filler", stage.get("filler"), "Filler lot", stage.get("filler_lot"), "Preheat", stage.get("preheat")], [], list(BOM_HEAD), ] bom = read.get("bom") or [] shorts = _shorts(conn, int(read["id"])) for b in bom: s = shorts.get(int(b["id"])) if b.get("id") is not None else None open_short = b.get("short") heat = b.get("heat") or ("N/A" if b.get("na") else "") received = "; ".join( f"{_num(r['qty'])} pc {r['heat'] or ''} {fmt_d(r['at'], tz)}" for r in (s or {}).get("receipts", []) ) out.append( [ b.get("item"), _num(b.get("qty")), _num(b.get("issued")), b.get("loc") or "", b.get("part_id"), b.get("description"), b.get("length"), b.get("material"), heat, "Yes" if b.get("mtr_asset_id") else "No", _num(open_short.get("qty_open")) if open_short else "", (s.get("reason") or "") if s else "", received, ] ) out += [[], list(WELD_HEAD)] heat_of = {str(b.get("item")): b.get("heat") for b in bom} for w in read.get("welds") or []: items = [str(i) for i in (w.get("items") or [])] out.append( [ f"W{w.get('weld_no')}", w.get("wo_base"), w.get("piece"), " + ".join(items), " + ".join((heat_of.get(i) or "-") for i in items), w.get("type"), w.get("size"), w.get("process"), read.get("wps"), w.get("stamp"), fmt_dt(w.get("at"), tz), ] ) out += [[], list(CHECK_HEAD)] checklist = stage.get("checklist") or {} for item in read.get("checklist_items") or []: c = checklist.get(item["key"]) e = _entry(c) out.append([item["text"], "Yes" if _done(c) else "No", e.get("stamp") or "", fmt_dt(e.get("at"), tz)]) insp = _inspection(conn, read) if insp is not None: out += [ [], [ "Q.C. inspection", insp.get("result") or "open", "Inspector", insp.get("inspector"), "Stamp", insp.get("stamp"), "Date", fmt_dt(insp.get("result_at"), tz), ], [ "NDE", insp.get("nde_method"), insp.get("nde_result"), insp.get("nde_report"), "Hardness", insp.get("hardness"), ], ] checks = _entry(insp.get("checks")) for item in read.get("qc_items") or []: c = checks.get(item["key"]) e = _entry(c) out.append( [ "Q.C.: " + str(item["text"]), "Yes" if _done(c) else "No", e.get("stamp") or "", fmt_dt(e.get("at"), tz), ] ) exam = _entry(insp.get("weld_exam")) for w in read.get("welds") or []: v = _entry(exam.get(str(w.get("weld_no")))) out.append([f"VT W{w.get('weld_no')}", v.get("result") or "", v.get("note") or ""]) return out def build(conn: Any, settings: Any, packet_id: int) -> tuple[str, dict[str, Any]]: """The CSV text and the read model it came from. 404 not_found for an unknown packet.""" read = packets_dom.read_packet(conn, packet_id, None) return to_csv(rows_for(conn, read, getattr(settings, "site_tz", None))), read