drawings: VISUAL's rev is the source of truth; attach it when M-Files holds it Effective or Retired
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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user