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:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user