Files

393 lines
14 KiB
Python
Raw Permalink Normal View History

"""Helpers for the phase 5 shop tests: one signed-in client per user, a packet brought to `ready` in 03 through
the warehouse, and the shop steps (assign, start, finish the stage, submit, review).
The packet: a hand draft in 03 on a fresh unit, two work orders (target 2 h and 2.5 h), three BOM lines, a
two-page PDF drawing, heat map answered, sent to the warehouse, every line pulled with a heat by wh.wade, released.
"""
from __future__ import annotations
import secrets
from collections.abc import Iterator
from dataclasses import dataclass, field
from typing import Any
from fakedir import FAKE_PASSWORD, FakeDirectory
from fastapi.testclient import TestClient
from pkthelpers import phase2_client, seed_users, step, tiny_pdf, upload
from pshelpers import ORIGIN_HEADERS, app_conn, sign_in
from psycopg.types.json import Jsonb
from packetstation.settings import Settings
SIGN = {"factor": "password", "secret": FAKE_PASSWORD}
IT = "Ticket 5501: shop lead is out today"
STALE = "Someone else already took this step."
USERS = (
"admin.ann",
"clerk.clara",
"clerk.leo",
"wh.wade",
"qc.quinn",
"pm.paula",
"weld.andy",
"lead.lou",
"sup.wes",
"multi.mia",
"both.bo",
"dup.dave",
)
WOS = [
{"wo_base": "L670001", "lot": "001", "split": "0", "sub": "0", "qty": 2, "target_hrs": 2},
{"wo_base": "L670002", "lot": "001", "split": "0", "sub": "0", "qty": 1, "target_hrs": 2.5},
]
BOM3 = [
{"item": 1, "qty": 1, "part_id": "PIPE-4", "description": "4in pipe SCH40", "material": "A106-B"},
{"item": 2, "qty": 2, "part_id": "GSKT-4", "description": "4in gasket"},
{"item": 3, "qty": 4, "part_id": "STUD-58", "description": "5/8 stud bolt"},
]
WELD_KEYS = ("dwg", "heat", "wps", "fit", "pre", "vt", "stamp", "clean", "qty")
PLAIN_KEYS = ("dwg", "prev", "mat", "work", "insp", "qty")
MSG_CHECKLIST = "Finish the checklist first."
MSG_WELDS = "Record at least one weld first."
MSG_FILLER = "Enter the filler metal first."
MSG_QC = "Confirm the book and the spool, and give the location."
MSG_OTHER_DEPT = "Choose a different department."
# ---------------------------------------------------------------- clients
@dataclass
class Shop:
"""One app, one signed-in TestClient per user (separate cookie jars)."""
main: TestClient
settings: Settings
clients: dict[str, TestClient] = field(default_factory=dict)
def __call__(self, sam: str) -> TestClient:
c = self.clients.get(sam)
if c is None:
c = self.fresh(sam)
self.clients[sam] = c
return c
def fresh(self, sam: str) -> TestClient:
"""A new client with its own session (two sessions of one user, or a thread of its own)."""
c = TestClient(self.main.app, headers=ORIGIN_HEADERS)
r = sign_in(c, sam)
assert r.status_code == 200, r.text
return c
def uid(self, sam: str) -> str:
return user_id(self.settings, sam)
def stall(self, dept: str = "03", name: str = "Stall 1") -> int:
return station_id(self.settings, dept, name)
def open_shop(settings: Settings, fake_dir: FakeDirectory) -> Iterator[Shop]:
"""The body of a `shop` fixture: the app, every test user signed in once (their users rows exist)."""
with phase2_client(settings, fake_dir) as client:
seed_users(client, USERS)
yield Shop(client, settings)
# ---------------------------------------------------------------- SQL
def one(settings: Settings, sql: str, params: Any = ()) -> Any:
with app_conn(settings) as conn:
return conn.execute(sql, params).fetchone()
def all_rows(settings: Settings, sql: str, params: Any = ()) -> list[Any]:
with app_conn(settings) as conn:
return list(conn.execute(sql, params).fetchall())
def run(settings: Settings, sql: str, params: Any = ()) -> None:
with app_conn(settings) as conn:
conn.execute(sql, params)
def user_id(settings: Settings, sam: str) -> str:
row = one(settings, "SELECT id FROM users WHERE sam_account = %s", (sam,))
assert row is not None, sam
return str(row[0])
def station_id(settings: Settings, dept: str = "03", name: str = "Stall 1") -> int:
row = one(settings, "SELECT id FROM stations WHERE dept_code = %s AND name = %s", (dept, name))
assert row is not None, (dept, name)
return int(row[0])
def set_stamp(settings: Settings, sam: str, stamp: str | None) -> None:
run(settings, "UPDATE users SET welder_stamp = %s WHERE sam_account = %s", (stamp, sam))
def set_limits(settings: Settings, sam: str, station_ids: list[int]) -> None:
"""Replace a user's station limits (an empty list removes them)."""
with app_conn(settings) as conn:
uid = conn.execute("SELECT id FROM users WHERE sam_account = %s", (sam,)).fetchone()[0]
conn.execute("DELETE FROM user_station_limits WHERE user_id = %s", (uid,))
for sid in station_ids:
conn.execute("INSERT INTO user_station_limits (user_id, station_id) VALUES (%s, %s)", (uid, sid))
def make_unit(settings: Settings, number: str | None = None, lot: str = "001") -> tuple[int, int]:
"""A project with one unit by SQL. Returns (project_id, unit_id)."""
number = number or "L5" + secrets.token_hex(3).upper()
with app_conn(settings) as conn:
pid = conn.execute(
"INSERT INTO projects (number, name, customer, po) VALUES (%s, 'Shop project', 'Acme Refining', 'PO-55')"
" RETURNING id",
(number,),
).fetchone()[0]
uid = conn.execute(
"INSERT INTO units (project_id, lot, name, status) VALUES (%s, %s, 'Unit one', 'open') RETURNING id",
(pid, lot),
).fetchone()[0]
return int(pid), int(uid)
def set_running_since(settings: Settings, packet_id: int, seconds_ago: float) -> None:
"""Move the open stage's running clock back in time, as if it had run that long."""
run(
settings,
"UPDATE packet_stages SET clock_running_since = now() - make_interval(secs => %s)"
" WHERE packet_id = %s AND closed_at IS NULL AND clock_running_since IS NOT NULL",
(seconds_ago, packet_id),
)
def set_templates(settings: Settings, value: Any) -> None:
run(
settings,
"INSERT INTO settings (key, value, version, updated_at) VALUES ('checklist_templates', %s, 1, now())"
" ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, version = settings.version + 1",
(Jsonb(value),),
)
# ---------------------------------------------------------------- reads
def fresh(c: TestClient, pid: int) -> dict[str, Any]:
r = c.get(f"/api/packets/{pid}")
assert r.status_code == 200, r.text
out: dict[str, Any] = r.json()
return out
def log_rows(c: TestClient, pid: int) -> list[dict[str, Any]]:
r = c.get(f"/api/packets/{pid}/log")
assert r.status_code == 200, r.text
out: list[dict[str, Any]] = r.json()
return out
def last_log(c: TestClient, pid: int) -> dict[str, Any]:
return log_rows(c, pid)[0]
def find_log(c: TestClient, pid: int, action: str) -> dict[str, Any]:
"""The newest log row with this action text."""
rows = [r for r in log_rows(c, pid) if r["action"] == action]
assert rows, (action, [r["action"] for r in log_rows(c, pid)])
return rows[0]
def ok(r: Any) -> dict[str, Any]:
assert r.status_code == 200, r.text
out: dict[str, Any] = r.json()
return out
def refused(r: Any, status: int, code: str, detail: str | None = None) -> dict[str, Any]:
assert r.status_code == status, r.text
body: dict[str, Any] = r.json()
assert body["code"] == code, body
if detail is not None:
assert body["detail"] == detail, body
return body
def weld_of(pkt: dict[str, Any], no: int) -> dict[str, Any]:
return next(w for w in pkt["welds"] if w["weld_no"] == no)
def tag_of(pkt: dict[str, Any], no: int) -> dict[str, Any]:
return next(t for t in pkt["heat_tags"] if t["tag_no"] == no)
def line_of(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)}
# ---------------------------------------------------------------- the packet
def draft(
shop: Shop,
unit_id: int | None = None,
*,
title: str = "Spool 7 outlet",
drawing: str = "D-5001",
rev: str = "B",
bom: list[dict[str, Any]] | None = None,
wos: list[dict[str, Any]] | None = None,
dept: str = "03",
pages: int = 2,
) -> dict[str, Any]:
"""A hand draft by clerk.clara with the heat map answered and a PDF drawing of `pages` pages."""
if unit_id is None:
_, unit_id = make_unit(shop.settings)
c = shop("clerk.clara")
body = {
"unit_id": unit_id,
"dept_code": dept,
"op": "WELD-PART",
"title": title,
"customer": "Acme Refining",
"drawing": drawing,
"rev": rev,
"wps": "WPS-11",
"work_orders": WOS if wos is None else wos,
"bom": BOM3 if bom is None else bom,
}
r = c.post("/api/packets", json=body)
assert r.status_code == 201, r.text
pid = r.json()["id"]
assert c.patch(f"/api/packets/{pid}", json={"set": {"heat_map": True}, "base": {}}).status_code == 200
a = upload(c, tiny_pdf(pages))
assert a.status_code == 201, a.text
r = c.post(f"/api/packets/{pid}/pages", json={"asset_id": a.json()["id"], "type": "drawing", "sign": SIGN})
return ok(r)
def to_warehouse(shop: Shop, pkt: dict[str, Any]) -> dict[str, Any]:
out = ok(step(shop("clerk.clara"), pkt, "send_to_warehouse"))
assert out["status"] == "warehouse"
return out
def pull_all(shop: Shop, pkt: dict[str, Any]) -> dict[str, Any]:
"""wh.wade ticks every line pulled and gives it a heat."""
c = shop("wh.wade")
for b in list(pkt["bom"]):
item = b["item"]
cur = line_of(pkt, item)
pkt = ok(c.patch(f"/api/packets/{pkt['id']}/bom/{item}", json={"pulled": True}, headers=im(cur["row_version"])))
cur = line_of(pkt, item)
pkt = ok(
c.patch(
f"/api/packets/{pkt['id']}/bom/{item}", json={"heat": f"HT-{item}0"}, headers=im(cur["row_version"])
)
)
return pkt
def release(shop: Shop, pkt: dict[str, Any]) -> dict[str, Any]:
out = ok(step(shop("wh.wade"), pkt, "wh_release"))
assert out["status"] == "ready" and out["dept"]["code"] == "03", out["status"]
return out
def ready_packet(shop: Shop, unit_id: int | None = None, **kw: Any) -> dict[str, Any]:
"""Draft, warehouse, every line pulled with a heat, released: `ready` in 03 with an open stage."""
pkt = to_warehouse(shop, draft(shop, unit_id, **kw))
return release(shop, pull_all(shop, pkt))
def ready_skipped(shop: Shop, unit_id: int | None = None, dept: str = "03", **kw: Any) -> dict[str, Any]:
"""A draft sent straight to a department (wh_skipped true)."""
pkt = draft(shop, unit_id, **kw)
out = ok(step(shop("clerk.clara"), pkt, "release_to_shop", dept=dept))
assert out["status"] == "ready" and out["wh_skipped"] is True
return out
def to_station(
shop: Shop,
pkt: dict[str, Any],
*,
by: str = "sup.wes",
welder: str = "weld.andy",
dept: str = "03",
station: str = "Stall 1",
**kw: Any,
) -> dict[str, Any]:
"""ASSIGN the packet to a station and a welder."""
out = ok(step(shop(by), pkt, "assign", station_id=shop.stall(dept, station), welder_id=shop.uid(welder), **kw))
assert out["status"] == "assigned"
return out
def start(shop: Shop, pkt: dict[str, Any], by: str = "weld.andy") -> dict[str, Any]:
out = ok(step(shop(by), pkt, "start"))
assert out["status"] == "in_progress"
return out
def add_weld(shop: Shop, pkt: dict[str, Any], by: str = "weld.andy", **over: Any) -> dict[str, Any]:
body: dict[str, Any] = {"page": 1, "fx": 0.5, "fy": 0.5, "items": [1, 2], "type": "Fillet", "process": "GMAW"}
body.update(over)
return ok(shop(by).post(f"/api/packets/{pkt['id']}/welds", json=body))
def tick_all(
shop: Shop, pkt: dict[str, Any], by: str = "weld.andy", keys: tuple[str, ...] = WELD_KEYS
) -> dict[str, Any]:
for k in keys:
pkt = ok(shop(by).put(f"/api/packets/{pkt['id']}/stage/checklist/{k}", json={"done": True}))
return pkt
def set_filler(shop: Shop, pkt: dict[str, Any], by: str = "weld.andy", filler: str = "ER70S-6") -> dict[str, Any]:
return ok(shop(by).patch(f"/api/packets/{pkt['id']}/stage", json={"set": {"filler": filler}, "base": {}}))
def finish_stage(shop: Shop, pkt: dict[str, Any], by: str = "weld.andy", welds: int = 1) -> dict[str, Any]:
"""Every checklist key ticked, `welds` welds marked, the filler set: SUBMIT can pass."""
keys = WELD_KEYS if pkt["dept"]["weld"] else PLAIN_KEYS
pkt = tick_all(shop, pkt, by, keys)
for _ in range(welds):
pkt = add_weld(shop, pkt, by)
return set_filler(shop, pkt, by)
def submit(shop: Shop, pkt: dict[str, Any], by: str = "weld.andy") -> dict[str, Any]:
out = ok(step(shop(by), pkt, "submit", sign=SIGN))
assert out["status"] == "review"
return out
def in_progress(shop: Shop, unit_id: int | None = None, **kw: Any) -> dict[str, Any]:
return start(shop, to_station(shop, ready_packet(shop, unit_id, **kw)))
def in_review(shop: Shop, unit_id: int | None = None, **kw: Any) -> dict[str, Any]:
pkt = in_progress(shop, unit_id, **kw)
return submit(shop, finish_stage(shop, pkt))
def issue_co(shop: Shop, pkt: dict[str, Any], required: list[str], **over: Any) -> dict[str, Any]:
"""A manual change order by clerk.clara (approved at once, holds until every required department verifies)."""
body = {"reason": "Nozzle moved 2 in on rev C", "required_depts": required, "sign": SIGN, **over}
return ok(shop("clerk.clara").post(f"/api/packets/{pkt['id']}/change-orders", json=body))
def verify(shop: Shop, pkt: dict[str, Any], no: int, dept: str, by: str, **over: Any) -> Any:
body = {"dept": dept, "note": "Reviewed with the crew", "sign": SIGN, **over}
return shop(by).post(f"/api/packets/{pkt['id']}/change-orders/{no}/verify", json=body)