From 72f0f182a65782cdca2cc8a2a01bf443e6fbb6db Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 25 May 2026 09:22:33 -0400 Subject: [PATCH] feat(fp.job.step): wrap button_finish with gate + advance (Task 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-super: when finishing the last open step on an in_progress job, run the bake/qty/QC gates from button_mark_done so failures surface as UserError on the click (per spec D12). Without this the auto-advance would silently fail with no error path. Post-super: trigger _fp_check_advance_post_shop so the state auto-advances cleanly (in_progress → awaiting_cert / awaiting_ship). Added _fp_check_finish_gates helper on fp.job and a fp_check_gates_only context flag honored by button_mark_done so the gate logic is single-sourced (DRY). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_jobs/models/fp_job.py | 29 +++++++++++++++++++ .../fusion_plating_jobs/models/fp_job_step.py | 28 ++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index 3f5f1aec..7c350fc8 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -1984,6 +1984,11 @@ class FpJob(models.Model): "Job %s requires QC. A new check has been created — " "complete it before marking the job Done." ) % job.name) + # When called as a gate-check from fp.job.step.button_finish + # (per spec 2026-05-25 D12), exit BEFORE flipping state — + # the post-shop advance helper handles the actual transition. + if self.env.context.get('fp_check_gates_only'): + continue job.state = 'done' job.date_finished = fields.Datetime.now() if not skip_side_effects: @@ -2153,6 +2158,30 @@ class FpJob(models.Model): # regresses awaiting_ship → awaiting_cert. All helpers are # idempotent — safe to call from any hook. # ================================================================== + def _fp_check_finish_gates(self): + """Run the bake-window / qty-reconciliation / QC gates that used + to live in button_mark_done. Called from + fp.job.step.button_finish when the operator is finishing the + LAST open step on the job (spec D12). + + Raises UserError on failure — operator stays on the step, fixes + the issue, retries the click. Manager bypass via the same + context flags as button_mark_done (fp_skip_bake_gate, + fp_skip_qty_reconcile, fp_skip_qc_gate). + + The trick: re-uses button_mark_done's gate logic but short- + circuits BEFORE the state flip via the fp_check_gates_only + context flag (honoured in button_mark_done below). + """ + self.ensure_one() + # Pass through with context flag; button_mark_done will run all + # its gates and then exit before flipping state. The actual + # state transition (in_progress → awaiting_cert/ship) is owned + # by _fp_check_advance_post_shop running AFTER super().button_finish. + self.with_context( + fp_check_gates_only=True, + ).button_mark_done() + def _fp_check_advance_post_shop(self): """Auto-advance in_progress jobs whose recipe steps are all terminal. Called from fp.job.step.button_finish post-super(). 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 77c87bd2..9bffefb6 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job_step.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py @@ -1311,8 +1311,36 @@ class FpJobStep(models.Model): self._fp_check_contract_review_complete() self._fp_check_receiving_gate() + # ----- Post-shop gate (spec 2026-05-25 D12) --------------------- + # When finishing the LAST open step on an in_progress job, run + # the bake/qty/QC gates that used to live in button_mark_done. + # Failure raises UserError on THIS click — operator fixes + # (qty, bake, QC) and retries the finish. Without this the + # auto-advance helper would silently fail with no error path. + for step in self: + if step.state not in ('in_progress', 'paused', 'ready'): + continue + job = step.job_id + if not job or job.state != 'in_progress': + continue + # Would this finish leave every step terminal? + siblings_open = job.step_ids.filtered( + lambda s: s.id != step.id + and s.state not in ('done', 'skipped', 'cancelled') + ) + if siblings_open: + continue # not the last open step — skip the gates + job._fp_check_finish_gates() + result = super().button_finish() + # ----- Post-shop auto-advance (spec 2026-05-25) ----------------- + # After super().button_finish flips step state to done, ask the + # job to check whether ALL steps are now terminal. If so the + # helper auto-advances state to awaiting_cert / awaiting_ship. + for job in self.mapped('job_id'): + job._fp_check_advance_post_shop() + # ----- Post-finish side effects -------------------------------- BW = self.env['fusion.plating.bake.window'] Bath = self.env['fusion.plating.bath']