feat(simple-editor): node_type bug fix + inline library authoring + back nav
Bucket 1 — Generation bug fix
- post-migrate.py for 19.0.18.8.0 promotes flat 'step' children of
recipes to 'operation' so fp.job._generate_steps() picks them up.
Filter is narrow: only direct children of node_type='recipe' get
flipped, tree-editor sub-steps (parent.node_type='operation') are
untouched. Idempotent. Posts an audit chatter note on each affected
recipe.
- Simple Editor controller hardcodes node_type='operation' on insert
+ snapshot-import path so future recipes start correct.
Bucket 2 — Inline library authoring
- 6 new JSONRPC routes (/fp/simple_recipe/library/load + save +
seed_defaults + input/{add,write,remove}, /fp/simple_recipe/tank/list).
- + New Step button in the right pane opens an inline form with name /
kind / icon / instructions / stations / flags / prompts table.
- Pencil icon on each library row reopens the same form prefilled.
- Step Kind picker leads with 'Generic — no automatic behaviour'.
- 'Seed defaults from kind' calls action_seed_default_inputs server-side
for kinds that have curated default prompts.
Bucket 3 — Back nav
- '← Recipes' button in the header (or '← Part' when opened from
Process Composer) mirrors recipe_tree_editor.js, with
clearBreadcrumbs:true to avoid stack pollution.
Verified on entech: LGPS1104's 19 'step' children now show as
'operation', migration chatter note posted on the recipe, asset cache
busted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,156 @@ class SimpleRecipeController(http.Controller):
|
||||
tpl.unlink()
|
||||
return {'ok': True, 'soft_deleted': False}
|
||||
|
||||
# ---- Inline authoring (Simple Editor right-pane "+ New Step" / pencil) ----
|
||||
#
|
||||
# Endpoints below let a shop foreman create or edit a library template
|
||||
# (with prompts) without leaving the Simple Editor. Manager-grade
|
||||
# features (transition form, advanced time/temp targets, common-audit
|
||||
# seeding) still live on the dedicated form view.
|
||||
|
||||
@http.route('/fp/simple_recipe/library/load', type='jsonrpc', auth='user')
|
||||
def library_load(self, template_id):
|
||||
"""Return the full payload for one library template."""
|
||||
tpl = request.env['fp.step.template'].browse(int(template_id))
|
||||
if not tpl.exists():
|
||||
return {'ok': False, 'error': 'not_found'}
|
||||
tpl.check_access('read')
|
||||
return {'ok': True, 'template': self._library_payload(tpl)}
|
||||
|
||||
def _library_payload(self, tpl):
|
||||
return {
|
||||
'id': tpl.id,
|
||||
'name': tpl.name or '',
|
||||
'code': tpl.code or '',
|
||||
'icon': tpl.icon or 'fa-cog',
|
||||
'default_kind': tpl.default_kind or '',
|
||||
'description': tpl.description or '',
|
||||
'requires_signoff': tpl.requires_signoff,
|
||||
'requires_predecessor_done': tpl.requires_predecessor_done,
|
||||
'requires_rack_assignment': tpl.requires_rack_assignment,
|
||||
'requires_transition_form': tpl.requires_transition_form,
|
||||
'tank_ids': [
|
||||
{'id': t.id, 'name': t.name, 'code': t.code or ''}
|
||||
for t in tpl.tank_ids
|
||||
],
|
||||
'inputs': [
|
||||
{
|
||||
'id': i.id,
|
||||
'name': i.name or '',
|
||||
'input_type': i.input_type or 'text',
|
||||
'target_min': i.target_min or 0.0,
|
||||
'target_max': i.target_max or 0.0,
|
||||
'target_unit': i.target_unit or '',
|
||||
'required': bool(i.required),
|
||||
'sequence': i.sequence or 0,
|
||||
'hint': i.hint or '',
|
||||
'selection_options': i.selection_options or '',
|
||||
}
|
||||
for i in tpl.input_template_ids.sorted('sequence')
|
||||
],
|
||||
}
|
||||
|
||||
@http.route('/fp/simple_recipe/library/save', type='jsonrpc', auth='user')
|
||||
def library_save(self, template_id, vals):
|
||||
"""Upsert: create when template_id is falsy, write otherwise.
|
||||
Returns the full template payload so the OWL component can
|
||||
refresh in one round-trip.
|
||||
"""
|
||||
Tpl = request.env['fp.step.template']
|
||||
# Whitelist — never trust client-provided write_uid / id / etc.
|
||||
allowed = {
|
||||
'name', 'code', 'icon', 'default_kind', 'description',
|
||||
'requires_signoff', 'requires_predecessor_done',
|
||||
'requires_rack_assignment', 'requires_transition_form',
|
||||
'tank_ids',
|
||||
}
|
||||
clean = {k: v for k, v in (vals or {}).items() if k in allowed}
|
||||
# tank_ids comes in as a plain list of ids from the OWL form;
|
||||
# translate into the Odoo (6, 0, ids) command form.
|
||||
if 'tank_ids' in clean:
|
||||
clean['tank_ids'] = [(6, 0, [int(x) for x in clean['tank_ids']])]
|
||||
if template_id:
|
||||
tpl = Tpl.browse(int(template_id))
|
||||
tpl.check_access('write')
|
||||
tpl.write(clean)
|
||||
else:
|
||||
tpl = Tpl.create(clean)
|
||||
return {'ok': True, 'template': self._library_payload(tpl)}
|
||||
|
||||
@http.route('/fp/simple_recipe/library/seed_defaults', type='jsonrpc', auth='user')
|
||||
def library_seed_defaults(self, template_id):
|
||||
"""Run action_seed_default_inputs on this template. Idempotent —
|
||||
only adds prompts whose name doesn't already exist.
|
||||
"""
|
||||
tpl = request.env['fp.step.template'].browse(int(template_id))
|
||||
if not tpl.exists():
|
||||
return {'ok': False, 'error': 'not_found'}
|
||||
tpl.check_access('write')
|
||||
if not tpl.default_kind:
|
||||
return {'ok': False, 'error': 'no_kind',
|
||||
'message': 'Pick a Step Kind first to seed defaults.'}
|
||||
tpl.action_seed_default_inputs()
|
||||
return {'ok': True, 'template': self._library_payload(tpl)}
|
||||
|
||||
@http.route('/fp/simple_recipe/library/input/add', type='jsonrpc', auth='user')
|
||||
def library_input_add(self, template_id, payload):
|
||||
tpl = request.env['fp.step.template'].browse(int(template_id))
|
||||
tpl.check_access('write')
|
||||
Input = request.env['fp.step.template.input']
|
||||
existing_max = max(tpl.input_template_ids.mapped('sequence') or [0])
|
||||
rec = Input.create({
|
||||
'template_id': tpl.id,
|
||||
'name': (payload or {}).get('name') or 'New Prompt',
|
||||
'input_type': (payload or {}).get('input_type') or 'text',
|
||||
'sequence': existing_max + 10,
|
||||
'required': bool((payload or {}).get('required')),
|
||||
})
|
||||
return {'ok': True, 'input_id': rec.id,
|
||||
'template': self._library_payload(tpl)}
|
||||
|
||||
@http.route('/fp/simple_recipe/library/input/write', type='jsonrpc', auth='user')
|
||||
def library_input_write(self, input_id, payload):
|
||||
Input = request.env['fp.step.template.input']
|
||||
rec = Input.browse(int(input_id))
|
||||
if not rec.exists():
|
||||
return {'ok': False, 'error': 'not_found'}
|
||||
rec.template_id.check_access('write')
|
||||
allowed = {
|
||||
'name', 'input_type', 'target_min', 'target_max', 'target_unit',
|
||||
'required', 'sequence', 'selection_options', 'hint',
|
||||
}
|
||||
clean = {k: v for k, v in (payload or {}).items() if k in allowed}
|
||||
if clean:
|
||||
rec.write(clean)
|
||||
return {'ok': True, 'template': self._library_payload(rec.template_id)}
|
||||
|
||||
@http.route('/fp/simple_recipe/library/input/remove', type='jsonrpc', auth='user')
|
||||
def library_input_remove(self, input_id):
|
||||
Input = request.env['fp.step.template.input']
|
||||
rec = Input.browse(int(input_id))
|
||||
if not rec.exists():
|
||||
return {'ok': False, 'error': 'not_found'}
|
||||
tpl = rec.template_id
|
||||
tpl.check_access('write')
|
||||
rec.unlink()
|
||||
return {'ok': True, 'template': self._library_payload(tpl)}
|
||||
|
||||
@http.route('/fp/simple_recipe/tank/list', type='jsonrpc', auth='user')
|
||||
def tank_list(self, query=''):
|
||||
"""Tank picker for the inline library form. Returns active tanks
|
||||
scoped to the current company.
|
||||
"""
|
||||
Tank = request.env['fusion.plating.tank']
|
||||
domain = [('active', '=', True)]
|
||||
if query:
|
||||
domain += ['|', ('name', 'ilike', query), ('code', 'ilike', query)]
|
||||
return {
|
||||
'tanks': [
|
||||
{'id': t.id, 'name': t.name, 'code': t.code or ''}
|
||||
for t in Tank.search(domain, limit=50)
|
||||
],
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ 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):
|
||||
@@ -175,7 +325,10 @@ class SimpleRecipeController(http.Controller):
|
||||
|
||||
new_vals = {
|
||||
'parent_id': recipe.id,
|
||||
'node_type': 'step',
|
||||
# Must be 'operation' — fp.job._generate_steps() only creates
|
||||
# fp.job.step rows for operation nodes. Flat 'step' children
|
||||
# of a recipe were silently skipped pre-19.0.18.8.0.
|
||||
'node_type': 'operation',
|
||||
'sequence': target_seq,
|
||||
}
|
||||
tpl = False
|
||||
@@ -291,7 +444,8 @@ class SimpleRecipeController(http.Controller):
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
new_vals = {
|
||||
'parent_id': target_recipe.id,
|
||||
'node_type': 'step',
|
||||
# See _SNAPSHOT_FIELDS comment — operation, not step.
|
||||
'node_type': 'operation',
|
||||
'sequence': src_node.sequence,
|
||||
'source_template_id': src_node.source_template_id.id or False,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user