drawings: on a rev mismatch, Clerical can keep the retired rev VISUAL calls (#41)
ci / server (pull_request) Failing after 17s
ci / frontend (pull_request) Successful in 42s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Skipped

When VISUAL calls a rev that M-Files has retired, the draft is still
flagged rev_mismatch, but the drawing job now also copies the rev VISUAL
calls (MFilesKit 0.1.3 DrawingLookup.revision) into drawing_revisions and
visual.called. A Clerical Supervisor picks either rev on the packet:
"Use latest rev N" as before, or "Use rev V as VISUAL calls it"
(rev-approve with use=called), with a reason and a signature.

The kept rev holds while M-Files' Effective rev stays the one chosen
against: no flag, no change order. A newer Effective rev flags the
packet again; a new VISUAL rev clears the choice.

Migration 0009 adds drawing_revisions. Needs mfileskit 0.1.3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 13:39:14 -06:00
co-authored by Claude Opus 5.5
parent 4c0cba4da5
commit bb8eb17748
21 changed files with 547 additions and 55 deletions
+52 -2
View File
@@ -17,7 +17,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, Protocol
if TYPE_CHECKING:
from mfileskit.drawings import DrawingStatus
from mfileskit.drawings import DrawingStatus, RevisionFile
from mfileskit.vault import Vault
from ..settings import Settings
@@ -50,6 +50,14 @@ class Found:
draft_where: str = ""
in_drafting: bool = False
note: str = ""
# Set only on a `Lookup.revision` result: M-Files' Controlled Status for that rev ("Effective", "Retired") and
# its workflow state ("Superseded").
controlled: str = ""
state: str = ""
@property
def effective(self) -> bool:
return self.controlled.casefold() == "effective"
@property
def identity(self) -> tuple[int | None, int | None, int | None, int | None]:
@@ -63,6 +71,11 @@ def failed(dwg: str, note: str) -> Found:
class Lookup(Protocol):
def status(self, dwg: str) -> Found: ...
def revision(self, dwg: str, rev: int) -> Found | None:
"""One rev's file whatever its status (a Retired rev VISUAL still calls), with `controlled` set; None when
M-Files has no document at that rev. Raises LookupError when M-Files can't be read."""
...
def download(self, found: Found, dest: Path) -> int:
"""Write the released PDF to `dest`; bytes written. Raises DrawingFetchError (a LookupError)."""
...
@@ -91,6 +104,25 @@ def from_status(st: DrawingStatus) -> Found:
)
def from_revision(rf: RevisionFile) -> Found:
"""MFilesKit's RevisionFile -> Found, with `controlled` and `state` set."""
ov, fi = rf.obj_ver, rf.file
return Found(
dwg=rf.drawing,
ok=True,
rev=rf.rev,
name=rf.name,
obj_id=ov.id if ov is not None else None,
obj_version=ov.version if ov is not None else None,
file_id=fi.id if fi is not None else None,
file_version=fi.version if fi is not None else None,
file_size=fi.size if fi is not None else None,
base=rf.base,
controlled=rf.status or "",
state=rf.state or "",
)
class MFilesLookup:
"""M-Files through `mfileskit.drawings.DrawingLookup`. Read-only."""
@@ -100,6 +132,7 @@ class MFilesLookup:
self.vault = vault
self._lookup = DrawingLookup(vault) # reads the vault schema; raises MFilesError when the vault is down
self._seen: dict[str, DrawingStatus] = {}
self._seen_rev: dict[tuple[str, int], RevisionFile] = {}
def status(self, dwg: str) -> Found:
from mfileskit.errors import MFilesError
@@ -112,10 +145,27 @@ class MFilesLookup:
self._seen[dwg] = st
return from_status(st)
def revision(self, dwg: str, rev: int) -> Found | None:
from mfileskit.errors import MFilesError
dwg = (dwg or "").strip()
try:
rf = self._lookup.revision(dwg, int(rev))
except MFilesError as e:
raise DrawingFetchError(f"{dwg}: M-Files lookup of rev {rev} failed: {e}"[:NOTE_MAX]) from None
if rf is None:
return None
self._seen_rev[(dwg, int(rev))] = rf
return from_revision(rf)
def download(self, found: Found, dest: Path) -> int:
from mfileskit.errors import MFilesError
st = self._seen.get(found.dwg)
st: DrawingStatus | RevisionFile | None
if found.controlled and found.rev is not None:
st = self._seen_rev.get((found.dwg, found.rev))
else:
st = self._seen.get(found.dwg)
if st is None or st.obj_ver is None or st.file is None:
raise DrawingFetchError(f"{found.dwg}: no released file to download")
try: