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
+40 -7
View File
@@ -4,7 +4,8 @@
`{"rev": 3, "file": "L073762X r3.pdf", "draft_rev": null, "draft_where": "", "in_drafting": false, "ok": true,
"note": "", "obj_id": 1, "obj_version": 1, "file_id": 1, "file_version": 1}`; every key is optional. A drawing
not in the file is not found, after the same H-sheet fallback MFilesKit uses (K978-01-6H13 -> K978-01-6).
`download` copies `dir/<file>`.
An entry may also carry `"revisions": {"2": {"file": "L073762X r2.pdf", "status": "Retired", "state": "Superseded",
"obj_id": 7, "file_id": 107}}` for `revision` (other revs of the drawing, any status). `download` copies `dir/<file>`.
"""
from __future__ import annotations
@@ -60,16 +61,12 @@ class FixtureLookup:
def status(self, dwg: str) -> Found:
dwg = (dwg or "").strip()
data = self._load()
self._load()
if self._error:
return failed(dwg, self._error)
if not dwg:
return Found(dwg="", note="no drawing number")
entry, base = data.get(dwg), None
if entry is None:
b = base_drawing(dwg)
if b and b in data:
entry, base = data[b], b
entry, base = self._entry(dwg)
if entry is None:
return Found(dwg=dwg, note=NOT_FOUND_NOTE)
if entry.get("ok") is False:
@@ -98,6 +95,40 @@ class FixtureLookup:
note=str(entry.get("note") or ""),
)
def _entry(self, dwg: str) -> tuple[dict[str, Any] | None, str | None]:
data = self._load()
entry, base = data.get(dwg), None
if entry is None:
b = base_drawing(dwg)
if b and b in data:
entry, base = data[b], b
return entry, base
def revision(self, dwg: str, rev: int) -> Found | None:
dwg = (dwg or "").strip()
entry, base = self._entry(dwg)
if self._error:
raise DrawingFetchError(self._error)
revs = (entry or {}).get("revisions") or {}
r = revs.get(str(int(rev))) if isinstance(revs, dict) else None
if not isinstance(r, dict) or not r.get("file"):
return None
p = self._file(str(r["file"]))
return Found(
dwg=dwg,
ok=True,
rev=int(rev),
name=str(r.get("name") or r["file"]),
obj_id=_int(r.get("obj_id", 1)),
obj_version=_int(r.get("obj_version", 1)),
file_id=_int(r.get("file_id", 1)),
file_version=_int(r.get("file_version", 1)),
file_size=p.stat().st_size if p is not None and p.is_file() else None,
base=base,
controlled=str(r.get("status") or "Retired"),
state=str(r.get("state") or ""),
)
def _file(self, name: str) -> Path | None:
root = self.dir.resolve()
p = (root / name).resolve()
@@ -105,6 +136,8 @@ class FixtureLookup:
def download(self, found: Found, dest: Path) -> int:
entry = self._load().get(found.base or found.dwg) or self._load().get(found.dwg) or {}
if found.controlled and found.rev is not None:
entry = ((entry.get("revisions") or {}).get(str(found.rev))) or {}
name = entry.get("file")
src = self._file(str(name)) if name else None
if src is None or not src.is_file():