From a52f2bbebd8d1ddcb30205808126e40cf3685e5e Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 2 Jun 2026 08:51:24 -0400 Subject: [PATCH] fix(fusion_plating_jobs): gating steps fall forward to next stage's column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- fusion_plating/CLAUDE.md | 23 +++++- .../fusion_plating_jobs/models/fp_job_step.py | 72 ++++++++++++++----- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index d1dd54dd..ad88d2fb 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -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, diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job_step.py b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py index 78e88534..b1fa580f 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job_step.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py @@ -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',