- C1 (issue 1): over target follows VISUAL ticket hours, not the timer. - C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one. - C3 (issue 25): an incomplete unit ITP blocks filing. - C4 (issue 27): prints open the print dialog on the page, no new tab. - C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets. - C6 (issue 8): the data book leaves out the history pages. - C7 (issue 18): the supervisor approves each received short; accept and filing wait for it. - C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START. - C9 (issue 32): Q.C. and Clerical may run the stamp check. - C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only. - C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall. Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG. Co-Authored-By: Claude Opus 5.5 <[email protected]>
141 lines
7.6 KiB
Python
141 lines
7.6 KiB
Python
"""Phase 8 server smoke: one small world through every new read (the find bar on the list and the boards, the PM
|
|
reports, the exports) so the server agent's shapes are exercised end to end. The detailed checks live in
|
|
test_search.py, test_filters.py, test_pm_api.py, test_pm_export.py and test_presence.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
from fakedir import FakeDirectory
|
|
from pshelpers import app_conn, needs_pg
|
|
from pypdf import PdfReader
|
|
from shophelpers import Shop, draft, make_unit, ok, open_shop, ready_packet, run, set_running_since, start, to_station
|
|
from shophelpers import to_warehouse as send_to_wh
|
|
|
|
from packetstation.settings import Settings
|
|
|
|
pytestmark = needs_pg
|
|
|
|
|
|
@pytest.fixture
|
|
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
|
|
yield from open_shop(pg_settings, fake_dir)
|
|
|
|
|
|
def test_find_bar_boards_and_pm_reports(shop: Shop) -> None:
|
|
settings = shop.settings
|
|
project_id, u1 = make_unit(settings, "L670", "052")
|
|
with app_conn(settings) as conn:
|
|
u2 = conn.execute(
|
|
"INSERT INTO units (project_id, lot, name, status, due) VALUES (%s, '053', 'Unit two', 'open',"
|
|
" current_date - 2) RETURNING id",
|
|
(project_id,),
|
|
).fetchone()[0]
|
|
conn.execute("UPDATE projects SET due = current_date + 30 WHERE id = %s", (project_id,))
|
|
|
|
p_draft = draft(shop, u1, title="Inlet nozzle =SUM(A1)")
|
|
p_wh = send_to_wh(shop, draft(shop, u1, drawing="D-7000"))
|
|
ok(
|
|
shop("wh.wade").post(
|
|
f"/api/packets/{p_wh['id']}/wh",
|
|
json={"stage": "hold", "reason": "Waiting on purchasing / PO", "expect_stage": "incoming"},
|
|
)
|
|
)
|
|
p_st = start(shop, to_station(shop, ready_packet(shop, u2)))
|
|
set_running_since(settings, p_st["id"], 6 * 3600)
|
|
with app_conn(settings) as conn:
|
|
line = conn.execute("SELECT id FROM bom_lines WHERE packet_id = %s AND item = 2", (p_wh["id"],)).fetchone()[0]
|
|
conn.execute(
|
|
"INSERT INTO bom_shorts (packet_id, bom_line_id, qty, qty_open, reason, ref, eta)"
|
|
" VALUES (%s, %s, 2, 2, 'Not in stock', 'PO-881', current_date - 1)",
|
|
(p_wh["id"], line),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO qc_history (packet_id, at, result, inspector, note, dept_code, snapshot)"
|
|
" VALUES (%s, now() - interval '3 days', 'rejected', 'Quinn QC', 'NCR-12 · undercut', '03', '{}')",
|
|
(p_st["id"],),
|
|
)
|
|
co = conn.execute(
|
|
"INSERT INTO change_orders (packet_id, co_no, origin, review, ecn, reason, issued_at)"
|
|
" VALUES (%s, 1, 'manual', 'approved', 'ECN-77', 'Rev C', now() - interval '4 days') RETURNING id",
|
|
(p_st["id"],),
|
|
).fetchone()[0]
|
|
conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, '03')", (co,))
|
|
run(settings, "UPDATE packets SET wps = 'WPS-99' WHERE id = %s", (p_draft["id"],))
|
|
|
|
sup = shop("sup.wes")
|
|
# The find bar on the list
|
|
got = ok(sup.get("/api/packets", params={"q": "lot 52", "project_id": project_id, "facets": 1}))
|
|
ids = {c["id"] for c in got["packets"]}
|
|
assert p_draft["id"] in ids and p_st["id"] not in ids, ids
|
|
assert len(got["facets"]["workers"]) == 1 and len(got["facets"]["stations"]) == 1
|
|
assert ok(sup.get("/api/packets", params={"q": "HT-20"}))["packets"][0]["id"] == p_st["id"]
|
|
assert [c["id"] for c in ok(sup.get("/api/packets", params={"q": "ECN-77"}))["packets"]] == [p_st["id"]]
|
|
assert [c["id"] for c in ok(sup.get("/api/packets", params={"status": "station"}))["packets"]] == [p_st["id"]]
|
|
shorts = ok(sup.get("/api/packets", params={"flags": "short"}))["packets"]
|
|
assert [c["id"] for c in shorts] == [p_wh["id"]]
|
|
mine = ok(sup.get("/api/packets", params={"flags": "mine", "mine_screen": "supervisor"}))["packets"]
|
|
assert {c["id"] for c in mine} == {p_draft["id"], p_wh["id"], p_st["id"]}
|
|
over = ok(sup.get("/api/packets", params={"flags": "overdue"}))["packets"]
|
|
assert [c["id"] for c in over] == [p_st["id"]]
|
|
for sort in ("updated", "project", "due", "status", "dept"):
|
|
assert len(ok(sup.get("/api/packets", params={"sort": sort, "project_id": project_id}))["packets"]) == 3
|
|
hand = ok(sup.get("/api/packets", params={"flag": "hand"}))["packets"]
|
|
assert {c["id"] for c in hand} == {p_draft["id"]}
|
|
|
|
# The boards keep their KPIs on the project scope
|
|
b = ok(sup.get("/api/shop/board", params={"q": "nothing-matches-this"}))
|
|
assert b["kpis"]["at_stations"] == 1 and not b["depts"][0]["stations"][0]["packets"]
|
|
assert "facets" in b
|
|
wh = ok(shop("wh.wade").get("/api/warehouse/board", params={"flags": "short"}))
|
|
assert [c["id"] for c in wh["columns"]["hold"]] == [p_wh["id"]] and "facets" in wh
|
|
qc = ok(shop("qc.quinn").get("/api/qc/home", params={"q": "zzz"}))
|
|
assert qc["kpis"]["waiting"] == 0 and "facets" in qc
|
|
stall = ok(shop("weld.andy").get("/api/shop/stall", params={"station_id": shop.stall(), "flags": "mine"}))
|
|
assert [c["id"] for c in stall["mine"]] == [p_st["id"]]
|
|
mine = ok(shop("weld.andy").get("/api/stall/mine")) # change C11: no station picked
|
|
assert [c["id"] for c in mine["mine"]] == [p_st["id"]] and mine["mine"][0]["station_id"] == shop.stall()
|
|
|
|
# Project Management
|
|
pmc = shop("pm.paula")
|
|
ov = ok(pmc.get("/api/pm/overview", params={"period": "30"}))
|
|
k = {x["key"]: x["n"] for x in ov["kpis"]}
|
|
assert k["open"] == 3 and k["holds"] == 1 and k["shorts"] == 1 and k["cos"] == 1 and k["rejects"] == 1, k
|
|
assert k["late_units"] == 1, k
|
|
assert ov["scope"]["label"] == "All projects" and ov["summary"][0].startswith("3 open packets")
|
|
assert sum(s["count"] for s in ov["pipeline"]) == 3
|
|
assert [(r["cells"][0], r["health"]) for r in ov["attention"]["rows"]] == [("L670052", "risk"), ("L670053", "late")]
|
|
pr = ok(pmc.get("/api/pm/projects", params={"expand": "all"}))
|
|
assert [r["lvl"] for r in pr["tables"][0]["rows"]][:3] == [0, 1, 1]
|
|
ag = ok(pmc.get("/api/pm/aging"))
|
|
assert [t["key"] for t in ag["tables"]] == ["aging_steps", "aging_open", "aging_history"]
|
|
hr = ok(pmc.get("/api/pm/hours"))
|
|
over_rows = hr["tables"][3]["rows"]
|
|
# change C1: the timer is over its target but there are no ticket hours, so it is not over target
|
|
assert over_rows == [], hr
|
|
ql = ok(pmc.get("/api/pm/quality"))
|
|
assert [len(t["rows"]) for t in ql["tables"]][:4] == [1, 1, 1, 1], ql
|
|
fd = ok(pmc.get("/api/pm/find", params={"sort": "project", "project_id": project_id}))
|
|
assert fd["scope"]["label"] == "Project L670" and len(fd["packets"]) == 3
|
|
|
|
r = pmc.get("/api/pm/export.csv", params={"table": "all"})
|
|
assert r.status_code == 200, r.text
|
|
text = r.content.decode("utf-8")
|
|
assert text.startswith("\ufeffPESCO Packet Station — All reports\r\n")
|
|
assert "attachment" in r.headers["content-disposition"]
|
|
assert pmc.get("/api/pm/export.csv", params={"table": "nope"}).status_code == 404
|
|
r = pmc.get("/api/pm/report.pdf")
|
|
assert r.status_code == 200 and r.content.startswith(b"%PDF")
|
|
|
|
reader = PdfReader(io.BytesIO(r.content))
|
|
assert len(reader.pages) > 5
|
|
assert "PRODUCTION STATUS REPORT" in reader.pages[0].extract_text()
|
|
assert all("Controlled copy" in pg.extract_text() for pg in reader.pages)
|
|
with app_conn(settings) as conn:
|
|
kinds = [x[0] for x in conn.execute("SELECT kind FROM print_log ORDER BY id")]
|
|
acts = [x[0] for x in conn.execute("SELECT action FROM audit_events WHERE action = 'report exported'")]
|
|
assert kinds == ["report-csv", "report-pdf"] and len(acts) == 2
|
|
assert shop("clerk.clara").get("/api/pm/overview").status_code == 403 # change C10: PM and Admin only
|