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',
|
||||
'version': '19.0.18.12.2',
|
||||
'version': '19.0.18.12.3',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -24,6 +24,7 @@ _SNAPSHOT_FIELDS = [
|
||||
'voltage_target', 'viscosity_target',
|
||||
'requires_signoff', 'requires_predecessor_done',
|
||||
'parallel_start',
|
||||
'triggers_workflow_state_id', # Sub 14 — workflow milestone trigger
|
||||
'requires_rack_assignment', 'requires_transition_form',
|
||||
'default_kind',
|
||||
]
|
||||
@@ -195,6 +196,15 @@ class SimpleRecipeController(http.Controller):
|
||||
'requires_signoff': tpl.requires_signoff,
|
||||
'requires_predecessor_done': tpl.requires_predecessor_done,
|
||||
'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_transition_form': tpl.requires_transition_form,
|
||||
'tank_ids': [
|
||||
@@ -230,6 +240,7 @@ class SimpleRecipeController(http.Controller):
|
||||
'name', 'code', 'icon', 'default_kind', 'description',
|
||||
'requires_signoff', 'requires_predecessor_done',
|
||||
'parallel_start',
|
||||
'triggers_workflow_state_id', # Sub 14
|
||||
'requires_rack_assignment', 'requires_transition_form',
|
||||
'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
|
||||
@http.route('/fp/simple_recipe/step/insert', type='jsonrpc', auth='user')
|
||||
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. '
|
||||
'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',
|
||||
help='Triggers Rack Parts sub-dialog at runtime (Sub 12b).')
|
||||
requires_transition_form = fields.Boolean(string='Requires Transition Form',
|
||||
|
||||
@@ -50,6 +50,10 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
libraryEditor: null,
|
||||
libraryEditorBusy: false,
|
||||
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;
|
||||
@@ -231,7 +235,8 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
* mirrors the shape returned by `/fp/simple_recipe/library/load` so
|
||||
* the same template renders both create + edit.
|
||||
*/
|
||||
onOpenLibraryCreate() {
|
||||
async onOpenLibraryCreate() {
|
||||
await this._fpEnsureWorkflowStatesLoaded();
|
||||
this.state.libraryEditor = {
|
||||
id: null, // null = create
|
||||
name: "",
|
||||
@@ -242,6 +247,8 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
requires_signoff: false,
|
||||
requires_predecessor_done: false,
|
||||
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_transition_form: false,
|
||||
tank_ids: [],
|
||||
@@ -250,8 +257,28 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
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) {
|
||||
this.state.libraryEditorBusy = true;
|
||||
await this._fpEnsureWorkflowStatesLoaded();
|
||||
const data = await rpc("/fp/simple_recipe/library/load", {
|
||||
template_id: templateId,
|
||||
});
|
||||
@@ -291,6 +318,8 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
requires_signoff: !!ed.requires_signoff,
|
||||
requires_predecessor_done: !!ed.requires_predecessor_done,
|
||||
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_transition_form: !!ed.requires_transition_form,
|
||||
tank_ids: (ed.tank_ids || []).map((t) => t.id),
|
||||
|
||||
@@ -435,6 +435,33 @@
|
||||
</label>
|
||||
</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 ============== -->
|
||||
<div class="o_fp_le_prompts">
|
||||
<div class="o_fp_le_prompts_header">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.8.18.2',
|
||||
'version': '19.0.8.18.3',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="Received"/></h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Identity">
|
||||
<field name="code" placeholder="received"/>
|
||||
@@ -56,27 +57,53 @@
|
||||
<field name="block_when_quality_hold"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Trigger conditions">
|
||||
|
||||
<separator string="Trigger Conditions"/>
|
||||
<group>
|
||||
<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.
|
||||
</group>
|
||||
|
||||
<!-- Help block — full sheet width, alert-info card so
|
||||
the explanation is readable instead of squeezed
|
||||
into a 2-column form layout. -->
|
||||
<div class="alert alert-info mt-3" role="alert">
|
||||
<h6 class="alert-heading mb-2">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
How triggers combine
|
||||
</h6>
|
||||
<p class="mb-2">
|
||||
A state is <strong>"passed"</strong> when
|
||||
<strong>either</strong>:
|
||||
</p>
|
||||
</group>
|
||||
<group string="Notes">
|
||||
<field name="description" nolabel="1"
|
||||
placeholder="What this milestone represents and when it should fire..."/>
|
||||
</group>
|
||||
<ul class="mb-2">
|
||||
<li>
|
||||
The special trigger is true
|
||||
(<code>trigger_first_step_started</code> or
|
||||
<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>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
Reference in New Issue
Block a user