"""Phase 6 digital ITP through the API: start from the frozen template, line initials with their compare-and-set, unsign by the same inspector or an admin, N/A on a line and a section, customer codes and witnesses, reject and the NCR, block fields and signatures, sign-off with the PDF hook, the lock after sign-off, reopen, and the log texts. The directory adds two people to the fakedir roster through this module's fixture: qc.sue (a second Q.C. user) and qcsup.sam (a Q.C. supervisor). fakedir.py itself has neither.""" from __future__ import annotations import uuid from collections.abc import Iterator from typing import Any import pytest from pshelpers import needs_pg from qchelpers import ( MSG_BLOCK_NOT_READY, MSG_BLOCK_SIGNED, MSG_BLOCKS_UNSIGNED, MSG_ITP_COMPLETE, MSG_NCR, MSG_NCR_REQUIRED, MSG_UNSIGN, NCR, NCR2, SECTION_SIZES, SECTION_TITLE_5, TEMPLATE_ITEMS, TEMPLATE_VER, approve_all, block, enter_ncr, get_itp, item_keys, itp_log, line, na_lines, open_qc_shop, patch_block, patch_hook, patch_itp, patch_line, qc_directory, reject_line, reopen_itp, section_keys, section_na, sign_all_blocks, sign_block, sign_line, sign_off, start_itp, today_iso, today_us, unsign_block, unsign_line, upload_doc, ) from shophelpers import IT, Shop, all_rows, make_unit, ok, one, refused from packetstation.settings import Settings pytestmark = needs_pg @pytest.fixture def qc_shop(pg_settings: Settings, dev_visual: None) -> Iterator[Shop]: """A Shop on a directory that also has qc.sue (Q.C.) and qcsup.sam (Q.C. supervisor).""" yield from open_qc_shop(pg_settings, qc_directory()) def numbers(shop: Shop, unit_id: int) -> tuple[str, str]: row = one( shop.settings, "SELECT p.number, p.number || u.lot FROM units u JOIN projects p ON p.id = u.project_id WHERE u.id = %s", (unit_id,), ) return str(row[0]), str(row[1]) def new_itp(shop: Shop) -> tuple[int, dict[str, Any]]: _, unit_id = make_unit(shop.settings) return unit_id, start_itp(shop, unit_id) def done_section(shop: Shop, itp: dict[str, Any], n: str) -> dict[str, Any]: """The first line of section n approved, the rest N/A: its block is ready.""" keys = section_keys(itp, n) itp = ok(sign_line(shop("qc.quinn"), itp, keys[0])) return na_lines(shop, itp, keys[1:]) # ================================================================ start def test_start(qc_shop: Shop) -> None: shop = qc_shop from packetstation.domain.itp_template import ITP_TEMPLATE project_id, unit_id = make_unit(shop.settings) pno, uno = numbers(shop, unit_id) empty = get_itp(shop("qc.quinn"), unit_id) assert empty["itp"] is None and empty["unit"]["id"] == unit_id and empty["unit"]["number"] == uno assert shop("qc.quinn").get("/api/units/999999/itp").status_code == 404 refused(shop("wh.wade").post(f"/api/units/{unit_id}/itp", json={}), 403, "forbidden") refused(shop("clerk.clara").post(f"/api/units/{unit_id}/itp", json={}), 403, "forbidden") itp = start_itp(shop, unit_id) assert itp["unit_id"] == unit_id and itp["no"] == f"{uno}-ITP" and itp["status"] == "open" assert itp["hdr"] == {"client": "Acme Refining", "pname": "Shop project", "pno": pno, "po": "PO-55", "unit": uno} assert itp["template"] == ITP_TEMPLATE and itp["template"]["ver"] == TEMPLATE_VER assert len(item_keys(itp)) == TEMPLATE_ITEMS assert {str(s["n"]): len(s["items"]) for s in itp["template"]["sections"]} == SECTION_SIZES assert itp["pages"] == [ {"code": "WP", "name": "Weld Parts"}, {"code": "C", "name": "Code"}, {"code": "FA", "name": "Final Assembly"}, ] assert set(itp["lines"]) == set(item_keys(itp)) assert all(ln["result"] is None and ln["na"] is False for ln in itp["lines"].values()) assert itp["progress"] == {"done": 0, "total": TEMPLATE_ITEMS, "holds": 17} assert len(itp["holds"]) == 17 assert [b["key"] for b in itp["blocks"]] == ["3", "4", "5", "6", "7", "8", "9"] assert block(itp, "3")["sections"] == ["1", "2", "3"] and block(itp, "3")["scope"] == "Sections 1–3" assert block(itp, "4")["scope"] == "Section 4" and block(itp, "9")["final"] is True assert not any(b["signed"] or b["ready"] for b in itp["blocks"]) assert itp["signoff"] is None and itp["reopen"] is None assert itp["created_by"]["display_name"] == "Quinn QC" and itp["created_at"] assert itp["can"] == {"edit": True, "sign_off": False, "reopen": False} assert itp_log(itp) == [("ITP started", f"{uno}-ITP · template {TEMPLATE_VER}")] # the template is frozen in the row row = one(shop.settings, "SELECT template, no FROM itps WHERE id = %s", (itp["id"],)) assert row[0] == ITP_TEMPLATE and row[1] == f"{uno}-ITP" # a second start refused(shop("qc.quinn").post(f"/api/units/{unit_id}/itp", json={}), 409, "itp_exists") # other people read it, without edit rights other = get_itp(shop("wh.wade"), unit_id) assert other["id"] == itp["id"] and other["can"]["edit"] is False # the audit row au = one(shop.settings, "SELECT subject_type, subject_id, detail FROM audit_events WHERE action = 'itp.itp_start'") assert au is not None and au[0] == "itp" and str(au[1]) == str(itp["id"]) assert au[2]["text"] == "ITP started" and au[2]["unit_id"] == unit_id del project_id def test_header_fields(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") v = itp["version"] itp = ok(patch_itp(q, itp, {"client": "ACME"}, {"client": "Acme Refining"})) assert itp["hdr"]["client"] == "ACME" and itp["version"] == v + 1 assert itp_log(itp)[0] == ("ITP header changed", "client: ACME") itp = ok(patch_itp(q, itp, {"no": "L9-ITP-B"})) assert itp["no"] == "L9-ITP-B" and itp_log(itp)[0][0] == "ITP plan no. changed" r = refused(patch_itp(q, itp, {"client": "Other"}, {"client": "Acme Refining"}), 409, "field_conflict") assert r["current"] == {"client": "ACME"} refused(patch_itp(shop("clerk.clara"), itp, {"client": "X"}), 403, "forbidden") # ================================================================ lines def test_line_sign_and_cas(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") itp = ok(sign_line(q, itp, "1.1")) ln = line(itp, "1.1") assert ln["result"] == "approve" and ln["by"]["display_name"] == "Quinn QC" and ln["initials"] == "QQ" assert ln["at"] and itp["progress"]["done"] == 1 assert itp_log(itp)[0][0] == "ITP line 1.1 approved" and not itp_log(itp)[0][1] refused(sign_line(q, itp, "1.1"), 409, "already_signed") # a stale If-Match rv = line(itp, "1.2")["row_version"] refused(sign_line(q, itp, "1.2", if_match=rv + 5), 409, "row_conflict") itp = ok(sign_line(q, itp, "1.2", if_match=rv)) assert line(itp, "1.2")["row_version"] > rv # another Q.C. user signs with their own initials itp = ok(sign_line(shop("qc.sue"), itp, "1.3")) assert line(itp, "1.3")["initials"] == "SS" and line(itp, "1.3")["by"]["display_name"] == "Sue Second" refused(sign_line(shop("sup.wes"), itp, "1.4"), 403, "forbidden") # a key that is not in the template assert sign_line(q, itp, "9.99").status_code in (404, 422) def test_line_unsign(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") itp = ok(sign_line(q, itp, "1.1")) # another Q.C. user may not remove it refused(unsign_line(shop("qc.sue"), itp, "1.1"), 403, "forbidden", MSG_UNSIGN) itp = ok(unsign_line(q, itp, "1.1")) assert line(itp, "1.1")["result"] is None and line(itp, "1.1")["by"] is None assert itp_log(itp)[0] == ("ITP line 1.1 initial removed", f"was Quinn QC {today_us()}") # an admin, with an IT reason itp = ok(sign_line(q, itp, "1.1")) refused(unsign_line(shop("admin.ann"), itp, "1.1"), 400, "it_reason_required") itp = ok(unsign_line(shop("admin.ann"), itp, "1.1", it_reason=IT)) assert line(itp, "1.1")["result"] is None log_row = itp["log"][0] assert log_row["by"]["display_name"] == "Ann Admin" # refused once the block is signed itp = done_section(shop, itp, "4") first = section_keys(itp, "4")[0] itp = ok(sign_block(q, itp, "4")) refused(unsign_line(q, itp, first), 409, "block_signed", MSG_BLOCK_SIGNED) # an open line has nothing to remove r = unsign_line(q, itp, "1.5") assert r.status_code in (404, 409, 422), r.text def test_line_na_and_section_na(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") itp = ok(patch_line(q, itp, "1.2", na=True)) assert line(itp, "1.2")["na"] is True and itp["progress"]["done"] == 1 assert itp_log(itp)[0] == ("ITP line 1.2 updated", "marked N/A") refused(sign_line(q, itp, "1.2"), 409, "line_na") itp = ok(patch_line(q, itp, "1.2", na=False)) assert line(itp, "1.2")["na"] is False and itp_log(itp)[0] == ("ITP line 1.2 updated", "applies again") itp = ok(sign_line(q, itp, "1.1")) refused(patch_line(q, itp, "1.1", na=True), 409, "already_signed") # a whole section v = itp["version"] itp = ok(section_na(q, itp, "5")) assert all(line(itp, k)["na"] for k in section_keys(itp, "5")) assert itp_log(itp)[0] == ("ITP section 5 marked N/A", SECTION_TITLE_5) and itp["version"] > v assert block(itp, "5")["ready"] is True and block(itp, "5")["all_na"] is True itp = ok(section_na(q, itp, "5", na=False)) assert not any(line(itp, k)["na"] for k in section_keys(itp, "5")) assert itp_log(itp)[0] == ("ITP section 5 applies again", SECTION_TITLE_5) # refused once a line of the section is approved refused(section_na(q, itp, "1"), 409, "already_signed") refused(section_na(shop("clerk.clara"), itp, "5"), 403, "forbidden") def test_line_customer_and_note(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") refused(patch_line(q, itp, "1.3", cust_code="X"), 422, "invalid_field") itp = ok(patch_line(q, itp, "1.3", cust_code="W")) assert line(itp, "1.3")["cust_code"] == "W" assert itp_log(itp)[0] == ("ITP line 1.3 updated", "customer code W") itp = ok(patch_line(q, itp, "1.3", cust_code="")) assert not line(itp, "1.3")["cust_code"] assert itp_log(itp)[0] == ("ITP line 1.3 updated", "customer code as template") itp = ok(patch_line(q, itp, "1.3", cust_by="Rick Inspector")) ln = line(itp, "1.3") assert ln["cust_by"] == "Rick Inspector" and ln["cust_at"] == today_iso() assert ln["cust_rec"]["display_name"] == "Quinn QC" assert itp_log(itp)[0] == ("ITP line 1.3 updated", "customer Rick Inspector") itp = ok(patch_line(q, itp, "1.3", cust_by="Rick Inspector", cust_at="2026-10-01")) assert line(itp, "1.3")["cust_at"] == "2026-10-01" itp = ok(patch_line(q, itp, "1.3", cust_by="")) assert not line(itp, "1.3")["cust_by"] and line(itp, "1.3")["cust_at"] is None assert itp_log(itp)[0] == ("ITP line 1.3 updated", "customer cleared") itp = ok(patch_line(q, itp, "1.3", cust_code="H", note="Reviewed WPS-101 R2")) assert line(itp, "1.3")["note"] == "Reviewed WPS-101 R2" assert itp_log(itp)[0] == ("ITP line 1.3 updated", "customer code H, note") for code in ("R", "R/AU", "AU", "W", "H", "NA"): assert ok(patch_line(q, itp, "1.4", cust_code=code))["lines"]["1.4"]["cust_code"] == code # nothing changed: no log, no version bump itp = ok(patch_line(q, itp, "1.4", cust_code="NA")) before = (len(itp["log"]), itp["version"]) itp = ok(patch_line(q, itp, "1.4", cust_code="NA")) assert (len(itp["log"]), itp["version"]) == before # limits r = patch_line(q, itp, "1.5", note="x" * 501) assert r.status_code == 422 or len(r.json()["lines"]["1.5"]["note"]) <= 500, r.text def test_reject_and_ncr(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") # reject without an NCR itp = ok(reject_line(q, itp, "1.4", note="Manual is rev C")) ln = line(itp, "1.4") assert ln["result"] == "reject" and ln["ncr"] is None and ln["ncr_missing"] is True assert [r["kind"] for r in ln["rejects"]] == ["reject"] and ln["rejects"][0]["note"] == "Manual is rev C" assert itp_log(itp)[0] == ("ITP line 1.4 rejected", "NCR # pending · Manual is rev C") assert any(h["n"] == "1.4" and h["rejected"] for h in itp["holds"]) refused(sign_line(q, itp, "1.4"), 409, "ncr_required", MSG_NCR_REQUIRED) refused(patch_line(q, itp, "1.4", na=True), 409, "line_rejected") r = reject_line(q, itp, "1.4", note="again") assert r.status_code == 409, r.text # the NCR later refused(enter_ncr(q, itp, "1.4", "NCR-26-1"), 422, "invalid_ncr", MSG_NCR) itp = ok(enter_ncr(q, itp, "1.4", " ncr-2026-0021")) ln = line(itp, "1.4") assert ln["ncr"] == NCR and ln["ncr_missing"] is False assert [r["kind"] for r in ln["rejects"]] == ["reject", "ncr"] assert itp_log(itp)[0] == ("ITP line 1.4 NCR number entered", f"NCR {NCR}") refused(enter_ncr(q, itp, "1.4", NCR2), 409, "no_open_reject") refused(enter_ncr(q, itp, "1.5", NCR2), 409, "no_open_reject") itp = ok(sign_line(q, itp, "1.4")) assert line(itp, "1.4")["result"] == "approve" assert itp_log(itp)[0] == ("ITP line 1.4 approved", f"after NCR {NCR}") assert not any(h["n"] == "1.4" for h in itp["holds"]) # the reject history survives the approve assert len(line(itp, "1.4")["rejects"]) == 2 rows = all_rows(shop.settings, "SELECT kind, ncr FROM itp_rejects WHERE itp_id = %s ORDER BY id", (itp["id"],)) assert [tuple(r) for r in rows] == [("reject", None), ("ncr", NCR)] # reject with the NCR at once itp = ok(reject_line(q, itp, "1.5", ncr="ncr-2026-0022", note="Map missing W4")) assert line(itp, "1.5")["ncr"] == NCR2 and line(itp, "1.5")["ncr_missing"] is False assert itp_log(itp)[0] == ("ITP line 1.5 rejected", f"NCR {NCR2} · Map missing W4") itp = ok(sign_line(q, itp, "1.5")) assert itp_log(itp)[0] == ("ITP line 1.5 approved", f"after NCR {NCR2}") # refused on an approved line and on an N/A line assert reject_line(q, itp, "1.5", note="x").status_code == 409 itp = ok(patch_line(q, itp, "1.6", na=True)) assert reject_line(q, itp, "1.6", note="x").status_code == 409 refused(reject_line(q, itp, "1.7", ncr="bad one"), 422, "invalid_ncr") # ================================================================ blocks def test_block_fields(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") itp = ok(patch_block(q, itp, "3", qn="QN-17")) assert block(itp, "3")["qn"] == "QN-17" and itp_log(itp)[0] == ("ITP block 3 QN # changed", "QN-17") itp = ok(patch_block(q, itp, "3", remarks="Fit-up per drawing")) assert itp_log(itp)[0] == ("ITP block 3 remarks changed", "Fit-up per drawing") itp = ok(patch_block(q, itp, "3", cust_name="Rick Inspector")) b = block(itp, "3") assert b["cust_name"] == "Rick Inspector" and b["cust_at"] == today_iso() assert b["cust_by"]["display_name"] == "Quinn QC" and b["signed"] is False assert itp_log(itp)[0] == ("ITP block 3 customer complete recorded", "Rick Inspector") itp = ok(patch_block(q, itp, "3", cust_clear=True)) b = block(itp, "3") assert not b["cust_name"] and b["cust_at"] is None and b["cust_by"] is None assert itp_log(itp)[0][0] == "ITP block 3 customer complete cleared" assert b["qn"] == "QN-17" refused(patch_block(shop("sup.wes"), itp, "3", qn="QN-1"), 403, "forbidden") def test_block_sign_gate(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") refused(sign_block(q, itp, "4"), 409, "block_not_ready", MSG_BLOCK_NOT_READY) itp = done_section(shop, itp, "4") assert block(itp, "4")["ready"] is True and block(itp, "4")["all_na"] is False r = refused(sign_block(q, itp, "4", sign=None), 400, "signature_required") assert r.get("meaning") == "Sign an ITP block" r = sign_block(q, itp, "4", sign={"factor": "password", "secret": "wrong-one"}) assert 400 <= r.status_code < 500, r.text itp = ok(sign_block(q, itp, "4")) b = block(itp, "4") assert b["signed"] is True and b["signed_by"]["display_name"] == "Quinn QC" and b["signed_at"] assert itp["log"][0]["action"] == "ITP block 4 signed" and itp["log"][0]["note"] == "Section 4" assert itp["log"][0]["signed"] is True refused(sign_block(q, itp, "4"), 409, "already_signed") sig = one( shop.settings, "SELECT subject_type, subject_id, packet_id, meaning FROM signatures ORDER BY id DESC LIMIT 1" ) assert (sig[0], str(sig[1]), sig[2], sig[3]) == ("itp", str(itp["id"]), None, "Sign an ITP block") # a block of all N/A lines signs itp = ok(section_na(q, itp, "5")) itp = ok(sign_block(q, itp, "5")) assert block(itp, "5")["signed"] is True and block(itp, "5")["all_na"] is True # block 3 covers sections 1 to 3 itp = ok(section_na(q, itp, "2")) itp = ok(section_na(q, itp, "3")) assert block(itp, "3")["ready"] is False itp = ok(section_na(q, itp, "1")) assert block(itp, "3")["ready"] is True itp = ok(sign_block(q, itp, "3")) assert itp["log"][0]["note"] == "Sections 1–3" refused(sign_block(shop("sup.wes"), itp, "6"), 403, "forbidden") def test_block_unsign(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") itp = done_section(shop, itp, "4") itp = ok(patch_block(q, itp, "4", qn="QN-4")) itp = ok(sign_block(q, itp, "4")) refused(unsign_block(shop("qc.sue"), itp, "4"), 403, "forbidden") itp = ok(unsign_block(q, itp, "4")) b = block(itp, "4") assert b["signed"] is False and b["signed_by"] is None and b["signed_at"] is None and b["qn"] == "QN-4" assert itp_log(itp)[0][0] == "ITP block 4 signature removed" # an admin may, with an IT reason itp = ok(sign_block(q, itp, "4")) itp = ok(unsign_block(shop("admin.ann"), itp, "4", it_reason=IT)) assert block(itp, "4")["signed"] is False # the line can be unsigned again now itp = ok(unsign_line(q, itp, section_keys(itp, "4")[0])) # ================================================================ sign-off, lock, reopen def finish_all(shop: Shop, itp: dict[str, Any]) -> dict[str, Any]: return sign_all_blocks(shop, approve_all(shop, itp)) def test_signoff_without_pdf(qc_shop: Shop) -> None: shop = qc_shop unit_id, itp = new_itp(shop) q = shop("qc.quinn") refused(sign_off(q, itp), 409, "blocks_unsigned", MSG_BLOCKS_UNSIGNED) itp = approve_all(shop, itp) assert itp["progress"] == {"done": TEMPLATE_ITEMS, "total": TEMPLATE_ITEMS, "holds": 0} assert itp["holds"] == [] and itp["can"]["sign_off"] is False blocks = [str(b["key"]) for b in itp["blocks"]] for key in blocks[:-1]: itp = ok(sign_block(q, itp, key)) refused(sign_off(q, itp), 409, "blocks_unsigned") itp = ok(sign_block(q, itp, blocks[-1])) assert itp["can"]["sign_off"] is True r = refused(sign_off(q, itp, sign=None), 400, "signature_required") assert r.get("meaning") == "Sign off the ITP" refused(sign_off(shop("clerk.clara"), itp), 403, "forbidden") itp = ok(sign_off(q, itp)) assert itp["status"] == "complete" and itp["can"] == {"edit": False, "sign_off": False, "reopen": False} assert itp["signoff"]["by"]["display_name"] == "Quinn QC" and itp["signoff"]["at"] assert itp["signoff"]["asset_id"] is None assert itp_log(itp)[0] == ("ITP signed off", "PDF filing reserved for phase 7") and itp["log"][0]["signed"] row = one( shop.settings, "SELECT signoff_asset_id, signoff_signature_id IS NOT NULL FROM itps WHERE id = %s", (itp["id"],) ) assert tuple(row) == (None, True) assert all_rows(shop.settings, "SELECT count(*) FROM qc_docs WHERE unit_id = %s", (unit_id,))[0][0] == 0 # the admin may reopen; qc.quinn and qc.sue may not assert get_itp(shop("admin.ann"), unit_id)["can"]["reopen"] is True assert get_itp(q, unit_id)["can"]["reopen"] is False def test_locked_after_signoff_and_reopen(qc_shop: Shop) -> None: shop = qc_shop _, itp = new_itp(shop) q = shop("qc.quinn") itp = ok(sign_off(q, finish_all(shop, itp))) first = section_keys(itp, "1")[0] for r in ( patch_itp(q, itp, {"client": "X"}), sign_line(q, itp, first), unsign_line(q, itp, first), patch_line(q, itp, first, note="late"), section_na(q, itp, "5"), reject_line(q, itp, first, ncr=NCR), enter_ncr(q, itp, first, NCR), patch_block(q, itp, "3", qn="QN-9"), sign_block(q, itp, "3"), unsign_block(q, itp, "3"), sign_off(q, itp), ): refused(r, 409, "itp_complete", MSG_ITP_COMPLETE) # reopen: Q.C. users may not; the Q.C. supervisor and the admin may refused(reopen_itp(q, itp, "Customer asked for 4.2 again"), 403, "forbidden") refused(reopen_itp(shop("qc.sue"), itp, "Customer asked for 4.2 again"), 403, "forbidden") r = reopen_itp(shop("admin.ann"), itp, "short") assert r.status_code == 400 and r.json()["code"] in ("reason_required", "it_reason_required"), r.text itp = ok(reopen_itp(shop("admin.ann"), itp, "Customer asked for 4.2 again")) assert itp["status"] == "open" and itp["reopen"]["reason"] == "Customer asked for 4.2 again" assert itp["reopen"]["by"]["display_name"] == "Ann Admin" and itp["reopen"]["at"] assert itp_log(itp)[0] == ("ITP reopened", "Customer asked for 4.2 again") au = one(shop.settings, "SELECT it_reason FROM audit_events WHERE action = 'itp.itp_reopen'") assert au is not None and au[0] == "Customer asked for 4.2 again" # the signatures stay; the inspector undoes what must change assert all(b["signed"] for b in itp["blocks"]) and itp["can"]["sign_off"] is True refused(reopen_itp(shop("admin.ann"), itp, "Customer asked again"), 409, "itp_open") itp = ok(patch_itp(q, itp, {"client": "ACME"})) # a Q.C. supervisor reopens too itp = ok(sign_off(q, itp)) itp = ok(reopen_itp(shop("qcsup.sam"), itp, "Re-inspect the nozzle welds")) assert itp["status"] == "open" and itp["reopen"]["by"]["display_name"] == "Sam Qcsup" def test_signoff_with_pdf_hook(qc_shop: Shop, monkeypatch: pytest.MonkeyPatch) -> None: shop = qc_shop unit_id, itp = new_itp(shop) q = shop("qc.quinn") calls: list[Any] = [] asset_ids = [upload_doc(q, "itp-1.pdf")["id"], upload_doc(q, "itp-2.pdf")["id"]] def fake_hook(conn: Any, itp_row: Any, actor: Any) -> uuid.UUID: calls.append(itp_row) return uuid.UUID(asset_ids[len(calls) - 1]) patch_hook(monkeypatch, fake_hook) itp = ok(sign_off(q, finish_all(shop, itp))) assert len(calls) == 1 assert itp["signoff"]["asset_id"] == asset_ids[0] assert itp_log(itp)[0] == ("ITP signed off", "PDF filed") row = one(shop.settings, "SELECT signoff_asset_id FROM itps WHERE id = %s", (itp["id"],)) assert str(row[0]) == asset_ids[0] docs = all_rows( shop.settings, "SELECT id, type, title, coalesce(rev, ''), note, auto, superseded_by, asset_id" " FROM qc_docs WHERE unit_id = %s", (unit_id,), ) assert len(docs) == 1 d = docs[0] assert (d[1], d[2], d[3], d[5], d[6]) == ("ITP", f"{itp['no']} — signed off", "", True, None) assert d[4] == "Filed automatically when the digital ITP was signed off." and str(d[7]) == asset_ids[0] # a second sign-off (after a reopen) supersedes the earlier auto document itp = ok(reopen_itp(shop("admin.ann"), itp, "Customer asked for 4.2 again")) itp = ok(sign_off(q, itp)) assert itp["signoff"]["asset_id"] == asset_ids[1] docs = all_rows( 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 def test_log_order_and_filed_packet(qc_shop: Shop) -> None: """The ITP belongs to the unit: a filed packet in the unit does not stop any ITP write. The log is newest first, at most 600 rows, and every write wrote one row.""" shop = qc_shop from qchelpers import filed_packet _, unit_id = make_unit(shop.settings) filed_packet(shop, unit_id) itp = start_itp(shop, unit_id) q = shop("qc.quinn") itp = ok(sign_line(q, itp, "1.1")) itp = ok(patch_line(q, itp, "1.2", na=True)) itp = ok(reject_line(q, itp, "1.3", ncr=NCR)) itp = ok(patch_block(q, itp, "3", qn="QN-1")) itp = ok(patch_itp(q, itp, {"po": "PO-99"})) assert [a for a, _ in itp_log(itp)] == [ "ITP header changed", "ITP block 3 QN # changed", "ITP line 1.3 rejected", "ITP line 1.2 updated", "ITP line 1.1 approved", "ITP started", ] ids = [r["id"] for r in itp["log"]] assert ids == sorted(ids, reverse=True) assert all(r["by"]["display_name"] == "Quinn QC" and r["by_name"] == "Quinn QC" for r in itp["log"]) count = all_rows(shop.settings, "SELECT count(*) FROM itp_log WHERE itp_id = %s", (itp["id"],))[0][0] assert count == 6 # the rest of the ITP still works next to the filed packet: unsign, sign a block, sign off itp = ok(unsign_line(q, itp, "1.1")) itp = ok(sign_off(q, finish_all(shop, itp))) assert itp["status"] == "complete"