drawings: VISUAL's rev is the source of truth; attach it when M-Files holds it Effective or Retired
ci / frontend (pull_request) Successful in 43s
ci / server (pull_request) Successful in 11m3s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 3m52s

When M-Files' Effective rev differs from the rev VISUAL calls, the drawing
job looks that rev up (MFilesKit DrawingLookup.revision). If M-Files has
it as Effective or Retired, the job copies it into drawing_revisions and
attaches it to the draft with no flag and no Clerical approval, so the
draft can auto-release. Past draft, already attached or with a hand
drawing: unchanged, no flag and no change order.

Any other status, or no document at that rev, still flags rev_mismatch
with a note saying why. Packet Station only ever attaches Effective or
Retired drawings. rev_approve stays for that case only.

This replaces the "Use rev V as VISUAL calls it" choice from #46: the
use=called path in rev-approve and its button are removed. A packet that
already kept the rev that way keeps it, since that rev is now used anyway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 16:05:57 -06:00
co-authored by Claude Opus 5.5
parent c5bf48ac50
commit ca407a2db3
16 changed files with 292 additions and 250 deletions
+73 -26
View File
@@ -15,11 +15,12 @@ from feedhelpers import (
packet_id,
run_drawings,
run_feed,
set_revisions,
system_audit,
)
from pshelpers import app_conn, needs_pg
from packetstation.drawings.decide import Decision, Target, decide, rev_confirmed
from packetstation.drawings.decide import Called, Decision, Target, decide, rev_confirmed
from packetstation.drawings.fake import FixtureLookup
from packetstation.drawings.lookup import Found
from packetstation.settings import Settings
@@ -129,24 +130,44 @@ def test_approved_other_rev_still_mismatches() -> None:
assert d(target(visual_rev="2", rev_ok="4"), found(rev=3)).kind == "rev_mismatch"
def test_kept_called_rev_stays_while_the_effective_rev_is_the_same() -> None:
t = target(visual_rev="2", drawing_state="attached", attached_rev="2", kept_latest="3")
r = d(t, found(rev=3))
RETIRED_2 = Called(2, "Retired", same_file=False)
def test_visual_rev_retired_in_m_files_is_attached_without_a_flag() -> None:
t = target(visual_rev="2", drawing_state="rev_mismatch")
r = decide(t, found(rev=3), same_file=False, called=RETIRED_2)
assert (r.kind, r.flag, r.drawing_state) == ("attach_called", None, "attached")
assert r.note == "Rev 2 is Retired in M-Files; VISUAL calls it (M-Files has rev 3)"
assert rev_confirmed(t, found(rev=3), RETIRED_2)
assert not rev_confirmed(t, found(rev=3))
def test_visual_rev_retired_already_attached_stays() -> None:
t = target(visual_rev="2", drawing_state="attached", attached_rev="2")
r = decide(t, found(rev=3), same_file=False, called=Called(2, "Retired", same_file=True))
assert (r.kind, r.flag, r.drawing_state) == ("unchanged", None, "attached")
assert r.note == "Rev 2 kept by Clerical; M-Files has rev 3"
assert rev_confirmed(t, found(rev=3))
def test_kept_called_rev_opens_no_change_order_past_draft() -> None:
t = target(visual_rev="2", status="ready", drawing_state="attached", attached_rev="2", kept_latest="3")
assert d(t, found(rev=3)).kind == "unchanged"
def test_visual_rev_retired_past_draft_and_hand_drawing_stay() -> None:
shop = target(visual_rev="2", status="ready", drawing_state="attached", attached_rev="2")
assert decide(shop, found(rev=4), same_file=False, called=RETIRED_2).kind == "unchanged" # no CO, no flag
hand = target(visual_rev="2", drawing_state="hand", has_hand_drawing=True)
r = decide(hand, found(rev=3), same_file=False, called=RETIRED_2)
assert (r.kind, r.flag, r.drawing_state) == ("unchanged", None, "hand")
def test_kept_called_rev_flags_again_on_a_newer_effective_rev() -> None:
t = target(visual_rev="2", drawing_state="attached", attached_rev="2", kept_latest="3")
r = d(t, found(rev=4))
assert (r.kind, r.flag, r.drawing_state) == ("rev_mismatch", "rev_mismatch", "attached")
assert not rev_confirmed(t, found(rev=4))
def test_visual_rev_retired_with_no_effective_rev() -> None:
r = decide(target(visual_rev="2"), found(rev=None, name=None), same_file=False, called=RETIRED_2)
assert r.kind == "attach_called" and r.note.endswith("(no Effective rev in M-Files)")
def test_an_approved_effective_rev_wins_over_the_called_rev() -> None:
r = decide(target(visual_rev="2", rev_ok="3"), found(rev=3), same_file=False, called=RETIRED_2)
assert (r.kind, r.flag) == ("attach", None)
def test_no_called_copy_still_flags_the_mismatch() -> None:
assert decide(target(visual_rev="2"), found(rev=3), same_file=False, called=None).kind == "rev_mismatch"
def test_match_on_a_draft_attaches() -> None:
@@ -266,24 +287,23 @@ def test_job_attaches_and_flags(fx: Settings) -> None:
lay = packet_by_key(conn, gen.KEY_LAYOUT_A)
assert lay is not None and lay["drawing_state"] == "attached"
mm = packet_by_key(conn, gen.KEY_TWO_OPS) # VISUAL rev 2, M-Files rev 3
assert mm is not None and "rev_mismatch" in mm["flags"] and mm["drawing_state"] == "rev_mismatch"
assert mm["visual"]["appr"]["rev"] == 3
staged = conn.execute(
"SELECT asset_id IS NOT NULL, rev FROM drawing_lookups WHERE dwg = 'L900-0201'"
).fetchone()
assert staged == (True, 3) # staged for Clerical Supervisor approval
assert _drawing_pages(conn, mm["id"]) == []
called = mm["visual"]["called"] # VISUAL's rev 2, Retired in M-Files, copied for Clerical's choice
# VISUAL rev 2, M-Files' Effective rev 3, rev 2 Retired in M-Files: VISUAL wins, rev 2 is attached, no flag
mm = packet_by_key(conn, gen.KEY_TWO_OPS)
assert mm is not None and not set(mm["flags"]) & DRAWING_FLAGS and mm["drawing_state"] == "attached"
assert mm["visual"]["appr"]["rev"] == 3 and mm["visual"]["rev_confirmed"] is True
assert mm["visual"]["vault"]["rev"] == 2 and mm["visual"]["vault"]["controlled"] == "Retired"
pages = _drawing_pages(conn, mm["id"])
assert pages and all(p[2] == "L900-0201" and p[3] == "2" for p in pages)
called = mm["visual"]["called"]
assert (called["rev"], called["controlled"], called["state"]) == ("2", "Retired", "Superseded")
assert called["asset_id"] and called["pages"] >= 1
rv = conn.execute(
"SELECT controlled, asset_id IS NOT NULL FROM drawing_revisions WHERE dwg = 'L900-0201' AND rev = 2"
).fetchone()
assert rv == ("Retired", True)
assert conn.execute(
"SELECT count(*) FROM packet_log WHERE packet_id = %s AND note LIKE %s",
(mm["id"], "%rev 2 is Retired in M-Files%"),
"SELECT count(*) FROM packet_log WHERE packet_id = %s AND action = 'Drawing attached from M-Files'"
" AND note LIKE %s",
(mm["id"], "%Rev 2 is Retired in M-Files; VISUAL calls it (M-Files has rev 3)%"),
).fetchone()[0]
nr = packet_by_key(conn, gen.KEY_DRAFTING)
@@ -305,6 +325,33 @@ def test_job_attaches_and_flags(fx: Settings) -> None:
assert system_audit(conn, "drawing.fetched")
@needs_pg
@pytest.mark.parametrize(
("revisions", "note"),
[
(None, "rev 2 is not in M-Files"),
(
{"2": {"file": "L900-0201 r2.pdf", "status": "Obsolete", "obj_id": 32, "file_id": 132}},
"rev 2 is Obsolete in M-Files - not attached",
),
],
)
def test_job_flags_when_the_visual_rev_cannot_be_attached(
fx: Settings, revisions: dict[str, Any] | None, note: str
) -> None:
set_revisions(fx.fixture_dir, "L900-0201", revisions)
run_feed(fx, 1)
run_drawings(fx)
with app_conn(fx) as conn:
mm = packet_by_key(conn, gen.KEY_TWO_OPS)
assert mm is not None and "rev_mismatch" in mm["flags"] and mm["drawing_state"] == "rev_mismatch"
assert _drawing_pages(conn, mm["id"]) == []
assert conn.execute("SELECT count(*) FROM drawing_revisions").fetchone()[0] == 0 # nothing copied
assert conn.execute(
"SELECT count(*) FROM packet_log WHERE packet_id = %s AND note LIKE %s", (mm["id"], f"%{note}%")
).fetchone()[0]
@needs_pg
def test_job_second_run_changes_nothing(fx: Settings) -> None:
run_feed(fx, 1)