2026-10-16 09:31:45 -06:00
|
|
|
"""The policy's view of a person (Principal) and of a packet (PacketState)."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from collections.abc import Iterable, Mapping
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from types import MappingProxyType
|
|
|
|
|
|
|
|
|
|
from ..auth.groups import SHOP_LEVELS, parse_role
|
|
|
|
|
|
|
|
|
|
SUPPORT = ("CL", "WH", "QC")
|
|
|
|
|
LEVELS: dict[str, int] = {level: i + 1 for i, level in enumerate(SHOP_LEVELS)} # worker 1, lead 2, supervisor 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Principal:
|
|
|
|
|
user_id: str | None # uuid as text; None for system
|
|
|
|
|
admin: bool = False
|
|
|
|
|
support: frozenset[str] = frozenset() # codes among CL, WH, QC the user is a member of (any level)
|
|
|
|
|
support_sup: frozenset[str] = frozenset() # codes among CL, WH, QC where the user is Supervisor
|
|
|
|
|
shop: Mapping[str, int] = field(default_factory=lambda: MappingProxyType({})) # code -> 1 worker, 2 lead, 3 sup
|
|
|
|
|
station_limits: frozenset[int] = frozenset() # station ids a worker is limited to; empty = any
|
|
|
|
|
pm: bool = False
|
|
|
|
|
session_kind: str = "password" # password | pin
|
|
|
|
|
system: str | None = None # "visual-feed" for the worker; never from a session
|
|
|
|
|
|
|
|
|
|
def level(self, dept: str) -> int:
|
|
|
|
|
"""Shop level in a department: 0 none, 1 worker, 2 lead, 3 supervisor."""
|
|
|
|
|
return int(self.shop.get(dept, 0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class PacketState:
|
|
|
|
|
id: int
|
|
|
|
|
status: str # packet_status value
|
|
|
|
|
dept: str # dept_code
|
|
|
|
|
station_id: int | None
|
|
|
|
|
welder_id: str | None # uuid text of the assigned welder
|
|
|
|
|
wh_skipped: bool
|
|
|
|
|
routed_to: str | None # databook | shipping | None
|
|
|
|
|
vclosed: bool
|
|
|
|
|
removed: bool
|
|
|
|
|
source_kind: str # visual | hand
|
|
|
|
|
weld_dept: bool # departments.weld
|
|
|
|
|
co_hold: bool # an approved CO with pending verifications, or a pending CO with hold=true
|
|
|
|
|
co_hold_why: str = "" # text for the error
|
|
|
|
|
open_shorts: int = 0
|
|
|
|
|
drawing_attached: bool = False
|
|
|
|
|
heat_map: bool | None = None
|
|
|
|
|
bom_total: int = 0 # lines not marked n/a
|
|
|
|
|
bom_ready: int = 0 # lines pulled, or with an open short
|
2026-10-16 17:13:31 -06:00
|
|
|
bom_missing_heat: int = 0 # pulled lines with pieces issued and no heat (not n/a, not a sub-assembly)
|
2026-10-16 09:31:45 -06:00
|
|
|
welds: int = 0
|
|
|
|
|
checklist_complete: bool = False # every key of the department checklist is done (empty list is complete)
|
|
|
|
|
filler_set: bool = False
|
|
|
|
|
qc_missing: tuple[str, ...] = () # the old qcMissing() list, computed by the read model
|
|
|
|
|
has_qc_sent: bool = False # qc_inspections row exists for the current stage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SYSTEM_FEED = Principal(user_id=None, system="visual-feed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def principal_from_roles(
|
|
|
|
|
user_id: str | None,
|
|
|
|
|
roles: Iterable[str],
|
|
|
|
|
station_limits: Iterable[int] = (),
|
|
|
|
|
session_kind: str = "password",
|
|
|
|
|
) -> Principal:
|
|
|
|
|
"""Build the policy Principal from phase 1 role strings. A role that does not parse is ignored."""
|
|
|
|
|
admin = False
|
|
|
|
|
pm = False
|
|
|
|
|
support: set[str] = set()
|
|
|
|
|
support_sup: set[str] = set()
|
|
|
|
|
shop: dict[str, int] = {}
|
|
|
|
|
for text in roles:
|
|
|
|
|
try:
|
|
|
|
|
role = parse_role(text)
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
if role.kind == "admin":
|
|
|
|
|
admin = True
|
|
|
|
|
elif role.kind == "pm":
|
|
|
|
|
pm = True
|
|
|
|
|
elif role.kind.startswith("shop_") and role.dept_code:
|
|
|
|
|
level = LEVELS[role.kind.removeprefix("shop_")]
|
|
|
|
|
shop[role.dept_code] = max(level, shop.get(role.dept_code, 0))
|
|
|
|
|
elif role.dept_code in SUPPORT:
|
|
|
|
|
support.add(role.dept_code)
|
|
|
|
|
if role.kind.endswith("_supervisor"):
|
|
|
|
|
support_sup.add(role.dept_code)
|
|
|
|
|
return Principal(
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
admin=admin,
|
|
|
|
|
support=frozenset(support),
|
|
|
|
|
support_sup=frozenset(support_sup),
|
|
|
|
|
shop=MappingProxyType(shop),
|
|
|
|
|
station_limits=frozenset(int(s) for s in station_limits),
|
|
|
|
|
pm=pm,
|
|
|
|
|
session_kind=session_kind,
|
|
|
|
|
)
|