From 64a202ff6e8f209e67c7d7975ad967e1397e1ea2 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 2 Jun 2026 08:29:47 -0400 Subject: [PATCH] fix(fusion_plating_shopfloor): partial advance blocked by from-step predecessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Move dialog's predecessor check flagged every unfinished step before the destination — including the from_step itself, which is in-progress by definition when advancing partial parts out of it. So any "Send → next" to a not-yet-started step showed a hard "Predecessor not done: " blocker and greyed out SEND (reproduced on WO-30061: Racking → Ready for processing). This broke partial advance for ALL quantities, not just 1-part orders. Fix: _blockers_for_move only blocks unfinished steps STRICTLY BETWEEN from_step and to_step (you'd be skipping an incomplete intermediate stage). Immediate-next advance is allowed; skip-ahead still blocked; backward (rework) moves unblocked. Verified on entech: blocker no longer fires for Racking → Ready for processing. Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_plating/CLAUDE.md | 2 +- .../controllers/move_controller.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 2d12b2c9..7a449be9 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -1849,7 +1849,7 @@ A 50-part job can have parts at several stages at once (10 Masking, 20 Plating, 4. **Partial-flow "light up" lives in `move_controller._do_move_parts_commit` / `_do_move_rack_commit`:** a forward (`transfer_type='step'`) move (a) flips the destination step `pending → ready` so the receiving operator gets an actionable card with no action by anyone, and (b) calls `from_step._fp_try_autofinish_on_drain()` (best-effort, swallows finish-gate UserErrors). It does **not** auto-START the destination — `button_start` stays explicit to keep the labour timer accurate (S16). No auto-ready/auto-finish for hold/scrap/rework moves. -5. **The predecessor gate is qty-aware: `_fp_should_block_predecessors()` returns False once `_fp_has_real_incoming()` is true** (an incoming move from a different step with `qty_moved > 0`). A step with parts physically parked at it is startable regardless of whether upstream steps are fully done. This is the single source of truth shared by `can_start`, `_compute_blocker`, `button_start`, and the Move dialog's `_blockers_for_move`. **Don't "fix" the predecessor gate back to pure sequence-based** — it would re-lock the next stage while the rest of the batch is still upstream. +5. **The predecessor gate is qty-aware: `_fp_should_block_predecessors()` returns False once `_fp_has_real_incoming()` is true** (an incoming move from a different step with `qty_moved > 0`). A step with parts physically parked at it is startable regardless of whether upstream steps are fully done. This is the single source of truth shared by `can_start`, `_compute_blocker`, `button_start`, and the Move dialog's `_blockers_for_move`. **Don't "fix" the predecessor gate back to pure sequence-based** — it would re-lock the next stage while the rest of the batch is still upstream. **Second, distinct trap (fixed 2026-06-02): the Move dialog's `_blockers_for_move` predecessor check must only flag unfinished steps STRICTLY BETWEEN `from_step` and `to_step` (`from_step.sequence < s.sequence < to_step.sequence`), NOT all steps before `to_step`.** The original `s.sequence < to_step.sequence` filter counted the `from_step` itself (which is in-progress *by definition* when you advance partial parts out of it) as an "unfinished predecessor" of the destination — so EVERY partial advance to a not-yet-started next step showed a hard "Predecessor not done: \" blocker and greyed out SEND (hit on WO-30061). The between-only rule allows the immediate-next advance, still blocks skip-ahead moves over incomplete intermediate stages, and leaves backward (rework) moves unblocked (empty range). 6. **Move-based scrap (`transfer_type='scrap'`) does NOT touch `job.qty_scrapped`.** At close, `button_mark_done` calls `_fp_scrapped_via_moves()` and folds it into `qty_scrapped`, then auto-fills `qty_done = qty − qty_scrapped` (was: blindly `= job.qty`, which over-counted when parts were scrapped). The reconciliation gate is still the safety net. diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/move_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/move_controller.py index 548905e8..d4514069 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/move_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/move_controller.py @@ -124,8 +124,18 @@ class FpTabletMoveController(http.Controller): hasattr(to_step, '_fp_should_block_predecessors') and to_step._fp_should_block_predecessors() ): + # Partial-flow (2026-06-02): only an unfinished step STRICTLY + # BETWEEN from_step and to_step blocks the move (you'd be skipping + # an incomplete intermediate stage). The from_step itself is + # in-progress BY DEFINITION when advancing partial parts out of + # it — counting it (or any earlier step) as an "unfinished + # predecessor" blocked every partial advance to a not-yet-started + # next step. Steps before from_step are irrelevant: the parts + # being moved are physically at from_step, ready for the next + # stage. Backward moves (rework: from > to) yield an empty range + # and are never predecessor-blocked. unfinished = to_step.job_id.step_ids.filtered( - lambda s: s.sequence < to_step.sequence + lambda s: from_step.sequence < s.sequence < to_step.sequence and s.state not in ('done', 'skipped', 'cancelled') ) if unfinished: