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
+38 -8
View File
@@ -12,7 +12,7 @@ import hashlib
import uuid
from collections.abc import Mapping
from datetime import UTC, datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Request
from psycopg.types.json import Jsonb
@@ -49,6 +49,9 @@ MSG_DRAFT_ONLY = "Only a draft takes a hand upload. Once it leaves Clerical, cha
class RevApproveBody(BaseModel):
reason: str | None = Field(default=None, max_length=2000)
# "latest": M-Files' Effective rev in place of the rev VISUAL calls. "called": the rev VISUAL calls, which M-Files
# holds at another status (Retired, say).
use: Literal["latest", "called"] = "latest"
sign: SignBody | None = None
it_reason: str | None = Field(default=None, max_length=1000)
@@ -186,17 +189,22 @@ def rev_approve(
p: Principal = Depends(current_principal), # noqa: B008
d: Db = Depends(get_db), # noqa: B008
) -> dict[str, Any]:
"""A Clerical Supervisor approves the rev M-Files has in place of the rev VISUAL calls, for this draft only."""
"""A Clerical Supervisor settles a rev mismatch on a draft: M-Files' Effective rev in place of the rev VISUAL calls
(`use = latest`), or the rev VISUAL calls although M-Files has retired it (`use = called`). For this draft only."""
conn = d.conn
reason = reason_ok(body.reason)
pp = _pp(d, p)
row = packets.load_row(conn, packet_id)
staged = packets.staged_lookup(conn, row["drawing"]) if row else None
if body.use == "called":
staged = packets.called_lookup(conn, row["drawing"], row["rev"]) if row else None
else:
staged = packets.staged_lookup(conn, row["drawing"]) if row else None
content = {
"action": str(A.REV_APPROVE),
"packet_id": packet_id,
"drawing": row["drawing"] if row else None,
"visual_rev": row["rev"] if row else None,
"use": body.use,
"rev": staged["rev"] if staged else None,
"sha256": bytes(staged["sha256"]).hex() if staged and staged["sha256"] else None,
"reason": reason,
@@ -215,7 +223,17 @@ def rev_approve(
) as w:
if w.packet["source_kind"] != "visual":
raise ApiError(409, "not_visual", MSG_NOT_VISUAL)
lk = packets.staged_lookup(conn, w.packet["drawing"])
latest = packets.staged_lookup(conn, w.packet["drawing"])
if body.use == "called":
lk = packets.called_lookup(conn, w.packet["drawing"], w.packet["rev"])
if lk is None:
raise ApiError(
409,
"nothing_staged",
"M-Files has no copy of the rev VISUAL calls ready. Try after the next run.",
)
else:
lk = latest
if lk is None:
raise ApiError(
409, "nothing_staged", "No M-Files copy of this drawing is ready yet. Try after the next run."
@@ -229,13 +247,21 @@ def rev_approve(
vis_rev = w.packet["rev"] or ""
replace_drawing_pages(conn, packet_id, asset, dwg=w.packet["drawing"], rev=rev)
visual = dict(w.packet["visual"] or {})
visual["rev_ok"] = {"rev": rev, "vis_rev": vis_rev, **who_of(w), "at": now_iso(), "reason": reason}
ok = {"rev": rev, "vis_rev": vis_rev, **who_of(w), "at": now_iso(), "reason": reason}
if body.use == "called":
# The decision holds while M-Files' Effective rev stays the one Clerical chose against.
ok |= {
"use": "called",
"latest": None if latest is None else str(latest["rev"]),
"controlled": lk["controlled"],
}
visual["rev_ok"] = ok
visual["rev_confirmed"] = True
visual["vault"] = {
"asset_id": str(asset["id"]),
"name": lk["name"],
"rev": rev,
"note": lk["note"],
"note": f"{lk['controlled']} in M-Files" if body.use == "called" else lk["note"],
"at": now_iso(),
}
visual.pop("manual_dwg", None)
@@ -245,8 +271,12 @@ def rev_approve(
(flags_without(w.packet["flags"], (*packets.DRAWING_FLAGS, "hand_drawing")), packet_id),
)
w.changed("pages", "drawing_state", "flags", "visual")
w.detail.update(asset_id=str(asset["id"]), rev=rev, vis_rev=vis_rev)
w.log("Different rev approved by hand", f"rev {rev} in place of VISUAL rev {vis_rev or '-'} · {reason}")
w.detail.update(asset_id=str(asset["id"]), rev=rev, vis_rev=vis_rev, use=body.use)
if body.use == "called":
effective = f"M-Files has rev {ok['latest']}" if ok["latest"] else "no Effective rev in M-Files"
w.log("Rev VISUAL calls kept by hand", f"rev {rev} ({lk['controlled']} in M-Files), {effective} · {reason}")
else:
w.log("Different rev approved by hand", f"rev {rev} in place of VISUAL rev {vis_rev or '-'} · {reason}")
return _read(d, pp, packet_id)