diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index e24214a1..39c16d6e 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Native Jobs', - 'version': '19.0.8.17.2', + 'version': '19.0.8.17.3', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'author': 'Nexa Systems Inc.', 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 e700757c..65f7bf9b 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job_step.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py @@ -85,84 +85,12 @@ class FpJobStep(models.Model): ) step.can_start = not bool(blocking) - def button_start(self): - """Override — soft gate when parts haven't been received yet, - plus hard predecessor gate driven by the recipe + per-step - sequential enforcement policy (Sub 13). - - Receiving check is soft (logs to chatter) — manager wants the - shop to start prep regardless when parts are in-transit late. - - Predecessor check IS hard-blocking — every earlier-sequence - step must be terminal (done / skipped / cancelled) before Start - fires, UNLESS: - * this step is flagged parallel_start (explicit per-step opt-out), OR - * the parent recipe has enforce_sequential=False AND this step - is not flagged requires_predecessor_done (legacy free-flow). - - Manager bypass via fp_skip_predecessor_check=True. - """ - skip_pred = self.env.context.get('fp_skip_predecessor_check') - for step in self: - if skip_pred: - continue - if not step._fp_should_block_predecessors(): - continue - blocking = step.job_id.step_ids.filtered( - lambda s: s.sequence < step.sequence and s.state not in ( - 'done', 'skipped', 'cancelled', - ) - ) - if blocking: - raise UserError(_( - "Step '%s' cannot start until earlier steps are " - "finished, skipped, or cancelled.\n\nBlocking step(s):\n %s\n\n" - "Options:\n" - " * Finish/Skip the blocking step(s) first.\n" - " * If this step legitimately runs in parallel, ask " - "a manager to flag it as Parallel Start on the recipe.\n" - " * Manager override via context fp_skip_predecessor_check=True." - ) % ( - step.name, - '\n '.join( - f'#{s.sequence} {s.name} ({s.state})' - for s in blocking[:5] - ), - )) - result = super().button_start() - for step in self: - so = step.job_id.sale_order_id - if not so: - continue - recv = so.x_fc_receiving_status if ( - 'x_fc_receiving_status' in so._fields - ) else None - if recv in (False, None, 'not_received'): - step.job_id.message_post(body=_( - 'Step "%(step)s" started before parts were received ' - '(SO %(so)s — receiving status: %(status)s). ' - 'Confirm the parts are physically on the floor before ' - 'continuing.' - ) % { - 'step': step.name, - 'so': so.name or '', - 'status': recv or 'unknown', - }) - - # Sub 12e v4 — when an operator starts a contract_review step, - # immediately route to the QA-005 form. The step stays - # in_progress in the background; the operator signs (or - # dismisses) the review on QA-005, navigates back to the job, - # then clicks Finish & Next to advance. This removes the - # earlier "click Start, then click Finish & Next, then maybe - # click Finish & Next again" friction. - # Single-record only — multi-record button_start (e.g. job - # bulk-start) shouldn't navigate. - if len(self) == 1: - cr_action = self._fp_contract_review_redirect() - if cr_action: - return cr_action - return result + # NOTE: the actual button_start override lives further down (~line + # 876) where it merges Sub 13 predecessor gate + Policy B Contract + # Review auto-open + Sub 8 Racking auto-open + the receiving soft + # check. Keeping ONE definition prevents the Python-overrides-the- + # earlier-method footgun that swallowed Sub 13 enforcement on + # WH/JOB/00342. def button_pause(self): """Pause an in-progress step (operator break, end of shift). @@ -874,7 +802,46 @@ class FpJobStep(models.Model): }) def button_start(self): - # Policy B — Contract Review takes priority (auto-opens QA-005). + """Single source of truth for step start: + 1. Sub 13 predecessor gate (raise UserError if blocking) + 2. Policy B Contract Review auto-open (route to QA-005) + 3. Sub 8 Racking auto-open (route to racking inspection) + 4. super().button_start() + receiving soft check + serial + promotion for the standard path + + Manager bypasses available via context: + fp_skip_predecessor_check=True skips the Sub 13 gate + """ + # ---- 1. Sub 13 predecessor gate ---------------------------------- + skip_pred = self.env.context.get('fp_skip_predecessor_check') + for step in self: + if skip_pred: + continue + if not step._fp_should_block_predecessors(): + continue + blocking = step.job_id.step_ids.filtered( + lambda s: s.sequence < step.sequence and s.state not in ( + 'done', 'skipped', 'cancelled', + ) + ) + if blocking: + raise UserError(_( + "Step '%s' cannot start until earlier steps are " + "finished, skipped, or cancelled.\n\nBlocking step(s):\n %s\n\n" + "Options:\n" + " * Finish/Skip the blocking step(s) first.\n" + " * If this step legitimately runs in parallel, ask " + "a manager to flag it as Parallel Start on the recipe.\n" + " * Manager override via context fp_skip_predecessor_check=True." + ) % ( + step.name, + '\n '.join( + f'#{s.sequence} {s.name} ({s.state})' + for s in blocking[:5] + ), + )) + + # ---- 2. Policy B Contract Review auto-open ----------------------- for step in self: if step._fp_is_contract_review_step(): action = step._fp_open_contract_review() @@ -883,7 +850,8 @@ class FpJobStep(models.Model): if step.state == 'in_progress': step._fp_promote_serials_on_start() return action - # Sub 8 — Racking step auto-opens the inspection form. + + # ---- 3. Sub 8 Racking auto-open ---------------------------------- for step in self: if step._fp_is_racking_step(): action = step._fp_open_racking_inspection() @@ -892,10 +860,29 @@ class FpJobStep(models.Model): if step.state == 'in_progress': step._fp_promote_serials_on_start() return action + + # ---- 4. Standard path: start + receiving check + serial promote -- result = super().button_start() for step in self: if step.state == 'in_progress': step._fp_promote_serials_on_start() + so = step.job_id.sale_order_id + if not so: + continue + recv = so.x_fc_receiving_status if ( + 'x_fc_receiving_status' in so._fields + ) else None + if recv in (False, None, 'not_received'): + step.job_id.message_post(body=_( + 'Step "%(step)s" started before parts were received ' + '(SO %(so)s — receiving status: %(status)s). ' + 'Confirm the parts are physically on the floor before ' + 'continuing.' + ) % { + 'step': step.name, + 'so': so.name or '', + 'status': recv or 'unknown', + }) return result def button_finish(self):