2026-10-16 09:31:45 -06:00
|
|
|
|
"""Packet child rows: the BOM and shorts, welds, heat tags, the heat table, the stage, the warehouse board, Q.C.
|
|
|
|
|
|
and change orders. Every write goes through the pipeline; these tests check the policy, the row and state checks,
|
|
|
|
|
|
and what each write leaves behind."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import threading
|
|
|
|
|
|
from collections.abc import Iterator
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
import psycopg
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
from fakedir import FakeDirectory
|
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
from pkthelpers import (
|
|
|
|
|
|
SIGN,
|
|
|
|
|
|
as_user,
|
|
|
|
|
|
finish_stage_by_sql,
|
|
|
|
|
|
get,
|
|
|
|
|
|
make_draft,
|
|
|
|
|
|
make_unit,
|
|
|
|
|
|
ok_step,
|
|
|
|
|
|
phase2_client,
|
|
|
|
|
|
ready_draft,
|
|
|
|
|
|
seed_users,
|
|
|
|
|
|
station_id,
|
|
|
|
|
|
step,
|
|
|
|
|
|
user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
from pshelpers import app_conn, needs_pg
|
|
|
|
|
|
|
|
|
|
|
|
from packetstation.settings import Settings
|
|
|
|
|
|
|
|
|
|
|
|
pytestmark = needs_pg
|
|
|
|
|
|
|
|
|
|
|
|
BOM = [
|
|
|
|
|
|
{"item": 1, "qty": 4, "part_id": "PIPE-4", "description": "4in pipe SCH40"},
|
|
|
|
|
|
{"item": 2, "qty": 2, "part_id": "FLG-4", "description": "4in flange"},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
with app_conn(pg_settings) as conn:
|
|
|
|
|
|
conn.execute("UPDATE users SET welder_stamp = 'A7' WHERE sam_account = 'weld.andy'")
|
|
|
|
|
|
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 _line(pkt: dict[str, Any], item: int) -> dict[str, Any]:
|
|
|
|
|
|
return next(b for b in pkt["bom"] if b["item"] == item)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _im(v: int) -> dict[str, str]:
|
|
|
|
|
|
return {"If-Match": str(v)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def warehouse_packet(client: TestClient, unit_id: int) -> dict[str, Any]:
|
|
|
|
|
|
as_user(client, "clerk.clara")
|
|
|
|
|
|
pkt = ready_draft(client, unit_id, bom=BOM)
|
|
|
|
|
|
return ok_step(client, pkt, "send_to_warehouse")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pull_all(client: TestClient, pkt: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
for b in pkt["bom"]:
|
|
|
|
|
|
if not b["pulled"] and b["short"] is None:
|
|
|
|
|
|
r = client.patch(
|
2026-10-16 17:13:31 -06:00
|
|
|
|
f"/api/packets/{pkt['id']}/bom/{b['item']}",
|
|
|
|
|
|
json={"pulled": True, "heat": f"HT-{b['item']}"},
|
|
|
|
|
|
headers=_im(b["row_version"]),
|
2026-10-16 09:31:45 -06:00
|
|
|
|
)
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
pkt = r.json()
|
|
|
|
|
|
return pkt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def shop_packet(client: TestClient, settings: Settings, unit_id: int) -> dict[str, Any]:
|
|
|
|
|
|
"""A packet in progress at Stall 1 with weld.andy, every BOM line pulled."""
|
|
|
|
|
|
pkt = warehouse_packet(client, unit_id)
|
|
|
|
|
|
as_user(client, "wh.wade")
|
|
|
|
|
|
pkt = pull_all(client, pkt)
|
|
|
|
|
|
pkt = ok_step(client, pkt, "wh_release")
|
|
|
|
|
|
with app_conn(settings) as conn:
|
|
|
|
|
|
stall = station_id(conn)
|
|
|
|
|
|
andy = user_id(conn, "weld.andy")
|
|
|
|
|
|
as_user(client, "sup.wes")
|
|
|
|
|
|
pkt = ok_step(client, pkt, "assign", station_id=stall, welder_id=andy)
|
|
|
|
|
|
as_user(client, "weld.andy")
|
|
|
|
|
|
pkt = ok_step(client, pkt, "start")
|
|
|
|
|
|
assert pkt["status"] == "in_progress"
|
|
|
|
|
|
return pkt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- BOM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bom_full_replace_on_a_draft(pg_client2: TestClient, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
pkt = ready_draft(c, unit[1])
|
|
|
|
|
|
url = f"/api/packets/{pkt['id']}/bom"
|
|
|
|
|
|
body = {"bom": [{**BOM[0], "cl_item": "A", "material": " A106-B ", "length": "20ft"}, BOM[1]]}
|
|
|
|
|
|
assert c.put(url, json=body).status_code == 428
|
|
|
|
|
|
r = c.put(url, json=body, headers=_im(pkt["version"]))
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
out = r.json()
|
|
|
|
|
|
assert [b["item"] for b in out["bom"]] == [1, 2] and out["version"] == pkt["version"] + 1
|
|
|
|
|
|
assert _line(out, 1)["material"] == "A106-B" and _line(out, 1)["src"] == "hand"
|
|
|
|
|
|
stale = c.put(url, json={"bom": [BOM[0]]}, headers=_im(pkt["version"]))
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "version_conflict", stale.text
|
|
|
|
|
|
dup = c.put(url, json={"bom": [BOM[0], BOM[0]]}, headers=_im(out["version"]))
|
|
|
|
|
|
assert dup.status_code == 422 and dup.json()["code"] == "duplicate_item"
|
|
|
|
|
|
zero = c.put(url, json={"bom": [{"item": 0, "qty": 1}]}, headers=_im(out["version"]))
|
|
|
|
|
|
assert zero.status_code == 422
|
|
|
|
|
|
r = c.put(url, json={"bom": [BOM[1]]}, headers=_im(out["version"]))
|
|
|
|
|
|
assert r.status_code == 200 and [b["item"] for b in r.json()["bom"]] == [2]
|
|
|
|
|
|
log = c.get(f"/api/packets/{pkt['id']}/log").json()
|
|
|
|
|
|
assert log[0]["action"] == "BOM replaced" and log[0]["note"] == "1 line"
|
|
|
|
|
|
|
|
|
|
|
|
wh = ok_step(c, r.json(), "send_to_warehouse")
|
|
|
|
|
|
r = c.put(url, json={"bom": BOM}, headers=_im(wh["version"]))
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden", r.text
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
r = c.put(url, json={"bom": BOM}, headers=_im(wh["version"]))
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "wrong_status", r.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bom_line_patch_in_the_warehouse(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = warehouse_packet(c, unit[1])
|
|
|
|
|
|
assert pkt["wh"]["stage"] == "incoming"
|
|
|
|
|
|
url = f"/api/packets/{pkt['id']}/bom/1"
|
|
|
|
|
|
r = c.patch(url, json={"heat": "H123"}, headers=_im(1))
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
|
|
|
|
assert "It's with Warehouse right now." in r.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
r = c.patch(url, json={"heat": "H123", "loc": "Rack 4"}, headers=_im(1))
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
line = _line(r.json(), 1)
|
|
|
|
|
|
assert line["heat"] == "H123" and line["loc"] == "Rack 4" and line["row_version"] == 2
|
|
|
|
|
|
assert r.json()["wh"]["stage"] == "incoming"
|
|
|
|
|
|
stale = c.patch(url, json={"heat": "H999"}, headers=_im(1))
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "row_conflict"
|
|
|
|
|
|
assert stale.json()["current"]["row_version"] == 2 and stale.json()["current"]["heat"] == "H123"
|
|
|
|
|
|
bad = c.patch(url, json={"heat": "@@ bad"}, headers=_im(2))
|
|
|
|
|
|
assert bad.status_code == 422 and bad.json()["code"] == "invalid_heat"
|
|
|
|
|
|
nomtr = c.patch(url, json={"mtr_asset_id": "6f1c1d1e-0000-4000-8000-000000000000"}, headers=_im(2))
|
|
|
|
|
|
assert nomtr.status_code == 422 and nomtr.json()["code"] == "invalid_asset"
|
|
|
|
|
|
|
|
|
|
|
|
r = c.patch(url, json={"pulled": True}, headers=_im(2))
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
out = r.json()
|
|
|
|
|
|
line = _line(out, 1)
|
|
|
|
|
|
assert line["pulled"] is True and float(line["issued"]) == 4 and line["row_version"] == 3
|
|
|
|
|
|
assert out["wh"]["stage"] == "pulling" and out["wh"]["auto"] is True and out["wh"]["note"] == "started pulling"
|
|
|
|
|
|
row = _one(
|
|
|
|
|
|
pg_settings, "SELECT pulled_at, pulled_by FROM bom_lines WHERE packet_id = %s AND item = 1", (pkt["id"],)
|
|
|
|
|
|
)
|
|
|
|
|
|
assert row[0] is not None and row[1] is not None
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pkt['id']}/bom/2", json={"pulled": True}, headers=_im(1))
|
|
|
|
|
|
assert r.json()["wh"]["stage"] == "pulling"
|
|
|
|
|
|
n = _one(pg_settings, "SELECT count(*) FROM packet_wh WHERE packet_id = %s", (pkt["id"],))[0]
|
|
|
|
|
|
assert n == 2
|
|
|
|
|
|
|
|
|
|
|
|
r = c.patch(url, json={"na": True}, headers=_im(3))
|
|
|
|
|
|
assert _line(r.json(), 1)["na"] is True and _line(r.json(), 1)["heat"] is None
|
|
|
|
|
|
r = c.patch(url, json={"pulled": False}, headers=_im(4))
|
|
|
|
|
|
assert _line(r.json(), 1)["pulled"] is False
|
|
|
|
|
|
row = _one(pg_settings, "SELECT pulled_at FROM bom_lines WHERE packet_id = %s AND item = 1", (pkt["id"],))
|
|
|
|
|
|
assert row[0] is None
|
|
|
|
|
|
same = c.patch(url, json={"pulled": False}, headers=_im(5))
|
|
|
|
|
|
assert same.status_code == 200 and same.json()["version"] == r.json()["version"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_wh_hold_and_first_pull_resumes(pg_client2: TestClient, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = warehouse_packet(c, unit[1])
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
r = c.post(
|
|
|
|
|
|
f"/api/packets/{pkt['id']}/wh",
|
|
|
|
|
|
json={"stage": "hold", "reason": "Waiting on purchasing / PO", "expect_stage": "incoming"},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["wh"]["stage"] == "hold", r.text
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pkt['id']}/bom/1", json={"pulled": True}, headers=_im(1))
|
|
|
|
|
|
assert r.json()["wh"]["stage"] == "pulling" and r.json()["wh"]["note"] == "pulling resumed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_shorts_and_receipts(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = warehouse_packet(c, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
url = f"/api/packets/{pid}/bom/1/short"
|
2026-10-16 17:13:31 -06:00
|
|
|
|
body = {
|
|
|
|
|
|
"qty": 3,
|
|
|
|
|
|
"reason": "Not in stock",
|
|
|
|
|
|
"ref": "PO-991",
|
|
|
|
|
|
"eta": "2026-11-02",
|
|
|
|
|
|
"heat": "H0",
|
|
|
|
|
|
"expect_open": False,
|
|
|
|
|
|
}
|
2026-10-16 09:31:45 -06:00
|
|
|
|
r = c.post(url, json=body)
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
out = r.json()
|
|
|
|
|
|
line = _line(out, 1)
|
|
|
|
|
|
assert float(line["short"]["qty"]) == 3 and float(line["short"]["qty_open"]) == 3
|
|
|
|
|
|
assert line["short"]["ref"] == "PO-991"
|
|
|
|
|
|
assert line["pulled"] is True and float(line["issued"]) == 1 and out["open_shorts"] == 1
|
|
|
|
|
|
assert out["wh"]["stage"] == "pulling" and out["wh"]["auto"] is True
|
|
|
|
|
|
again = c.post(url, json=body)
|
|
|
|
|
|
assert again.status_code == 409 and again.json()["code"] == "short_conflict"
|
|
|
|
|
|
assert c.post(url, json={**body, "qty": 9, "expect_open": True}).status_code == 422
|
|
|
|
|
|
r = c.post(url, json={**body, "qty": 2, "expect_open": True})
|
|
|
|
|
|
assert r.status_code == 200 and float(_line(r.json(), 1)["short"]["qty_open"]) == 2
|
|
|
|
|
|
assert float(_line(r.json(), 1)["issued"]) == 2
|
|
|
|
|
|
rv = _line(r.json(), 1)["row_version"]
|
|
|
|
|
|
blocked = c.patch(f"/api/packets/{pid}/bom/1", json={"heat": "H1"}, headers=_im(rv))
|
|
|
|
|
|
assert blocked.status_code == 409 and blocked.json()["code"] == "short_open"
|
|
|
|
|
|
|
|
|
|
|
|
# A short on item 2, then cleared: the whole quantity counts as issued.
|
2026-10-16 17:13:31 -06:00
|
|
|
|
r = c.post(f"/api/packets/{pid}/bom/2/short", json={"qty": 1, "reason": "Damaged / rejected", "expect_open": False})
|
2026-10-16 09:31:45 -06:00
|
|
|
|
assert _line(r.json(), 2)["short"] is not None
|
|
|
|
|
|
r = c.request("DELETE", f"/api/packets/{pid}/bom/2/short", json={"expect_open": True})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
assert _line(r.json(), 2)["short"] is None and float(_line(r.json(), 2)["issued"]) == 2
|
|
|
|
|
|
assert c.request("DELETE", f"/api/packets/{pid}/bom/2/short", json={"expect_open": True}).status_code == 409
|
2026-10-16 17:13:31 -06:00
|
|
|
|
# The release gate needs a heat on every pulled line.
|
|
|
|
|
|
rv2 = _line(r.json(), 2)["row_version"]
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pid}/bom/2", json={"heat": "H2"}, headers=_im(rv2))
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
2026-10-16 09:31:45 -06:00
|
|
|
|
|
|
|
|
|
|
pkt = ok_step(c, r.json(), "wh_release")
|
|
|
|
|
|
assert pkt["status"] == "ready" and pkt["open_shorts"] == 1
|
|
|
|
|
|
rec = f"/api/packets/{pid}/bom/1/short/receipts"
|
|
|
|
|
|
r = c.post(rec, json={"qty": 1, "heat": "H100", "note": "First piece"})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
line = _line(r.json(), 1)
|
|
|
|
|
|
assert float(line["short"]["qty_open"]) == 1 and len(line["short"]["receipts"]) == 1
|
|
|
|
|
|
assert line["short"]["receipts"][0]["by"]["display_name"] == "Wade Warehouse"
|
2026-10-16 17:13:31 -06:00
|
|
|
|
assert line["heat"] == "H0 / H100" and float(line["issued"]) == 3
|
2026-10-16 09:31:45 -06:00
|
|
|
|
assert c.post(f"/api/packets/{pid}/bom/2/short/receipts", json={"qty": 1, "heat": "H1"}).status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
with app_conn(pg_settings) as conn:
|
|
|
|
|
|
stall = station_id(conn)
|
|
|
|
|
|
andy = user_id(conn, "weld.andy")
|
|
|
|
|
|
as_user(c, "sup.wes")
|
|
|
|
|
|
pkt = ok_step(c, r.json(), "assign", station_id=stall, welder_id=andy)
|
|
|
|
|
|
as_user(c, "weld.andy")
|
|
|
|
|
|
pkt = ok_step(c, pkt, "start")
|
2026-10-16 17:13:31 -06:00
|
|
|
|
# Receive by custody: someone who neither is in the warehouse nor holds the packet is refused.
|
|
|
|
|
|
as_user(c, "qc.quinn")
|
2026-10-16 09:31:45 -06:00
|
|
|
|
no = c.post(rec, json={"qty": 1, "heat": "H200"})
|
|
|
|
|
|
assert no.status_code == 403 and no.json()["code"] == "forbidden"
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
assert c.post(rec, json={"qty": 2, "heat": "H200"}).status_code == 422
|
|
|
|
|
|
nh = c.post(rec, json={"qty": 1})
|
|
|
|
|
|
assert nh.status_code == 422 and nh.json()["code"] == "heat_required"
|
|
|
|
|
|
r = c.post(rec, json={"qty": 1, "heat": "H200"})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
line = _line(r.json(), 1)
|
|
|
|
|
|
assert line["short"] is None and r.json()["open_shorts"] == 0 and r.json()["status"] == "in_progress"
|
2026-10-16 17:13:31 -06:00
|
|
|
|
assert line["heat"] == "H0 / H100 / H200" and float(line["issued"]) == 4
|
2026-10-16 09:31:45 -06:00
|
|
|
|
closed = _one(
|
|
|
|
|
|
pg_settings,
|
|
|
|
|
|
"SELECT s.closed_at, s.qty_open FROM bom_shorts s JOIN bom_lines b ON b.id = s.bom_line_id"
|
|
|
|
|
|
" WHERE s.packet_id = %s AND b.item = 1",
|
|
|
|
|
|
(pid,),
|
|
|
|
|
|
)
|
|
|
|
|
|
assert closed[0] is not None and closed[1] == 0
|
|
|
|
|
|
with app_conn(pg_settings) as conn, pytest.raises(psycopg.Error):
|
|
|
|
|
|
conn.execute("UPDATE short_receipts SET qty = 9 WHERE packet_id = %s", (pid,))
|
|
|
|
|
|
with app_conn(pg_settings) as conn, pytest.raises(psycopg.Error):
|
|
|
|
|
|
conn.execute("DELETE FROM short_receipts WHERE packet_id = %s", (pid,))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- welds, heat tags, heat table
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_welds(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = shop_packet(c, pg_settings, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
weld = {"page": 1, "fx": 0.25, "fy": 0.5, "wo_base": "L670001", "piece": 1, "items": [2, 1], "type": "Fillet"}
|
|
|
|
|
|
r1 = c.post(f"/api/packets/{pid}/welds", json={**weld, "size": "1/4"})
|
|
|
|
|
|
assert r1.status_code == 200, r1.text
|
2026-10-16 22:46:56 -06:00
|
|
|
|
r2 = c.post(f"/api/packets/{pid}/welds", json={**weld, "type": "Groove"})
|
2026-10-16 09:31:45 -06:00
|
|
|
|
assert r2.status_code == 200, r2.text
|
|
|
|
|
|
welds = r2.json()["welds"]
|
|
|
|
|
|
assert [w["weld_no"] for w in welds] == [1, 2]
|
|
|
|
|
|
assert welds[0]["items"] == [1, 2] and welds[0]["stamp"] == "A7"
|
|
|
|
|
|
assert welds[0]["welder"]["display_name"] == "Andy Welder"
|
|
|
|
|
|
row = _one(pg_settings, "SELECT dept_code, stage_id FROM welds WHERE packet_id = %s AND weld_no = 1", (pid,))
|
|
|
|
|
|
assert row[0] == "03" and row[1] == pkt["stage"]["id"]
|
|
|
|
|
|
bad = c.post(f"/api/packets/{pid}/welds", json={**weld, "items": [9]})
|
|
|
|
|
|
assert bad.status_code == 422 and bad.json()["code"] == "invalid_items"
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/welds", json={**weld, "items": []}).status_code == 422
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/welds", json={**weld, "stamp": "no stamp!"}).status_code == 422
|
|
|
|
|
|
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pid}/welds/1", json={"size": "3/8"}, headers=_im(1))
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["welds"][0]["size"] == "3/8" and r.json()["welds"][0]["row_version"] == 2
|
|
|
|
|
|
stale = c.patch(f"/api/packets/{pid}/welds/1", json={"size": "1/2"}, headers=_im(1))
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "row_conflict"
|
|
|
|
|
|
r = c.request("DELETE", f"/api/packets/{pid}/welds/2", json={"reason": "Marked twice"})
|
|
|
|
|
|
assert r.status_code == 200 and [w["weld_no"] for w in r.json()["welds"]] == [1]
|
|
|
|
|
|
row = _one(pg_settings, "SELECT removed_reason FROM welds WHERE packet_id = %s AND weld_no = 2", (pid,))
|
|
|
|
|
|
assert row[0] == "Marked twice"
|
|
|
|
|
|
assert c.patch(f"/api/packets/{pid}/welds/2", json={"size": "1"}, headers=_im(1)).status_code == 404
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/welds", json=weld)
|
|
|
|
|
|
assert r.json()["welds"][-1]["weld_no"] == 3
|
|
|
|
|
|
|
|
|
|
|
|
# Another worker of the department, limited to another stall, does not hold this packet.
|
|
|
|
|
|
with app_conn(pg_settings) as conn:
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
|
"INSERT INTO user_station_limits (user_id, station_id) VALUES (%s, %s)",
|
|
|
|
|
|
(user_id(conn, "dup.dave"), station_id(conn, "03", "Stall 2")),
|
|
|
|
|
|
)
|
|
|
|
|
|
as_user(c, "dup.dave")
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/welds", json=weld)
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden", r.text
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/welds", json=weld).status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_heat_tags_and_heat_table(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = warehouse_packet(c, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/heat-tags", json={"page": 1, "item": 1}).status_code == 403
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
for item in (1, 2):
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/heat-tags", json={"page": 1, "item": item, "fx": 0.1, "fy": 0.2})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
tags = r.json()["heat_tags"]
|
|
|
|
|
|
assert [(t["tag_no"], t["item"]) for t in tags] == [(1, 1), (2, 2)]
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/heat-tags", json={"page": 1, "item": 7}).status_code == 422
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pid}/heat-tags/2", json={"item": 1}, headers=_im(1))
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["heat_tags"][1]["item"] == 1
|
|
|
|
|
|
stale = c.patch(f"/api/packets/{pid}/heat-tags/2", json={"item": 2}, headers=_im(1))
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "row_conflict"
|
|
|
|
|
|
r = c.request("DELETE", f"/api/packets/{pid}/heat-tags/1", json={})
|
|
|
|
|
|
assert r.status_code == 200 and [t["tag_no"] for t in r.json()["heat_tags"]] == [2]
|
|
|
|
|
|
|
|
|
|
|
|
url = f"/api/packets/{pid}/heat-table/1"
|
|
|
|
|
|
r1 = c.put(url, json={"x": 0.7, "y": 0.1, "s": 1.0})
|
2026-10-16 22:46:56 -06:00
|
|
|
|
assert r1.status_code == 200
|
|
|
|
|
|
ht = r1.json()["heat_table"]
|
|
|
|
|
|
assert [{k: h[k] for k in ("page", "x", "y", "s")} for h in ht] == [{"page": 1, "x": 0.7, "y": 0.1, "s": 1.0}]
|
|
|
|
|
|
assert ht[0]["carried_from"] is None and ht[0]["by"]["display_name"] == "Wade Warehouse"
|
2026-10-16 09:31:45 -06:00
|
|
|
|
r2 = c.put(url, json={"x": 0.7, "y": 0.1, "s": 1.0})
|
|
|
|
|
|
assert r2.status_code == 200 and r2.json()["version"] == r1.json()["version"]
|
|
|
|
|
|
r3 = c.put(url, json={"x": 0.6, "y": 0.1, "s": 1.2})
|
|
|
|
|
|
assert r3.json()["heat_table"][0]["x"] == 0.6 and r3.json()["version"] == r1.json()["version"] + 1
|
|
|
|
|
|
n = _one(pg_settings, "SELECT count(*) FROM heat_table_placements WHERE packet_id = %s", (pid,))[0]
|
|
|
|
|
|
assert n == 1
|
|
|
|
|
|
assert c.put(url, json={"x": 0.6, "y": 0.1, "s": 0}).status_code == 422
|
|
|
|
|
|
r = c.request("DELETE", url, json={})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["heat_table"] == []
|
|
|
|
|
|
assert c.request("DELETE", url, json={}).status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- the stage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stage_fields_and_checklist(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = shop_packet(c, pg_settings, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
rv = pkt["stage"]["row_version"]
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pid}/stage", json={"filler": "ER70S-6", "preheat": "50F"}, headers=_im(rv))
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
st = r.json()["stage"]
|
|
|
|
|
|
assert st["filler"] == "ER70S-6" and st["preheat"] == "50F" and st["row_version"] == rv + 1
|
|
|
|
|
|
stale = c.patch(f"/api/packets/{pid}/stage", json={"filler": "E7018"}, headers=_im(rv))
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "row_conflict"
|
|
|
|
|
|
|
|
|
|
|
|
andy = user_id_of(pg_settings, "weld.andy")
|
|
|
|
|
|
r = c.put(f"/api/packets/{pid}/stage/checklist/dwg", json={"done": True})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
entry = r.json()["stage"]["checklist"]["dwg"]
|
|
|
|
|
|
assert entry["done"] is True and entry["by"] == andy and entry["stamp"] == "A7" and entry["at"]
|
|
|
|
|
|
assert r.json()["stage"]["row_version"] == st["row_version"]
|
|
|
|
|
|
bad = c.put(f"/api/packets/{pid}/stage/checklist/nope", json={"done": True})
|
|
|
|
|
|
assert bad.status_code == 422 and bad.json()["code"] == "invalid_key"
|
|
|
|
|
|
as_user(c, "lead.lou")
|
|
|
|
|
|
r = c.put(f"/api/packets/{pid}/stage/checklist/heat", json={"done": True})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["stage"]["checklist"]["heat"]["by"] == user_id_of(pg_settings, "lead.lou")
|
|
|
|
|
|
r = c.put(f"/api/packets/{pid}/stage/checklist/dwg", json={"done": False})
|
|
|
|
|
|
assert r.json()["stage"]["checklist"]["dwg"] == {"done": False}
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
assert c.put(f"/api/packets/{pid}/stage/checklist/dwg", json={"done": True}).status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def user_id_of(settings: Settings, sam: str) -> str:
|
|
|
|
|
|
with app_conn(settings) as conn:
|
|
|
|
|
|
return user_id(conn, sam)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stage_clock(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = shop_packet(c, pg_settings, unit[1])
|
|
|
|
|
|
url = f"/api/packets/{pkt['id']}/stage/clock"
|
|
|
|
|
|
assert pkt["stage"]["clock"]["running_since"] is not None
|
|
|
|
|
|
r = c.post(url, json={"op": "start"})
|
|
|
|
|
|
assert r.status_code == 409 and r.json()["code"] == "clock_state"
|
|
|
|
|
|
r = c.post(url, json={"op": "pause"})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["stage"]["clock"]["running_since"] is None, r.text
|
|
|
|
|
|
assert r.json()["stage"]["clock"]["elapsed_ms"] >= 0
|
|
|
|
|
|
assert c.post(url, json={"op": "pause"}).json()["code"] == "clock_state"
|
|
|
|
|
|
r = c.post(url, json={"op": "resume"})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["stage"]["clock"]["running_since"] is not None
|
|
|
|
|
|
assert c.post(url, json={"op": "resume"}).status_code == 409
|
|
|
|
|
|
assert c.post(url, json={"op": "stop"}).status_code == 422
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
assert c.post(url, json={"op": "pause"}).status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_stage(pg_client2: TestClient, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
pkt = make_draft(c, unit[1])
|
|
|
|
|
|
r = c.post(f"/api/packets/{pkt['id']}/stage/clock", json={"op": "start"})
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "wrong_status"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- the warehouse board
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_wh_stage_board(pg_client2: TestClient, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = warehouse_packet(c, unit[1])
|
|
|
|
|
|
url = f"/api/packets/{pkt['id']}/wh"
|
|
|
|
|
|
r = c.post(url, json={"stage": "pulling", "expect_stage": "incoming"})
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
r = c.post(url, json={"stage": "pulling", "expect_stage": "incoming"})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["wh"]["stage"] == "pulling" and r.json()["wh"]["auto"] is False
|
|
|
|
|
|
assert r.json()["wh"]["by"]["display_name"] == "Wade Warehouse"
|
|
|
|
|
|
stale = c.post(url, json={"stage": "pulling", "expect_stage": "incoming"})
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "stale_step"
|
|
|
|
|
|
assert stale.json()["current"]["stage"] == "pulling"
|
|
|
|
|
|
r = c.post(url, json={"stage": "hold", "expect_stage": "pulling"})
|
2026-10-16 17:13:31 -06:00
|
|
|
|
assert r.status_code == 400 and r.json()["code"] == "invalid_reason"
|
2026-10-16 09:31:45 -06:00
|
|
|
|
r = c.post(url, json={"stage": "hold", "reason": "Other", "expect_stage": "pulling"})
|
|
|
|
|
|
assert r.status_code == 400 and r.json()["code"] == "reason_required"
|
|
|
|
|
|
hold = {"stage": "hold", "reason": "Drawing / BOM problem", "note": "Asked Leo", "expect_stage": "pulling"}
|
|
|
|
|
|
r = c.post(url, json=hold)
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["wh"]["reason"] == "Drawing / BOM problem"
|
|
|
|
|
|
assert r.json()["wh"]["note"] == "Asked Leo"
|
|
|
|
|
|
bad = c.post(url, json={"stage": "pulled", "expect_stage": "hold"})
|
|
|
|
|
|
assert bad.status_code == 409 and bad.json()["code"] == "wh_transition"
|
|
|
|
|
|
r = c.post(url, json={"stage": "pulling", "expect_stage": "hold"})
|
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
|
r = c.post(url, json={"stage": "pulled", "expect_stage": "pulling"})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["wh"]["stage"] == "pulled"
|
|
|
|
|
|
log = c.get(f"/api/packets/{pkt['id']}/log").json()
|
|
|
|
|
|
assert log[0]["action"] == "Warehouse: Marked pulled, ready for production"
|
|
|
|
|
|
assert log[1]["action"] == "Warehouse: Pulling resumed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- Q.C.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_qc_patch(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = shop_packet(c, pg_settings, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
r = c.patch(f"/api/packets/{pid}/qc", json={"inspector": "Quinn"}, headers=_im(1))
|
|
|
|
|
|
assert r.status_code == 403
|
|
|
|
|
|
finish_stage_by_sql(pg_settings, pid)
|
|
|
|
|
|
pkt = ok_step(c, get(c, pid), "submit", sign=SIGN)
|
|
|
|
|
|
as_user(c, "sup.wes")
|
|
|
|
|
|
pkt = ok_step(c, pkt, "send_to_qc", book=True, spool=True, location="Bay 3", sign=SIGN)
|
|
|
|
|
|
rv = pkt["qc"]["row_version"]
|
|
|
|
|
|
as_user(c, "qc.quinn")
|
|
|
|
|
|
url = f"/api/packets/{pid}/qc"
|
|
|
|
|
|
body = {"checks": {"db_heats": True}, "weld_exam": {"1": {"result": "accept"}}, "inspector": "Quinn QC"}
|
|
|
|
|
|
r = c.patch(url, json={**body, "stamp": "Q5"}, headers=_im(rv))
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
qc = r.json()["qc"]
|
|
|
|
|
|
assert qc["checks"]["db_heats"]["done"] is True and qc["checks"]["db_heats"]["stamp"] == "Q5"
|
|
|
|
|
|
assert qc["checks"]["db_heats"]["by"] == user_id_of(pg_settings, "qc.quinn")
|
|
|
|
|
|
assert qc["weld_exam"]["1"]["result"] == "accept" and qc["inspector"] == "Quinn QC"
|
|
|
|
|
|
assert qc["row_version"] == rv + 1
|
|
|
|
|
|
r = c.patch(
|
|
|
|
|
|
url, json={"checks": {"db_mtr": True}, "weld_exam": {"1": {"note": "Good tie-in"}}}, headers=_im(rv + 1)
|
|
|
|
|
|
)
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
qc = r.json()["qc"]
|
|
|
|
|
|
assert qc["checks"]["db_heats"]["done"] and qc["checks"]["db_mtr"]["done"]
|
|
|
|
|
|
assert qc["weld_exam"]["1"]["result"] == "accept"
|
|
|
|
|
|
assert qc["weld_exam"]["1"]["note"] == "Good tie-in"
|
|
|
|
|
|
stale = c.patch(url, json={"notes": "x"}, headers=_im(rv))
|
|
|
|
|
|
assert stale.status_code == 409 and stale.json()["code"] == "row_conflict"
|
|
|
|
|
|
assert c.patch(url, json={"checks": {"nope": True}}, headers=_im(rv + 2)).status_code == 422
|
|
|
|
|
|
assert c.patch(url, json={"weld_exam": {"1": {"result": "maybe"}}}, headers=_im(rv + 2)).status_code == 422
|
|
|
|
|
|
assert c.patch(url, json={"weld_exam": {"8": {"result": "accept"}}}, headers=_im(rv + 2)).status_code == 422
|
|
|
|
|
|
r = c.patch(url, json={"checks": {"db_mtr": False}}, headers=_im(rv + 2))
|
|
|
|
|
|
assert r.json()["qc"]["checks"]["db_mtr"] == {"done": False}
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
assert c.patch(url, json={"notes": "x"}, headers=_im(rv + 3)).status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- change orders
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_change_orders(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = shop_packet(c, pg_settings, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
finish_stage_by_sql(pg_settings, pid)
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
body = {"reason": "Rev C moves nozzle N2", "ecn": "ECN-55", "items": [{"old_page": 1, "new_page": 1}]}
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/change-orders", json=body)
|
|
|
|
|
|
assert r.status_code == 400 and r.json()["code"] == "signature_required"
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/change-orders", json={**body, "sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
co = r.json()["change_orders"][0]
|
|
|
|
|
|
assert co["co_no"] == 1 and co["origin"] == "manual" and co["review"] == "approved"
|
|
|
|
|
|
assert co["required"] == ["03", "WH"] and co["issued_by"]["display_name"] == "Clara Clerk"
|
|
|
|
|
|
assert co["items"][0]["old_page"] == 1 and r.json()["open_cos"] == [1]
|
|
|
|
|
|
bad = c.post(f"/api/packets/{pid}/change-orders", json={**body, "required_depts": ["CL"], "sign": SIGN})
|
|
|
|
|
|
assert bad.status_code == 422 and bad.json()["code"] == "invalid_dept"
|
|
|
|
|
|
listed = c.get(f"/api/packets/{pid}/change-orders")
|
|
|
|
|
|
assert listed.status_code == 200 and [x["co_no"] for x in listed.json()] == [1]
|
|
|
|
|
|
|
|
|
|
|
|
as_user(c, "weld.andy")
|
|
|
|
|
|
r = step(c, get(c, pid), "submit", sign=SIGN)
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "co_hold", r.text
|
2026-10-16 22:46:56 -06:00
|
|
|
|
assert "Change order CO-1" in r.json()["detail"]
|
2026-10-16 09:31:45 -06:00
|
|
|
|
vurl = f"/api/packets/{pid}/change-orders/1/verify"
|
|
|
|
|
|
assert c.post(vurl, json={"dept": "03", "sign": SIGN}).status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
as_user(c, "sup.wes")
|
|
|
|
|
|
r = c.post(vurl, json={"dept": "03", "note": "Checked N2", "sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
assert r.json()["change_orders"][0]["verified"]["03"]["note"] == "Checked N2"
|
|
|
|
|
|
assert r.json()["open_cos"] == [1]
|
|
|
|
|
|
again = c.post(vurl, json={"dept": "03", "sign": SIGN})
|
|
|
|
|
|
assert again.status_code == 409 and again.json()["code"] == "already_verified"
|
|
|
|
|
|
assert c.post(vurl, json={"dept": "WH", "sign": SIGN}).status_code == 403
|
|
|
|
|
|
|
|
|
|
|
|
as_user(c, "wh.wade")
|
|
|
|
|
|
r = c.post(vurl, json={"dept": "WH", "sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "forbidden"
|
|
|
|
|
|
|
|
|
|
|
|
as_user(c, "admin.ann")
|
|
|
|
|
|
r = c.post(vurl, json={"dept": "WH", "sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 400 and r.json()["code"] == "it_reason_required"
|
|
|
|
|
|
r = c.post(vurl, json={"dept": "QC", "sign": SIGN, "it_reason": "Warehouse lead is out today"})
|
|
|
|
|
|
assert r.status_code == 422 and r.json()["code"] == "invalid_dept"
|
|
|
|
|
|
r = c.post(vurl, json={"dept": "WH", "sign": SIGN, "it_reason": "Warehouse lead is out today"})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
assert r.json()["open_cos"] == []
|
|
|
|
|
|
row = _one(pg_settings, "SELECT it_reason FROM co_verifications WHERE dept_code = 'WH'")
|
|
|
|
|
|
assert row[0] == "Warehouse lead is out today"
|
|
|
|
|
|
|
|
|
|
|
|
as_user(c, "weld.andy")
|
|
|
|
|
|
r = step(c, get(c, pid), "submit", sign=SIGN)
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["status"] == "review", r.text
|
|
|
|
|
|
|
|
|
|
|
|
# A pending change from VISUAL holds nothing until Clerical holds it.
|
|
|
|
|
|
with app_conn(pg_settings) as conn:
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
|
"INSERT INTO change_orders (packet_id, co_no, origin, review, reason) VALUES (%s, 2, 'visual', 'pending',"
|
|
|
|
|
|
" 'Drawing rev changed in VISUAL')",
|
|
|
|
|
|
(pid,),
|
|
|
|
|
|
)
|
|
|
|
|
|
conn.execute("UPDATE packets SET next_co_no = 3 WHERE id = %s", (pid,))
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
assert get(c, pid)["open_cos"] == []
|
|
|
|
|
|
hurl = f"/api/packets/{pid}/change-orders/2/hold"
|
|
|
|
|
|
r = c.post(hurl, json={"hold": True})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["open_cos"] == [2], r.text
|
|
|
|
|
|
assert r.json()["change_orders"][1]["hold"] is True
|
|
|
|
|
|
as_user(c, "sup.wes")
|
|
|
|
|
|
r = step(c, get(c, pid), "send_to_qc", book=True, spool=True, location="Bay 3", sign=SIGN)
|
|
|
|
|
|
assert r.status_code == 403 and r.json()["code"] == "co_hold"
|
|
|
|
|
|
as_user(c, "clerk.clara")
|
|
|
|
|
|
r = c.post(hurl, json={"hold": False})
|
|
|
|
|
|
assert r.status_code == 200 and r.json()["open_cos"] == []
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/change-orders/1/hold", json={"hold": True}).json()["code"] == "co_state"
|
|
|
|
|
|
rurl = f"/api/packets/{pid}/change-orders/2/reject"
|
|
|
|
|
|
r = c.post(rurl, json={"sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 400 and r.json()["code"] == "reason_required"
|
|
|
|
|
|
r = c.post(rurl, json={"reason": "Not a real change", "sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
co2 = r.json()["change_orders"][1]
|
|
|
|
|
|
assert co2["review"] == "rejected" and co2["rejected_reason"] == "Not a real change"
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/change-orders/2/approve", json={"sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 409 and r.json()["code"] == "co_state"
|
|
|
|
|
|
assert c.post(f"/api/packets/{pid}/change-orders/9/approve", json={"sign": SIGN}).status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_co_approve_pending(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None:
|
|
|
|
|
|
c = pg_client2
|
|
|
|
|
|
pkt = warehouse_packet(c, unit[1])
|
|
|
|
|
|
pid = pkt["id"]
|
|
|
|
|
|
with app_conn(pg_settings) as conn:
|
|
|
|
|
|
conn.execute(
|
|
|
|
|
|
"INSERT INTO change_orders (packet_id, co_no, origin, review) VALUES (%s, 1, 'visual', 'pending')", (pid,)
|
|
|
|
|
|
)
|
|
|
|
|
|
conn.execute("UPDATE packets SET next_co_no = 2 WHERE id = %s", (pid,))
|
|
|
|
|
|
r = c.post(f"/api/packets/{pid}/change-orders/1/approve", json={"sign": SIGN})
|
|
|
|
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
co = r.json()["change_orders"][0]
|
|
|
|
|
|
assert co["review"] == "approved" and co["approved_by"]["display_name"] == "Clara Clerk"
|
|
|
|
|
|
assert co["required"] == ["WH"] and r.json()["open_cos"] == [1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- concurrency
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_concurrent_welds_get_distinct_numbers(
|
|
|
|
|
|
pg_client2: TestClient, pg_settings: Settings, fake_dir: FakeDirectory, unit: tuple[int, int]
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
pkt = shop_packet(pg_client2, pg_settings, unit[1])
|
|
|
|
|
|
with phase2_client(pg_settings, fake_dir) as other:
|
|
|
|
|
|
as_user(other, "weld.andy")
|
|
|
|
|
|
results: list[Any] = [None, None]
|
|
|
|
|
|
barrier = threading.Barrier(2)
|
|
|
|
|
|
|
|
|
|
|
|
def run(i: int, client: TestClient) -> None:
|
|
|
|
|
|
barrier.wait()
|
|
|
|
|
|
results[i] = client.post(f"/api/packets/{pkt['id']}/welds", json={"items": [1], "type": "Fillet"})
|
|
|
|
|
|
|
|
|
|
|
|
threads = [threading.Thread(target=run, args=(i, cl)) for i, cl in enumerate((pg_client2, other))]
|
|
|
|
|
|
for t in threads:
|
|
|
|
|
|
t.start()
|
|
|
|
|
|
for t in threads:
|
|
|
|
|
|
t.join(30)
|
|
|
|
|
|
assert [r.status_code for r in results] == [200, 200], [r.text for r in results]
|
|
|
|
|
|
nos = [r[0] for r in _all(pg_settings, "SELECT weld_no FROM welds WHERE packet_id = %s", (pkt["id"],))]
|
|
|
|
|
|
assert sorted(nos) == [1, 2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_concurrent_bom_patch_one_wins(
|
|
|
|
|
|
pg_client2: TestClient, pg_settings: Settings, fake_dir: FakeDirectory, unit: tuple[int, int]
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
pkt = warehouse_packet(pg_client2, unit[1])
|
|
|
|
|
|
as_user(pg_client2, "wh.wade")
|
|
|
|
|
|
with phase2_client(pg_settings, fake_dir) as other:
|
|
|
|
|
|
as_user(other, "wh.wade")
|
|
|
|
|
|
results: list[Any] = [None, None]
|
|
|
|
|
|
barrier = threading.Barrier(2)
|
|
|
|
|
|
|
|
|
|
|
|
def run(i: int, client: TestClient) -> None:
|
|
|
|
|
|
barrier.wait()
|
|
|
|
|
|
results[i] = client.patch(f"/api/packets/{pkt['id']}/bom/1", json={"heat": f"H{i}"}, headers=_im(1))
|
|
|
|
|
|
|
|
|
|
|
|
threads = [threading.Thread(target=run, args=(i, cl)) for i, cl in enumerate((pg_client2, other))]
|
|
|
|
|
|
for t in threads:
|
|
|
|
|
|
t.start()
|
|
|
|
|
|
for t in threads:
|
|
|
|
|
|
t.join(30)
|
|
|
|
|
|
codes = sorted(r.status_code for r in results)
|
|
|
|
|
|
assert codes == [200, 409], [r.text for r in results]
|
|
|
|
|
|
loser = next(r for r in results if r.status_code == 409)
|
|
|
|
|
|
assert loser.json()["code"] == "row_conflict" and loser.json()["current"]["row_version"] == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _all(settings: Settings, sql: str, params: Any = ()) -> list[Any]:
|
|
|
|
|
|
with app_conn(settings) as conn:
|
|
|
|
|
|
return conn.execute(sql, params).fetchall()
|