Files

290 lines
9.4 KiB
Python
Raw Permalink Normal View History

2026-10-17 23:49:14 -06:00
"""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