Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
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

- 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]>
This commit is contained in:
2026-10-30 13:33:51 -06:00
co-authored by Claude Opus 5.5
parent 1ae6819289
commit 1cd6688247
101 changed files with 3871 additions and 1098 deletions
+31 -15
View File
@@ -88,7 +88,8 @@ NOTE_AGING_STEPS = "How long packets have been sitting where they are right now.
NOTE_AGING_OPEN = "Over 3 days in one step is flagged."
NOTE_AGING_HISTORY = "From the step history recorded on every move."
NOTE_HOURS_OVER = (
"Station timer hours and VISUAL ticket hours against the work-order target hours. The timer is the clock of record."
"VISUAL ticket hours against the work-order target hours decide over target. The station timer is shown beside"
" them. A packet with no ticket hours is not over target."
)
PROJECTS_HEAD = [
@@ -108,7 +109,16 @@ PROJECTS_HEAD = [
"Health",
]
ATTENTION_HEAD = ["Unit", "Description", "Due", "Left", "Packets", "% done", "Health"]
HOURS_HEAD = ["Packets", "Timer h", "Ticket h", "Target h", "Variance h", "Timer / target", "Over target"]
HOURS_HEAD = [
"Packets",
"Timer h",
"Ticket h",
"Target h",
"Ticket variance h",
"Ticket h / target",
"Over target (tickets)",
]
OVER_HEAD = ["Job", "Station / worker", "Status", "Timer h", "Ticket h", "Target h", "Over by (tickets)"]
JOBS_HEAD = [
"Job",
"Project",
@@ -849,6 +859,12 @@ def _counts_in_hours(c: Mapping[str, Any], h: Mapping[str, float]) -> bool:
return h["target_h"] > 0 and (h["timer_h"] > 0 or h["ticket_h"] > 0 or c["status"] in HOURS_STATUSES)
def is_over(h: Mapping[str, float]) -> bool:
"""Over target: the VISUAL ticket hours are over the work-order target (change C1). The timer never decides, and
a packet with no ticket hours is not over."""
return h["target_h"] > 0 and h["ticket_h"] > 0 and h["ticket_h"] > h["target_h"]
def station_worker(c: Mapping[str, Any]) -> str:
parts = [(c.get("station") or {}).get("name"), (c.get("welder") or {}).get("display_name")]
return " · ".join(str(x) for x in parts if x) or DASH
@@ -856,7 +872,7 @@ def station_worker(c: Mapping[str, Any]) -> str:
def hours_tables(rep: Report) -> list[dict[str, Any]]:
"""The old rptHours, with the station timer and the VISUAL ticket hours side by side against the target. The
timer decides "over target"."""
ticket hours decide "over target" (change C1); the timer is shown as today."""
hrs = rep.hours()
zero = {"timer_h": 0.0, "ticket_h": 0.0, "target_h": 0.0}
hp = [(c, hrs.get(int(c["id"]), zero)) for c in rep.cards]
@@ -871,9 +887,9 @@ def hours_tables(rep: Report) -> list[dict[str, Any]]:
g["ticket"] += h["ticket_h"]
g["target"] += h["target_h"]
g["n"] += 1
if h["timer_h"] > h["target_h"]:
if is_over(h):
g["over"] += 1
return sorted(m.items(), key=lambda kv: -(kv[1]["timer"] - kv[1]["target"]))
return sorted(m.items(), key=lambda kv: -(kv[1]["ticket"] - kv[1]["target"]))
def rows_of(groups: list[tuple[str, dict[str, float]]]) -> list[dict[str, Any]]:
return [
@@ -884,11 +900,11 @@ def hours_tables(rep: Report) -> list[dict[str, Any]]:
fmt_h(v["timer"]),
fmt_h(v["ticket"]),
fmt_h(v["target"]),
signed_h(v["timer"] - v["target"]),
f"{half_up(v['timer'] / v['target'] * 100)}%" if v["target"] else DASH,
signed_h(v["ticket"] - v["target"]),
f"{half_up(v['ticket'] / v['target'] * 100)}%" if v["target"] else DASH,
int(v["over"]),
],
flag=v["timer"] > v["target"],
flag=v["ticket"] > v["target"],
)
for k, v in groups
]
@@ -911,13 +927,13 @@ def hours_tables(rep: Report) -> list[dict[str, Any]]:
)
t3 = table("hours_project", "By project", ["Project", *HOURS_HEAD], rows_of(group(project_key)))
over = sorted(
((c, h) for c, h in hp if h["timer_h"] > h["target_h"]),
key=lambda ch: -(ch[1]["timer_h"] - ch[1]["target_h"]),
((c, h) for c, h in hp if is_over(h)),
key=lambda ch: -(ch[1]["ticket_h"] - ch[1]["target_h"]),
)
t4 = table(
"hours_over",
"Packets over target",
["Job", "Station / worker", "Status", "Timer h", "Ticket h", "Target h", "Over by"],
OVER_HEAD,
[
row(
[
@@ -927,7 +943,7 @@ def hours_tables(rep: Report) -> list[dict[str, Any]]:
fmt_h(h["timer_h"]),
fmt_h(h["ticket_h"]),
fmt_h(h["target_h"]),
"+" + fmt_h(h["timer_h"] - h["target_h"]) + " h",
"+" + fmt_h(h["ticket_h"] - h["target_h"]) + " h",
],
pid=c["id"],
flag=True,
@@ -944,7 +960,7 @@ def over_target_count(rep: Report) -> int:
n = 0
for c in rep.cards:
h = hrs.get(int(c["id"]))
if h and h["target_h"] > 0 and h["timer_h"] > h["target_h"]:
if h and is_over(h):
n += 1
return n
@@ -1177,7 +1193,7 @@ def kpis(rep: Report) -> list[dict[str, Any]]:
{
"key": "over",
"n": over_target_count(rep),
"label": "Packets over target hours",
"label": "Packets over target hours (tickets)",
"tab": "hours",
"alert": True,
},
@@ -1230,7 +1246,7 @@ def summary(rep: Report, k: list[dict[str, Any]] | None = None, pipe: list[dict[
)
over = int(n["over"])
if over:
out.append(f"{over} packet{' is' if over == 1 else 's are'} over target hours.")
out.append(f"{over} packet{' is' if over == 1 else 's are'} over target hours on the VISUAL tickets.")
return out