Files
PacketStation/server/packetstation/policy/principal.py
T
spencermandClaude Opus 5.5 1cd6688247
ci / frontend (pull_request) Successful in 57s
ci / server (pull_request) Successful in 12m33s
ci / image (pull_request) Skipped
ci / e2e (pull_request) Successful in 5m4s
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
- C1 (issue 1): over target follows VISUAL ticket hours, not the timer.
- C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one.
- C3 (issue 25): an incomplete unit ITP blocks filing.
- C4 (issue 27): prints open the print dialog on the page, no new tab.
- C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets.
- C6 (issue 8): the data book leaves out the history pages.
- C7 (issue 18): the supervisor approves each received short; accept and filing wait for it.
- C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START.
- C9 (issue 32): Q.C. and Clerical may run the stamp check.
- C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only.
- C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall.

Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 13:33:51 -06:00

105 lines
4.1 KiB
Python

"""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
bom_missing_heat: int = 0 # pulled lines with pieces issued and no heat (not n/a, not a sub-assembly)
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
shorts_unapproved: int = 0 # open shorts plus received shorts the supervisor has not approved (C7)
itp_open: bool = False # the packet's unit has an ITP that is not complete (C3)
ncr_required: bool = False # a Q.C. reject since the last accept has no NCR number (C2)
visual_open_wos: tuple[str, ...] = () # the work orders the feed last saw open in VISUAL (C5)
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,
)