feat(jobs): Sub 14 polish — workflow state form layout + Simple Editor field
Two follow-ups on the workflow state work:
1) Form layout
The "How triggers combine" help text was crammed into a 2-column
group, taking ~25% of the available width. Pulled it out of the
group and rendered as a full-width <div class="alert alert-info">
below the trigger fields. Same fix applied to Notes — uses a
<separator> + bare <field> for full sheet width.
2) Simple Recipe Editor support
The trigger field was only exposed in the Tree Editor. Added it
to the Simple Editor's inline library form too:
* fp.step.template.triggers_workflow_state_id (new Many2one) —
per-template default, snapshot-copied to recipe nodes when
dropped into a recipe (added to _SNAPSHOT_FIELDS).
* /fp/simple_recipe/workflow_states/list — new endpoint to feed
the dropdown. Soft-fails when fusion_plating_jobs isn't
installed (returns []).
* Library editor JS — _fpEnsureWorkflowStatesLoaded helper
caches the catalog on first open (create + edit paths both
warm it). Save vals carry the trigger id.
* Library editor XML — dropdown rendered after the flag
checkboxes. Hidden when the catalog is empty so the form
doesn't show a useless "— None —" pick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.18.12.2',
|
'version': '19.0.18.12.3',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ _SNAPSHOT_FIELDS = [
|
|||||||
'voltage_target', 'viscosity_target',
|
'voltage_target', 'viscosity_target',
|
||||||
'requires_signoff', 'requires_predecessor_done',
|
'requires_signoff', 'requires_predecessor_done',
|
||||||
'parallel_start',
|
'parallel_start',
|
||||||
|
'triggers_workflow_state_id', # Sub 14 — workflow milestone trigger
|
||||||
'requires_rack_assignment', 'requires_transition_form',
|
'requires_rack_assignment', 'requires_transition_form',
|
||||||
'default_kind',
|
'default_kind',
|
||||||
]
|
]
|
||||||
@@ -195,6 +196,15 @@ class SimpleRecipeController(http.Controller):
|
|||||||
'requires_signoff': tpl.requires_signoff,
|
'requires_signoff': tpl.requires_signoff,
|
||||||
'requires_predecessor_done': tpl.requires_predecessor_done,
|
'requires_predecessor_done': tpl.requires_predecessor_done,
|
||||||
'parallel_start': tpl.parallel_start,
|
'parallel_start': tpl.parallel_start,
|
||||||
|
# Sub 14 — workflow trigger (id + name for display)
|
||||||
|
'triggers_workflow_state_id': (
|
||||||
|
tpl.triggers_workflow_state_id.id
|
||||||
|
if tpl.triggers_workflow_state_id else False
|
||||||
|
),
|
||||||
|
'triggers_workflow_state_name': (
|
||||||
|
tpl.triggers_workflow_state_id.name
|
||||||
|
if tpl.triggers_workflow_state_id else ''
|
||||||
|
),
|
||||||
'requires_rack_assignment': tpl.requires_rack_assignment,
|
'requires_rack_assignment': tpl.requires_rack_assignment,
|
||||||
'requires_transition_form': tpl.requires_transition_form,
|
'requires_transition_form': tpl.requires_transition_form,
|
||||||
'tank_ids': [
|
'tank_ids': [
|
||||||
@@ -230,6 +240,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
'name', 'code', 'icon', 'default_kind', 'description',
|
'name', 'code', 'icon', 'default_kind', 'description',
|
||||||
'requires_signoff', 'requires_predecessor_done',
|
'requires_signoff', 'requires_predecessor_done',
|
||||||
'parallel_start',
|
'parallel_start',
|
||||||
|
'triggers_workflow_state_id', # Sub 14
|
||||||
'requires_rack_assignment', 'requires_transition_form',
|
'requires_rack_assignment', 'requires_transition_form',
|
||||||
'tank_ids',
|
'tank_ids',
|
||||||
}
|
}
|
||||||
@@ -320,6 +331,34 @@ class SimpleRecipeController(http.Controller):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@http.route('/fp/simple_recipe/workflow_states/list',
|
||||||
|
type='jsonrpc', auth='user')
|
||||||
|
def workflow_states_list(self):
|
||||||
|
"""Sub 14 — workflow-state picker for the inline library form.
|
||||||
|
Returns active states ordered by sequence so the dropdown
|
||||||
|
renders left-to-right matching the status bar.
|
||||||
|
|
||||||
|
Soft-fail when fp.job.workflow.state isn't installed (rare,
|
||||||
|
only when fusion_plating_jobs is missing) — empty list lets the
|
||||||
|
dropdown render disabled instead of throwing.
|
||||||
|
"""
|
||||||
|
WS = request.env.get('fp.job.workflow.state')
|
||||||
|
if WS is None:
|
||||||
|
return {'workflow_states': []}
|
||||||
|
return {
|
||||||
|
'workflow_states': [
|
||||||
|
{
|
||||||
|
'id': ws.id,
|
||||||
|
'name': ws.name or '',
|
||||||
|
'code': ws.code or '',
|
||||||
|
'sequence': ws.sequence,
|
||||||
|
}
|
||||||
|
for ws in WS.search(
|
||||||
|
[('active', '=', True)], order='sequence, id',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------ step
|
# ------------------------------------------------------------------ step
|
||||||
@http.route('/fp/simple_recipe/step/insert', type='jsonrpc', auth='user')
|
@http.route('/fp/simple_recipe/step/insert', type='jsonrpc', auth='user')
|
||||||
def step_insert(self, recipe_id, template_id=False, position=99, vals=None):
|
def step_insert(self, recipe_id, template_id=False, position=99, vals=None):
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ class FpStepTemplate(models.Model):
|
|||||||
'earlier-sequence steps are still in progress (e.g. '
|
'earlier-sequence steps are still in progress (e.g. '
|
||||||
'paperwork that runs alongside production).',
|
'paperwork that runs alongside production).',
|
||||||
)
|
)
|
||||||
|
# Sub 14 — workflow milestone trigger (optional)
|
||||||
|
# The fp.job.workflow.state model lives in fusion_plating_jobs, so
|
||||||
|
# this Many2one resolves at runtime only when that module is loaded.
|
||||||
|
# When the library template is dropped into a recipe, the value is
|
||||||
|
# snapshot-copied to the new process_node via _SNAPSHOT_FIELDS in
|
||||||
|
# simple_recipe_controller.py.
|
||||||
|
triggers_workflow_state_id = fields.Many2one(
|
||||||
|
'fp.job.workflow.state',
|
||||||
|
string='Triggers Workflow State',
|
||||||
|
ondelete='set null',
|
||||||
|
help='Sub 14. When a recipe step generated from this template '
|
||||||
|
'finishes (or is skipped/cancelled), the parent job '
|
||||||
|
'advances to this workflow state. Leave blank to fall '
|
||||||
|
'back to default-kind matching defined on the workflow '
|
||||||
|
'state catalog.',
|
||||||
|
)
|
||||||
requires_rack_assignment = fields.Boolean(string='Requires Rack Assignment',
|
requires_rack_assignment = fields.Boolean(string='Requires Rack Assignment',
|
||||||
help='Triggers Rack Parts sub-dialog at runtime (Sub 12b).')
|
help='Triggers Rack Parts sub-dialog at runtime (Sub 12b).')
|
||||||
requires_transition_form = fields.Boolean(string='Requires Transition Form',
|
requires_transition_form = fields.Boolean(string='Requires Transition Form',
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
libraryEditor: null,
|
libraryEditor: null,
|
||||||
libraryEditorBusy: false,
|
libraryEditorBusy: false,
|
||||||
tankSearchResults: [],
|
tankSearchResults: [],
|
||||||
|
// Sub 14 — workflow-state catalog cache for the inline
|
||||||
|
// library form's "Triggers Workflow State" dropdown. Lazy-
|
||||||
|
// loaded the first time the user opens the library editor.
|
||||||
|
workflowStates: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
this._recipeId = null;
|
this._recipeId = null;
|
||||||
@@ -231,7 +235,8 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
* mirrors the shape returned by `/fp/simple_recipe/library/load` so
|
* mirrors the shape returned by `/fp/simple_recipe/library/load` so
|
||||||
* the same template renders both create + edit.
|
* the same template renders both create + edit.
|
||||||
*/
|
*/
|
||||||
onOpenLibraryCreate() {
|
async onOpenLibraryCreate() {
|
||||||
|
await this._fpEnsureWorkflowStatesLoaded();
|
||||||
this.state.libraryEditor = {
|
this.state.libraryEditor = {
|
||||||
id: null, // null = create
|
id: null, // null = create
|
||||||
name: "",
|
name: "",
|
||||||
@@ -242,6 +247,8 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
requires_signoff: false,
|
requires_signoff: false,
|
||||||
requires_predecessor_done: false,
|
requires_predecessor_done: false,
|
||||||
parallel_start: false, // Sub 13 — per-step opt-out
|
parallel_start: false, // Sub 13 — per-step opt-out
|
||||||
|
triggers_workflow_state_id: false, // Sub 14 — workflow trigger
|
||||||
|
triggers_workflow_state_name: "",
|
||||||
requires_rack_assignment: false,
|
requires_rack_assignment: false,
|
||||||
requires_transition_form: false,
|
requires_transition_form: false,
|
||||||
tank_ids: [],
|
tank_ids: [],
|
||||||
@@ -250,8 +257,28 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
this.state.tankSearchResults = [];
|
this.state.tankSearchResults = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sub 14 — fetch the workflow-state catalog once per editor session,
|
||||||
|
* cache on this.state.workflowStates. Used by both create + edit
|
||||||
|
* flows to populate the "Triggers Workflow State" dropdown.
|
||||||
|
*/
|
||||||
|
async _fpEnsureWorkflowStatesLoaded() {
|
||||||
|
if (this.state.workflowStates && this.state.workflowStates.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await rpc(
|
||||||
|
"/fp/simple_recipe/workflow_states/list", {}
|
||||||
|
);
|
||||||
|
this.state.workflowStates = data.workflow_states || [];
|
||||||
|
} catch (err) {
|
||||||
|
this.state.workflowStates = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async onOpenLibraryEdit(templateId) {
|
async onOpenLibraryEdit(templateId) {
|
||||||
this.state.libraryEditorBusy = true;
|
this.state.libraryEditorBusy = true;
|
||||||
|
await this._fpEnsureWorkflowStatesLoaded();
|
||||||
const data = await rpc("/fp/simple_recipe/library/load", {
|
const data = await rpc("/fp/simple_recipe/library/load", {
|
||||||
template_id: templateId,
|
template_id: templateId,
|
||||||
});
|
});
|
||||||
@@ -291,6 +318,8 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
requires_signoff: !!ed.requires_signoff,
|
requires_signoff: !!ed.requires_signoff,
|
||||||
requires_predecessor_done: !!ed.requires_predecessor_done,
|
requires_predecessor_done: !!ed.requires_predecessor_done,
|
||||||
parallel_start: !!ed.parallel_start,
|
parallel_start: !!ed.parallel_start,
|
||||||
|
// Sub 14 — workflow trigger (Many2one int or false)
|
||||||
|
triggers_workflow_state_id: ed.triggers_workflow_state_id || false,
|
||||||
requires_rack_assignment: !!ed.requires_rack_assignment,
|
requires_rack_assignment: !!ed.requires_rack_assignment,
|
||||||
requires_transition_form: !!ed.requires_transition_form,
|
requires_transition_form: !!ed.requires_transition_form,
|
||||||
tank_ids: (ed.tank_ids || []).map((t) => t.id),
|
tank_ids: (ed.tank_ids || []).map((t) => t.id),
|
||||||
|
|||||||
@@ -435,6 +435,33 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub 14 — workflow milestone trigger dropdown.
|
||||||
|
Hidden when no states exist (e.g. catalog
|
||||||
|
not seeded yet). -->
|
||||||
|
<div class="o_fp_le_field"
|
||||||
|
t-if="state.workflowStates and state.workflowStates.length">
|
||||||
|
<label class="form-label">Triggers Workflow State</label>
|
||||||
|
<select class="form-select"
|
||||||
|
t-on-change="(ev) => { state.libraryEditor.triggers_workflow_state_id = ev.target.value ? parseInt(ev.target.value, 10) : false; }">
|
||||||
|
<option value=""
|
||||||
|
t-att-selected="!state.libraryEditor.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.libraryEditor.triggers_workflow_state_id === ws.id"
|
||||||
|
t-esc="ws.name"/>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted d-block mt-1">
|
||||||
|
When a recipe step generated from this template
|
||||||
|
finishes (or is skipped/cancelled), the parent
|
||||||
|
job advances to the chosen state on its status
|
||||||
|
bar. Leave blank to fall back to default-kind
|
||||||
|
matching configured on the workflow state catalog.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============== PROMPTS ============== -->
|
<!-- ============== PROMPTS ============== -->
|
||||||
<div class="o_fp_le_prompts">
|
<div class="o_fp_le_prompts">
|
||||||
<div class="o_fp_le_prompts_header">
|
<div class="o_fp_le_prompts_header">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.8.18.2',
|
'version': '19.0.8.18.3',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||||
'author': 'Nexa Systems Inc.',
|
'author': 'Nexa Systems Inc.',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
<label for="name"/>
|
<label for="name"/>
|
||||||
<h1><field name="name" placeholder="Received"/></h1>
|
<h1><field name="name" placeholder="Received"/></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<group>
|
<group>
|
||||||
<group string="Identity">
|
<group string="Identity">
|
||||||
<field name="code" placeholder="received"/>
|
<field name="code" placeholder="received"/>
|
||||||
@@ -56,27 +57,53 @@
|
|||||||
<field name="block_when_quality_hold"/>
|
<field name="block_when_quality_hold"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Trigger conditions">
|
|
||||||
|
<separator string="Trigger Conditions"/>
|
||||||
|
<group>
|
||||||
<field name="trigger_default_kinds"
|
<field name="trigger_default_kinds"
|
||||||
placeholder="receiving, inspect"/>
|
placeholder="receiving, inspect"/>
|
||||||
<field name="trigger_first_step_started"/>
|
<field name="trigger_first_step_started"/>
|
||||||
<field name="trigger_all_steps_done"/>
|
<field name="trigger_all_steps_done"/>
|
||||||
<p class="text-muted oe_grey mt-2">
|
</group>
|
||||||
<strong>How triggers combine:</strong> a state is "passed"
|
|
||||||
when EITHER the special trigger is true, OR every
|
<!-- Help block — full sheet width, alert-info card so
|
||||||
recipe step matching the listed default_kinds (or
|
the explanation is readable instead of squeezed
|
||||||
tagged via the per-node override on the recipe) is
|
into a 2-column form layout. -->
|
||||||
in done/skipped/cancelled state.
|
<div class="alert alert-info mt-3" role="alert">
|
||||||
<br/>
|
<h6 class="alert-heading mb-2">
|
||||||
<em>block_when_quality_hold</em>: holds back the
|
<i class="fa fa-info-circle me-2"/>
|
||||||
advance even if the trigger conditions are met,
|
How triggers combine
|
||||||
until all open quality holds on the job are closed.
|
</h6>
|
||||||
|
<p class="mb-2">
|
||||||
|
A state is <strong>"passed"</strong> when
|
||||||
|
<strong>either</strong>:
|
||||||
</p>
|
</p>
|
||||||
</group>
|
<ul class="mb-2">
|
||||||
<group string="Notes">
|
<li>
|
||||||
<field name="description" nolabel="1"
|
The special trigger is true
|
||||||
placeholder="What this milestone represents and when it should fire..."/>
|
(<code>trigger_first_step_started</code> or
|
||||||
</group>
|
<code>trigger_all_steps_done</code>),
|
||||||
|
<strong>OR</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Every recipe step matching the listed
|
||||||
|
<code>trigger_default_kinds</code> (or tagged
|
||||||
|
via the per-node override on the recipe) is
|
||||||
|
in <code>done</code> / <code>skipped</code> /
|
||||||
|
<code>cancelled</code> state.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
<strong>Blocked by Quality Hold:</strong> holds
|
||||||
|
back the advance even if the trigger conditions
|
||||||
|
are met, until all open quality holds on the job
|
||||||
|
are closed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<separator string="Notes"/>
|
||||||
|
<field name="description" nolabel="1"
|
||||||
|
placeholder="What this milestone represents and when it should fire..."/>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
Reference in New Issue
Block a user