Phase 5: Supervisor and Stall screens, weld map, heat table, clock and CO verification
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]>
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
"""Phase 5 change-order approval applies the staged VISUAL change: new pages added with the change order, old pages
|
||||
voided (void_co_id, void_at, void_by, void_new_n), rev and drawing set, work orders remapped, the BOM merged
|
||||
keeping the shop's values (heat, na, issued, loc, pulled, pulled_at, pulled_by, the MTR and the open short) on
|
||||
lines matched by vkey or by item label plus part; the required departments, the hold text, the verifications
|
||||
with their texts (insert-only, signed, a supervisor of the department only), and the heat table copied to the
|
||||
new sheet with carried_from. A minor change order needs no verification.
|
||||
|
||||
The change order row is inserted by SQL in the shape the feed writes (feed/changes.py vis_for and open_or_reaim,
|
||||
drawing_lookup staged). Nothing here reads VISUAL or M-Files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
from fakedir import FakeDirectory
|
||||
from pkthelpers import step
|
||||
from pshelpers import app_conn, needs_pg
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from shophelpers import (
|
||||
IT,
|
||||
SIGN,
|
||||
Shop,
|
||||
add_weld,
|
||||
all_rows,
|
||||
find_log,
|
||||
fresh,
|
||||
in_progress,
|
||||
line_of,
|
||||
ok,
|
||||
one,
|
||||
open_shop,
|
||||
refused,
|
||||
set_filler,
|
||||
tick_all,
|
||||
)
|
||||
|
||||
from packetstation.settings import Settings
|
||||
|
||||
pytestmark = needs_pg
|
||||
|
||||
FP = "fp-co-test-0001"
|
||||
NEW_DWG = "D-5001A"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
|
||||
yield from open_shop(pg_settings, fake_dir)
|
||||
|
||||
|
||||
def landscape_pdf(pages: int = 2) -> bytes:
|
||||
"""A PDF that differs from the packet's drawing (so the upload is a new asset, not a dedupe)."""
|
||||
from pypdf import PdfWriter
|
||||
|
||||
w = PdfWriter()
|
||||
for _ in range(pages):
|
||||
w.add_blank_page(width=792, height=612)
|
||||
buf = io.BytesIO()
|
||||
w.write(buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def stage_asset(shop: Shop) -> dict[str, Any]:
|
||||
r = shop("clerk.clara").post(
|
||||
"/api/assets",
|
||||
files={"file": ("D-5001A_C.pdf", landscape_pdf(), "application/pdf")},
|
||||
data={"purpose": "drawing"},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
out: dict[str, Any] = r.json()
|
||||
assert out["pages"] == 2
|
||||
return out
|
||||
|
||||
|
||||
def vis_bom_line(item: str, qty: str, part: str, desc: str, vkey: str, **over: Any) -> dict[str, Any]:
|
||||
line = {
|
||||
"item": item,
|
||||
"qty": qty,
|
||||
"part_id": part,
|
||||
"desc": desc,
|
||||
"loc": "VIS-LOC",
|
||||
"whs": "MAIN",
|
||||
"comm": "",
|
||||
"cons": False,
|
||||
"sub": False,
|
||||
"na": False,
|
||||
"grp": None,
|
||||
"vkey": vkey,
|
||||
"issued": "0",
|
||||
"istat": None,
|
||||
}
|
||||
line.update(over)
|
||||
return line
|
||||
|
||||
|
||||
def leg(base: str, qty: str, tgt: str) -> dict[str, Any]:
|
||||
return {
|
||||
"wo_base": base,
|
||||
"lot": "001",
|
||||
"split": "0",
|
||||
"sub": "0",
|
||||
"seq": None,
|
||||
"op": "WELD-PART",
|
||||
"qty": qty,
|
||||
"target_hrs": tgt,
|
||||
"dwg": None,
|
||||
"rev": None,
|
||||
"partspecs": None,
|
||||
"st": "R",
|
||||
}
|
||||
|
||||
|
||||
def insert_feed_co(
|
||||
shop: Shop,
|
||||
pid: int,
|
||||
*,
|
||||
vis: dict[str, Any],
|
||||
staged: dict[str, Any] | None,
|
||||
minor: bool = False,
|
||||
reason: str = "VISUAL changed the drawing: D-5001 -> D-5001A Rev B -> Rev C",
|
||||
) -> tuple[int, int]:
|
||||
"""A pending VISUAL change order, as the feed and the drawing job leave it. Returns (co id, co_no)."""
|
||||
with app_conn(shop.settings) as conn:
|
||||
no = conn.execute(
|
||||
"UPDATE packets SET next_co_no = next_co_no + 1 WHERE id = %s RETURNING next_co_no - 1", (pid,)
|
||||
).fetchone()[0]
|
||||
co_id = conn.execute(
|
||||
"INSERT INTO change_orders (packet_id, co_no, origin, review, minor, ecn, reason, issued_by, vis, waiting,"
|
||||
" staged, target_fp) VALUES (%s, %s, 'visual', 'pending', %s, 'VISUAL', %s, NULL, %s, NULL, %s, %s)"
|
||||
" RETURNING id",
|
||||
(pid, no, minor, reason, Jsonb(vis), Jsonb(staged) if staged else None, FP),
|
||||
).fetchone()[0]
|
||||
return int(co_id), int(no)
|
||||
|
||||
|
||||
def bom_rows(shop: Shop, pid: int) -> dict[int, dict[str, Any]]:
|
||||
with app_conn(shop.settings) as conn, conn.cursor(row_factory=dict_row) as cur:
|
||||
return {int(r["item"]): dict(r) for r in cur.execute("SELECT * FROM bom_lines WHERE packet_id = %s", (pid,))}
|
||||
|
||||
|
||||
def held(no: int, names: str) -> str:
|
||||
return f"Change order CO-{no} must be verified first by: {names}."
|
||||
|
||||
|
||||
def prepared_packet(shop: Shop) -> tuple[dict[str, Any], dict[int, dict[str, Any]], str]:
|
||||
"""An in_progress packet in 03 with VISUAL-shaped BOM lines, a short on line 2, an MTR on line 1, a weld and a
|
||||
heat table on page 1. Returns (packet, bom rows before, the MTR asset id)."""
|
||||
pkt = in_progress(shop, drawing="D-5001", rev="B")
|
||||
pid = pkt["id"]
|
||||
andy = shop("weld.andy")
|
||||
pkt = add_weld(shop, pkt)
|
||||
pkt = ok(andy.put(f"/api/packets/{pid}/heat-table/1", json={"x": 0.2, "y": 0.3, "s": 1.15}))
|
||||
mtr = pkt["pages"][0]["asset_id"]
|
||||
with app_conn(shop.settings) as conn:
|
||||
conn.execute(
|
||||
"UPDATE bom_lines SET src = 'visual', vkey = 'VK-1', cl_item = '1', loc = 'Rack 4', issued = 1,"
|
||||
" mtr_asset_id = %s WHERE packet_id = %s AND item = 1",
|
||||
(mtr, pid),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE bom_lines SET src = 'visual', vkey = NULL, cl_item = '2', issued = 1 WHERE packet_id = %s"
|
||||
" AND item = 2",
|
||||
(pid,),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE bom_lines SET src = 'visual', vkey = 'VK-3', cl_item = '3' WHERE packet_id = %s AND item = 3",
|
||||
(pid,),
|
||||
)
|
||||
line2 = conn.execute("SELECT id FROM bom_lines WHERE packet_id = %s AND item = 2", (pid,)).fetchone()[0]
|
||||
conn.execute(
|
||||
"INSERT INTO bom_shorts (packet_id, bom_line_id, qty, qty_open, reason, ref) VALUES (%s, %s, 1, 1,"
|
||||
" 'Not in stock', 'PO-4471')",
|
||||
(pid, line2),
|
||||
)
|
||||
before = bom_rows(shop, pid)
|
||||
assert all(b["pulled"] and b["pulled_at"] and b["pulled_by"] for b in before.values()), before
|
||||
return fresh(andy, pid), before, mtr
|
||||
|
||||
|
||||
def drawing_vis() -> dict[str, Any]:
|
||||
return {
|
||||
"from_dwg": "D-5001",
|
||||
"from_rev": "B",
|
||||
"to_dwg": NEW_DWG,
|
||||
"to_rev": "C",
|
||||
"dwg_change": True,
|
||||
"changes": [],
|
||||
"sig": FP,
|
||||
"legs": [leg("L670001", "3", "2"), leg("L670003", "1", "1.25")],
|
||||
"bom": [
|
||||
vis_bom_line("1", "2", "PIPE-4", "4in pipe SCH80", "VK-1", loc="VIS-A", na=False, issued="0"),
|
||||
vis_bom_line("2", "3", "GSKT-4", "4in gasket spiral", "VK-2-NEW"),
|
||||
vis_bom_line("4", "6", "NUT-58", "5/8 heavy hex nut", "VK-4"),
|
||||
],
|
||||
"want": None,
|
||||
"rel": None,
|
||||
"folded": [],
|
||||
"partspecs": [],
|
||||
"fp_parts": {},
|
||||
}
|
||||
|
||||
|
||||
def test_feed_co_approval_applies_the_change(shop: Shop) -> None:
|
||||
s = shop.settings
|
||||
pkt, before, mtr = prepared_packet(shop)
|
||||
pid = pkt["id"]
|
||||
wo_before = {w["wo_base"]: w["id"] for w in pkt["work_orders"]}
|
||||
asset = stage_asset(shop)
|
||||
staged = {"asset_id": asset["id"], "dwg": NEW_DWG, "rev": "C", "name": "D-5001A_C.pdf", "sha256": asset["sha256"]}
|
||||
co_id, no = insert_feed_co(shop, pid, vis=drawing_vis(), staged=staged)
|
||||
assert no == 1
|
||||
# pending without hold: nothing held yet
|
||||
assert fresh(shop("weld.andy"), pid)["open_cos"] == []
|
||||
|
||||
clara = shop("clerk.clara")
|
||||
refused(clara.post(f"/api/packets/{pid}/change-orders/1/approve", json={}), 400, "signature_required")
|
||||
refused(shop("sup.wes").post(f"/api/packets/{pid}/change-orders/1/approve", json={"sign": SIGN}), 403, "forbidden")
|
||||
out = ok(clara.post(f"/api/packets/{pid}/change-orders/1/approve", json={"sign": SIGN}))
|
||||
clara_id = shop.uid("clerk.clara")
|
||||
|
||||
# header
|
||||
assert (out["drawing"], out["rev"], out["drawing_state"]) == (NEW_DWG, "C", "attached")
|
||||
assert one(s, "SELECT visual_fp_applied FROM packets WHERE id = %s", (pid,))[0] == FP
|
||||
|
||||
# pages: 1 and 2 voided pointing at 3; 3 and 4 new, carrying the change order
|
||||
pages = {p["n"]: p for p in out["pages"]}
|
||||
assert sorted(pages) == [1, 2, 3, 4]
|
||||
for n in (1, 2):
|
||||
v = pages[n]["void"]
|
||||
assert v is not None and v["co_id"] == co_id and v["co_no"] == 1 and v["at"]
|
||||
assert v["by"] == {"id": clara_id, "display_name": "Clara Clerk"}
|
||||
assert v["new"] == {"n": 3, "dwg": NEW_DWG, "rev": "C"}
|
||||
for n in (3, 4):
|
||||
assert pages[n]["void"] is None and pages[n]["asset_id"] == asset["id"]
|
||||
assert (pages[n]["dwg"], pages[n]["rev"]) == (NEW_DWG, "C")
|
||||
rows = all_rows(
|
||||
s,
|
||||
"SELECT n, void_co_id, void_at IS NOT NULL, void_by::text, void_new_n, co_id FROM packet_pages"
|
||||
" WHERE packet_id = %s ORDER BY n",
|
||||
(pid,),
|
||||
)
|
||||
assert [tuple(r) for r in rows] == [
|
||||
(1, co_id, True, clara_id, 3, None),
|
||||
(2, co_id, True, clara_id, 3, None),
|
||||
(3, None, False, None, None, co_id),
|
||||
(4, None, False, None, None, co_id),
|
||||
]
|
||||
items = all_rows(s, "SELECT old_page, new_page, added FROM co_items WHERE co_id = %s ORDER BY id", (co_id,))
|
||||
assert [tuple(r) for r in items] == [(1, 3, False), (2, 4, False)]
|
||||
|
||||
# work orders remapped: L670001 kept in place, L670003 added, L670002 removed
|
||||
wos = {w["wo_base"]: w for w in out["work_orders"]}
|
||||
assert set(wos) == {"L670001", "L670003"}
|
||||
assert wos["L670001"]["id"] == wo_before["L670001"] and float(wos["L670001"]["qty"]) == 3
|
||||
assert float(wos["L670003"]["target_hrs"]) == pytest.approx(1.25)
|
||||
assert float(out["target_hrs"]) == pytest.approx(3.25)
|
||||
|
||||
# the BOM merge keeps the shop's values on matched lines
|
||||
after = bom_rows(shop, pid)
|
||||
one_b, one_a = before[1], after[1]
|
||||
assert one_a["id"] == one_b["id"], "line 1 matched on vkey"
|
||||
assert (float(one_a["qty"]), one_a["description"]) == (2.0, "4in pipe SCH80")
|
||||
# the old coKeepShop: a value the shop recorded wins; an empty one takes VISUAL's
|
||||
for k in ("heat", "na", "issued", "loc", "pulled", "pulled_at", "pulled_by", "mtr_asset_id"):
|
||||
assert one_a[k] == one_b[k], (k, one_b[k], one_a[k])
|
||||
assert str(one_a["mtr_asset_id"]) == mtr
|
||||
two_b, two_a = before[2], after[2]
|
||||
assert two_a["id"] == two_b["id"], "line 2 matched on item label plus part"
|
||||
assert (float(two_a["qty"]), two_a["description"]) == (3.0, "4in gasket spiral")
|
||||
for k in ("heat", "na", "issued", "pulled", "pulled_at", "pulled_by"):
|
||||
assert two_a[k] == two_b[k], (k, two_b[k], two_a[k])
|
||||
assert two_b["loc"] is None and two_a["loc"] == "VIS-LOC" # empty in the shop: VISUAL's value
|
||||
short = one(s, "SELECT bom_line_id, qty_open, closed_at FROM bom_shorts WHERE packet_id = %s", (pid,))
|
||||
assert (short[0], float(short[1]), short[2]) == (two_b["id"], 1.0, None)
|
||||
assert line_of(out, 2)["short"] is not None and line_of(out, 2)["short"]["reason"] == "Not in stock"
|
||||
# dropped by VISUAL after it was pulled: kept as N/A
|
||||
assert after[3]["id"] == before[3]["id"] and after[3]["na"] is True and after[3]["heat"] == before[3]["heat"]
|
||||
# new from VISUAL
|
||||
four = after[4]
|
||||
assert (four["part_id"], four["src"], four["pulled"], four["vkey"]) == ("NUT-58", "visual", False, "VK-4")
|
||||
|
||||
# the heat table follows to the new sheet; welds and tags stay on the VOID sheet
|
||||
tables = {h["page"]: h for h in out["heat_table"]}
|
||||
assert set(tables) == {1, 3}
|
||||
assert (tables[3]["x"], tables[3]["y"], tables[3]["s"], tables[3]["carried_from"]) == (0.2, 0.3, 1.15, 1)
|
||||
assert tables[1]["carried_from"] is None
|
||||
log = find_log(clara, pid, "Heat table copied to new sheet")
|
||||
assert log["note"] == "from the VOID copy, check its position on the new revision"
|
||||
assert [w["page"] for w in out["welds"]] == [1]
|
||||
appr = find_log(clara, pid, "Change order CO-1 approved")
|
||||
assert appr["signed"] is True
|
||||
|
||||
# required: the touched departments without Clerical (WH and 03)
|
||||
co = out["change_orders"][0]
|
||||
assert co["review"] == "approved" and co["label"] == "CO-1" and co["open"] is True
|
||||
assert sorted(co["required"]) == ["03", "WH"] and sorted(co["pending_depts"]) == ["03", "WH"]
|
||||
req = all_rows(s, "SELECT dept_code FROM co_required_depts WHERE co_id = %s ORDER BY dept_code", (co_id,))
|
||||
assert [r[0] for r in req] == ["03", "WH"]
|
||||
assert out["open_cos"] == [1]
|
||||
|
||||
# the hold: SUBMIT waits for every verification; ASSIGN and START are never held
|
||||
andy = shop("weld.andy")
|
||||
pkt = set_filler(shop, tick_all(shop, fresh(andy, pid)))
|
||||
both = (held(1, "Warehouse, 03 Weld Parts"), held(1, "03 Weld Parts, Warehouse"))
|
||||
mine = fresh(andy, pid)
|
||||
assert "submit" not in mine["can"] and mine["blocked"]["submit"][0] in both
|
||||
r = refused(step(andy, pkt, "submit", sign=SIGN), 403, "co_hold")
|
||||
assert r["detail"] in both
|
||||
|
||||
# a lead cannot verify; a supervisor of another department cannot verify 03
|
||||
refused(verify_call(shop, pid, "lead.lou", "03"), 403, "forbidden")
|
||||
refused(verify_call(shop, pid, "multi.mia", "03"), 403, "forbidden")
|
||||
refused(verify_call(shop, pid, "sup.wes", "WH"), 403, "forbidden")
|
||||
refused(verify_call(shop, pid, "sup.wes", "03", sign=None), 400, "signature_required")
|
||||
refused(verify_call(shop, pid, "admin.ann", "09", it_reason=IT), 422, "invalid_dept")
|
||||
|
||||
out = ok(verify_call(shop, pid, "sup.wes", "03", note="Checked rev C"))
|
||||
log = find_log(shop("sup.wes"), pid, "Change order CO-1 verified by Weld Parts")
|
||||
assert (log["note"], log["signed"]) == ("Checked rev C · still waiting on Warehouse", True)
|
||||
v = out["change_orders"][0]["verified"]["03"]
|
||||
assert v["by"]["display_name"] == "Wes Super" and v["signed"] is True and v["note"] == "Checked rev C"
|
||||
assert out["change_orders"][0]["pending_depts"] == ["WH"]
|
||||
refused(verify_call(shop, pid, "sup.wes", "03"), 409, "already_verified")
|
||||
mine = fresh(andy, pid)
|
||||
assert mine["blocked"]["submit"] == [held(1, "Warehouse")]
|
||||
|
||||
# verifications are insert-only for the app role
|
||||
with app_conn(s) as conn, pytest.raises(psycopg.Error):
|
||||
conn.execute("UPDATE co_verifications SET note = 'changed' WHERE co_id = %s", (co_id,))
|
||||
with app_conn(s) as conn, pytest.raises(psycopg.Error):
|
||||
conn.execute("DELETE FROM co_verifications WHERE co_id = %s", (co_id,))
|
||||
|
||||
# the warehouse department, by the admin with an IT reason: fully verified, SUBMIT passes
|
||||
refused(verify_call(shop, pid, "admin.ann", "WH", note="WH ok"), 400, "it_reason_required")
|
||||
out = ok(verify_call(shop, pid, "admin.ann", "WH", note="WH ok", it_reason=IT))
|
||||
log = find_log(shop("sup.wes"), pid, "Change order CO-1 verified by Warehouse")
|
||||
assert log["note"] == "WH ok · change order fully verified"
|
||||
assert out["open_cos"] == [] and out["change_orders"][0]["open"] is False
|
||||
mine = fresh(andy, pid)
|
||||
assert "submit" in mine["can"] and "submit" not in mine["blocked"]
|
||||
out = ok(step(andy, mine, "submit", sign=SIGN))
|
||||
assert out["status"] == "review"
|
||||
|
||||
|
||||
def verify_call(
|
||||
shop: Shop,
|
||||
pid: int,
|
||||
sam: str,
|
||||
dept: str,
|
||||
*,
|
||||
note: str | None = None,
|
||||
sign: Any = SIGN,
|
||||
it_reason: str | None = None,
|
||||
) -> Any:
|
||||
body: dict[str, Any] = {"dept": dept}
|
||||
if note is not None:
|
||||
body["note"] = note
|
||||
if sign is not None:
|
||||
body["sign"] = sign
|
||||
if it_reason is not None:
|
||||
body["it_reason"] = it_reason
|
||||
return shop(sam).post(f"/api/packets/{pid}/change-orders/1/verify", json=body)
|
||||
|
||||
|
||||
def test_feed_co_waits_for_the_staged_drawing(shop: Shop) -> None:
|
||||
pkt, _, _ = prepared_packet(shop)
|
||||
insert_feed_co(shop, pkt["id"], vis=drawing_vis(), staged=None)
|
||||
r = refused(
|
||||
shop("clerk.clara").post(f"/api/packets/{pkt['id']}/change-orders/1/approve", json={"sign": SIGN}),
|
||||
409,
|
||||
"co_not_staged",
|
||||
)
|
||||
assert r["detail"] == "The new drawing has not been fetched from M-Files yet."
|
||||
after = fresh(shop("clerk.clara"), pkt["id"])
|
||||
assert after["rev"] == "B" and all(p["void"] is None for p in after["pages"])
|
||||
|
||||
|
||||
def test_minor_co_needs_no_verification(shop: Shop) -> None:
|
||||
pkt = in_progress(shop)
|
||||
pid = pkt["id"]
|
||||
vis = {
|
||||
"from_dwg": "D-5001",
|
||||
"from_rev": "B",
|
||||
"to_dwg": "D-5001",
|
||||
"to_rev": "B",
|
||||
"dwg_change": False,
|
||||
"changes": ["target hours 2 -> 3"],
|
||||
"sig": FP,
|
||||
"legs": [leg("L670001", "2", "3"), leg("L670002", "1", "2.5")],
|
||||
"bom": None,
|
||||
"want": None,
|
||||
"rel": None,
|
||||
"folded": [],
|
||||
"partspecs": [],
|
||||
"fp_parts": {},
|
||||
}
|
||||
co_id, no = insert_feed_co(shop, pid, vis=vis, staged=None, minor=True, reason="VISUAL changed the packet: hours")
|
||||
out = ok(shop("clerk.clara").post(f"/api/packets/{pid}/change-orders/{no}/approve", json={"sign": SIGN}))
|
||||
co = out["change_orders"][0]
|
||||
assert co["review"] == "approved" and co["required"] == [] and co["pending_depts"] == [] and co["open"] is False
|
||||
assert out["open_cos"] == [] and all(p["void"] is None for p in out["pages"])
|
||||
assert float(next(w for w in out["work_orders"] if w["wo_base"] == "L670001")["target_hrs"]) == 3
|
||||
assert one(shop.settings, "SELECT count(*) FROM co_required_depts WHERE co_id = %s", (co_id,))[0] == 0
|
||||
# nothing to verify, so a verify is refused and SUBMIT is not held
|
||||
refused(verify_call(shop, pid, "sup.wes", "03"), 422, "invalid_dept")
|
||||
pkt = set_filler(shop, add_weld(shop, tick_all(shop, fresh(shop("weld.andy"), pid))))
|
||||
assert "submit" in fresh(shop("weld.andy"), pid)["can"]
|
||||
|
||||
|
||||
def test_co_hold_text_while_clerical_reviews(shop: Shop) -> None:
|
||||
pkt = in_progress(shop)
|
||||
pid = pkt["id"]
|
||||
_, no = insert_feed_co(shop, pid, vis=drawing_vis(), staged=None)
|
||||
ok(shop("clerk.clara").post(f"/api/packets/{pid}/change-orders/{no}/hold", json={"hold": True}))
|
||||
log = find_log(shop("clerk.clara"), pid, f"Change order CO-{no} holds the packet")
|
||||
assert log["note"] is None
|
||||
pkt = set_filler(shop, add_weld(shop, tick_all(shop, fresh(shop("weld.andy"), pid))))
|
||||
text = f"CO-{no}: Clerical is reviewing a change from VISUAL. Held until Clerical approves or rejects it."
|
||||
refused(step(shop("weld.andy"), pkt, "submit", sign=SIGN), 403, "co_hold", text)
|
||||
out = ok(shop("clerk.clara").post(f"/api/packets/{pid}/change-orders/{no}/hold", json={"hold": False}))
|
||||
assert find_log(shop("clerk.clara"), pid, f"Change order CO-{no} no longer holds the packet")
|
||||
assert out["open_cos"] == []
|
||||
# rejected with a reason: the text names it
|
||||
reason = "Rev C is not released yet"
|
||||
ok(shop("clerk.clara").post(f"/api/packets/{pid}/change-orders/{no}/reject", json={"reason": reason, "sign": SIGN}))
|
||||
log = find_log(shop("clerk.clara"), pid, f"Change order CO-{no} rejected")
|
||||
assert log["note"] == reason
|
||||
Reference in New Issue
Block a user