"""The job traveler, letter portrait, after the old buildTraveler: job info, the work orders with QR codes for clocking on at the labor kiosk, the BOM and heat numbers, the checklist with blanks to stamp, the welds, the weld record, the sign-offs and a notes box. The QR text is exactly `*%2{wo_base}${lot}${split or "0"}${sub or "0"}${seq}%*`, built from the text columns as they are (never cast). """ from __future__ import annotations import io from collections.abc import Mapping from datetime import datetime from typing import Any from pypdf import PdfReader, PdfWriter from reportlab.graphics import renderPDF from reportlab.graphics.barcode.qr import QrCodeWidget from reportlab.graphics.shapes import Drawing from . import parts, stamps from .databook import heats_for from .doc import Doc from .style import CW, GREY, LINE, Built, L, PdfError, R, WhoRef, clean, fmt_d, fmt_dt, fmt_num, site_tz, utc_now BLANK = "________" BLANK_LONG = "________________" QR_BOX_H = 96.0 QR_SIZE = 84.0 def qr_text(wo: Mapping[str, Any]) -> str: """The labor kiosk code of a work order (the old qrText).""" split = str(wo.get("split") or "") or "0" sub = str(wo.get("sub") or "") or "0" seq = "" if wo.get("seq") is None else str(wo.get("seq")) return f"*%2{wo.get('wo_base') or ''}${wo.get('lot') or ''}${split}${sub}${seq}%*" def draw_qr(d: Doc, text: str, x: float, y_top: float, size: float) -> None: """A QR code (error correction M) `size` points square with its top-left corner at (x, y_top).""" w = QrCodeWidget(text, barLevel="M", barBorder=0) x0, y0, x1, y1 = w.getBounds() bw, bh = x1 - x0, y1 - y0 drawing = Drawing(size, size, transform=[size / bw, 0, 0, size / bh, -x0 * size / bw, -y0 * size / bh]) drawing.add(w) renderPDF.draw(drawing, d.c, x, d.Y(y_top + size)) def build(conn: Any, settings: Any, packet_id: int, *, who: WhoRef, now: datetime | None = None) -> Built: """The traveler with the controlled-copy stamp (mode traveler) and page numbers.""" now = utc_now(now) p = parts.load_packet(conn, packet_id) data, pages = pages_of(conn, settings, p) warnings: list[str] = [] text = stamps.controlled_stamp(who, now, mode="traveler") stamp, sig = stamps.signed_stamp( settings, text, packet_id=p["id"], kind="traveler", seal_hash_hex=None, who=who, now=now, warnings=warnings ) data = finish(data, p, stamp, now, box=False) return Built(bytes=data, pages=pages, warnings=warnings, stamp=stamp, stamp_sig=sig, what="Job traveler") def finish(data: bytes, p: Mapping[str, Any], stamp: str, now: datetime, *, box: bool) -> bytes: """Page numbers, the stamp (the line, or the PRINTED COPY box) and the metadata.""" project, lot = parts.tag_of(p) writer = PdfWriter(clone_from=PdfReader(io.BytesIO(data))) n = len(writer.pages) def draw(c: Any, i: int, w: float, h: float) -> None: del h c.setFont("Helvetica", 7) c.setFillColorRGB(GREY[0] / 255, GREY[1] / 255, GREY[2] / 255) c.drawRightString(R, 20, clean(f"{project} Lot {lot} - Page {i + 1} of {n}")) if not stamp: return if box: stamps.draw_box_stamp(c, w, stamp) else: stamps.draw_line_stamp(c, w, stamp, own=True) stamps.overlay_writer(writer, draw) meta = stamps.metadata( f"{project} Lot {lot} Job Traveler", f"Job traveler, drawing {p.get('drawing') or ''} R{p.get('rev') or ''}", ", ".join(x for x in (project, f"Lot {lot}", str(p.get("drawing") or ""), "traveler") if x), "Packet Station - controlled copy", now=now, ) return stamps.write(writer, meta) def pages_of(conn: Any, settings: Any, p: Mapping[str, Any]) -> tuple[bytes, int]: """The traveler pages without page numbers or stamp, and their count.""" try: d = _draw(conn, settings, p) return d.finish(), d.pages except PdfError: raise except Exception as e: raise PdfError(f"The traveler could not be built: {type(e).__name__}.") from e def _draw(conn: Any, settings: Any, p: Mapping[str, Any]) -> Doc: tz = site_tz(settings) project, lot = parts.tag_of(p) proj = parts.project_of(p) unit = p.get("unit") or {} depts = parts.departments(conn) dept_code = (p.get("dept") or {}).get("code") bom = p.get("bom") or [] stage = p.get("stage") or ((p.get("stages") or [None])[-1] if p.get("stages") else None) or {} d = Doc( ( "Job Traveler", f"{project} Lot {lot} - {p.get('op') or ''} - drawing {p.get('drawing') or ''} R{p.get('rev') or ''}", ) ) y = 124.0 welder = p.get("welder") or {} station = (p.get("station") or {}).get("name") if p.get("station") else "" worker = [ str(station or ""), str(welder.get("display_name") or ""), f"stamp {welder['stamp']}" if welder.get("stamp") else "", ] rows: list[tuple[str, Any]] = [ ("Job / lot", f"{project} · Lot {lot}"), ("Project", f"{proj.get('number') or ''}" + (f" - {proj['name']}" if proj.get("name") else "")), ("Unit", f"{unit.get('number') or ''}" + (f" - {unit['name']}" if unit.get("name") else "")), ] if proj.get("po"): rows.append(("Customer PO", proj["po"])) if unit.get("due_effective"): rows.append(("Due / ship date", fmt_d(unit["due_effective"], tz))) rows += [ ("Operation", p.get("op")), ("Title", p.get("title")), ("Customer", p.get("customer")), ("Drawing", f"{p.get('drawing') or ''} Rev {p.get('rev') or ''}"), ("WPS", p.get("wps")), ("Department", (depts.get(str(dept_code)) or {}).get("label") or dept_code), ("Station / worker", " · ".join(x for x in worker if x)), ("Status", parts.STATUS_LABEL.get(str(p.get("status")), str(p.get("status") or ""))), ("Target hours", fmt_num(p.get("target_hrs") or 0)), ] y = d.kv_table(rows, y) # work orders with QR codes y += 16 y = d.section_label("Work orders - scan at the labor kiosk to clock on in Visual", y) wos = p.get("work_orders") or [] bw = (CW - 12) / 2 for i, w in enumerate(wos): col = i % 2 if col == 0: y = d.ensure_space(y, QR_BOX_H + 8) x = L + col * (bw + 12) d.rect(x, y, bw, QR_BOX_H, stroke=LINE, width=0.8) draw_qr(d, qr_text(w), x + 6, y + 6, QR_SIZE) tx0 = x + QR_BOX_H + 2 d.tx(w.get("wo_base") or "", tx0, y + 20, size=12, bold=True) d.tx( f"Lot {w.get('lot') or ''} Seq {'' if w.get('seq') is None else w.get('seq')} {w.get('op') or ''}", tx0, y + 36, size=8.5, ) d.tx(f"Target {fmt_num(w.get('target_hrs'))} h Qty {fmt_num(w.get('qty'))}", tx0, y + 50, size=8.5) d.tx( f"Drawing {w.get('dwg') or p.get('drawing') or ''} Rev {w.get('rev') or p.get('rev') or ''}", tx0, y + 64, size=7.5, color=GREY, ) if col == 1 or i == len(wos) - 1: y += QR_BOX_H + 8 if not wos: y = d.note_box("No work orders were read from this packet.", y, True) # BOM, the job pull y += 10 y = d.section_label("Bill of materials and heat numbers (job pull)", y) body = [] for b in bom: short = b.get("short") or {} body.append( [ str(b.get("item")), fmt_num(b.get("qty")), fmt_num(b.get("issued")), b.get("loc") or "", (f"{b['part_id']}\n" if b.get("part_id") else "") + str(b.get("description") or ""), b.get("material") or "", str(b.get("heat") or "").strip() or ("N/A" if b.get("na") else BLANK), "Yes" if b.get("mtr_asset_id") else "", fmt_num(short.get("qty_open")) if short else "", ] ) y = d.table( ["ITEM", "QTY", "ISSUED", "LOC", "PART ID / DESCRIPTION", "MATERIAL", "HEAT NO.", "MTR", "SHORT"], body, y + 4, { 0: {"w": 28, "align": "center"}, 1: {"w": 28, "align": "center"}, 2: {"w": 34, "align": "center"}, 3: {"w": 34}, 5: {"w": 66}, 6: {"w": 78, "bold": True}, 7: {"w": 28}, 8: {"w": 32, "align": "center"}, }, ) # checklist of the current department y += 16 y = d.section_label("Checklist - stamp each line when done", y) ck = stage.get("checklist") or {} body = [] for it in p.get("checklist_items") or []: e = ck.get(it.get("key")) e = e if isinstance(e, Mapping) else ({"done": True} if e is True else {}) done = bool(e.get("done")) body.append( [ it.get("text") or "", "YES" if done else "[ ]", (e.get("stamp") or "") if done else BLANK, fmt_d(e.get("at"), tz, empty="") if done else BLANK, ] ) y = d.table( ["CHECK", "DONE", "STAMP", "DATE"], body, y + 4, {1: {"w": 40, "align": "center"}, 2: {"w": 70}, 3: {"w": 70}} ) # welds welds = p.get("welds") or [] if welds: y += 16 y = d.section_label("Weld log", y) body = [ [ f"W{w.get('weld_no')}", " + ".join(str(i) for i in w.get("items") or []), heats_for(bom, w.get("items")), f"{w.get('type') or ''} {w.get('size') or ''}".strip(), w.get("process") or "", w.get("stamp") or "", ] for w in welds ] y = d.table( ["WELD", "JOINS ITEMS", "HEATS", "TYPE / SIZE", "PROCESS", "STAMP"], body, y + 4, {0: {"w": 38, "bold": True}}, ) # filler and preheat y += 16 y = d.section_label("Weld record", y) y = d.kv_table( [ ("Filler metal", stage.get("filler") or BLANK_LONG), ("Filler lot / heat", stage.get("filler_lot") or BLANK_LONG), ("Preheat", stage.get("preheat") or BLANK_LONG), ], y + 4, ) # sign-offs y += 16 y = d.section_label("Sign-offs", y) signed = [e for e in parts.log_rows(conn, int(p["id"])) if e.get("signature_id") is not None] if signed: body = [ [ fmt_dt(e.get("at"), tz), e.get("action") or "", f"{e.get('sig_name') or e.get('by_name') or ''} ({e.get('sig_emp') or e.get('emp_no') or '?'})", e.get("sig_stamp") or "", ] for e in signed ] y = d.table(["WHEN", "STEP", "SIGNED BY", "STAMP"], body, y + 4, {0: {"w": 96}, 3: {"w": 50}}) else: body = [ [s, "____________________", BLANK] for s in ("Work complete (stall)", "Supervisor review", "Q.C. inspection") ] y = d.table(["STEP", "SIGNED BY", "DATE"], body, y + 4) q = _last_qc(p) if q: y += 10 text = f"Q.C. {str(q['result']).upper()} {fmt_dt(q.get('at'), tz)}" if q.get("inspector"): text += f" by {q['inspector']}" if q.get("note"): text += f" - {q['note']}" y = d.note_box(text, y, q["result"] != "accepted") # notes box for the shop floor y += 12 y = d.ensure_space(y, 130) d.lbl("Shop notes", L, y) y += 8 d.rect(L, y, CW, 110, stroke=LINE, width=0.8) for k in range(1, 5): d.line(L + 8, y + k * 22, R - 8, y + k * 22, LINE, 0.8) return d def _last_qc(p: Mapping[str, Any]) -> dict[str, Any] | None: """The open inspection when it has a result, else the newest closed one: {result, at, inspector, note}.""" cur = p.get("qc") if cur and cur.get("result"): return { "result": cur["result"], "at": cur.get("result_at"), "inspector": cur.get("inspector"), "note": cur.get("result_note"), } hist = [h for h in p.get("qc_history") or [] if h.get("result")] if hist: h = hist[-1] return {"result": h.get("result"), "at": h.get("at"), "inspector": h.get("inspector"), "note": h.get("note")} return None