Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
- 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]>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""Change C8 (Gitea issue 23): checklists per department.
|
||||
|
||||
The department supervisor edits their own department's list, Admin edits any, a lead, a worker and Q.C. do not. A
|
||||
save carries the version (`base` or If-Match) and is audited. A stage freezes its list at START, so a change reaches
|
||||
only stages not yet started. A department without its own list uses the default for its kind. Nothing here reads
|
||||
VISUAL or M-Files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fakedir import FakeDirectory
|
||||
from shophelpers import (
|
||||
PLAIN_KEYS,
|
||||
WELD_KEYS,
|
||||
Shop,
|
||||
add_weld,
|
||||
fresh,
|
||||
ok,
|
||||
one,
|
||||
open_shop,
|
||||
ready_packet,
|
||||
refused,
|
||||
set_filler,
|
||||
start,
|
||||
tick_all,
|
||||
to_station,
|
||||
)
|
||||
|
||||
from packetstation.domain import checklists
|
||||
from packetstation.settings import Settings
|
||||
|
||||
URL = "/api/checklists"
|
||||
NEW_LIST = [
|
||||
{"group": "Before welding", "key": "dwg", "text": "Drawing {drawing} Rev {rev} on the bench"},
|
||||
{"group": "Before welding", "key": "purge", "text": "Back purge set"},
|
||||
{"group": "After welding", "key": "vt", "text": "Visual done"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shop(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[Shop]:
|
||||
yield from open_shop(pg_settings, fake_dir)
|
||||
|
||||
|
||||
def keys(pkt: dict[str, Any]) -> list[str]:
|
||||
return [i["key"] for i in pkt["checklist_items"]]
|
||||
|
||||
|
||||
def put(shop: Shop, sam: str, dept: str, items: Any, base: int | None) -> Any:
|
||||
body: dict[str, Any] = {"items": items}
|
||||
if base is not None:
|
||||
body["base"] = base
|
||||
return shop(sam).put(f"{URL}/{dept}", json=body)
|
||||
|
||||
|
||||
def test_list_and_get(shop: Shop) -> None:
|
||||
rows = {r["dept_code"]: r for r in ok(shop("sup.wes").get(URL))}
|
||||
assert "03" in rows and "CL" not in rows and "QC" not in rows
|
||||
assert rows["03"]["editable"] is True and rows["03"]["weld"] is True and rows["03"]["custom"] is False
|
||||
assert rows["03"]["version"] == 0 and rows["03"]["dept_name"] == "Weld Parts"
|
||||
assert not any(r["editable"] for code, r in rows.items() if code != "03")
|
||||
assert all(r["editable"] for r in ok(shop("admin.ann").get(URL)))
|
||||
for sam in ("lead.lou", "weld.andy", "qc.quinn", "clerk.clara"):
|
||||
assert not any(r["editable"] for r in ok(shop(sam).get(URL))), sam
|
||||
got = ok(shop("qc.quinn").get(f"{URL}/03"))
|
||||
assert [i["key"] for i in got["items"]] == list(WELD_KEYS) and got["defaults"] == got["items"]
|
||||
assert got["editable"] is False and got["custom"] is False
|
||||
assert "{drawing}" in got["items"][0]["text"] # unfilled
|
||||
plain = ok(shop("sup.wes").get(f"{URL}/09"))
|
||||
assert [i["key"] for i in plain["items"]] == list(PLAIN_KEYS) and plain["editable"] is False
|
||||
refused(shop("sup.wes").get(f"{URL}/QC"), 404, "not_found")
|
||||
|
||||
|
||||
def test_who_may_save(shop: Shop) -> None:
|
||||
for sam in ("lead.lou", "weld.andy", "qc.quinn", "clerk.clara", "pm.paula"):
|
||||
refused(put(shop, sam, "03", NEW_LIST, 0), 403, "forbidden")
|
||||
refused(put(shop, "sup.wes", "09", NEW_LIST, 0), 403, "forbidden") # not his department
|
||||
out = ok(put(shop, "sup.wes", "03", NEW_LIST, 0))
|
||||
assert out["custom"] is True and out["version"] == 1 and [i["key"] for i in out["items"]] == ["dwg", "purge", "vt"]
|
||||
# Admin edits any department
|
||||
out = ok(put(shop, "admin.ann", "09", NEW_LIST, 0))
|
||||
assert out["custom"] is True and out["dept_code"] == "09"
|
||||
# the other departments keep the default for their kind
|
||||
assert ok(shop("sup.wes").get(f"{URL}/04"))["custom"] is False
|
||||
row = one(
|
||||
shop.settings,
|
||||
"SELECT count(*) FROM audit_events WHERE action = 'checklists.dept_updated' AND subject_id = 'checklist:03'",
|
||||
)
|
||||
assert row is not None and row[0] == 1
|
||||
|
||||
|
||||
def test_version_cas_and_validation(shop: Shop) -> None:
|
||||
ok(put(shop, "sup.wes", "03", NEW_LIST, 0))
|
||||
stale = refused(put(shop, "sup.wes", "03", NEW_LIST, 0), 409, "version_conflict")
|
||||
assert stale["current"]["version"] == 1 and stale["current"]["custom"] is True
|
||||
refused(put(shop, "sup.wes", "03", NEW_LIST, None), 428, "if_match_required")
|
||||
r = shop("sup.wes").put(f"{URL}/03", json={"items": NEW_LIST}, headers={"If-Match": '"1"'})
|
||||
assert ok(r)["version"] == 2
|
||||
bad = refused(put(shop, "sup.wes", "03", [{"group": "G", "key": "Bad-Key", "text": "t"}], 2), 422, "bad_checklist")
|
||||
assert "key" in bad["detail"]
|
||||
refused(put(shop, "sup.wes", "03", [], 2), 422, "bad_checklist")
|
||||
refused(put(shop, "sup.wes", "03", [{"group": "G", "key": "x", "text": "{stamp}"}], 2), 422, "bad_checklist")
|
||||
# null sets the department back to the default for its kind
|
||||
back = ok(put(shop, "sup.wes", "03", None, 2))
|
||||
assert back["custom"] is False and back["version"] == 3 and [i["key"] for i in back["items"]] == list(WELD_KEYS)
|
||||
|
||||
|
||||
def test_a_change_reaches_only_stages_not_started(shop: Shop) -> None:
|
||||
andy = shop("weld.andy")
|
||||
started = start(shop, to_station(shop, ready_packet(shop, drawing="D-7007", rev="A")))
|
||||
assigned = to_station(shop, ready_packet(shop), station="Stall 2")
|
||||
waiting = ready_packet(shop)
|
||||
ok(put(shop, "sup.wes", "03", NEW_LIST, 0))
|
||||
|
||||
# the started stage keeps the default it started with
|
||||
assert keys(fresh(andy, started["id"])) == list(WELD_KEYS)
|
||||
refused(andy.put(f"/api/packets/{started['id']}/stage/checklist/purge", json={"done": True}), 422, "invalid_key")
|
||||
# the stages not yet started read the new list
|
||||
assert keys(fresh(andy, assigned["id"])) == ["dwg", "purge", "vt"]
|
||||
assert keys(fresh(shop("sup.wes"), waiting["id"])) == ["dwg", "purge", "vt"]
|
||||
|
||||
# START freezes: a later change does not reach it
|
||||
later = start(shop, assigned)
|
||||
ok(put(shop, "sup.wes", "03", [*NEW_LIST, {"group": "After welding", "key": "tag", "text": "Tagged"}], 1))
|
||||
assert keys(fresh(andy, later["id"])) == ["dwg", "purge", "vt"]
|
||||
assert keys(fresh(shop("sup.wes"), waiting["id"])) == ["dwg", "purge", "vt", "tag"]
|
||||
stage = one(
|
||||
shop.settings,
|
||||
"SELECT checklist_template FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL",
|
||||
(later["id"],),
|
||||
)
|
||||
assert stage is not None and [i["key"] for i in stage[0]] == ["dwg", "purge", "vt"]
|
||||
|
||||
# completeness reads the frozen list: three ticks, a weld and the filler let it submit
|
||||
out = tick_all(shop, later, keys=("dwg", "purge", "vt"))
|
||||
out = set_filler(shop, add_weld(shop, out))
|
||||
assert "submit" in fresh(andy, out["id"])["can"]
|
||||
# the started packet still needs the default keys
|
||||
assert "submit" not in fresh(andy, started["id"])["can"]
|
||||
|
||||
# the data book's checklist section reads the frozen list too
|
||||
with shop_conn(shop) as conn:
|
||||
t = checklists.stage_templates(conn, stage_id(shop, later["id"]), "03")
|
||||
assert [i["key"] for i in t["weld"]] == ["dwg", "purge", "vt"]
|
||||
live = checklists.stage_templates(conn, None, "03")
|
||||
assert [i["key"] for i in live["weld"]] == ["dwg", "purge", "vt", "tag"]
|
||||
|
||||
|
||||
def stage_id(shop: Shop, packet_id: int) -> int:
|
||||
row = one(shop.settings, "SELECT id FROM packet_stages WHERE packet_id = %s AND closed_at IS NULL", (packet_id,))
|
||||
assert row is not None
|
||||
return int(row[0])
|
||||
|
||||
|
||||
def shop_conn(shop: Shop) -> Any:
|
||||
from pshelpers import app_conn
|
||||
|
||||
return app_conn(shop.settings)
|
||||
Reference in New Issue
Block a user