From 9794a98de94c4290ae4f069770b62d931613ec18 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 3 May 2026 21:24:12 -0400 Subject: [PATCH] feat(jobs): Sub 13 sequential step enforcement + Sub 12e v3 wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coherent feature drops shipping together because their fp_job_step edits overlap. Both target operator workflow correctness. ## Sub 13 — Sequential step enforcement (recipe + per-step) Background: Investigation on WH/JOB/00339 showed operators starting Incoming Inspection while Contract Review was still in_progress. Audit: 98.7% of recipe operations system-wide had requires_predecessor_done = false (the legacy per-step opt-in defaults off, recipe authors rarely tick the box). Architecture: Recipe-level toggle + per-step opt-out (Option A from /investigate). * fusion.plating.process.node.enforce_sequential — Boolean on the recipe root. Default True. When True, every operation under this recipe waits for earlier-sequence steps to finish before it can start. * fusion.plating.process.node.parallel_start — Boolean on operation nodes. When True, this step bypasses the sequential gate (e.g. paperwork or QA review that runs alongside production). * Mirrored on fp.step.template (parallel_start) so library steps carry the flag into snapshots. * fp.job.enforce_sequential — related from recipe_id. Snapshotted at job creation so a recipe author flipping the recipe's flag AFTER job generation does NOT change behaviour mid-run. * fp.job.step.parallel_start — related from recipe_node_id. * Decision matrix (encapsulated in fp.job.step._fp_should_block_predecessors): recipe.enforce_sequential | step.parallel_start | step.req_pred_done | block? --------------------------|---------------------|--------------------|------ True | False | any | YES True | True | any | no False | any | True | YES False | any | False | no * Manager bypass via context fp_skip_predecessor_check=True (existing). Runtime gates: * fp.job.step.button_start — calls _fp_should_block_predecessors; raises UserError naming the blocking earlier step(s). * fp.job.step.can_start — computed Boolean for view-side disable. * Move wizard predecessor check (fusion_plating_shopfloor/controllers/move_controller.py) — uses the same helper so tablet + backend behave identically. UI surface: * Recipe form (fp_process_node_views.xml) — enforce_sequential toggle on recipe root, parallel_start checkbox on operations. * Step template form — parallel_start checkbox. * Simple Recipe Editor (inline library form) — Parallel Start checkbox + legacy flag demoted with muted styling + supervisor group gate. * Recipe Tree Editor (properties panel) — both flags exposed, only-show on the right node_type. * Controllers updated to allowlist + payload the new fields. Migration: fusion_plating/migrations/19.0.18.12.0/post-migrate.py — sets enforce_sequential = TRUE on every existing recipe-root node. Idempotent. User confirmed dev-stage data, so retroactive flip is safe (no production jobs to disrupt). Tests: TestSequentialEnforcement (10 tests) covering: * sequential mode blocks out-of-order start * first step always startable * predecessor finish/skip unlocks next * parallel_start opts out of gate * free-flow mode bypasses gate * legacy requires_predecessor_done still honoured in free-flow * manager bypass via context * can_start compute reflects state correctly * library template parallel_start snapshots into recipe-node ## Sub 12e — Record Inputs Wizard v3 (card layout, dark-mode aware) Background: v2 wizard was a 17-column wide editable table. Operators got lost finding which value column applied to their row's type, horizontal scroll required on tablets, composite types crammed into one row. New layout: * Each measurement renders as a stacked card (CSS Grid + display transformation on the existing list widget — preserves inline editing, no JS rewrite). * Card header: prompt name (large, bold) + type/unit pills. * Card body: ONLY the value widget for this row's type (number / boolean / date / text / photo / multi-point / panel). * Composite types (multi-point thickness 5x reading + avg, bath panel 4 fields) get inline sub-grid inside the card. * Empty state ("no measurement prompts") with friendly CTA. Dark mode: * SCSS branches at compile time on $o-webclient-color-scheme (per fusion-plating/CLAUDE.md note). * Tokens: 7 surface colours + 4 ink levels with light/dark hex pairs, all behind var(--fp-*) custom properties for per-deploy override. * Registered in BOTH web.assets_backend AND web.assets_web_dark so each bundle compiles its own palette. Tablet polish: @media (max-width: 900px) — collapse meta below prompt + bump numeric input min-height to 56px. Defensive: * v2 view kept in the XML file (instant rollback by changing one view_id ref). * `:has(.o_invisible_modifier)` rule drops empty cells out of the grid so Odoo's invisible="..." doesn't punch holes in layout. Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/fusion_plating/__manifest__.py | 2 +- .../controllers/recipe_controller.py | 2 + .../controllers/simple_recipe_controller.py | 3 + .../migrations/19.0.18.12.0/post-migrate.py | 70 +++ .../fusion_plating/models/fp_job_step.py | 22 +- .../fusion_plating/models/fp_process_node.py | 45 +- .../fusion_plating/models/fp_step_template.py | 16 +- .../static/src/js/recipe_tree_editor.js | 3 + .../static/src/js/simple_recipe_editor.js | 2 + .../static/src/xml/recipe_tree_editor.xml | 22 + .../static/src/xml/simple_recipe_editor.xml | 10 +- .../views/fp_process_node_views.xml | 12 + .../views/fp_step_template_views.xml | 6 +- .../fusion_plating_jobs/__manifest__.py | 9 +- .../fusion_plating_jobs/models/fp_job.py | 15 + .../fusion_plating_jobs/models/fp_job_step.py | 98 +++- .../src/scss/fp_job_step_input_wizard_v3.scss | 422 ++++++++++++++++++ .../tests/test_fp_job_extensions.py | 228 ++++++++++ .../fp_job_step_input_wizard_views.xml | 145 +++++- .../controllers/move_controller.py | 11 +- 20 files changed, 1109 insertions(+), 34 deletions(-) create mode 100644 fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py create mode 100644 fusion_plating/fusion_plating_jobs/static/src/scss/fp_job_step_input_wizard_v3.scss diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index 9af0a456..697d2988 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating', - 'version': '19.0.18.11.0', + 'version': '19.0.18.12.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ diff --git a/fusion_plating/fusion_plating/controllers/recipe_controller.py b/fusion_plating/fusion_plating/controllers/recipe_controller.py index f0d73d3d..a3f988b6 100644 --- a/fusion_plating/fusion_plating/controllers/recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/recipe_controller.py @@ -86,6 +86,8 @@ class FpRecipeController(http.Controller): 'estimated_duration', 'auto_complete', 'customer_visible', 'is_manual', 'requires_signoff', 'opt_in_out', 'sequence', 'version', + # Sub 13 — sequential enforcement + 'enforce_sequential', 'parallel_start', } safe_vals = {k: v for k, v in vals.items() if k in allowed} if not safe_vals: diff --git a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py index 51f8f192..f6c3f1b6 100644 --- a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py @@ -23,6 +23,7 @@ _SNAPSHOT_FIELDS = [ 'temp_min_target', 'temp_max_target', 'temp_unit', 'voltage_target', 'viscosity_target', 'requires_signoff', 'requires_predecessor_done', + 'parallel_start', 'requires_rack_assignment', 'requires_transition_form', 'default_kind', ] @@ -193,6 +194,7 @@ class SimpleRecipeController(http.Controller): 'description': tpl.description or '', 'requires_signoff': tpl.requires_signoff, 'requires_predecessor_done': tpl.requires_predecessor_done, + 'parallel_start': tpl.parallel_start, 'requires_rack_assignment': tpl.requires_rack_assignment, 'requires_transition_form': tpl.requires_transition_form, 'tank_ids': [ @@ -227,6 +229,7 @@ class SimpleRecipeController(http.Controller): allowed = { 'name', 'code', 'icon', 'default_kind', 'description', 'requires_signoff', 'requires_predecessor_done', + 'parallel_start', 'requires_rack_assignment', 'requires_transition_form', 'tank_ids', } diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py new file mode 100644 index 00000000..1657004f --- /dev/null +++ b/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# Part of the Fusion Plating product family. +""" +Migration 19.0.18.12.0 — Sub 13 sequential step enforcement. + +Background: + The legacy per-step `requires_predecessor_done` opt-in defaulted to + False, so 98.7% of operations system-wide had no enforcement and + operators were able to start arbitrary steps out of order (e.g. job + WH/JOB/00339 — Incoming Inspection ran while Contract Review was + still in progress). + +This migration: + * Sets `enforce_sequential = True` on every existing recipe-root + node so the new default behaviour kicks in immediately on upgrade. + Existing per-step `requires_predecessor_done` flags are preserved + and continue to work for any recipe whose author opts back into + free-flow mode (sets enforce_sequential = False). + +Idempotent — safe to re-run. +""" + +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return # Brand new install — defaults already correct. + + _logger.info( + '[migration 19.0.18.12.0] Promoting all existing recipes to ' + 'enforce_sequential=True (Sub 13 — sequential step enforcement).' + ) + + # Direct SQL — avoids loading the ORM at this stage and is fast. + # We intentionally only flip nodes that are CURRENTLY False; nodes + # already set True (or set by a manual install of a newer version) + # are skipped so the operation is clean to re-run. + cr.execute(""" + UPDATE fusion_plating_process_node + SET enforce_sequential = TRUE + WHERE node_type = 'recipe' + AND (enforce_sequential IS NULL OR enforce_sequential = FALSE) + """) + flipped = cr.rowcount + _logger.info( + '[migration 19.0.18.12.0] enforce_sequential flipped to True ' + 'on %d existing recipe(s).', flipped, + ) + + # Audit chatter on each affected recipe so the change is visible + # to recipe authors (they get a notification on their next visit). + if flipped: + cr.execute(""" + SELECT id, name FROM fusion_plating_process_node + WHERE node_type = 'recipe' AND enforce_sequential = TRUE + """) + recipes = cr.fetchall() + _logger.info( + '[migration 19.0.18.12.0] %d recipe(s) now Sequential by default. ' + 'Affected recipes: %s', + len(recipes), + ', '.join(name for _, name in recipes[:20]) + ( + ', ...' if len(recipes) > 20 else '' + ), + ) diff --git a/fusion_plating/fusion_plating/models/fp_job_step.py b/fusion_plating/fusion_plating/models/fp_job_step.py index 9e95857c..ac4c27fe 100644 --- a/fusion_plating/fusion_plating/models/fp_job_step.py +++ b/fusion_plating/fusion_plating/models/fp_job_step.py @@ -135,8 +135,26 @@ class FpJobStep(models.Model): requires_predecessor_done = fields.Boolean( related='recipe_node_id.requires_predecessor_done', store=True, - help='If True, button_start blocks until every earlier-sequence ' - 'step in this job is done/skipped/cancelled.', + help='LEGACY: per-step opt-in for predecessor enforcement. ' + 'Still honoured when the parent recipe has ' + 'enforce_sequential=False (free-flow recipe with one ' + 'specific step that needs to wait).', + ) + # Sub 13 — sequential enforcement (recipe + per-step). New default + # behaviour is "every step waits for predecessors", with two escape + # hatches: enforce_sequential=False on the recipe (free-flow), or + # parallel_start=True on this specific step (explicit parallelism). + # The per-step parallel_start field is on this CORE model because + # it just mirrors a core field (recipe_node_id.parallel_start). + # The runtime gate logic (can_start, _fp_should_block_predecessors) + # lives in fusion_plating_jobs because it reads the recipe-level + # enforce_sequential which only exists when that bridge is loaded. + parallel_start = fields.Boolean( + related='recipe_node_id.parallel_start', + store=True, + help='If True, this step can start while earlier-sequence ' + 'steps are still in progress. Only meaningful when the ' + 'parent recipe has enforce_sequential=True.', ) # ===== Sub 12b — chain-of-custody + rack awareness ===================== diff --git a/fusion_plating/fusion_plating/models/fp_process_node.py b/fusion_plating/fusion_plating/models/fp_process_node.py index 33e732fc..890d01c3 100644 --- a/fusion_plating/fusion_plating/models/fp_process_node.py +++ b/fusion_plating/fusion_plating/models/fp_process_node.py @@ -181,14 +181,41 @@ class FpProcessNode(models.Model): help='Quality hold point — requires operator sign-off.', ) requires_predecessor_done = fields.Boolean( - string='Requires Predecessor Done', + string='Requires Predecessor Done (legacy)', default=False, - help='If checked, this step cannot start until ALL earlier-' - 'sequence steps in the job are done / skipped / cancelled. ' - 'Use for serial-required operations (e.g. Plating must ' - 'follow Acid Etch with no time gap — passivation layer ' - 'forms in seconds). Leaving unchecked allows parallel ' - 'work across tanks (the default).', + help='LEGACY per-step opt-in for predecessor enforcement. As of ' + '19.0.X, recipes default to enforce_sequential=True so every ' + 'step naturally waits for its predecessors. This flag still ' + 'works on recipes whose enforce_sequential is False — turn ' + 'it on to make a single step block in an otherwise free-flow ' + 'recipe.', + ) + # ===== Sub 13 — sequential step enforcement (recipe + per-step) ========== + # Replaces the unused per-step requires_predecessor_done as the primary + # enforcement vector. Two layers: + # 1. enforce_sequential (recipe root) — entire recipe is sequential + # by default. Author can disable for free-flow recipes. + # 2. parallel_start (operation step) — escape hatch within a + # sequential recipe, for steps that legitimately run in parallel + # (e.g. paperwork that doesn't need previous step done). + enforce_sequential = fields.Boolean( + string='Enforce Sequential Order', + default=True, + help='Only meaningful on the recipe root node. When True (the ' + 'default), every operation under this recipe waits for all ' + 'earlier-sequence steps to be done/skipped/cancelled before ' + 'it can start. Mark a specific step as Parallel Start to ' + 'opt it out. Disable on the recipe to fall back to the ' + 'legacy per-step Requires Predecessor Done flag.', + ) + parallel_start = fields.Boolean( + string='Parallel Start', + default=False, + help='Only meaningful on operation nodes inside a recipe with ' + 'Enforce Sequential Order = True. When checked, this step ' + 'can be started while earlier-sequence steps are still in ' + 'progress (e.g. paperwork or QA review that runs alongside ' + 'production).', ) opt_in_out = fields.Selection( [ @@ -538,6 +565,10 @@ class FpProcessNode(models.Model): 'customer_visible': self.customer_visible, 'is_manual': self.is_manual, 'requires_signoff': self.requires_signoff, + # Sub 13 — sequential enforcement + 'enforce_sequential': self.enforce_sequential, + 'parallel_start': self.parallel_start, + 'requires_predecessor_done': self.requires_predecessor_done, 'version': self.version, 'child_count': len(children), 'opt_in_out': self.opt_in_out or 'disabled', diff --git a/fusion_plating/fusion_plating/models/fp_step_template.py b/fusion_plating/fusion_plating/models/fp_step_template.py index 3fe3eec9..113960e0 100644 --- a/fusion_plating/fusion_plating/models/fp_step_template.py +++ b/fusion_plating/fusion_plating/models/fp_step_template.py @@ -66,9 +66,19 @@ class FpStepTemplate(models.Model): viscosity_target = fields.Float(string='Viscosity Target') requires_signoff = fields.Boolean(string='Require QA Sign-off') - requires_predecessor_done = fields.Boolean(string='Require Predecessor Done', - help='S14 lock — operator cannot start this step until earlier ' - 'sequenced steps are done.') + requires_predecessor_done = fields.Boolean( + string='Require Predecessor Done (legacy)', + help='Legacy per-step opt-in for predecessor enforcement. Recipes ' + 'now default to Enforce Sequential Order — use Parallel ' + 'Start instead when you want a step to run alongside others.', + ) + parallel_start = fields.Boolean( + string='Parallel Start', + help='Sub 13. When this template lands inside a sequential ' + 'recipe, the resulting step can be started while ' + 'earlier-sequence steps are still in progress (e.g. ' + 'paperwork that runs alongside production).', + ) requires_rack_assignment = fields.Boolean(string='Requires Rack Assignment', help='Triggers Rack Parts sub-dialog at runtime (Sub 12b).') requires_transition_form = fields.Boolean(string='Requires Transition Form', diff --git a/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js b/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js index 5a63f01e..29bd5aea 100644 --- a/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js @@ -268,6 +268,9 @@ export class RecipeTreeEditor extends Component { customer_visible: node.customer_visible, is_manual: node.is_manual, requires_signoff: node.requires_signoff, + // Sub 13 — sequential enforcement + enforce_sequential: !!node.enforce_sequential, + parallel_start: !!node.parallel_start, }; const result = await rpc("/fp/recipe/node/write", { node_id: node.id, diff --git a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js index 45dd1805..7a6871a8 100644 --- a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js @@ -241,6 +241,7 @@ export class FpSimpleRecipeEditor extends Component { description: "", requires_signoff: false, requires_predecessor_done: false, + parallel_start: false, // Sub 13 — per-step opt-out requires_rack_assignment: false, requires_transition_form: false, tank_ids: [], @@ -289,6 +290,7 @@ export class FpSimpleRecipeEditor extends Component { description: ed.description, requires_signoff: !!ed.requires_signoff, requires_predecessor_done: !!ed.requires_predecessor_done, + parallel_start: !!ed.parallel_start, requires_rack_assignment: !!ed.requires_rack_assignment, requires_transition_form: !!ed.requires_transition_form, tank_ids: (ed.tank_ids || []).map((t) => t.id), diff --git a/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml b/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml index 8c2e3b83..a09aecf2 100644 --- a/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml @@ -350,6 +350,28 @@ t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/> + +
+ + +
+ +
+ + +
diff --git a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml index da0362c8..2e8370b3 100644 --- a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml @@ -412,10 +412,16 @@ t-model="state.libraryEditor.requires_signoff"/> Require QA Sign-off -