feat(plant_kanban): post-shop states visible on board (Tasks 9-13)

Controller (plant_kanban.py):
  - Widen domain: state IN (confirmed, in_progress, awaiting_cert,
    awaiting_ship). Done jobs still drop off.
  - _resolve_card_area: state=awaiting_cert → 'inspection' column,
    state=awaiting_ship → 'shipping' column. State drives column
    regardless of recipe shape.
  - _state_chip: 🏷️ Awaiting CoC (amber) + 📦 Ready to ship (green).
  - _SORT_PRIORITY: awaiting_cert=3.5, awaiting_ship=8.5.
  - KPI dict: awaiting_cert + awaiting_ship counts.
  - Filter clauses for the two new chips.

Model (fp_job.py):
  - _compute_card_state handles new states in BOTH branches: the
    no-active-step early return (where awaiting_cert/ship cards
    land — all steps terminal) AND the per-step branch (defensive).
  - _compute_mini_timeline_json: awaiting_cert paints inspection
    dot 'current'; awaiting_ship paints shipping dot 'current'.
    All earlier dots show 'done'.

SCSS (_plant_tokens.scss + _plant_card.scss):
  - New tokens for amber (cert) + green (ship), light + dark variants
    via the existing $o-webclient-color-scheme compile-time branch.
  - .state-awaiting_cert / .state-awaiting_ship modifier classes
    match the existing border-left pattern.

XML (plant_kanban.xml):
  - Two new KPI tiles + two new filter chips wired to the state
    filter clauses.

Manifest: fusion_plating_shopfloor 19.0.33.2.0 → 19.0.34.0.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-25 09:47:17 -04:00
parent 4dc0a7cca5
commit 3a120dd400
6 changed files with 141 additions and 4 deletions

View File

@@ -287,6 +287,13 @@ class FpJob(models.Model):
if not job.active_step_id:
if job.state == 'done':
job.card_state = 'done'
elif job.state == 'awaiting_cert':
# Spec 2026-05-25 — state drives card_state for
# post-shop jobs (active_step_id is False because
# every step is terminal).
job.card_state = 'awaiting_cert'
elif job.state == 'awaiting_ship':
job.card_state = 'awaiting_ship'
elif (job.state == 'confirmed'
and job._fp_inbound_not_received()):
job.card_state = 'no_parts'
@@ -327,6 +334,17 @@ class FpJob(models.Model):
and step._fp_is_idle(threshold_hours=8)):
job.card_state = 'idle_warning'
continue
# Rule 7.5 — awaiting_cert + awaiting_ship (spec 2026-05-25)
# State drives card_state regardless of step state. Inserted
# BEFORE the done rule because state='done' jobs are filtered
# off the board upstream so the done rule is unreachable
# from cards we'd actually render.
if job.state == 'awaiting_cert':
job.card_state = 'awaiting_cert'
continue
if job.state == 'awaiting_ship':
job.card_state = 'awaiting_ship'
continue
# Rule 8 — done
if step.area_kind == 'shipping' and job.state == 'done':
job.card_state = 'done'
@@ -356,10 +374,50 @@ class FpJob(models.Model):
'step_ids.area_kind',
'active_step_id',
'card_state',
'state',
)
def _compute_mini_timeline_json(self):
"""9-element JSON array, one per Shop Floor column."""
"""9-element JSON array, one per Shop Floor column.
For awaiting_cert / awaiting_ship (spec 2026-05-25): the
Final-inspection or Shipping dot renders as 'current' with the
state-named variant; all earlier dots render 'done'. Lets the
QM see at a glance "this card has cleared the line, just
waiting on paperwork/shipping".
"""
for job in self:
# Post-shop state override (spec 2026-05-25): visually walk
# the card across the two right-most columns even though
# the recipe may not have steps with those area_kinds.
if job.state == 'awaiting_cert':
timeline = []
for area in _COLUMN_SEQUENCE:
if area == 'inspection':
timeline.append({
'area': area,
'state': 'current',
'variant': 'awaiting_cert',
})
elif area == 'shipping':
timeline.append({'area': area, 'state': 'upcoming'})
else:
timeline.append({'area': area, 'state': 'done'})
job.mini_timeline_json = json.dumps(timeline)
continue
if job.state == 'awaiting_ship':
timeline = []
for area in _COLUMN_SEQUENCE:
if area == 'shipping':
timeline.append({
'area': area,
'state': 'current',
'variant': 'awaiting_ship',
})
else:
timeline.append({'area': area, 'state': 'done'})
job.mini_timeline_json = json.dumps(timeline)
continue
# Standard path — pre-existing logic.
active_area = (job.active_step_id.area_kind
if job.active_step_id else None)
timeline = []