112 lines
4.6 KiB
Python
112 lines
4.6 KiB
Python
"""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
|