fix(fusion_plating_jobs): gating steps fall forward to next stage's column

A "Ready for X" gating step (fp.step.kind code='gating') maps to
area_kind='receiving' in the taxonomy. For a MID-recipe gate (e.g.
"Ready for processing" between Racking and Plating) that snapped the
job's card back to the far-left Receiving column when work advanced into
it — the job looked like it vanished from the board.

_compute_area_kind now detects gating via the stable kind code and
resolves a gating step's column to the NEXT non-gating step's area (so
"Ready for processing" shows in Plating), keeping cards flowing
left→right. Falls back to the last real stage for a trailing gate.
Non-gating steps unchanged. Helpers: _fp_is_gating_step / _fp_raw_area_kind
(no recursion) / _fp_resolve_area_kind.

area_kind is a stored compute — recomputed all 537 live steps on entech.
Verified: WO-30061 "Ready for processing" area receiving→plating, card now
renders in the Plating column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-02 08:51:24 -04:00
parent 451fc5eafd
commit a52f2bbebd
2 changed files with 76 additions and 19 deletions

View File

@@ -630,8 +630,27 @@ De-Racking → Final inspection → Shipping`
Columns are first-class — they always render in this exact order, never
reorder, never collapse when empty. Driven by `fp.work.centre.area_kind`
Selection (added 2026-05-23). Each `fp.job.step.area_kind` is computed
(stored) from `work_centre.area_kind` with a fallback to a step-kind
dispatch table (`_STEP_KIND_TO_AREA` in `fusion_plating_jobs/models/fp_job_step.py`).
(stored) in `_compute_area_kind` (`fusion_plating_jobs/models/fp_job_step.py`):
`work_centre.area_kind` → else `recipe_node.kind_id.area_kind` (the
`fp.step.kind` taxonomy is authoritative; the legacy `_STEP_KIND_TO_AREA`
dict is gone) → else catch-all `'plating'`.
**Gating/"Ready for X" marker steps fall FORWARD (fixed 2026-06-02).** The
`fp.step.kind` named *Gating* has `code='gating'` **and `area_kind='receiving'`**.
A gating step is a non-physical "ready for the next stage" marker, so
mapping it to Receiving made a *mid-recipe* gate snap the job's card back
to the first column (Racking → "Ready for processing" jumped to Receiving,
so the job looked like it vanished). `_compute_area_kind` therefore detects
a gating step via the **stable `kind_id.code == 'gating'`** (never the
display name) and resolves its column to the **next non-gating step's** raw
area (so "Ready for processing" before plating shows in the **Plating**
column); if nothing real follows, it falls back to the last real stage.
Helpers: `_fp_is_gating_step`, `_fp_raw_area_kind` (own work_centre/kind
only — no look-ahead, avoids recursion), `_fp_resolve_area_kind`. **NB:**
`area_kind` is a STORED compute, so after changing this logic you must
force-recompute existing rows (`env['fp.job.step'].search([])._compute_area_kind()`
+ `flush_recordset(['area_kind'])` + commit) — a `-u`/restart alone leaves
old values stale.
**Spec D3:** all wet-line steps (Soak Clean, Electroclean, Acid Dip,
Etch, Desmut, Zincate, Rinse, E-Nickel, Chrome, Anodize, Black Oxide,

View File

@@ -157,33 +157,71 @@ class FpJobStep(models.Model):
@api.depends(
'work_centre_id.area_kind',
'recipe_node_id.kind_id.area_kind',
'recipe_node_id.kind_id.code',
'sequence',
'job_id.step_ids.sequence',
'job_id.step_ids.work_centre_id.area_kind',
'job_id.step_ids.recipe_node_id.kind_id.area_kind',
'job_id.step_ids.recipe_node_id.kind_id.code',
)
def _compute_area_kind(self):
"""Resolve the plant-view column this step belongs in.
Priority chain:
Priority chain (non-gating steps):
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.
Gating/marker steps (kind `code == 'gating'` — the "Ready for X"
steps) have NO physical location; the taxonomy maps them to
'receiving', which made a mid-recipe gate snap the job's card back
to the first column (Racking -> "Ready for processing" jumped to
Receiving, so the job looked like it vanished — 2026-06-02). A
gating step now FALLS FORWARD to the next non-gating step's column
(it's "ready for [that stage]"), keeping the card moving
left->right. If nothing real follows, it falls back to the last
real stage.
"""
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
# 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'
step.area_kind = step._fp_resolve_area_kind()
def _fp_raw_area_kind(self):
"""Area from this step's OWN work_centre / kind only — no look-ahead
and no dependence on the computed `area_kind` field (so the gating
fall-forward below can't recurse)."""
self.ensure_one()
if self.work_centre_id and self.work_centre_id.area_kind:
return self.work_centre_id.area_kind
node = self.recipe_node_id
if node and node.kind_id and node.kind_id.area_kind:
return node.kind_id.area_kind
return 'plating'
def _fp_is_gating_step(self):
"""True for a 'Ready for X' marker step (no physical location).
Detected via the STABLE kind code, never the display name."""
self.ensure_one()
node = self.recipe_node_id
return bool(node and node.kind_id and node.kind_id.code == 'gating')
def _fp_resolve_area_kind(self):
"""Column for this step: its own raw area, EXCEPT a gating marker
falls forward to the next non-gating step's column."""
self.ensure_one()
if not self._fp_is_gating_step():
return self._fp_raw_area_kind()
siblings = self.job_id.step_ids
later = siblings.filtered(
lambda s: s.sequence > self.sequence and not s._fp_is_gating_step()
).sorted('sequence')
if later:
return later[0]._fp_raw_area_kind()
earlier = siblings.filtered(
lambda s: s.sequence < self.sequence and not s._fp_is_gating_step()
).sorted('sequence')
if earlier:
return earlier[-1]._fp_raw_area_kind()
return self._fp_raw_area_kind()
last_activity_at = fields.Datetime(
string='Last Activity',