"""Phase 7 ITP PDF: the builder on an open and a complete ITP (the number, a section title, the approvals, the stamp, three page groups), and the sign-off through the API that now files the signed PDF (the asset on the ITP, the automatic Q.C. folder document, the log note), superseded by a second sign-off after a reopen.""" from __future__ import annotations from collections.abc import Iterator from datetime import UTC, datetime from typing import Any import pytest from closehelpers import builder_conn, flat, page_texts, pdf_reader, q1, qall, who from pshelpers import needs_pg from psycopg.rows import dict_row from qchelpers import ( SECTION_TITLE_5, approve_all, itp_log, open_qc_shop, qc_directory, reopen_itp, sign_all_blocks, sign_line, sign_off, start_itp, ) from shophelpers import Shop, make_unit, ok from packetstation.pdf import itp as itp_pdf from packetstation.settings import Settings pytestmark = needs_pg NOW = datetime(2026, 10, 17, 21, 4, tzinfo=UTC) PAGE_HEADS = ("1 OF 3: WP", "2 OF 3: C", "3 OF 3: FA") # the header sub is drawn in capitals @pytest.fixture def qc_shop(pg_settings: Settings, dev_visual: None) -> Iterator[Shop]: yield from open_qc_shop(pg_settings, qc_directory()) def build(shop: Shop, itp_id: int) -> Any: with builder_conn(shop.settings) as conn: with conn.cursor(row_factory=dict_row) as cur: row = cur.execute("SELECT * FROM itps WHERE id = %s", (itp_id,)).fetchone() return itp_pdf.build(conn, shop.settings, row, who=who(shop, "qc.quinn"), now=NOW) def test_open_itp_pdf(qc_shop: Shop) -> None: shop = qc_shop _, unit_id = make_unit(shop.settings) itp = start_itp(shop, unit_id) itp = ok(sign_line(shop("qc.quinn"), itp, "1.1")) built = build(shop, itp["id"]) assert built.bytes[:5] == b"%PDF-" assert len(pdf_reader(built.bytes).pages) == built.pages >= 3 texts = [flat(t) for t in page_texts(built.bytes)] text = " ".join(texts) assert itp["no"] in text and SECTION_TITLE_5 in text assert "Approved" in text and "INSPECTION & TEST PLAN" in text # three page groups, in order WP, C, FA firsts = [next(i for i, t in enumerate(texts) if head in t) for head in PAGE_HEADS] assert firsts == sorted(firsts) and firsts[0] == 0, firsts for t in texts: assert "OPEN, not signed off" in t assert "Controlled copy - by Quinn QC (5001) 2026-10-17 21:04 UTC - OPEN, not signed off - stamp " in t assert pdf_reader(built.bytes).metadata.title == f"ITP {itp['no']}" def test_signoff_files_the_signed_pdf(qc_shop: Shop) -> None: shop = qc_shop _, unit_id = make_unit(shop.settings) q = shop("qc.quinn") itp = sign_all_blocks(shop, approve_all(shop, start_itp(shop, unit_id))) itp = ok(sign_off(q, itp)) assert itp["status"] == "complete" asset_id = itp["signoff"]["asset_id"] assert asset_id is not None assert itp_log(itp)[0] == ("ITP signed off", "PDF filed") row = q1(shop.settings, "SELECT signoff_asset_id FROM itps WHERE id = %s", (itp["id"],)) assert str(row[0]) == asset_id a = q1(shop.settings, "SELECT original_name, mime, source FROM assets WHERE id = %s", (asset_id,)) assert a[0] == f"{itp['no']}_signed.pdf" and a[1] == "application/pdf" and a[2] == "itp_signoff" docs = qall( shop.settings, "SELECT id, type, title, auto, superseded_by, asset_id FROM qc_docs WHERE unit_id = %s ORDER BY id", (unit_id,), ) assert len(docs) == 1 assert tuple(docs[0][1:5]) == ("ITP", f"{itp['no']} — signed off", True, None) assert str(docs[0][5]) == asset_id r = q.get(f"/_blob/{asset_id}") assert r.status_code == 200 and r.content[:5] == b"%PDF-" text = flat(" ".join(page_texts(r.content))) assert itp["no"] in text and "signed off" in text and "OPEN, not signed off" not in text assert "ITP signed off" in text and "Quinn QC (5001)" in text # the builder on the complete ITP says signed off on every page for t in page_texts(build(shop, itp["id"]).bytes): assert "signed off" in flat(t) and "OPEN, not signed off" not in flat(t) # reopen and sign off again: a second PDF supersedes the first automatic document itp = ok(reopen_itp(shop("admin.ann"), itp, "Customer asked for 4.2 again")) itp = ok(sign_off(q, itp)) second = itp["signoff"]["asset_id"] assert second is not None and second != asset_id docs = qall( shop.settings, "SELECT id, superseded_by, asset_id FROM qc_docs WHERE unit_id = %s ORDER BY id", (unit_id,) ) assert len(docs) == 2 and docs[0][1] == docs[1][0] and docs[1][1] is None and str(docs[1][2]) == second