Files

248 lines
11 KiB
Python
Raw Permalink Normal View History

"""The drawing decision table. Pure: no database, no M-Files.
`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.
Rows, in the order they are tried:
1. lookup failed -> flag lookup_failed, keep what is attached (state lookup_failed only when it was needed).
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', 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).
10. revs match, past draft, nothing attached -> unchanged.
11. revs match, past draft, same file as attached -> unchanged.
12. revs match, past draft, a pending change order already stages this file, or Clerical rejected a change order
for this file -> unchanged.
13. revs match, past draft, attached rev lower -> open_co "M-Files released rev N of <dwg>; this packet has rev M".
14. revs match, past draft, attached rev equal, new file -> open_co "M-Files re-released rev N of <dwg> (new file)".
15. revs match, past draft, attached rev higher or not a number -> unchanged.
"""
from __future__ import annotations
from dataclasses import dataclass
from .lookup import Found
LOOKUP_FAILED = "lookup_failed"
NOT_RELEASED = "not_released"
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,
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.
STATE_OF = {NOT_RELEASED: "not_released", NOT_FOUND: "not_found", BAD_REV: "rev_mismatch", REV_MISMATCH: "rev_mismatch"}
HELD_STATES = ("attached", "hand")
NOT_FOUND_NOTE = "Drawing not in M-Files - check the drawing number in VISUAL"
_FAILED_PREFIX = "M-Files lookup failed: "
@dataclass(frozen=True)
class Target:
dwg: str
visual_rev: str
attached_rev: str | None # rev of the live drawing pages; None when no drawing page is attached
attached_sha: bytes | None
status: str # packet status
drawing_state: str
has_hand_drawing: bool
rev_ok: str | None # the rev Clerical approved for this VISUAL rev, if any
# Additions to the contract, all defaulted: the pending feed change order (origin visual or drawing), if any.
co_waiting: tuple[str, str] | None = None # (dwg, rev) the pending CO waits for
co_staged: tuple[str, str] | None = None # (dwg, rev) the pending CO already stages
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
@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)
class Decision:
kind: str
flag: str | None
drawing_state: str
note: str
def as_int(v: object) -> int | None:
"""A rev as a number: "3" -> 3; "A", "" and None -> None."""
s = "" if v is None else str(v).strip()
return int(s) if s.isdigit() else None
_int = as_int
def draft_line(f: Found) -> str:
"""The old draftLine: "rev N <where>", or "in drafting, no released PDF yet", or ""."""
if f.draft_rev is not None and f.draft_where:
return f"rev {f.draft_rev} {f.draft_where}"
if f.in_drafting:
return "in drafting, no released PDF yet"
if f.draft_rev is not None:
return f"rev {f.draft_rev}"
return ""
def failed_note(f: Found) -> str:
detail = (f.note or "").strip()
if detail.startswith(_FAILED_PREFIX): # MFilesKit's own note already carries the prefix
detail = detail[len(_FAILED_PREFIX) :]
return f"{_FAILED_PREFIX}{detail or 'no detail'}"
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, 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
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, 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
M-Files file, when it was fetched (row 12 compares it with the pending and the rejected change orders).
"""
held = t.drawing_state in HELD_STATES
draft = t.status == "draft"
def flag(kind: str, note: str) -> Decision:
return Decision(kind, kind, t.drawing_state if held else STATE_OF[kind], note)
def keep(kind: str, note: str = "") -> Decision:
return Decision(kind, None, t.drawing_state, note)
# 1
if not f.ok:
return Decision(
LOOKUP_FAILED,
LOOKUP_FAILED,
LOOKUP_FAILED if t.drawing_state == "needed" else t.drawing_state,
failed_note(f),
)
# 2
if not draft and f.rev is not None and t.co_waiting is not None:
w_dwg, w_rev = t.co_waiting
if w_dwg.strip() == t.dwg.strip() and str(w_rev).strip() == str(f.rev):
return keep(STAGE_FOR_CO, f"Drawing for the change order is in M-Files (rev {f.rev})")
if not draft and f.rev is not None and t.co_staged is not None:
s_dwg, s_rev = t.co_staged
if s_dwg.strip() == t.dwg.strip() and str(s_rev).strip() == str(f.rev):
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)
if d:
on = f" (sheet of {f.base})" if f.base else ""
return flag(NOT_RELEASED, f"Not released in M-Files yet{on} - drafting status: {d}")
return flag(NOT_FOUND, NOT_FOUND_NOTE)
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:
# 5
if v is None:
return flag(BAD_REV, f"VISUAL rev {vrev or '-'} is not a number; M-Files has rev {f.rev} - not attached")
# 6
if v != f.rev:
d = f"; drafting status: {draft_line(f)}" if f.draft_rev is not None and f.draft_rev > f.rev else ""
return flag(REV_MISMATCH, f"Rev mismatch: VISUAL calls rev {vrev}, M-Files has rev {f.rev}{d}")
sheet = f"sheet of drawing {f.base}" if f.base else ""
if draft:
# 7
if t.has_hand_drawing:
return keep(UNCHANGED, "A drawing uploaded by hand stays at the same rev")
# 8
if t.drawing_state == "attached" and same_file and t.attached_rev == str(f.rev):
return keep(UNCHANGED, sheet)
# 9
return Decision(ATTACH, None, "attached", sheet)
# 10, 11
if t.attached_rev is None or same_file:
return keep(UNCHANGED, sheet)
# 12
if fetched_sha is not None and fetched_sha in (t.co_staged_sha, t.rejected_sha):
return keep(UNCHANGED, sheet)
att = _int(t.attached_rev)
if att is None or att > f.rev:
return keep(UNCHANGED, sheet) # 15
if att < f.rev:
reason = f"M-Files released rev {f.rev} of {t.dwg}; this packet has rev {att}"
else:
reason = f"M-Files re-released rev {f.rev} of {t.dwg} (new file)"
return keep(OPEN_CO, reason)