feat(jobs): Sub 14 — configurable workflow state bar (Path B)

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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-03 23:39:38 -04:00
parent 4c6bad04c5
commit 4e0b74d7ae
12 changed files with 564 additions and 2 deletions

View File

@@ -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:

View File

@@ -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',

View File

@@ -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,

View File

@@ -374,6 +374,31 @@
</div>
</div>
<!-- Sub 14 — workflow milestone trigger (operation / step nodes) -->
<div class="o_fp_re_field"
t-if="(state.selectedNode.node_type === 'operation' or state.selectedNode.node_type === 'step') and state.workflowStates.length">
<label for="fp_re_workflow_state">Triggers Workflow State</label>
<select id="fp_re_workflow_state"
class="form-select"
t-on-change="(ev) => { state.selectedNode.triggers_workflow_state_id = ev.target.value ? parseInt(ev.target.value, 10) : false; }">
<option value=""
t-att-selected="!state.selectedNode.triggers_workflow_state_id">
— None (use default-kind matching) —
</option>
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
<option t-att-value="ws.id"
t-att-selected="state.selectedNode.triggers_workflow_state_id === ws.id"
t-esc="ws.name"/>
</t>
</select>
<small class="text-muted d-block mt-1">
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.
</small>
</div>
<div class="o_fp_re_field">
<label>Step Usage</label>
<select class="form-select"

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating — Native Jobs',
'version': '19.0.8.17.5',
'version': '19.0.8.18.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.',
@@ -53,6 +53,10 @@ full design rationale and §6.2 of the implementation plan for task list.
'security/legacy_groups.xml',
'security/ir.model.access.csv',
'data/fp_cron_data.xml',
# Sub 14 — workflow state catalog (must load before fp_job_form_inherit
# so the statusbar's m2o has its targets available at view-render time).
'data/fp_workflow_state_data.xml',
'views/fp_workflow_state_views.xml',
'views/res_config_settings_views.xml',
'views/fp_job_step_quick_look_views.xml',
'views/fp_job_form_inherit.xml',

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Sub 14 — Default 7-state workflow milestone catalog.
Shops can edit, deactivate, or add their own states via
Settings → Fusion Plating → Workflow States. These records use
noupdate="1" so per-shop edits aren't blown away on module upgrade.
-->
<odoo>
<data noupdate="1">
<record id="workflow_state_draft" model="fp.job.workflow.state">
<field name="name">Draft</field>
<field name="code">draft</field>
<field name="sequence">10</field>
<field name="color">grey</field>
<field name="is_initial" eval="True"/>
<field name="description">Job created from SO, not yet kicked off. Awaiting setup or start-of-day batch.</field>
</record>
<record id="workflow_state_confirmed" model="fp.job.workflow.state">
<field name="name">Confirmed</field>
<field name="code">confirmed</field>
<field name="sequence">20</field>
<field name="color">blue</field>
<field name="description">SO confirmed, recipe locked, parts on order. Financially committed.</field>
</record>
<record id="workflow_state_received" model="fp.job.workflow.state">
<field name="name">Received</field>
<field name="code">received</field>
<field name="sequence">30</field>
<field name="color">cyan</field>
<field name="trigger_default_kinds">receiving</field>
<field name="description">Parts physically on the floor — production CAN start. Triggered by completion of any step with default_kind='receiving'.</field>
</record>
<record id="workflow_state_in_progress" model="fp.job.workflow.state">
<field name="name">In Progress</field>
<field name="code">in_progress</field>
<field name="sequence">40</field>
<field name="color">yellow</field>
<field name="trigger_first_step_started" eval="True"/>
<field name="description">First wet/production step started. Line is running, real shop time accruing. Triggered by any step with kind in (wet, bake, mask, rack) reaching in_progress or beyond.</field>
</record>
<record id="workflow_state_inspected" model="fp.job.workflow.state">
<field name="name">Inspected</field>
<field name="code">inspected</field>
<field name="sequence">50</field>
<field name="color">green</field>
<field name="trigger_default_kinds">final_inspect</field>
<field name="block_when_quality_hold" eval="True"/>
<field name="description">Final inspection passed AND no open quality hold. Safe to generate CoC + invoice. Blocked while any quality hold is open.</field>
</record>
<record id="workflow_state_shipped" model="fp.job.workflow.state">
<field name="name">Shipped</field>
<field name="code">shipped</field>
<field name="sequence">60</field>
<field name="color">success</field>
<field name="trigger_default_kinds">ship</field>
<field name="description">Shipment confirmed (BOL or carrier pickup). Customer can be notified.</field>
</record>
<record id="workflow_state_done" model="fp.job.workflow.state">
<field name="name">Done</field>
<field name="code">done</field>
<field name="sequence">70</field>
<field name="color">success</field>
<field name="is_terminal" eval="True"/>
<field name="trigger_all_steps_done" eval="True"/>
<field name="description">Every non-cancelled step is in done/skipped state. Audit trail closed.</field>
</record>
</data>
</odoo>

View File

@@ -5,6 +5,7 @@
# Phase 2 of the native plating job model migration. Models are added
# task-by-task in Tasks 2.2 onwards.
from . import fp_job_workflow_state # Sub 14 — must load before fp_job (FK target)
from . import fp_job
from . import fp_job_step
from . import fp_job_node_override

View File

@@ -80,6 +80,46 @@ class FpJob(models.Model):
'idempotency. Cleared post-cutover.',
)
# ------------------------------------------------------------------
# Sub 14 — Configurable workflow state (status bar milestone)
# ------------------------------------------------------------------
# workflow_state_id auto-advances along the highest passed milestone
# in fp.job.workflow.state's sequence order. Replaces the hardcoded
# state Selection on the form's statusbar.
workflow_state_id = fields.Many2one(
'fp.job.workflow.state',
string='Workflow Stage',
compute='_compute_workflow_state_id',
store=True,
readonly=True,
help='Highest workflow milestone this job has passed, computed '
'from step states + per-state trigger conditions. Updates '
'automatically — the operator never sets it.',
)
@api.depends(
'state',
'step_ids',
'step_ids.state',
'step_ids.kind',
'step_ids.recipe_node_id',
'step_ids.recipe_node_id.default_kind',
'step_ids.recipe_node_id.triggers_workflow_state_id',
'quality_hold_count',
)
def _compute_workflow_state_id(self):
WS = self.env['fp.job.workflow.state']
all_states = WS.search([], order='sequence, id')
for job in self:
passed = WS.browse()
for ws in all_states:
if ws._fp_is_passed_for_job(job):
passed = ws
else:
# First non-passed state stops the bar's progress
break
job.workflow_state_id = passed
# ------------------------------------------------------------------
# Smart-button counts (Feature A — operator workflow)
#
@@ -1350,3 +1390,26 @@ class FpJobStep(models.Model):
'migrated from. Used by the migration script for '
'idempotency. Cleared post-cutover.',
)
# ==========================================================================
# Sub 14 — Recipe-side trigger field
# ==========================================================================
# Adds an optional Many2one on every recipe operation node so the recipe
# author can explicitly map "completion of this step triggers workflow
# state X". Wins over the default-kind matching defined on the workflow
# state itself. Lives here (not core) because the target model
# (fp.job.workflow.state) is defined in this module.
class FusionPlatingProcessNodeWorkflow(models.Model):
_inherit = 'fusion.plating.process.node'
triggers_workflow_state_id = fields.Many2one(
'fp.job.workflow.state',
string='Triggers Workflow State',
ondelete='set null',
help='When a job step generated from this recipe node finishes '
'(or is skipped/cancelled), the job advances to this '
'workflow state. Leave blank to fall back to default-kind '
'matching defined on the workflow state catalog.',
)

View File

@@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Sub 14 — Configurable workflow status bar.
Each job carries a workflow_state_id that auto-advances along a
shop-configurable sequence of milestones (Draft → Confirmed → Received
→ In Progress → Inspected → Shipped → Done — by default).
Recipe authors tag specific recipe steps as "this step's completion
triggers workflow state X" via process_node.triggers_workflow_state_id.
The default mapping is by step.default_kind (so out-of-the-box recipes
just work), with per-recipe override on each operation node.
Why this lives in fusion_plating_jobs (not core):
* It depends on fp.job.step which is implemented here
* Recipe-side trigger fields are added via _inherit on
fusion.plating.process.node (also here, in fp_job.py)
The catalog seed lives in data/fp_workflow_state_data.xml and ships
the 7 default milestones. Settings UI lets shops add more.
"""
from odoo import _, api, fields, models
class FpJobWorkflowState(models.Model):
_name = 'fp.job.workflow.state'
_description = 'Fusion Plating — Job Workflow State (status bar milestone)'
_order = 'sequence, id'
_rec_name = 'name'
name = fields.Char(
string='State Name',
required=True,
translate=True,
help='Operator-facing label shown in the job status bar '
'(e.g. "Received", "Inspected", "Shipped").',
)
code = fields.Char(
string='Code',
required=True,
help='Stable identifier — used by code/migrations to reference '
'this state without depending on the (translatable) name. '
'Lowercase snake_case (e.g. "received", "inspected").',
)
sequence = fields.Integer(
string='Sequence',
default=10,
required=True,
help='Position of this state on the bar (left to right). '
'10-spacing convention so authors can insert new states '
'between existing ones without renumbering.',
)
color = fields.Selection(
[
('grey', 'Grey'),
('blue', 'Blue'),
('cyan', 'Cyan'),
('yellow', 'Yellow'),
('orange', 'Orange'),
('green', 'Green'),
('success', 'Success Green'),
('danger', 'Danger Red'),
('purple', 'Purple'),
],
string='Color',
default='grey',
help='Status pill colour on the bar.',
)
is_initial = fields.Boolean(
string='Initial State',
default=False,
help='Marks this as the starting state for new jobs. Only one '
'state should be marked initial.',
)
is_terminal = fields.Boolean(
string='Terminal State',
default=False,
help='Marks this as the final state. The bar stops advancing '
'once a job reaches it. Only one state should be marked '
'terminal.',
)
active = fields.Boolean(default=True)
description = fields.Text(
string='Description',
help='Internal notes on what this milestone represents and '
'when it should fire. Not shown to operators.',
)
# ---- Trigger conditions --------------------------------------------------
#
# A state is "passed" when ALL recipe steps matching its trigger
# conditions are in done/skipped/cancelled. Two ways to define
# which steps trigger:
# 1. trigger_default_kinds — match on recipe step's default_kind
# Selection. Easiest path — covers standard recipes that use
# the curated kind values (receiving, final_inspect, ship, etc.)
# 2. Per-recipe-node override via
# fusion.plating.process.node.triggers_workflow_state_id
# (defined in fp_job.py). Wins over default_kind matching.
trigger_default_kinds = fields.Char(
string='Trigger Default Kinds',
help='Comma-separated list of step.default_kind values. When the '
'last recipe step matching any of these kinds is finished, '
'the state passes. Example: "receiving,inspect" for a '
'"Received" state. Leave blank if you only want to use '
'per-recipe-node overrides.',
)
trigger_first_step_started = fields.Boolean(
string='Trigger on First Step Started',
default=False,
help='Special trigger — passes as soon as the first wet step '
'(or any step with kind not in inspection/admin) starts. '
'Used for the "In Progress" milestone.',
)
trigger_all_steps_done = fields.Boolean(
string='Trigger on All Steps Done',
default=False,
help='Special trigger — passes when every non-cancelled step '
'is in done/skipped state. Used for the "Done" milestone.',
)
block_when_quality_hold = fields.Boolean(
string='Blocked by Quality Hold',
default=False,
help='If True, this state will NOT pass while there is an open '
'quality hold on the job. Used for the "Inspected" '
'milestone — you can finish the inspection step but the '
'state stays at the previous milestone until the NCR clears.',
)
_sql_constraints = [
('fp_workflow_state_code_uniq', 'unique(code)',
'Workflow state code must be unique.'),
]
@api.depends('name', 'code')
def _compute_display_name(self):
for s in self:
s.display_name = '%s [%s]' % (s.name or '', s.code or '')
# ---- Trigger evaluation --------------------------------------------------
def _fp_kinds_set(self):
"""Parse trigger_default_kinds into a set of kind strings."""
self.ensure_one()
if not self.trigger_default_kinds:
return set()
return {
k.strip() for k in self.trigger_default_kinds.split(',')
if k.strip()
}
def _fp_is_passed_for_job(self, job):
"""Return True if this state's trigger conditions are met by
the given fp.job. Called from the job's compute method.
"""
self.ensure_one()
# Initial state — always passed (every job starts here)
if self.is_initial:
return True
Step = self.env['fp.job.step']
steps = job.step_ids
# Special trigger: all steps done
if self.trigger_all_steps_done:
non_cancelled = steps.filtered(lambda s: s.state != 'cancelled')
if not non_cancelled:
return False
return all(s.state in ('done', 'skipped') for s in non_cancelled)
# Special trigger: first wet step started
if self.trigger_first_step_started:
wet_kinds = ('wet', 'bake', 'mask', 'rack')
production_started = any(
s.state in ('in_progress', 'paused', 'done')
and (s.kind in wet_kinds)
for s in steps
)
if not production_started:
return False
# Production milestone — not blocked by quality hold here
return True
# Standard trigger: ALL recipe steps matching the trigger
# (default_kind in our list OR per-node override pointing at
# us) must be in a terminal state.
kinds = self._fp_kinds_set()
matching_steps = steps.filtered(
lambda s: (
# Per-node override wins
(s.recipe_node_id
and s.recipe_node_id.triggers_workflow_state_id
and s.recipe_node_id.triggers_workflow_state_id.id == self.id)
or
# Default-kind match
(kinds and s.recipe_node_id
and s.recipe_node_id.default_kind in kinds)
)
)
if not matching_steps:
# Nothing matches — this state can't pass for this recipe.
# Treat as not-passed so the bar stays at the previous state.
return False
# Every matching step must be terminal
if not all(
s.state in ('done', 'skipped', 'cancelled')
for s in matching_steps
):
return False
# Quality-hold gate (optional)
if self.block_when_quality_hold:
QH = self.env.get('fusion.plating.quality.hold')
if QH is not None:
open_holds = QH.search_count([
('x_fc_job_id', '=', job.id),
('state', 'not in', ('closed', 'cancelled')),
])
if open_holds:
return False
return True

View File

@@ -17,3 +17,6 @@ access_fp_job_step_input_wiz_mgr,fp.job.step.input.wiz.manager,model_fp_job_step
access_fp_job_step_input_wiz_l_op,fp.job.step.input.wiz.l.operator,model_fp_job_step_input_wizard_line,fusion_plating.group_fusion_plating_operator,1,1,1,1
access_fp_job_step_input_wiz_l_sup,fp.job.step.input.wiz.l.supervisor,model_fp_job_step_input_wizard_line,fusion_plating.group_fusion_plating_supervisor,1,1,1,1
access_fp_job_step_input_wiz_l_mgr,fp.job.step.input.wiz.l.manager,model_fp_job_step_input_wizard_line,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_workflow_state_op,fp.workflow.state.operator,model_fp_job_workflow_state,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_workflow_state_sup,fp.workflow.state.supervisor,model_fp_job_workflow_state,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_workflow_state_mgr,fp.workflow.state.manager,model_fp_job_workflow_state,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
17 access_fp_job_step_input_wiz_l_op fp.job.step.input.wiz.l.operator model_fp_job_step_input_wizard_line fusion_plating.group_fusion_plating_operator 1 1 1 1
18 access_fp_job_step_input_wiz_l_sup fp.job.step.input.wiz.l.supervisor model_fp_job_step_input_wizard_line fusion_plating.group_fusion_plating_supervisor 1 1 1 1
19 access_fp_job_step_input_wiz_l_mgr fp.job.step.input.wiz.l.manager model_fp_job_step_input_wizard_line fusion_plating.group_fusion_plating_manager 1 1 1 1
20 access_fp_workflow_state_op fp.workflow.state.operator model_fp_job_workflow_state fusion_plating.group_fusion_plating_operator 1 0 0 0
21 access_fp_workflow_state_sup fp.workflow.state.supervisor model_fp_job_workflow_state fusion_plating.group_fusion_plating_supervisor 1 0 0 0
22 access_fp_workflow_state_mgr fp.workflow.state.manager model_fp_job_workflow_state fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -48,6 +48,16 @@
invisible="state in ('draft', 'cancelled')"/>
</xpath>
<!-- Sub 14 — Replace the generic Draft/Confirmed/In Progress/Done
statusbar with the configurable workflow_state_id bar.
Operators see meaningful plating milestones (Received,
Inspected, Shipped, etc.) instead of generic Odoo states. -->
<xpath expr="//header/field[@name='state']" position="replace">
<field name="workflow_state_id"
widget="statusbar"
options="{'clickable': '0'}"/>
</xpath>
<!-- Surface part / coating / recipe on the header so the
floor knows WHAT they're plating without diving into
Source. The "Reference Product" line in core is just

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Sub 14 — Workflow state catalog UI (admin / Settings).
-->
<odoo>
<!-- ====================== List view ====================== -->
<record id="view_fp_workflow_state_list" model="ir.ui.view">
<field name="name">fp.job.workflow.state.list</field>
<field name="model">fp.job.workflow.state</field>
<field name="arch" type="xml">
<list string="Workflow States" default_order="sequence, id">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="code"/>
<field name="color" widget="badge"
decoration-info="color == 'blue'"
decoration-success="color == 'success' or color == 'green'"
decoration-warning="color == 'yellow' or color == 'orange'"
decoration-danger="color == 'danger'"
decoration-muted="color == 'grey'"/>
<field name="trigger_default_kinds"/>
<field name="trigger_first_step_started"/>
<field name="trigger_all_steps_done"/>
<field name="block_when_quality_hold"/>
<field name="is_initial"/>
<field name="is_terminal"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<!-- ====================== Form view ====================== -->
<record id="view_fp_workflow_state_form" model="ir.ui.view">
<field name="name">fp.job.workflow.state.form</field>
<field name="model">fp.job.workflow.state</field>
<field name="arch" type="xml">
<form string="Workflow State">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="Received"/></h1>
</div>
<group>
<group string="Identity">
<field name="code" placeholder="received"/>
<field name="sequence"/>
<field name="color" widget="badge"/>
<field name="active"/>
</group>
<group string="Lifecycle role">
<field name="is_initial"/>
<field name="is_terminal"/>
<field name="block_when_quality_hold"/>
</group>
</group>
<group string="Trigger conditions">
<field name="trigger_default_kinds"
placeholder="receiving, inspect"/>
<field name="trigger_first_step_started"/>
<field name="trigger_all_steps_done"/>
<p class="text-muted oe_grey mt-2">
<strong>How triggers combine:</strong> a state is "passed"
when EITHER the special trigger is true, OR every
recipe step matching the listed default_kinds (or
tagged via the per-node override on the recipe) is
in done/skipped/cancelled state.
<br/>
<em>block_when_quality_hold</em>: holds back the
advance even if the trigger conditions are met,
until all open quality holds on the job are closed.
</p>
</group>
<group string="Notes">
<field name="description" nolabel="1"
placeholder="What this milestone represents and when it should fire..."/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ====================== Action + Menu ====================== -->
<record id="action_fp_workflow_state" model="ir.actions.act_window">
<field name="name">Workflow States</field>
<field name="res_model">fp.job.workflow.state</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first workflow state.
</p>
<p>
Workflow states are the milestones that appear on the
status bar of every plating job. Each state passes
automatically when its trigger conditions are met
(recipe step kind finishes, all steps done, etc.).
</p>
</field>
</record>
<menuitem id="menu_fp_workflow_state"
name="Workflow States"
parent="fusion_plating.menu_fp_config"
action="action_fp_workflow_state"
sequence="80"/>
</odoo>