The shop at parity with the old build. Supervisor screen (department chips, CO headline, numbers, Change orders
on hold, station grid with clocks and CO tags, boards) and Stall screen (station picker, hero, My packets,
Submitted). Shop steps on the packet page: assign with station, welder picker and stamp; pull back; Start, Pause,
Resume; Submit with a PIN signature and the missing steps as links; review with Send to Q.C., Return to station
and Skip Q.C. route; a lead's read-only review. Weld map (pdf.js viewer with pan, pinch, zoom, full screen; marks
with leader lines; weld sheet; weld record with field CAS; carried marks). Heat map (heat table placed, dragged,
sized, removed; legacy amber tags; borrowed heat map read-only). Checklist tab with templates from settings.
VOID stamps on voided pages. Change orders: CO-n labels, supervisor verify with signature, approval copies the
heat table to the new sheet and the BOM merge keeps the shop's values. Server clock of record with VISUAL ticket
hours beside it. Presence per packet over SSE. Migration 0006 (bfx, bfy, carried_from, checklist_templates).
New endpoints /api/shop/board, /api/shop/stations, /api/shop/stall, /api/departments/{code}/people.
Tests: policy oracle for every shop action, role and status; clock arithmetic; concurrent inserts; CO apply;
frontend geometry, heat table, clock, checklist, weld sheet, screens; seven Playwright scenarios. Docs updated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
487 lines
26 KiB
Python
487 lines
26 KiB
Python
"""The packet API: hand drafts, header CAS, drawing pages, transitions, filing and the seal, the boards, assets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fakedir import FakeDirectory
|
|
from fastapi.testclient import TestClient
|
|
from pkthelpers import (
|
|
SIGN,
|
|
as_user,
|
|
attach_drawing,
|
|
get,
|
|
happy_path,
|
|
make_draft,
|
|
make_unit,
|
|
ok_step,
|
|
phase2_client,
|
|
ready_draft,
|
|
seed_users,
|
|
set_header,
|
|
step,
|
|
tiny_pdf,
|
|
upload,
|
|
)
|
|
from pshelpers import app_conn, needs_pg
|
|
from psycopg.types.json import Jsonb
|
|
|
|
from packetstation import events
|
|
from packetstation.settings import Settings
|
|
|
|
pytestmark = needs_pg
|
|
|
|
|
|
@pytest.fixture
|
|
def pg_client2(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[TestClient]:
|
|
with phase2_client(pg_settings, fake_dir) as client:
|
|
seed_users(client)
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
def unit(pg_settings: Settings) -> tuple[int, int]:
|
|
with app_conn(pg_settings) as conn:
|
|
return make_unit(conn)
|
|
|
|
|
|
def _one(settings: Settings, sql: str, params: Any = ()) -> Any:
|
|
with app_conn(settings) as conn:
|
|
return conn.execute(sql, params).fetchone()
|
|
|
|
|
|
def _all(settings: Settings, sql: str, params: Any = ()) -> list[Any]:
|
|
with app_conn(settings) as conn:
|
|
return conn.execute(sql, params).fetchall()
|
|
|
|
|
|
# ---------------------------------------------------------------- hand drafts
|
|
|
|
|
|
def test_hand_draft_by_clerical(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
assert pkt["status"] == "draft" and pkt["source_kind"] == "hand" and pkt["source"] == {"kind": "hand"}
|
|
assert pkt["flags"] == ["hand"] and pkt["drawing_state"] == "none" and pkt["version"] == 1
|
|
assert pkt["flag"] == "Made by hand - check it and send it on"
|
|
assert pkt["holder"] == "Clerical" and pkt["dept"] == {"code": "03", "name": "Weld Parts", "weld": True}
|
|
wo = pkt["work_orders"][0]
|
|
assert (wo["wo_base"], wo["lot"], wo["split"], wo["sub"]) == ("L670001", "001", "0", "0")
|
|
assert pkt["unit"]["number"].endswith("001") and pkt["unit"]["project"]["customer"] == "Acme Refining"
|
|
assert [i["key"] for i in pkt["checklist_items"]][:2] == ["dwg", "heat"]
|
|
assert "D-1001 Rev B" in pkt["checklist_items"][0]["text"] and len(pkt["qc_items"]) == 10
|
|
assert "edit_header" in pkt["can"] and "send_to_warehouse" not in pkt["can"]
|
|
assert pkt["blocked"]["send_to_warehouse"] == [
|
|
"Say whether this packet needs a heat map.",
|
|
"Attach the drawing first.",
|
|
]
|
|
log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()
|
|
assert [r["action"] for r in log] == ["Packet created by hand"] and log[0]["by"]["display_name"] == "Clara Clerk"
|
|
row = _one(pg_settings, "SELECT action, detail FROM audit_events WHERE action = 'packet.created'")
|
|
assert row[1]["source"] == "hand" and row[1]["work_orders"] == 1
|
|
|
|
|
|
def test_hand_draft_rules(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
from pkthelpers import draft_body
|
|
|
|
as_user(pg_client2, "wh.wade")
|
|
r = pg_client2.post("/api/packets", json=draft_body(unit[1]))
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
as_user(pg_client2, "clerk.clara")
|
|
r = pg_client2.post("/api/packets", json=draft_body(unit[1], dept_code="QC"))
|
|
assert r.status_code == 422 and r.json()["code"] == "invalid_dept"
|
|
r = pg_client2.post("/api/packets", json=draft_body(unit[1], work_orders=[{"wo_base": 670001}]))
|
|
assert r.status_code == 422 # work order keys are text, never numbers
|
|
with app_conn(pg_settings) as conn:
|
|
conn.execute("UPDATE units SET status = 'closed' WHERE id = %s", (unit[1],))
|
|
r = pg_client2.post("/api/packets", json=draft_body(unit[1]))
|
|
assert r.status_code == 409 and r.json()["code"] == "unit_closed"
|
|
r = pg_client2.post("/api/packets", json=draft_body(unit[1]) | {"pad": "x" * 210_000})
|
|
assert r.status_code == 413 and r.json()["code"] == "too_large"
|
|
assert pg_client2.get("/api/packets/999999").status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------- header CAS
|
|
|
|
|
|
def test_patch_header_cas(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
pid = pkt["id"]
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "New title"}, "base": {"title": pkt["title"]}})
|
|
assert r.status_code == 200 and r.json()["title"] == "New title" and r.json()["version"] == 2
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "Third"}, "base": {"title": "Spool 4 inlet"}})
|
|
assert r.status_code == 409 and r.json()["code"] == "field_conflict"
|
|
assert r.json()["current"] == {"title": "New title"}
|
|
# heat map chips
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"heat_map": True}, "base": {"heat_map": None}})
|
|
assert r.status_code == 200 and r.json()["heat_map"] is True
|
|
assert pg_client2.get(f"/api/packets/{pid}/log").json()[0]["action"] == "Heat map required"
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"heat_map": False}, "base": {"heat_map": True}})
|
|
assert r.status_code == 200 and r.json()["heat_map"] is False
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"status": "filed"}})
|
|
assert r.status_code == 422
|
|
as_user(pg_client2, "wh.wade")
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "Mine"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
assert r.json()["detail"] == (
|
|
"Your access doesn't allow changing the packet details on this packet. It's with Clerical right now."
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------- drawing pages and assets
|
|
|
|
|
|
def test_attach_drawing_needs_signature(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
a = upload(pg_client2, tiny_pdf(2), name="D-1001 B.pdf")
|
|
assert a.status_code == 201, a.text
|
|
asset = a.json()
|
|
assert asset["mime"] == "application/pdf" and asset["pages"] == 2 and asset["url"] == f"/_blob/{asset['id']}"
|
|
url = f"/api/packets/{pkt['id']}/pages"
|
|
r = pg_client2.post(url, json={"asset_id": asset["id"], "type": "drawing"})
|
|
assert r.status_code == 400 and r.json()["code"] == "signature_required"
|
|
assert r.json()["meaning"] == "Attach a drawing by hand"
|
|
r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": {"factor": "password", "secret": "nope"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "wrong_secret" and r.json()["attempts_left"] == 2
|
|
fails = _one(pg_settings, "SELECT stepup_failures FROM sessions WHERE revoked_at IS NULL ORDER BY created_at DESC")
|
|
assert fails[0] == 1 # the wrong attempt was kept although the write was refused
|
|
r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": SIGN})
|
|
assert r.status_code == 200, r.text
|
|
out = r.json()
|
|
assert [(p["n"], p["pn"], p["dwg"]) for p in out["pages"]] == [(1, 1, "D-1001"), (2, 2, "D-1001")]
|
|
assert out["drawing_state"] == "hand" and out["pages"][0]["url"] == f"/_blob/{asset['id']}"
|
|
log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]
|
|
assert log["action"] == "Drawing attached by hand" and log["signed"] is True
|
|
r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": SIGN})
|
|
assert r.status_code == 409 and r.json()["code"] == "already_attached"
|
|
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/pages/2", json={"reason": "wrong sheet"})
|
|
assert r.status_code == 200 and r.json()["pages"] == [] and r.json()["drawing_state"] == "none"
|
|
|
|
|
|
def test_assets_blob_and_limits(
|
|
pg_client2: TestClient, pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None
|
|
) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
data = tiny_pdf(1)
|
|
a = upload(pg_client2, data, name="..\\evil/name.pdf").json()
|
|
assert a["original_name"] == "name.pdf" and a["size"] == len(data)
|
|
again = upload(pg_client2, data, name="other.pdf").json()
|
|
assert again["id"] == a["id"]
|
|
assert pg_client2.get(f"/api/assets/{a['id']}").json()["sha256"] == a["sha256"]
|
|
b = pg_client2.get(a["url"])
|
|
assert b.status_code == 200 and b.content == data
|
|
assert b.headers["x-content-type-options"] == "nosniff"
|
|
assert b.headers["content-disposition"] == 'inline; filename="name.pdf"'
|
|
assert b.headers["cache-control"] == "private, max-age=3600" and a["sha256"] in b.headers["etag"]
|
|
assert pg_client2.get(a["url"] + "?download=1").headers["content-disposition"].startswith("attachment;")
|
|
assert pg_client2.get("/_blob/00000000-0000-0000-0000-000000000000").status_code == 404
|
|
r = upload(pg_client2, b"just some text, not a document", name="x.txt", mime="text/plain")
|
|
assert r.status_code == 415 and r.json()["code"] == "unsupported_type"
|
|
r = upload(pg_client2, b"%PDF-1.7\nthis is not really a pdf at all\n%%EOF\n", name="bad.pdf")
|
|
assert r.status_code == 422 and r.json()["code"] == "bad_pdf"
|
|
small = pg_settings.model_copy(update={"upload_max_bytes": 2048})
|
|
with phase2_client(small, fake_dir) as c2:
|
|
as_user(c2, "clerk.clara")
|
|
r = upload(c2, b"%PDF-1.4\n" + b"0" * 5000)
|
|
assert r.status_code == 413 and r.json()["code"] == "too_large"
|
|
with TestClient(c2.app) as anon:
|
|
assert anon.get(a["url"]).status_code == 401
|
|
|
|
|
|
# ---------------------------------------------------------------- transitions
|
|
|
|
|
|
def test_send_to_warehouse(
|
|
pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int], monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
r = step(pg_client2, pkt, "send_to_warehouse")
|
|
assert r.status_code == 403 and r.json() == {
|
|
"detail": "Say whether this packet needs a heat map.",
|
|
"code": "precondition",
|
|
}
|
|
pkt = set_header(pg_client2, pkt["id"], heat_map=False).json()
|
|
r = step(pg_client2, pkt, "send_to_warehouse")
|
|
assert r.status_code == 403 and r.json()["detail"] == "Attach the drawing first."
|
|
pkt = attach_drawing(pg_client2, pkt["id"])
|
|
assert "send_to_warehouse" in pkt["can"] and "send_to_warehouse" not in pkt["blocked"]
|
|
r = pg_client2.post(
|
|
f"/api/packets/{pkt['id']}/transitions",
|
|
json={"action": "send_to_warehouse", "expect": {"status": "warehouse", "dept": "03"}},
|
|
)
|
|
assert r.status_code == 409 and r.json()["code"] == "stale_step"
|
|
assert r.json()["current"] == {"status": "draft", "dept": "03", "version": pkt["version"]}
|
|
published: list[dict[str, Any]] = []
|
|
real = events.publish
|
|
monkeypatch.setattr(events, "publish", lambda conn, payload: (published.append(payload), real(conn, payload)))
|
|
out = ok_step(pg_client2, pkt, "send_to_warehouse")
|
|
assert out["status"] == "warehouse" and out["version"] == pkt["version"] + 1 and out["holder"] == "Warehouse"
|
|
assert out["wh"]["stage"] == "incoming" and out["wh"]["by"]["display_name"] == "Clara Clerk"
|
|
assert out["wh_sent_at"] and out["wh_skipped"] is False
|
|
assert published == [
|
|
{"event": "changed", "packet_id": pkt["id"], "version": out["version"], "status": "warehouse", "dept": "03"}
|
|
]
|
|
tr = _one(
|
|
pg_settings,
|
|
"SELECT action, from_status, to_status, by FROM packet_transitions WHERE packet_id = %s",
|
|
(pkt["id"],),
|
|
)
|
|
assert tr[:3] == ("send_to_warehouse", "draft", "warehouse")
|
|
lg = _one(
|
|
pg_settings, "SELECT action, transition_id FROM packet_log WHERE packet_id = %s ORDER BY id DESC", (pkt["id"],)
|
|
)
|
|
assert lg[0] == "Sent to warehouse" and lg[1] is not None
|
|
au = _one(pg_settings, "SELECT detail FROM audit_events WHERE action = 'packet.send_to_warehouse'")
|
|
assert au[0]["from"] == "draft" and au[0]["to"] == "warehouse"
|
|
as_user(pg_client2, "clerk.clara")
|
|
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "wrong_status"
|
|
|
|
|
|
def test_happy_path_file_and_reopen(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
pkt = happy_path(pg_client2, pg_settings, unit[1])
|
|
pid = pkt["id"]
|
|
seal = _one(pg_settings, "SELECT key_id, signature_id, broken_at FROM packet_seals WHERE packet_id = %s", (pid,))
|
|
assert seal[1] is not None and seal[2] is None
|
|
view = pg_client2.get(f"/api/packets/{pid}/seal").json()
|
|
assert view["valid"] is True and view["seal"]["key_id"] == seal[0]
|
|
actions = [
|
|
r[0]
|
|
for r in _all(pg_settings, "SELECT action FROM packet_transitions WHERE packet_id = %s ORDER BY id", (pid,))
|
|
]
|
|
assert actions == [
|
|
"send_to_warehouse",
|
|
"wh_release",
|
|
"assign",
|
|
"start",
|
|
"submit",
|
|
"send_to_qc",
|
|
"qc_accept",
|
|
"file",
|
|
]
|
|
assert pg_client2.get(f"/api/packets/{pid}/log").json()[0]["action"] == "Filed in the data book"
|
|
# filed: every write is refused, Admin included
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"drawing": "D-2"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "filed"
|
|
r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "x"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "filed"
|
|
as_user(pg_client2, "admin.ann")
|
|
r = pg_client2.patch(
|
|
f"/api/packets/{pid}", json={"set": {"drawing": "D-2"}, "it_reason": "Fix a typo in the drawing"}
|
|
)
|
|
assert r.status_code == 403 and r.json()["code"] == "filed"
|
|
# the data book date: Clerical and Q.C., not the warehouse
|
|
for sam, code in (("clerk.clara", 200), ("qc.quinn", 200), ("wh.wade", 403)):
|
|
as_user(pg_client2, sam)
|
|
r = pg_client2.post(f"/api/packets/{pid}/databook", json={})
|
|
assert r.status_code == code, (sam, r.text)
|
|
assert pg_client2.get(f"/api/packets/{pid}/seal").json()["valid"] is True
|
|
# reopen by Admin with a reason and a signature breaks the seal
|
|
as_user(pg_client2, "clerk.clara")
|
|
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed", "sign": SIGN})
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
as_user(pg_client2, "admin.ann")
|
|
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "short", "sign": SIGN})
|
|
assert r.status_code == 400 and r.json()["code"] == "reason_required"
|
|
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed"})
|
|
assert r.status_code == 400 and r.json()["code"] == "signature_required"
|
|
r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed", "sign": SIGN})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["status"] == "complete" and r.json()["seal"]["broken_at"] is not None
|
|
view = pg_client2.get(f"/api/packets/{pid}/seal").json()
|
|
assert view["seal"] is None and view["seals"][0]["broken_reason"] == "Wrong heat number filed"
|
|
|
|
|
|
def test_co_hold_blocks_filing(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
pid = pkt["id"]
|
|
with app_conn(pg_settings) as conn:
|
|
conn.execute("UPDATE packets SET status = 'complete', routed_to = 'shipping' WHERE id = %s", (pid,))
|
|
co = conn.execute(
|
|
"INSERT INTO change_orders (packet_id, co_no, origin, review) VALUES (%s, 1, 'manual', 'approved')"
|
|
" RETURNING id",
|
|
(pid,),
|
|
).fetchone()[0]
|
|
conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, '03')", (co,))
|
|
pkt = get(pg_client2, pid)
|
|
assert pkt["open_cos"] == [1] and "file" in pkt["blocked"]
|
|
r = step(pg_client2, pkt, "file", sign=SIGN)
|
|
assert r.status_code == 403 and r.json()["code"] == "co_hold"
|
|
assert r.json()["detail"] == "Change order CO-1 must be verified first by: 03 Weld Parts."
|
|
as_user(pg_client2, "admin.ann")
|
|
r = step(pg_client2, pkt, "file", sign=SIGN, it_reason="Close out the old job now")
|
|
assert r.status_code == 403 and r.json()["code"] == "co_hold"
|
|
card = pg_client2.get("/api/packets", params={"status": "complete"}).json()["packets"][0]
|
|
assert card["open_cos"] == [1] and "file" in card["blocked"]
|
|
|
|
|
|
def test_admin_bypass_needs_it_reason(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
as_user(pg_client2, "admin.ann")
|
|
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "IT title"}})
|
|
assert r.status_code == 400 and r.json()["code"] == "it_reason_required"
|
|
r = pg_client2.patch(
|
|
f"/api/packets/{pkt['id']}", json={"set": {"title": "IT title"}, "it_reason": "Ticket 4411 fix"}
|
|
)
|
|
assert r.status_code == 200 and r.json()["title"] == "IT title"
|
|
log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]
|
|
assert log["it_reason"] == "Ticket 4411 fix" and log["by"]["display_name"] == "Ann Admin"
|
|
au = _one(pg_settings, "SELECT it_reason, detail FROM audit_events WHERE action = 'packet.edit_header'")
|
|
assert au[0] == "Ticket 4411 fix" and au[1]["fields"] == ["title"]
|
|
|
|
|
|
def test_return_to_clerical_only_when_wh_skipped(
|
|
pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]
|
|
) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = ready_draft(pg_client2, unit[1])
|
|
r = step(pg_client2, pkt, "release_to_shop", dept="QC")
|
|
assert r.status_code == 422 and r.json()["code"] == "invalid_dept"
|
|
pkt = ok_step(pg_client2, pkt, "release_to_shop", dept="06", note="Rush job")
|
|
assert pkt["status"] == "ready" and pkt["dept"]["code"] == "06" and pkt["wh_skipped"] is True
|
|
assert pkt["stage"]["dept_code"] == "06" and pkt["return_note"] == "Rush job"
|
|
as_user(pg_client2, "lead.lou")
|
|
r = step(pg_client2, pkt, "return_to_clerical", reason="Not ours")
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden" # lead.lou leads 03, the packet is in 06
|
|
with app_conn(pg_settings) as conn:
|
|
conn.execute("UPDATE packets SET dept_code = '03' WHERE id = %s", (pkt["id"],))
|
|
pkt = get(pg_client2, pkt["id"])
|
|
r = step(pg_client2, pkt, "return_to_clerical", reason="short")
|
|
assert r.status_code == 400 and r.json()["code"] == "reason_required"
|
|
out = ok_step(pg_client2, pkt, "return_to_clerical", reason="Missing the second sheet")
|
|
assert out["status"] == "draft" and out["return_note"] == "Missing the second sheet" and out["stage"] is None
|
|
|
|
|
|
def test_assign_validation_and_draft_to_qc(
|
|
pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]
|
|
) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = ok_step(pg_client2, ready_draft(pg_client2, unit[1]), "release_to_shop", dept="03")
|
|
with app_conn(pg_settings) as conn:
|
|
other = conn.execute("SELECT id FROM stations WHERE dept_code = '06' LIMIT 1").fetchone()[0]
|
|
andy = conn.execute("SELECT id FROM users WHERE sam_account = 'weld.andy'").fetchone()[0]
|
|
as_user(pg_client2, "sup.wes")
|
|
r = step(pg_client2, pkt, "assign", station_id=other, welder_id=str(andy))
|
|
assert r.status_code == 422 and r.json()["code"] == "invalid_station"
|
|
r = step(pg_client2, pkt, "assign")
|
|
assert r.status_code == 403 and r.json()["detail"] == "Pick a station and a welder."
|
|
as_user(pg_client2, "clerk.clara")
|
|
q = ok_step(pg_client2, ready_draft(pg_client2, unit[1]), "send_draft_to_qc", note="Inspect as is")
|
|
assert q["status"] == "qc" and q["wh_skipped"] is True and q["qc"]["sent_note"] == "Inspect as is"
|
|
assert q["holder"] == "Q.C."
|
|
|
|
|
|
def test_move_and_remove(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
from pkthelpers import add_unit
|
|
|
|
with app_conn(pg_settings) as conn:
|
|
u2 = add_unit(conn, unit[0], "002")
|
|
u3 = add_unit(conn, unit[0], "7A", status="closed")
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
r = pg_client2.post(f"/api/packets/{pkt['id']}/move", json={"unit_id": u3})
|
|
assert r.status_code == 409 and r.json()["code"] == "unit_closed"
|
|
r = pg_client2.post(f"/api/packets/{pkt['id']}/move", json={"unit_id": u2, "reason": "Belongs to unit two"})
|
|
assert r.status_code == 200 and r.json()["unit"]["id"] == u2 and r.json()["unit"]["lot"] == "002"
|
|
assert pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]["action"] == "Moved to another unit"
|
|
assert _one(pg_settings, "SELECT 1 FROM audit_events WHERE action = 'packet.moved'") is not None
|
|
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}", json={"reason": "dup"})
|
|
assert r.status_code == 400 and r.json()["code"] == "reason_required"
|
|
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}", json={"reason": "Made twice by mistake"})
|
|
assert r.status_code == 200 and r.json()["removed"]["reason"] == "Made twice by mistake"
|
|
assert get(pg_client2, pkt["id"])["removed"] is not None and get(pg_client2, pkt["id"])["can"] == []
|
|
assert pg_client2.get("/api/packets").json()["packets"] == []
|
|
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "removed"
|
|
assert pg_client2.get("/api/packets", params={"removed": 1}).json()["packets"] == []
|
|
as_user(pg_client2, "admin.ann")
|
|
cards = pg_client2.get("/api/packets", params={"removed": 1}).json()["packets"]
|
|
assert [c["id"] for c in cards] == [pkt["id"]]
|
|
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}, "it_reason": "Restore the title"})
|
|
assert r.status_code == 403 and r.json()["code"] == "removed"
|
|
|
|
|
|
def test_list_filters_and_cards(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
with app_conn(pg_settings) as conn:
|
|
_, other_unit = make_unit(conn, number="L777")
|
|
as_user(pg_client2, "clerk.clara")
|
|
a = make_draft(pg_client2, unit[1], drawing="D-55", work_orders=[{"wo_base": "007123", "lot": "7A"}])
|
|
b = ready_draft(pg_client2, other_unit, title="Other one")
|
|
ok_step(pg_client2, b, "send_to_warehouse")
|
|
cards = pg_client2.get("/api/packets").json()["packets"]
|
|
assert {c["id"] for c in cards} == {a["id"], b["id"]}
|
|
card = next(c for c in cards if c["id"] == a["id"])
|
|
assert card["holder"] == "Clerical" and card["flag"] == "Made by hand - check it and send it on"
|
|
assert card["can"] == [] and set(card["blocked"]) == {"send_to_warehouse", "release_to_shop", "send_draft_to_qc"}
|
|
assert card["unit"]["id"] == unit[1] and card["dept"] == {"code": "03", "name": "Weld Parts"}
|
|
assert card["wo_count"] == 1 and card["open_shorts"] == 0 and card["open_cos"] == []
|
|
bcard = next(c for c in cards if c["id"] == b["id"])
|
|
assert bcard["wh_stage"] == "incoming" and bcard["flag"] is None and bcard["holder"] == "Warehouse"
|
|
|
|
def ids(**params: Any) -> list[int]:
|
|
return [c["id"] for c in pg_client2.get("/api/packets", params=params).json()["packets"]]
|
|
|
|
assert ids(status="warehouse") == [b["id"]]
|
|
assert ids(status="draft,warehouse", unit_id=unit[1]) == [a["id"]]
|
|
assert ids(q="d-55") == [a["id"]] and ids(q="007123") == [a["id"]]
|
|
assert ids(q="L777 other") == [b["id"]] and ids(q="L777 D-55") == []
|
|
assert ids(flag="1") == [a["id"]] and ids(flag="0") == [b["id"]]
|
|
as_user(pg_client2, "wh.wade")
|
|
wcard = next(c for c in pg_client2.get("/api/packets", params={"status": "warehouse"}).json()["packets"])
|
|
assert set(wcard["can"]) == {"wh_send_back", "wh_release"} # transitions only, no edits
|
|
|
|
|
|
def test_pm_reads_but_cannot_write(pg_client2: TestClient, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
as_user(pg_client2, "pm.paula")
|
|
view = get(pg_client2, pkt["id"])
|
|
assert view["can"] == [] and view["holder"] == "Clerical"
|
|
assert pg_client2.get("/api/packets").json()["packets"][0]["id"] == pkt["id"]
|
|
r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}})
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
r = step(pg_client2, view, "send_to_warehouse")
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
|
|
|
|
def test_work_orders_put_and_docs(pg_client2: TestClient, unit: tuple[int, int]) -> None:
|
|
as_user(pg_client2, "clerk.clara")
|
|
pkt = make_draft(pg_client2, unit[1])
|
|
url = f"/api/packets/{pkt['id']}/work-orders"
|
|
wos = {"work_orders": [{"wo_base": "0070001", "lot": "7A", "split": "", "sub": "01"}]}
|
|
assert pg_client2.put(url, json=wos).status_code == 428
|
|
r = pg_client2.put(url, json=wos, headers={"If-Match": str(pkt["version"] + 5)})
|
|
assert r.status_code == 409 and r.json()["code"] == "version_conflict"
|
|
r = pg_client2.put(url, json=wos, headers={"If-Match": str(pkt["version"])})
|
|
assert r.status_code == 200
|
|
wo = r.json()["work_orders"]
|
|
assert [(w["wo_base"], w["lot"], w["sub"]) for w in wo] == [("0070001", "7A", "01")]
|
|
a = upload(pg_client2, tiny_pdf(1)).json()
|
|
r = pg_client2.post(f"/api/packets/{pkt['id']}/docs", json={"asset_id": a["id"], "kind": "mtr", "title": "MTR 1"})
|
|
assert r.status_code == 200 and r.json()["docs"][0]["title"] == "MTR 1"
|
|
doc_id = r.json()["docs"][0]["id"]
|
|
as_user(pg_client2, "wh.wade")
|
|
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/docs/{doc_id}", json={})
|
|
assert r.status_code == 403
|
|
as_user(pg_client2, "qc.quinn")
|
|
r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/docs/{doc_id}", json={"reason": "wrong file"})
|
|
assert r.status_code == 200 and r.json()["docs"] == []
|
|
|
|
|
|
def test_seal_detects_tampering(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
pkt = happy_path(pg_client2, pg_settings, unit[1])
|
|
with app_conn(pg_settings) as conn:
|
|
conn.execute("UPDATE packets SET title = 'changed behind the app' WHERE id = %s", (pkt["id"],))
|
|
conn.execute("UPDATE bom_lines SET heat = 'X' WHERE packet_id = %s", (pkt["id"],))
|
|
conn.execute(
|
|
"UPDATE packet_stages SET checklist = %s WHERE packet_id = %s", (Jsonb({"dwg": {"done": False}}), pkt["id"])
|
|
)
|
|
assert pg_client2.get(f"/api/packets/{pkt['id']}/seal").json()["valid"] is False
|