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]>
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""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"}
|