fix(simple-editor): stop seed resurrection + add promote/demote + drag substeps

Three bugs reported on 2026-05-20:

1. RESURRECTION. User deletes a substep in the Simple Editor (e.g.
   Soak Clean (S-3) under Cleaner), then on the next -u fusion_plating
   the substep comes back. Root cause: the recipe XML lived in the
   manifest's `data` list with `noupdate="1"`. Odoo's noupdate=1 only
   blocks UPDATE of existing records — when a record's ir.model.data
   row is missing, the loader treats it as "not yet created" and
   re-creates from XML. Every upgrade resurrected every user-deleted
   seed node.

   Fix: pull the recipe XML files out of `data` and load them once
   via post_init_hook → _seed_starter_recipes_once. Sentinel checks
   ir.model.data for each recipe's root xmlid; if present, skip
   loading entirely. Result: deletions are permanent across all
   future upgrades. Existing entech recipes untouched.

   Files affected: fp_recipe_enp_alum_basic, fp_recipe_enp_steel_basic,
   fp_recipe_enp_sp, fp_recipe_general_processing, fp_recipe_anodize,
   fp_recipe_chem_conversion.

2. PROMOTE / DEMOTE. Simple Editor had no way to turn a substep into
   a top-level operation, or to tuck an operation under another as a
   substep. Authors had to delete + re-create. New endpoints:

   * /fp/simple_recipe/step/promote → flips node_type 'step' →
     'operation', re-parents to the recipe (or sub-process) root,
     places right after the old parent operation.
   * /fp/simple_recipe/step/demote → flips 'operation' → 'step',
     re-parents under the preceding operation (or a caller-supplied
     target_op_id). Blocks demoting an operation that has its own
     children, with a helpful message.

   UI: each row in the editor now carries an up-arrow (promote, only
   shown on substeps) and a down-arrow (demote, only shown on
   operations). Confirmation dialog explains what's about to happen.

3. DRAG SUBSTEPS. Last commit (2142a66b) disabled drag on substep
   rows. Operators couldn't reorder substeps within an operation.
   Re-enabled drag on substeps. The step_reorder endpoint now groups
   incoming node_ids by parent_id and renumbers within each parent
   (10, 20, 30…). Cross-parent drag still no-ops on parent change —
   Promote/Demote buttons are the way to move between parents.

Drive-by:
- Added `from odoo import _` to the controller (missing import the
  new endpoints surfaced).
- Edit-panel field wiring audited: all fields visible in the screen
  (Step name, Default instructions, Step Type, Triggers Workflow,
  Parallel Start, QA Sign-off, Collect measurements, Instruction
  Images, custom prompts) persist correctly through step_write or
  dedicated endpoints. No broken wires.

Tests: 15 total in TestSimpleRecipeFlatten (was 10). 5 new cover
promote happy-path, promote reject (non-substep), demote happy-path,
demote block on has_children, and reorder parent-scoping.

Module: fusion_plating 19.0.20.4.0 → 19.0.20.5.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-19 22:53:09 -04:00
parent 2142a66bc0
commit 7c31269691
7 changed files with 398 additions and 13 deletions

View File

@@ -188,6 +188,103 @@ class TestSimpleRecipeFlatten(TransactionCase):
self.assertIn(('Op B', 'Sub-X'), names_with_path)
self.assertIn(('Op A', ''), names_with_path)
def test_promote_turns_substep_into_operation(self):
# Add a substep under op_a, promote it, verify it moved.
sub = self.env['fusion.plating.process.node'].create({
'name': 'Sub1', 'node_type': 'step',
'parent_id': self.op_a.id, 'sequence': 10,
})
from odoo.addons.fusion_plating.controllers.simple_recipe_controller \
import SimpleRecipeController
from unittest.mock import patch
class FakeReq:
env = self.env
path = ('odoo.addons.fusion_plating.controllers.'
'simple_recipe_controller.request')
with patch(path, FakeReq()):
res = SimpleRecipeController().step_promote(sub.id)
self.assertTrue(res['ok'])
sub.invalidate_recordset()
self.assertEqual(sub.node_type, 'operation')
self.assertEqual(sub.parent_id.id, self.recipe.id)
def test_promote_rejects_non_substep(self):
from odoo.addons.fusion_plating.controllers.simple_recipe_controller \
import SimpleRecipeController
from unittest.mock import patch
class FakeReq:
env = self.env
path = ('odoo.addons.fusion_plating.controllers.'
'simple_recipe_controller.request')
with patch(path, FakeReq()):
res = SimpleRecipeController().step_promote(self.op_a.id)
self.assertFalse(res['ok'])
self.assertEqual(res['error'], 'not_a_substep')
def test_demote_turns_operation_into_substep_under_previous(self):
from odoo.addons.fusion_plating.controllers.simple_recipe_controller \
import SimpleRecipeController
from unittest.mock import patch
class FakeReq:
env = self.env
path = ('odoo.addons.fusion_plating.controllers.'
'simple_recipe_controller.request')
# Demote Op D into Sub-X (its preceding operation is op_a at
# the recipe root, but Sub-X is between them — the preceding
# OPERATION sibling at the recipe root is op_a).
with patch(path, FakeReq()):
res = SimpleRecipeController().step_demote(self.op_d.id)
self.assertTrue(res['ok'])
self.op_d.invalidate_recordset()
self.assertEqual(self.op_d.node_type, 'step')
# The preceding operation at the recipe root is op_a (Sub-X is
# not an operation, gets filtered out).
self.assertEqual(self.op_d.parent_id.id, self.op_a.id)
def test_demote_blocks_when_operation_has_children(self):
# op_a gets a substep — now demoting op_a should fail because
# it has children.
self.env['fusion.plating.process.node'].create({
'name': 'A-child', 'node_type': 'step',
'parent_id': self.op_a.id, 'sequence': 10,
})
from odoo.addons.fusion_plating.controllers.simple_recipe_controller \
import SimpleRecipeController
from unittest.mock import patch
class FakeReq:
env = self.env
path = ('odoo.addons.fusion_plating.controllers.'
'simple_recipe_controller.request')
with patch(path, FakeReq()):
res = SimpleRecipeController().step_demote(self.op_a.id)
self.assertFalse(res['ok'])
self.assertEqual(res['error'], 'has_children')
def test_reorder_renumbers_per_parent(self):
# Add two substeps under op_a so reorder has something to swap.
s1 = self.env['fusion.plating.process.node'].create({
'name': 's1', 'node_type': 'step',
'parent_id': self.op_a.id, 'sequence': 10,
})
s2 = self.env['fusion.plating.process.node'].create({
'name': 's2', 'node_type': 'step',
'parent_id': self.op_a.id, 'sequence': 20,
})
from odoo.addons.fusion_plating.controllers.simple_recipe_controller \
import SimpleRecipeController
from unittest.mock import patch
class FakeReq:
env = self.env
path = ('odoo.addons.fusion_plating.controllers.'
'simple_recipe_controller.request')
# Send reversed order — s2 should come out at seq=10, s1 at 20.
with patch(path, FakeReq()):
SimpleRecipeController().step_reorder([s2.id, s1.id])
s1.invalidate_recordset()
s2.invalidate_recordset()
self.assertEqual(s2.sequence, 10)
self.assertEqual(s1.sequence, 20)
def test_deeply_nested_sub_processes_chain_path_labels(self):
# Three levels: recipe → Sub-Outer → Sub-Inner → Op-Deep
outer = self.env['fusion.plating.process.node'].create({