Phase 3: VISUAL feed and M-Files drawings in the worker
Migration 0004 (VISUAL and drawing caches, go-live ignored list, resource map, job tables, order-derived columns with provenance; work order key includes the operation sequence). The real worker on APScheduler with settings re-read each fire, pause, run now, advisory locks, job_runs and a heartbeat. The visual-feed job (five VisualKit reads, COPY and MERGE cache, suspect-batch guard, the apply in one transaction: drafts, change orders, retire-moved, work order gone, closed and cancelled observed, projects and units from order links, auto-release) and the fingerprint. The drawing-lookup job (queue, budget, download on version change, sha256 into assets, decision table, staging for rev approval, drawing-origin change orders). Clerical From VISUAL panel, flags, rev approval, hand uploads, change-order review; Admin Feed settings and resource map. Synthetic fixture generator and a fixture-driven worker in CI with a Playwright scenario. Docs updated. Verified: 3249 server tests and 13 Playwright scenarios on the docker-1 CI stack, 76 frontend tests, doctor at 0004; one feed and one drawing run against VISUAL DEV and the vault from the developer machine (counts in the changelog). Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Which customer order a project or unit takes its customer, PO and dates from, when VISUAL has several.
|
||||
|
||||
The one rule, in order:
|
||||
|
||||
1. a Clerical pin that names one of the project's orders (`pin`);
|
||||
2. the base's own linked order, the first of `by_base[base]` (`base`);
|
||||
3. the project order whose top jobs name the unit, open orders first (`unit_top_job`);
|
||||
4. the project's first open order, status F, H or R (`project_open`);
|
||||
5. the project's first order, the newest (`project_newest`).
|
||||
|
||||
`derive` maps the chosen order to the fields a project or unit keeps: customer, po, order_date, ship_date.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from .types import Order
|
||||
|
||||
OPEN_STATUSES = ("F", "H", "R")
|
||||
VIA = ("pin", "base", "unit_top_job", "project_open", "project_newest")
|
||||
FIELDS = ("customer", "po", "order_date", "ship_date")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Chosen:
|
||||
order: Order
|
||||
via: str # pin | base | unit_top_job | project_open | project_newest
|
||||
|
||||
|
||||
def _is_open(o: Order) -> bool:
|
||||
return (o.status or "").strip().upper() in OPEN_STATUSES
|
||||
|
||||
|
||||
def choose(
|
||||
*,
|
||||
unit_number: str,
|
||||
base: str,
|
||||
by_base: Mapping[str, list[Order]],
|
||||
by_project: Mapping[str, list[Order]],
|
||||
project: str,
|
||||
pin: str | None,
|
||||
) -> Chosen | None:
|
||||
proj = list(by_project.get(project) or [])
|
||||
own = list(by_base.get(base) or []) if base else []
|
||||
if pin:
|
||||
p = pin.strip().upper()
|
||||
for o in [*proj, *own]:
|
||||
if o.cust_order_id.strip().upper() == p:
|
||||
return Chosen(o, "pin")
|
||||
if own:
|
||||
return Chosen(own[0], "base")
|
||||
if unit_number:
|
||||
u = unit_number.upper()
|
||||
hits = [o for o in proj if any(t.upper() == u for t in o.top_jobs)]
|
||||
hits.sort(key=lambda o: not _is_open(o)) # stable: open first, the list's own order within
|
||||
if hits:
|
||||
return Chosen(hits[0], "unit_top_job")
|
||||
for o in proj:
|
||||
if _is_open(o):
|
||||
return Chosen(o, "project_open")
|
||||
if proj:
|
||||
return Chosen(proj[0], "project_newest")
|
||||
return None
|
||||
|
||||
|
||||
def derive(order: Order) -> dict[str, Any]:
|
||||
"""customer <- customer_name, po <- customer_po_ref, order_date, ship_date <- promise or desired ship date."""
|
||||
return {
|
||||
"customer": order.customer_name,
|
||||
"po": order.customer_po_ref,
|
||||
"order_date": order.order_date,
|
||||
"ship_date": order.promise_date or order.desired_ship_date,
|
||||
}
|
||||
|
||||
|
||||
def _iso(v: date | None) -> str | None:
|
||||
return v.isoformat() if v else None
|
||||
|
||||
|
||||
def order_ref(chosen: Chosen, at: datetime) -> dict[str, Any]:
|
||||
"""The jsonb kept on projects.order_ref and units.order_ref."""
|
||||
o = chosen.order
|
||||
return {
|
||||
"cust_order_id": o.cust_order_id,
|
||||
"via": chosen.via,
|
||||
"status": o.status,
|
||||
"customer_name": o.customer_name,
|
||||
"customer_po_ref": o.customer_po_ref,
|
||||
"order_date": _iso(o.order_date),
|
||||
"desired_ship_date": _iso(o.desired_ship_date),
|
||||
"promise_date": _iso(o.promise_date),
|
||||
"chosen_at": at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def same_ref(a: Mapping[str, Any] | None, b: Mapping[str, Any] | None) -> bool:
|
||||
"""Two order_refs are the same when everything but chosen_at matches."""
|
||||
if a is None or b is None:
|
||||
return a is None and b is None
|
||||
return {k: v for k, v in a.items() if k != "chosen_at"} == {k: v for k, v in b.items() if k != "chosen_at"}
|
||||
Reference in New Issue
Block a user