diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51caba1..2ed2474 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@ All notable changes to this project are recorded here. The format follows [Keep
### Added
+- Rev mismatch with a retired rev (Gitea issue 41): when VISUAL calls a rev that M-Files still holds but has retired, the packet is still flagged, and a Clerical Supervisor can choose either rev from the packet: "Use latest rev N" (M-Files' Effective rev, as before) or "Use rev V as VISUAL calls it". Both PDFs open from the packet first. Keeping VISUAL's rev needs a reason and a signature, is logged, and holds until M-Files releases a newer rev or VISUAL's rev changes. Needs MFilesKit 0.1.3 (`DrawingLookup.revision`) and migration 0009.
+
- Phase 8: Project Management, the find bar, presence and motion. The Project Management screen (`/pm`, view only) with the tabs Overview (the headline numbers, a plain-words summary, the pipeline by step with the slowest step in pink, the units late or at risk), Projects & units (every project with its units, due date, days left, packets by step, % done and health: On track, At risk, Late, Complete, No packets yet; a project is as healthy as its worst unit; tap a project for its units and a unit for its jobs), Bottlenecks & aging (time in the current step, every open packet longest waiting first with over 3 days flagged, average time per step for completed moves), Hours vs target (station timer hours and VISUAL ticket hours side by side against the work-order target by department, worker and project, plus every packet over target), Quality & issues (Q.C. rejects, open shorts, open change orders, warehouse holds, sent back) and Find jobs. The period picker (30 days, 90 days, 12 months, all) sets the window for history figures. Every table has Excel (CSV); "Excel (CSV) — all" saves every report; "PDF report" builds the PESCO production status report (cover with the headline numbers, summary and pipeline, projects, flow, labor, issues) on the server with the controlled-copy stamp. Every export is logged as "report exported" in the audit and the print log.
- The find bar on every home screen: search over project number or name, lot, operation, drawing and rev, WPS, customer, PO, old project numbers, work orders, heat numbers, part IDs, welder name or stamp, change order and ECN numbers, with several words narrowing and Esc clearing; the project then unit picker; Filters with Department, Status (including At a station and Everything not filed), Station, Worker and, for Clerical and Project Management, the flag kind; the quick flags Change order open, Shorts open, Project overdue, Due within 7 days and Mine (what that screen is responsible for; hidden on the station and Project Management screens); Board or List with the list sortable by last updated, project / lot, due date, status or department. Filters and the view are saved per browser (`ps.flt`); the search text is not. Tapping a number tile shows only those jobs; tapping again clears. The numbers follow the project pick but not the other filters.
- Every filter is applied by the server: `GET /api/packets` and the shop, stall, warehouse and Q.C. boards take `q`, `status`, `dept`, `station_id`, `welder_id`, `flags`, `flag`, `sort`, `mine_screen` and `mine_station_id`, and answer `facets` (the stations and workers in the project scope) for the dropdowns. A board never fetches more than it shows.
diff --git a/docs/FEED.md b/docs/FEED.md
index a6f4b07..45ad808 100644
--- a/docs/FEED.md
+++ b/docs/FEED.md
@@ -291,7 +291,8 @@ The file is downloaded only when the object id and version or the file id and ve
| No released rev, but a drafting rev or a drafting assignment | `not_released` | `not_released` | Note "Not released in M-Files yet (sheet of ) - drafting status: rev N ". |
| No released rev and nothing in drafting | `not_found` | `not_found` | Note "Drawing not in M-Files - check the drawing number in VISUAL". |
| VISUAL's rev is not a whole number | `bad_rev` | `bad_rev` | Not attached. |
-| Revs differ, and no approved rev matches M-Files | `rev_mismatch` | `rev_mismatch` | Note "Rev mismatch: VISUAL calls rev V, M-Files has rev N", plus the drafting status. The M-Files copy is fetched and staged for Clerical Supervisor approval. No change order: VISUAL did not change. |
+| Revs differ, and no approved rev matches M-Files | `rev_mismatch` | `rev_mismatch` | Note "Rev mismatch: VISUAL calls rev V, M-Files has rev N", plus the drafting status. The M-Files copy is fetched and staged for Clerical Supervisor approval. On a draft the rev VISUAL calls is looked up too: when M-Files still holds it (Retired, say), that copy is fetched into `drawing_revisions` and kept on the packet as `visual.called`, and the log note says "rev V is Retired in M-Files". When M-Files has no document at that rev, the note says "rev V is not in M-Files". No change order: VISUAL did not change. |
+| Revs differ, but Clerical kept the rev VISUAL calls while M-Files' Effective rev is still the one they chose against | `unchanged` | none | The kept rev stays attached and confirmed. No flag and no change order, also past draft. A newer Effective rev in M-Files flags the packet again. |
| Revs match (or the approved rev matches), draft | `attach` | none | Drawing pages attached, `drawing_state` `attached`, drawing flags cleared, rev confirmed. Log "Drawing attached from M-Files". |
| Revs match, draft with a hand drawing at the same rev | `unchanged` | none | The hand drawing stays. |
| Revs match, past draft, same file (same sha256) | `unchanged` | none | Nothing. |
@@ -322,7 +323,7 @@ Flags live in `packets.flags`. A draft with any flag shows in Clerical's Flagged
| Flag | Text shown | What Clerical does |
|---|---|---|
| `drawing_needed` | Made from VISUAL - drawing not attached yet | Usually nothing: the drawing job attaches it. Run now to hurry it. Upload by hand only if it stays stuck. |
-| `rev_mismatch` | The M-Files note, for example "Rev mismatch: VISUAL calls rev 2, M-Files has rev 3" | Get the rev corrected in VISUAL, or a Clerical Supervisor approves the staged M-Files rev with a reason and a signature. |
+| `rev_mismatch` | The M-Files note, for example "Rev mismatch: VISUAL calls rev 2, M-Files has rev 3" | Get the rev corrected in VISUAL. Or a Clerical Supervisor chooses, with a reason and a signature: the latest (Effective) rev in M-Files, or the rev VISUAL calls when M-Files still holds it (Retired). Both PDFs can be opened from the packet first. |
| `not_released` | The note: "Not released in M-Files yet ... drafting status: rev N " | Wait for drafting to release it, or upload the drawing by hand with a reason and a signature. |
| `not_found` | Drawing not in M-Files - check the drawing number in VISUAL | Check the drawing number in VISUAL and have it fixed there. Or upload by hand. |
| `lookup_failed` | M-Files lookup failed: (the reason) | Nothing at first: the job retries with backoff. If it persists, tell IT. |
diff --git a/docs/POLICY.md b/docs/POLICY.md
index 436ba3b..0de7301 100644
--- a/docs/POLICY.md
+++ b/docs/POLICY.md
@@ -867,6 +867,8 @@ What the feed may never write:
When M-Files has a different released rev from VISUAL, the drawing job stages the M-Files copy. A Clerical Supervisor (PS-Clerical-Supervisor) can approve it on a draft with rev_approve: a reason of at least 8 characters and a signature ("Approve the M-Files revision"). The pages are attached and the approval is kept on the packet (`visual.rev_ok`). With nothing staged the answer is 409 `nothing_staged`.
+The same action, with `use: "called"`, keeps the rev VISUAL calls instead, when M-Files still holds that rev at another status (Retired). The drawing job copies it on a draft flagged `rev_mismatch`. The same role, reason and signature apply, and the signed content carries `use`. `visual.rev_ok` records `use: "called"`, the M-Files status of the kept rev and the Effective rev at the time (`latest`). The log says "Rev VISUAL calls kept by hand". The choice holds while M-Files' Effective rev stays `latest`: no flag and no change order. A newer Effective rev flags the packet again, and a new VISUAL rev clears the choice. With no copy of that rev the answer is 409 `nothing_staged`.
+
### Hand uploads on flagged drafts
On a draft made by the feed that is flagged (or already has a hand upload), Clerical can upload a drawing (edit_document_set) or a BOM (edit_bom) by hand. Both need a reason and a signature. A hand drawing sets `drawing_state` `hand` and flag `hand_drawing`; a hand BOM sets flag `hand_bom` and the feed stops replacing the BOM. An unflagged draft refuses with 409 `not_flagged`. Auto-release never takes a hand-flagged draft, so a person sends it on. "Use VISUAL's BOM again" removes the hand BOM.
diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md
index 29d43e3..a1e16e7 100644
--- a/docs/SCHEMA.md
+++ b/docs/SCHEMA.md
@@ -377,6 +377,7 @@ Migration 0004. Every ID part is text (`wo_base`, `lot`, `split`, `sub`, part ID
- `visual_ignored_wos`: work orders kept out by the go-live date, unique on (`wo_base`, `lot`, `split`, `sub`). `rel`, `project`, `unit_number`, `res`, `dwg`, `first_seen_batch`, `last_seen_at`, and `pulled_in_at`, `pulled_in_by`, `pulled_in_reason` when Clerical pulls one in.
- `visual_resource_map`: `resource` (PK), `target` (`fold`, `ignore` or a two-digit department code, by CHECK), `confirmed`, `updated_at`, `updated_by`. Seeded from the old build's `visual_map.json`, every entry unconfirmed. "Electical" is spelled that way in VISUAL.
- `drawing_lookups`: one row per drawing number as VISUAL writes it (`dwg` PK, trimmed). The last answer: `checked_at`, `ok`, `rev` (released), `name`, `obj_id`, `obj_version`, `file_id`, `file_version`, `file_size`, `base` (H sheet), `draft_rev`, `draft_where`, `in_drafting`, `note`. Retry state: `fail_count`, `failing_since`, `next_check_at`. The fetched file: `asset_id`, `sha256`. `visual_rev_seen` is the VISUAL rev at the last lookup, so a rev change moves the drawing to the front of the queue. A rev mismatch keeps the staged M-Files copy here for approval.
+- `drawing_revisions` (0009): other revs of a drawing that M-Files still holds, one row per (`dwg`, `rev`). The drawing job writes it when a draft's VISUAL rev differs from the Effective rev and M-Files has a document at the VISUAL rev. `controlled` (M-Files' Controlled Status, for example Retired), `state` (workflow state, for example Superseded), `checked_at`, `name`, the M-Files identity (`obj_id`, `obj_version`, `file_id`, `file_version`, `file_size`), `base` (H sheet), and the fetched copy (`asset_id`, `sha256`). rev_approve with `use: "called"` attaches from here.
Column additions in 0004:
diff --git a/frontend/src/api/packets.ts b/frontend/src/api/packets.ts
index a42ba62..1c29f39 100644
--- a/frontend/src/api/packets.ts
+++ b/frontend/src/api/packets.ts
@@ -68,7 +68,8 @@ export const packetsApi = {
databook: (id: number) => api.post(`${base(id)}/databook`, {}),
/* Phase 3: VISUAL packets. */
- revApprove: (id: number, body: { reason: string } & Guarded) => api.post(`${base(id)}/rev-approve`, body),
+ revApprove: (id: number, body: { reason: string; use?: "latest" | "called" } & Guarded) =>
+ api.post(`${base(id)}/rev-approve`, body),
handDrawing: (id: number, body: { asset_id: string; reason: string } & Guarded) =>
api.post(`${base(id)}/hand/drawing`, body),
/** Multipart cannot carry JSON, so the signature rides as sign_factor and sign_secret. */
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index 4bc79bc..46b37f5 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -905,16 +905,39 @@ export interface FoldedOp {
tgt?: number | string;
}
+/** The rev VISUAL calls, as M-Files holds it at another status (Retired, say). Set by the drawing job on a draft flagged
+ * rev_mismatch. `missing` when M-Files has no document at that rev. */
+export interface CalledRev {
+ rev: string;
+ missing?: boolean;
+ controlled?: string;
+ state?: string | null;
+ name?: string | null;
+ asset_id?: string;
+ pages?: number;
+}
+
/** The packet's `visual` jsonb. Every key is optional; hand drafts have {}. */
export interface VisualInfo {
appr?: VisualAppr;
/** Set by the read model from drawing_lookups when M-Files has a staged copy. */
- staged_rev?: { rev: number | string; name: string | null };
+ staged_rev?: { rev: number | string; name: string | null; asset_id?: string };
+ called?: CalledRev | null;
partspecs?: string[];
folded?: FoldedOp[];
manual_dwg?: HandUpload;
manual_bom?: HandUpload;
- rev_ok?: { rev: string | number; vis_rev?: string | null; by?: WhoRef | string | null; at?: string | null; reason?: string | null };
+ rev_ok?: {
+ rev: string | number;
+ vis_rev?: string | null;
+ by?: WhoRef | string | null;
+ at?: string | null;
+ reason?: string | null;
+ /** "called": Clerical kept the rev VISUAL calls over M-Files' Effective rev (`latest`). */
+ use?: "latest" | "called";
+ latest?: string | null;
+ controlled?: string | null;
+ };
rev_confirmed?: boolean;
[key: string]: unknown;
}
diff --git a/frontend/src/packet/visual.test.tsx b/frontend/src/packet/visual.test.tsx
index 092a15b..24f4e8a 100644
--- a/frontend/src/packet/visual.test.tsx
+++ b/frontend/src/packet/visual.test.tsx
@@ -116,6 +116,31 @@ describe("Packet page, VISUAL box", () => {
expect(await screen.findByText("Rev 3 approved and attached.")).toBeInTheDocument();
});
+ it("offers the retired rev VISUAL calls next to the latest, and keeps it with use=called", async () => {
+ const called = { rev: "2", controlled: "Retired", state: "Superseded", name: "M00543 r2.pdf", asset_id: "as-r2", pages: 1 };
+ const draft = visualDraft({ visual: { ...visualDraft().visual, staged_rev: { rev: 3, name: "M00543 r3.pdf", asset_id: "as-r3" }, called } });
+ const kept = visualDraft({
+ version: 4,
+ flags: [],
+ flag: null,
+ drawing_state: "attached",
+ visual: { rev_ok: { rev: "2", vis_rev: "2", use: "called", latest: "3", controlled: "Retired", reason: "Lot 1 builds to rev 2" } },
+ });
+ const fetch = mockFetch({ ...base(draft, LEO), "POST /api/packets/1/rev-approve": { body: kept } });
+ renderApp("/packets/1");
+ expect(await screen.findByRole("button", { name: "Use latest rev 3" })).toBeInTheDocument();
+ expect(screen.getByText(/retired \(Superseded\) in M-Files/)).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Use rev 2 as VISUAL calls it" }));
+ const ask = await screen.findByRole("dialog", { name: "Use rev 2 for M00543" });
+ fireEvent.change(within(ask).getByLabelText("Reason"), { target: { value: "Lot 1 builds to rev 2" } });
+ fireEvent.click(within(ask).getByRole("button", { name: "Use rev 2" }));
+ await sign();
+ expect(callsTo(fetch, "POST", "/api/packets/1/rev-approve")).toEqual([
+ { reason: "Lot 1 builds to rev 2", use: "called", sign: { factor: "password", secret: "pw-1" } },
+ ]);
+ expect(await screen.findByText("Rev 2 attached as VISUAL calls it.")).toBeInTheDocument();
+ });
+
it("uploads a hand BOM as multipart with the signature fields", async () => {
const fetch = mockFetch({
...base(visualDraft()),
diff --git a/frontend/src/packet/visual.tsx b/frontend/src/packet/visual.tsx
index df98c3c..f3d42e6 100644
--- a/frontend/src/packet/visual.tsx
+++ b/frontend/src/packet/visual.tsx
@@ -163,6 +163,7 @@ export function VisualBox({
const drawingFlag = flags.some((f) => (DRAWING_FLAGS as readonly string[]).includes(f));
const appr = v.appr;
const staged = v.staged_rev;
+ const called = v.called && !v.called.missing && v.called.asset_id ? v.called : null;
const mismatch = flags.includes("rev_mismatch");
const md = v.manual_dwg;
const mb = v.manual_bom;
@@ -187,6 +188,25 @@ export function VisualBox({
await mutate((g) => packetsApi.revApprove(p.id, { reason, ...g }), { sign: SIGN_REV_APPROVE, ok: `Rev ${staged.rev} approved and attached.` });
};
+ const keepCalled = async () => {
+ if (!called) return;
+ const status = (called.controlled || "not Effective").toLowerCase();
+ const reason = await askReason({
+ title: `Use rev ${called.rev} for ${p.drawing ?? "this drawing"}`,
+ sub,
+ note: `VISUAL calls rev ${called.rev}, which is ${status} in M-Files${staged ? `. The Effective rev is ${staged.rev}` : ""}. Rev ${called.rev}${called.name ? ` (${called.name})` : ""} is attached to this packet and the shop builds to it. It is recorded with your name, signature and the reason. If M-Files releases a newer rev or VISUAL's rev changes, the packet is flagged again.`,
+ placeholder: `e.g. Engineering says build this lot to rev ${called.rev}`,
+ min: 3,
+ max: 400,
+ submitLabel: `Use rev ${called.rev}`,
+ });
+ if (reason === null) return;
+ await mutate((g) => packetsApi.revApprove(p.id, { reason, use: "called", ...g }), {
+ sign: SIGN_REV_APPROVE,
+ ok: `Rev ${called.rev} attached as VISUAL calls it.`,
+ });
+ };
+
const upload = async (kind: "drawing" | "bom", file: File, reason: string) => {
setHand(null);
if (kind === "bom") {
@@ -220,8 +240,20 @@ export function VisualBox({
return (
{v.rev_ok && hasDwg ? (
-
- Rev {String(v.rev_ok.rev)} approved by hand in place of VISUAL rev {v.rev_ok.vis_rev || "-"}
+
+ {v.rev_ok.use === "called" ? (
+ <>
+
+ Rev {String(v.rev_ok.rev)} kept as VISUAL calls it
+ {" "}
+ - {(v.rev_ok.controlled || "not Effective").toLowerCase()} in M-Files
+ {v.rev_ok.latest ? `, which has rev ${v.rev_ok.latest}` : ""}
+ >
+ ) : (
+ <>
+ Rev {String(v.rev_ok.rev)} approved by hand in place of VISUAL rev {v.rev_ok.vis_rev || "-"}
+ >
+ )}
{v.rev_ok.by ? ` - ${whoName(v.rev_ok.by)}` : ""} {v.rev_ok.at ? fmtTime(v.rev_ok.at) : ""}
{v.rev_ok.reason ? `: ${v.rev_ok.reason}` : ""}.
@@ -233,18 +265,54 @@ export function VisualBox({
is attached from M-Files automatically when its rev matches the rev VISUAL calls. Only Clerical can upload one by hand, and only while the packet is flagged.
{appr?.note ?