"""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