From 4e0b74d7ae82d40a62ef0aee1b8a6cf05e2d43c2 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 3 May 2026 23:39:38 -0400 Subject: [PATCH] =?UTF-8?q?feat(jobs):=20Sub=2014=20=E2=80=94=20configurab?= =?UTF-8?q?le=20workflow=20state=20bar=20(Path=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the generic Draft/Confirmed/In Progress/Done statusbar with a shop-configurable list of plating-specific milestones. Bar advances automatically as recipe steps complete; no manual button clicks. What ships ========== * New model: fp.job.workflow.state Catalog of milestones (name, code, sequence, color, triggers). Triggers can be: - trigger_default_kinds: "receiving,inspect" matches by step.default_kind - trigger_first_step_started: any wet/bake/mask/rack step started - trigger_all_steps_done: every non-cancelled step in done/skipped - block_when_quality_hold: held back while NCR/hold open Plus per-recipe-node override (see below). * Default 7-state seed (data/fp_workflow_state_data.xml): Draft → Confirmed → Received → In Progress → Inspected → Shipped → Done noupdate=1 so per-shop edits survive module upgrade. * Recipe-side trigger field on fusion.plating.process.node: triggers_workflow_state_id (Many2one, optional) Wins over default_kind matching. Lets the recipe author pin a specific step as a milestone trigger even when default_kind isn't set or doesn't match. Exposed in the Recipe Tree Editor properties panel (dropdown sourced from the catalog). * fp.job.workflow_state_id (computed, stored) Iterates the catalog in sequence order; lands at the highest passed milestone. Recomputes on step state / kind / recipe_node / quality hold changes. Replaces fp.job.state on the form's statusbar. * Settings UI: Configuration > Workflow States Standard list+form pages so admins can add / edit / deactivate states. Manager-group write permission, supervisor read. What this does NOT do ===================== * Doesn't drop fp.job.state — that field still drives the internal state machine (button_confirm, action_cancel, etc.). Only the UI statusbar is reassigned. * No migration for existing jobs — they auto-recompute on next read because workflow_state_id is a stored compute with the right api.depends. Existing WH/JOB/00342 will display its current workflow state on next page load. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controllers/recipe_controller.py | 21 +- .../fusion_plating/models/fp_process_node.py | 13 + .../static/src/js/recipe_tree_editor.js | 6 + .../static/src/xml/recipe_tree_editor.xml | 25 ++ .../fusion_plating_jobs/__manifest__.py | 6 +- .../data/fp_workflow_state_data.xml | 79 ++++++ .../fusion_plating_jobs/models/__init__.py | 1 + .../fusion_plating_jobs/models/fp_job.py | 63 +++++ .../models/fp_job_workflow_state.py | 230 ++++++++++++++++++ .../security/ir.model.access.csv | 3 + .../views/fp_job_form_inherit.xml | 10 + .../views/fp_workflow_state_views.xml | 109 +++++++++ 12 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 fusion_plating/fusion_plating_jobs/data/fp_workflow_state_data.xml create mode 100644 fusion_plating/fusion_plating_jobs/models/fp_job_workflow_state.py create mode 100644 fusion_plating/fusion_plating_jobs/views/fp_workflow_state_views.xml diff --git a/fusion_plating/fusion_plating/controllers/recipe_controller.py b/fusion_plating/fusion_plating/controllers/recipe_controller.py index a3f988b6..63f97ac0 100644 --- a/fusion_plating/fusion_plating/controllers/recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/recipe_controller.py @@ -19,11 +19,27 @@ class FpRecipeController(http.Controller): # ------------------------------------------------------------------ @http.route('/fp/recipe/tree', type='jsonrpc', auth='user') def get_tree(self, recipe_id): - """Return the full nested tree for a recipe.""" + """Return the full nested tree for a recipe + the workflow + states catalog for the per-step "Triggers Workflow State" + dropdown in the properties panel (Sub 14). + """ Node = request.env['fusion.plating.process.node'] recipe = Node.browse(int(recipe_id)) if not recipe.exists(): return {'ok': False, 'error': f'Recipe {recipe_id} not found.'} + # Workflow states for the dropdown — runtime-detect the model + # so the tree editor still works on installs without + # fusion_plating_jobs (where the model lives). + workflow_states = [] + WS = request.env.get('fp.job.workflow.state') + if WS is not None: + for ws in WS.search([('active', '=', True)], order='sequence, id'): + workflow_states.append({ + 'id': ws.id, + 'name': ws.name or '', + 'code': ws.code or '', + 'sequence': ws.sequence, + }) return { 'ok': True, 'recipe': { @@ -34,6 +50,7 @@ class FpRecipeController(http.Controller): 'process_type': recipe.process_type_id.name if recipe.process_type_id else '', }, 'tree': recipe.get_tree_data(), + 'workflow_states': workflow_states, } # ------------------------------------------------------------------ @@ -88,6 +105,8 @@ class FpRecipeController(http.Controller): 'requires_signoff', 'opt_in_out', 'sequence', 'version', # Sub 13 — sequential enforcement 'enforce_sequential', 'parallel_start', + # Sub 14 — workflow milestone trigger + 'triggers_workflow_state_id', } 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/models/fp_process_node.py b/fusion_plating/fusion_plating/models/fp_process_node.py index 890d01c3..757e371e 100644 --- a/fusion_plating/fusion_plating/models/fp_process_node.py +++ b/fusion_plating/fusion_plating/models/fp_process_node.py @@ -569,6 +569,19 @@ class FpProcessNode(models.Model): 'enforce_sequential': self.enforce_sequential, 'parallel_start': self.parallel_start, 'requires_predecessor_done': self.requires_predecessor_done, + # Sub 14 — workflow milestone trigger (Many2one or False) + 'triggers_workflow_state_id': ( + self.triggers_workflow_state_id.id + if 'triggers_workflow_state_id' in self._fields + and self.triggers_workflow_state_id + else False + ), + 'triggers_workflow_state_name': ( + self.triggers_workflow_state_id.name + if 'triggers_workflow_state_id' in self._fields + and self.triggers_workflow_state_id + else '' + ), 'version': self.version, 'child_count': len(children), 'opt_in_out': self.opt_in_out or 'disabled', 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 29bd5aea..3dd85f9f 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 @@ -102,6 +102,7 @@ export class RecipeTreeEditor extends Component { this.state = useState({ recipe: null, tree: null, + workflowStates: [], // Sub 14 — populated by loadTree loading: false, saving: false, selectedNodeId: null, @@ -157,6 +158,9 @@ export class RecipeTreeEditor extends Component { if (result && result.ok) { this.state.recipe = result.recipe; this.state.tree = result.tree; + // Sub 14 — workflow states for the per-step trigger + // dropdown in the properties panel. + this.state.workflowStates = result.workflow_states || []; // Auto-expand every node on first load AND auto-expand // any node we haven't seen before (e.g. freshly imported // nodes after a "Import from recipe" run). Nodes the @@ -271,6 +275,8 @@ export class RecipeTreeEditor extends Component { // Sub 13 — sequential enforcement enforce_sequential: !!node.enforce_sequential, parallel_start: !!node.parallel_start, + // Sub 14 — workflow milestone trigger + triggers_workflow_state_id: node.triggers_workflow_state_id || false, }; const result = await rpc("/fp/recipe/node/write", { node_id: node.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 a09aecf2..75cf6d26 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 @@ -374,6 +374,31 @@ + +
+ + + + When this step finishes (or is skipped/cancelled), the + job's status bar advances to the chosen state. Leave + blank to fall back to the default-kind mapping + configured on the workflow state catalog. + +
+