feat(jobs+shopfloor): live-step priority chain + done-job filter

Fix the Shop Floor plant kanban so cards land in the right column:
- fp.job._compute_active_step_id walks priority chain
  (in_progress > paused > ready > pending), not just in_progress
- fp.job._compute_card_state edge case respects job.state='done'
  (no more bogus 'contract_review' label on done jobs)
- fp.job.step._compute_area_kind reads kind.area_kind directly;
  legacy _STEP_KIND_TO_AREA dict removed (50+ lines deleted)
- /fp/landing/plant_kanban filters out done/cancelled jobs from
  the live board

Migration 19.0.10.25.0 backfills template metadata (codes,
descriptions, icons, kind_id) on 30 unfinished library templates
and repoints recipe nodes for 6 unambiguous name patterns
(Blasting -> blast, Ready For X -> gating, De-Masking -> demask,
Scheduling -> gating, Nickel Strip -> wet_process,
Pre-Meas/Check Sulfamate -> inspect).

Battle test bt_s24_between_steps.py covers between-step routing,
paused step lifecycle, and done-job board filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 17:06:53 -04:00
parent 7b90f210b9
commit b06d28e7f6
7 changed files with 346 additions and 75 deletions

View File

@@ -257,13 +257,21 @@ class FpJob(models.Model):
def _compute_card_state(self):
"""Dispatch matching spec §6.2 / §9.3 explicit precedence list."""
for job in self:
# Edge: no active step (all pending or all done)
# Edge: no live step (all steps done OR no steps at all).
# - job.state='done' → 'done' (defensive — done jobs are
# filtered off the Shop Floor board upstream, but the
# field still needs a value).
# - confirmed + parts not yet received → 'no_parts'.
# - else → 'ready' (job awaiting work, no steps yet OR
# recipe not assigned).
if not job.active_step_id:
if (job.state == 'confirmed'
if job.state == 'done':
job.card_state = 'done'
elif (job.state == 'confirmed'
and job._fp_inbound_not_received()):
job.card_state = 'no_parts'
else:
job.card_state = 'contract_review'
job.card_state = 'ready'
continue
step = job.active_step_id
@@ -384,11 +392,32 @@ class FpJob(models.Model):
@api.depends('step_ids.state', 'step_ids.sequence')
def _compute_active_step_id(self):
"""Pick the "live" step — first match by priority then sequence.
Priority order:
in_progress > paused > ready > first pending
in_progress is the most informative (someone is actively
working on it). paused means someone was working and stopped —
the card belongs at that station so the next operator can
pick it up. ready is the next-up step waiting for an operator.
The first pending after a done step is the "next gate"
where the card visually waits between steps.
Returns False only when every step is `done` (job finished)
or when there are no steps at all (recipe not assigned).
See spec 2026-05-24-shopfloor-live-step-fix-design.md Change 1.
"""
PRIORITY_STATES = ('in_progress', 'paused', 'ready', 'pending')
for job in self:
active = job.step_ids.filtered(
lambda s: s.state == 'in_progress'
).sorted('sequence')
job.active_step_id = active[:1].id if active else False
ordered = job.step_ids.sorted('sequence')
live = job.env['fp.job.step']
for state in PRIORITY_STATES:
live = ordered.filtered(lambda s: s.state == state)
if live:
break
job.active_step_id = live[:1].id if live else False
# ------------------------------------------------------------------
# Sub 14 — Configurable workflow state (status bar milestone)

View File

@@ -17,60 +17,11 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# Mapping from fp.process.node.default_kind → fp.work.centre.area_kind.
# Used as fallback by fp.job.step.area_kind compute when the step has no
# work_centre_id or its work_centre has no area_kind set. Authoritative
# source per the plant-view spec §4.2.
# 2026-05-23 — Shop Floor plant-view redesign.
_STEP_KIND_TO_AREA = {
# Receiving (admin / pre-physical-work)
'receiving': 'receiving',
'incoming_inspection': 'receiving',
'contract_review': 'receiving',
'gating': 'receiving',
'ready_for_processing': 'receiving',
# Masking
'masking': 'masking',
# Blasting
'blasting': 'blasting',
'bead_blast': 'blasting',
'media_blast': 'blasting',
# Racking
'racking': 'racking',
# Plating (everything wet — rolled up into one column per spec D3)
'soak_clean': 'plating',
'electroclean': 'plating',
'acid_dip': 'plating',
'etch': 'plating',
'desmut': 'plating',
'zincate': 'plating',
'rinse': 'plating',
'water_break_test': 'plating',
'activation': 'plating',
'e_nickel_plate': 'plating',
'chrome': 'plating',
'anodize': 'plating',
'black_oxide': 'plating',
'drying': 'plating',
# Baking
'bake': 'baking',
'oven_bake': 'baking',
'post_bake_relief': 'baking',
# De-Racking (folds in de-masking per spec D4)
'de_rack': 'de_racking',
'de_mask': 'de_racking',
'unrack': 'de_racking',
# Final inspection (post-plate inspection / FAIR / thickness QC)
'inspection': 'inspection',
'final_inspection': 'inspection',
'post_plate_inspection':'inspection',
'thickness_qc': 'inspection',
'fair': 'inspection',
'dimensional_check': 'inspection',
# Shipping
'shipping': 'shipping',
'pack_ship': 'shipping',
}
# 2026-05-24 — Shop Floor live-step fix (19.0.10.24.0):
# The legacy `_STEP_KIND_TO_AREA` dict that lived here was removed.
# fp.step.kind now self-declares its area_kind, so the kind taxonomy
# IS the source of truth for Shop Floor column routing.
# See docs/superpowers/specs/2026-05-24-shopfloor-live-step-fix-design.md.
class FpJobStep(models.Model):
@@ -169,21 +120,40 @@ class FpJobStep(models.Model):
store=True,
index=True,
help='Which Shop Floor column this step belongs to. Resolved as: '
'(1) work_centre.area_kind if set; else (2) fallback to '
'_STEP_KIND_TO_AREA[recipe_node.default_kind]; else (3) the '
'safe catch-all "plating". Drives plant-view kanban grouping.',
'(1) work_centre.area_kind if set; else (2) the area_kind on '
'recipe_node.kind_id; else (3) the safe catch-all "plating". '
'Drives plant-view kanban grouping.',
)
@api.depends('work_centre_id.area_kind', 'recipe_node_id.default_kind')
@api.depends(
'work_centre_id.area_kind',
'recipe_node_id.kind_id.area_kind',
)
def _compute_area_kind(self):
"""Resolve the plant-view column this step belongs in.
Priority chain:
1. work_centre.area_kind (explicit operator setup wins)
2. recipe_node.kind_id.area_kind (kind taxonomy authoritative)
3. catch-all 'plating' (data integrity issue if we land here)
The legacy _STEP_KIND_TO_AREA dict was removed — fp.step.kind
now self-declares its area_kind, so the kind taxonomy IS the
source of truth. See spec
2026-05-24-shopfloor-live-step-fix-design.md Change 6.
"""
for step in self:
# 1. Explicit work_centre wins
if step.work_centre_id and step.work_centre_id.area_kind:
step.area_kind = step.work_centre_id.area_kind
continue
kind = step.recipe_node_id.default_kind if step.recipe_node_id else False
if kind and kind in _STEP_KIND_TO_AREA:
step.area_kind = _STEP_KIND_TO_AREA[kind]
# 2. Kind taxonomy
node = step.recipe_node_id
if node and node.kind_id and node.kind_id.area_kind:
step.area_kind = node.kind_id.area_kind
continue
# 3. Catch-all — only reached for orphaned steps (no
# work_centre AND no recipe_node).
step.area_kind = 'plating'
last_activity_at = fields.Datetime(