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
+8 -38
View File
@@ -12,7 +12,7 @@ import hashlib
import uuid
from collections.abc import Mapping
from datetime import UTC, datetime
from typing import Any, Literal
from typing import Any
from fastapi import APIRouter, Depends, Request
from psycopg.types.json import Jsonb
@@ -49,9 +49,6 @@ 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)
@@ -189,22 +186,17 @@ def rev_approve(
p: Principal = Depends(current_principal), # noqa: B008
d: Db = Depends(get_db), # noqa: B008
) -> dict[str, Any]:
"""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."""
"""A Clerical Supervisor approves the rev M-Files has in place of the rev VISUAL calls, for this draft only."""
conn = d.conn
reason = reason_ok(body.reason)
pp = _pp(d, p)
row = packets.load_row(conn, packet_id)
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
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,
@@ -223,17 +215,7 @@ def rev_approve(
) as w:
if w.packet["source_kind"] != "visual":
raise ApiError(409, "not_visual", MSG_NOT_VISUAL)
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
lk = packets.staged_lookup(conn, w.packet["drawing"])
if lk is None:
raise ApiError(
409, "nothing_staged", "No M-Files copy of this drawing is ready yet. Try after the next run."
@@ -247,21 +229,13 @@ 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 {})
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_ok"] = {"rev": rev, "vis_rev": vis_rev, **who_of(w), "at": now_iso(), "reason": reason}
visual["rev_confirmed"] = True
visual["vault"] = {
"asset_id": str(asset["id"]),
"name": lk["name"],
"rev": rev,
"note": f"{lk['controlled']} in M-Files" if body.use == "called" else lk["note"],
"note": lk["note"],
"at": now_iso(),
}
visual.pop("manual_dwg", None)
@@ -271,12 +245,8 @@ 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, 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}")
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}")
return _read(d, pp, packet_id)
-13
View File
@@ -646,19 +646,6 @@ def staged_rev(conn: Any, drawing: Any) -> dict[str, Any] | None:
return {"rev": lk["rev"], "name": lk["name"], "asset_id": str(lk["asset_id"])}
def called_lookup(conn: Any, drawing: Any, rev: Any) -> dict[str, Any] | None:
"""The drawing_revisions row for the rev VISUAL calls, when the drawing job copied it (asset set)."""
dwg = str(drawing or "").strip()
r = str(rev or "").strip()
if not dwg or not r.isdigit():
return None
return one(
conn,
"SELECT * FROM drawing_revisions WHERE dwg = %s AND rev = %s AND asset_id IS NOT NULL",
(dwg, int(r)),
)
def staged_lookup(conn: Any, drawing: Any) -> dict[str, Any] | None:
"""The drawing_lookups row for a drawing number when it holds a fetched copy (asset and rev set)."""
dwg = str(drawing or "").strip()
+58 -20
View File
@@ -1,6 +1,9 @@
"""The drawing decision table. Pure: no database, no M-Files.
`decide(target, found, same_file=...)` says what the drawing job does to one packet for one lookup. Note texts are
`decide(target, found, same_file=..., called=...)` says what the drawing job does to one packet for one lookup.
VISUAL is the source of truth for the rev: when M-Files' Effective rev differs from the rev VISUAL calls but M-Files
still holds that rev as Effective or Retired, the rev VISUAL calls is used, with no flag and no Clerical approval.
The job passes `called` only for those two statuses; no drawing at any other status is ever attached. Note texts are
the app's own (from the old revMismatch and draftLine in visual.js, "-" in place of the em-dash), never M-Files'
note passed through, except inside the lookup_failed text.
@@ -9,14 +12,15 @@ Rows, in the order they are tried:
2. past draft, a pending feed change order waits for this drawing at the released rev -> stage_for_co.
Past draft, the pending change order already stages this drawing at the released rev -> unchanged, or
stage_for_co again when M-Files has a different file for that rev.
2a. M-Files' Effective rev is not the rev VISUAL calls (or there is none), no approved rev equals it, and M-Files
holds the rev VISUAL calls as Effective or Retired (`called`) -> on a draft, attach_called (unchanged when that very
file is attached, or a hand drawing is); past draft, unchanged. No flag. Note "Rev V is Retired in M-Files;
VISUAL calls it (M-Files has rev N)".
3. no released rev, a draft rev or a drafting assignment -> flag not_released with the drafting status.
4. no released rev otherwise -> flag not_found.
5. VISUAL rev not a number (and no approved rev equal to M-Files') -> flag bad_rev, not attached.
6. revs differ (and no approved rev equal to M-Files') -> flag rev_mismatch. The job stages the M-Files copy
for Clerical Supervisor approval on drafts only, and on a draft also copies the rev VISUAL calls when M-Files
still holds it (Retired, say), so Clerical can choose either.
Revs differ, but Clerical chose to keep the rev VISUAL calls while M-Files' Effective rev was this one ->
unchanged (no flag, no change order). A newer Effective rev flags the packet again.
6. revs differ (and no approved rev equal to M-Files', and M-Files has no document at the rev VISUAL calls) ->
flag rev_mismatch. The job stages the M-Files copy for Clerical Supervisor approval on drafts only.
7. revs match, draft with a hand drawing -> unchanged (the feed already took the hand drawing off on a rev move).
8. revs match, draft with this very file attached at this rev -> unchanged.
9. revs match, draft -> attach ("sheet of drawing <base>" for an H sheet).
@@ -41,11 +45,23 @@ NOT_FOUND = "not_found"
BAD_REV = "bad_rev"
REV_MISMATCH = "rev_mismatch"
ATTACH = "attach"
ATTACH_CALLED = "attach_called"
UNCHANGED = "unchanged"
OPEN_CO = "open_co"
STAGE_FOR_CO = "stage_for_co"
KINDS = (ATTACH, UNCHANGED, REV_MISMATCH, NOT_RELEASED, NOT_FOUND, LOOKUP_FAILED, BAD_REV, OPEN_CO, STAGE_FOR_CO)
KINDS = (
ATTACH,
ATTACH_CALLED,
UNCHANGED,
REV_MISMATCH,
NOT_RELEASED,
NOT_FOUND,
LOOKUP_FAILED,
BAD_REV,
OPEN_CO,
STAGE_FOR_CO,
)
FLAG_KINDS = (LOOKUP_FAILED, NOT_RELEASED, NOT_FOUND, BAD_REV, REV_MISMATCH)
# drawing_state enum values (0003). bad_rev has no state of its own: it is a rev mismatch Clerical can approve.
@@ -72,8 +88,15 @@ class Target:
co_pending: bool = False
co_staged_sha: bytes | None = None # sha256 of the file the pending CO already stages
rejected_sha: bytes | None = None # sha256 staged on the last drawing-origin CO Clerical rejected
# Clerical chose the rev VISUAL calls (a Retired rev) over M-Files' Effective rev; this is that Effective rev.
kept_latest: str | None = None
@dataclass(frozen=True)
class Called:
"""The rev VISUAL calls, as M-Files holds it (Effective or Retired), with Packet Station's copy."""
rev: int
controlled: str # M-Files' Controlled Status, e.g. "Retired"
same_file: bool # this copy is the drawing attached to the packet
@dataclass(frozen=True)
@@ -111,25 +134,33 @@ def failed_note(f: Found) -> str:
return f"{_FAILED_PREFIX}{detail or 'no detail'}"
def kept_called(t: Target, f: Found) -> bool:
"""Clerical kept the rev VISUAL calls, and M-Files' Effective rev is still the one they chose against."""
return f.ok and f.rev is not None and t.kept_latest is not None and str(t.kept_latest).strip() == str(f.rev)
def uses_called(t: Target, f: Found, called: Called | None) -> bool:
"""The rev VISUAL calls is used from M-Files at another status: it differs from M-Files' Effective rev (or there
is none), Clerical approved no other rev, and M-Files holds it."""
if not f.ok or called is None:
return False
v = _int(t.visual_rev)
if v is None or v != called.rev or v == f.rev:
return False
return not (f.rev is not None and t.rev_ok is not None and str(t.rev_ok).strip() == str(f.rev))
def rev_confirmed(t: Target, f: Found) -> bool:
"""The old revConfirmed: M-Files' released rev is the rev VISUAL calls, or Clerical approved that rev, or Clerical
kept the rev VISUAL calls over that rev."""
def rev_confirmed(t: Target, f: Found, called: Called | None = None) -> bool:
"""The old revConfirmed: M-Files' released rev is the rev VISUAL calls, or Clerical approved that rev, or M-Files
holds the rev VISUAL calls at another status."""
if uses_called(t, f, called):
return True
if not f.ok or f.rev is None:
return False
v = _int(t.visual_rev)
if v is not None and v == f.rev:
return True
if kept_called(t, f):
return True
return t.rev_ok is not None and str(t.rev_ok).strip() == str(f.rev)
def decide(t: Target, f: Found, *, same_file: bool, fetched_sha: bytes | None = None) -> Decision:
def decide(
t: Target, f: Found, *, same_file: bool, fetched_sha: bytes | None = None, called: Called | None = None
) -> Decision:
"""What to do to one packet.
`same_file`: the M-Files file has the sha256 of the attached drawing asset. `fetched_sha`: the sha256 of the
@@ -163,6 +194,15 @@ def decide(t: Target, f: Found, *, same_file: bool, fetched_sha: bytes | None =
if fetched_sha is not None and t.co_staged_sha is not None and fetched_sha != t.co_staged_sha:
return keep(STAGE_FOR_CO, f"New file for rev {f.rev} in M-Files; staged again for the change order")
return keep(UNCHANGED, f"Rev {f.rev} is staged on the pending change order")
# 2a
if called is not None and uses_called(t, f, called):
has = f"M-Files has rev {f.rev}" if f.rev is not None else "no Effective rev in M-Files"
note = f"Rev {called.rev} is {called.controlled} in M-Files; VISUAL calls it ({has})"
if not draft or t.has_hand_drawing:
return keep(UNCHANGED, note)
if t.drawing_state == "attached" and called.same_file and t.attached_rev == str(called.rev):
return keep(UNCHANGED, note)
return Decision(ATTACH_CALLED, None, "attached", note)
# 3, 4
if f.rev is None:
d = draft_line(f)
@@ -173,8 +213,6 @@ def decide(t: Target, f: Found, *, same_file: bool, fetched_sha: bytes | None =
approved = t.rev_ok is not None and str(t.rev_ok).strip() == str(f.rev)
vrev = (t.visual_rev or "").strip()
v = _int(vrev)
if not approved and kept_called(t, f):
return keep(UNCHANGED, f"Rev {vrev} kept by Clerical; M-Files has rev {f.rev}")
if not approved:
# 5
if v is None:
@@ -4,9 +4,11 @@ flag, stage or open a change order on the packets that use them.
Per run: the queue (drawings/queue.py) cut to `drawing_budget`; per drawing one `Lookup.status`, a fetch when a
released file exists (drawings/fetch.py downloads only when its M-Files identity changed), the `drawing_lookups`
row, then `decide` for every live packet that uses the drawing (its own drawing, or the target of its pending feed
change order), applied through `feed.writes.system_write`. A busy packet is left for the next run. On a draft
flagged rev_mismatch, the rev VISUAL calls is looked up too (`Lookup.revision`) and copied into assets when M-Files
still holds it (Retired, say), so Clerical can choose it or the Effective rev.
change order), applied through `feed.writes.system_write`. A busy packet is left for the next run.
VISUAL is the source of truth for the rev. When M-Files' Effective rev is not the rev VISUAL calls, that rev is
looked up too (`Lookup.revision`). If M-Files has it as Effective or Retired, it is copied into assets and attached
to the draft with no flag. Any other status (or no document at that rev) leaves the packet flagged rev_mismatch.
Never writes to M-Files.
"""
@@ -127,7 +129,7 @@ def _drawing(ctx: JobContext, lk: Lookup, item: queue.QueueItem, counts: Counts,
vrev_seen = item.visual_rev if item.co_id is None else own_rev(conn, item.dwg)
save_lookup(conn, item, found, fetched, now, fetch_failed=fetch_failed, visual_rev_seen=vrev_seen)
scope = [str(d) for d in (getattr(ctx.feed_settings, "departments", None) or [])]
called: dict[int, dict[str, Any] | None] = {} # VISUAL rev -> what M-Files holds at that rev, this drawing
called: dict[int, CalledCopy | None] = {} # VISUAL rev -> what M-Files holds at that rev, this drawing
for pid, relations in sharing(conn, item.dwg, scope):
_packet(ctx, lk, item.dwg, found, fetched, pid, relations, counts, called)
@@ -239,12 +241,27 @@ def save_revision(conn: Any, dwg: str, rf: Found, fetched: FetchResult, now: dat
)
# The M-Files statuses a rev VISUAL calls may be attached at. Anything else is never attached.
ATTACHABLE = frozenset({"effective", "retired"})
@dataclass(frozen=True)
class CalledCopy:
"""The rev VISUAL calls as M-Files holds it. `record` goes on the packet as `visual.called`: {rev, controlled,
state, name, asset_id, sha256, pages} for a copy, {rev, missing: true} when M-Files has no document at that rev,
{rev, controlled, state, refused: true} when its status may not be attached. `found` and `fetched` are set only
for a copy."""
record: dict[str, Any]
found: Found | None = None
fetched: FetchResult | None = None
def called_rev(
ctx: JobContext, lk: Lookup, dwg: str, visual_rev: str, memo: dict[int, dict[str, Any] | None], counts: Counts
) -> dict[str, Any] | None:
"""The rev VISUAL calls, as M-Files holds it: {rev, controlled, state, name, asset_id, sha256, pages}, or
{rev, missing: true} when M-Files has no document at that rev. None when it could not be read this run (the
packet keeps what it had) or the VISUAL rev is not a number."""
ctx: JobContext, lk: Lookup, dwg: str, visual_rev: str, memo: dict[int, CalledCopy | None], counts: Counts
) -> CalledCopy | None:
"""The rev VISUAL calls, as M-Files holds it, copied into assets when its status is Effective or Retired. None when
it could not be read this run (the packet keeps what it had) or the VISUAL rev is not a number."""
v = D.as_int(visual_rev)
if v is None:
return None
@@ -258,7 +275,10 @@ def called_rev(
ctx.error(f"drawing {dwg}: rev {v}: {e}")
return None
if rf is None:
memo[v] = {"rev": str(v), "missing": True}
memo[v] = CalledCopy({"rev": str(v), "missing": True})
return memo[v]
if rf.controlled.casefold() not in ATTACHABLE:
memo[v] = CalledCopy({"rev": str(v), "controlled": rf.controlled, "state": rf.state or None, "refused": True})
return memo[v]
try:
fetched = fetch(conn, ctx.settings, lk, rf, dwg=dwg)
@@ -284,7 +304,7 @@ def called_rev(
"reused": fetched.reused,
},
)
memo[v] = {
record = {
"rev": str(v),
"controlled": rf.controlled,
"state": rf.state or None,
@@ -293,6 +313,7 @@ def called_rev(
"sha256": fetched.sha256.hex(),
"pages": fetched.pages,
}
memo[v] = CalledCopy(record, rf, fetched)
return memo[v]
@@ -397,13 +418,17 @@ def target_for(dwg: str, pf: PacketFacts, *, own: bool) -> D.Target:
co_waiting = _dwg_rev(waiting)
co_staged = _dwg_rev(staged)
packet_rev = str(row["rev"] or "").strip()
rev_ok = kept_latest = None
rev_ok = None
ro = visual.get("rev_ok")
if isinstance(ro, dict) and ro.get("rev") is not None and str(ro.get("vis_rev") or "").strip() == packet_rev:
if ro.get("use") == "called":
kept_latest = None if ro.get("latest") is None else str(ro["latest"]).strip()
else:
rev_ok = str(ro["rev"]).strip()
# A rev_ok with use = "called" (Clerical kept the rev VISUAL calls, before that became automatic) approves no other
# rev: the rev VISUAL calls is used anyway.
if (
isinstance(ro, dict)
and ro.get("rev") is not None
and ro.get("use") != "called"
and str(ro.get("vis_rev") or "").strip() == packet_rev
):
rev_ok = str(ro["rev"]).strip()
visual_rev = packet_rev if own or co_waiting is None else co_waiting[1]
return D.Target(
dwg=dwg,
@@ -419,7 +444,6 @@ def target_for(dwg: str, pf: PacketFacts, *, own: bool) -> D.Target:
co_pending=co is not None,
co_staged_sha=_sha(staged.get("sha256")) if isinstance(staged, dict) else None,
rejected_sha=pf.rejected_sha,
kept_latest=kept_latest,
)
@@ -461,7 +485,7 @@ def _packet(
pid: int,
relations: set[str],
counts: Counts,
called_memo: dict[int, dict[str, Any] | None],
called_memo: dict[int, CalledCopy | None],
) -> None:
conn = ctx.conn
pf = facts(conn, pid)
@@ -470,7 +494,14 @@ def _packet(
own = "own" in relations
t = target_for(dwg, pf, own=own)
same_file = fetched is not None and fetched.sha256 in pf.attached_shas
d = D.decide(t, found, same_file=same_file, fetched_sha=fetched.sha256 if fetched else None)
cc: CalledCopy | None = None
dc: D.Called | None = None
v = D.as_int(t.visual_rev)
if own and found.ok and v is not None and v != found.rev:
cc = called_rev(ctx, lk, dwg, t.visual_rev, called_memo, counts)
if cc is not None and cc.found is not None and cc.fetched is not None:
dc = D.Called(v, cc.found.controlled, cc.fetched.sha256 in pf.attached_shas)
d = D.decide(t, found, same_file=same_file, fetched_sha=fetched.sha256 if fetched else None, called=dc)
if not own and d.kind != D.STAGE_FOR_CO:
return # only the CO's target drawing: nothing to do until it can be staged
if d.kind in (D.ATTACH, D.OPEN_CO, D.STAGE_FOR_CO) and fetched is None:
@@ -486,18 +517,17 @@ def _packet(
if not _same_appr(visual.get("appr"), appr):
patch["appr"] = appr
if found.ok:
confirmed = D.rev_confirmed(t, found)
confirmed = D.rev_confirmed(t, found, dc)
if bool(visual.get("rev_confirmed")) != confirmed:
patch["rev_confirmed"] = confirmed
if d.kind == D.REV_MISMATCH and row["status"] == "draft":
called = called_rev(ctx, lk, dwg, t.visual_rev, called_memo, counts)
if called is not None and visual.get("called") != called:
patch["called"] = called
elif visual.get("called") is not None:
if cc is not None:
if visual.get("called") != cc.record:
patch["called"] = cc.record
elif visual.get("called") is not None and (v is None or v == found.rev):
patch["called"] = None
state_after = d.drawing_state if own else str(row["drawing_state"])
material = (
d.kind in (D.ATTACH, D.OPEN_CO, D.STAGE_FOR_CO)
d.kind in (D.ATTACH, D.ATTACH_CALLED, D.OPEN_CO, D.STAGE_FOR_CO)
or flags_after != flags
or state_after != row["drawing_state"]
or bool(patch)
@@ -508,20 +538,28 @@ def _packet(
action = A.VIS_UPDATE_DRAFT if row["status"] == "draft" else A.VIS_OPEN_CO
label, note = "Drawing status from M-Files", d.note or None
called_now = patch.get("called") or {}
if note and called_now.get("controlled"):
note = f"{note}; rev {called_now['rev']} is {called_now['controlled']} in M-Files"
elif note and called_now.get("missing"):
if d.kind == D.REV_MISMATCH and note and called_now.get("refused"):
note = f"{note}; rev {called_now['rev']} is {called_now['controlled']} in M-Files - not attached"
elif d.kind == D.REV_MISMATCH and note and called_now.get("missing"):
note = f"{note}; rev {called_now['rev']} is not in M-Files"
if d.kind == D.ATTACH and fetched is not None:
use_found, use_fetched = found, fetched
if d.kind == D.ATTACH_CALLED and cc is not None and cc.found is not None and cc.fetched is not None:
use_found, use_fetched = cc.found, cc.fetched
if d.kind in (D.ATTACH, D.ATTACH_CALLED) and use_fetched is not None:
label = "Drawing attached from M-Files"
note = "; ".join(x for x in (fetched.name or found.name or "", d.note) if x) or None
detail = {"dwg": dwg, "rev": found.rev, "kind": d.kind, "asset_id": str(fetched.asset_id) if fetched else None}
note = "; ".join(x for x in (use_fetched.name or use_found.name or "", d.note) if x) or None
detail = {
"dwg": dwg,
"rev": use_found.rev,
"kind": d.kind,
"asset_id": str(use_fetched.asset_id) if use_fetched else None,
}
try:
with writes.system_write(conn, pid, action, label=label, note=note, detail=detail) as sw:
if int(sw.packet["version"]) != int(row["version"]):
raise _Stale
if d.kind == D.ATTACH and fetched is not None:
_attach(sw, pid, dwg, found, fetched, d, flags_after, patch, now)
if d.kind in (D.ATTACH, D.ATTACH_CALLED) and use_fetched is not None:
_attach(sw, pid, dwg, use_found, use_fetched, d, flags_after, patch, now)
counts["attached"] += 1
else:
_status(sw, pid, state_after, flags_after, patch)
@@ -581,6 +619,7 @@ def _attach(
"rev": found.rev,
"note": d.note,
"at": now.isoformat(),
**({"controlled": found.controlled} if found.controlled else {}),
}
merged = {**patch, "rev_confirmed": True, "vault": vault}
conn.execute(