fix(simple-editor): surface operations nested inside sub_process nodes

Bug on ENP-STEEL-BASIC (2026-05-20): authoring used the Tree Editor
to build a recipe with a "Steel Line" sub_process holding 7 nested
operations (Cleaner, Acid Dip, Nickel Strike, E-Nickel Plate, etc.).
The Simple Editor's /fp/simple_recipe/load endpoint only walked
`recipe.child_ids`, so it returned 10 steps. The work order generator
(fp.job._generate_steps) walked the same tree depth-first and emitted
16 steps. Author and operator disagreed about what was in the recipe.

Fix: new `_flatten_recipe_operations(recipe)` helper walks the tree
depth-first, recurses into `recipe` and `sub_process`, emits each
`operation` exactly once, skips `step` children (they're sub-
instructions of operations). Mirrors the WO walker.

Step payload now carries a `nested_under` string — the chained sub-
process name(s) the operation lives inside (empty for top-level).
The Simple Editor XML renders that as a small "↳ Steel Line" badge
next to the step name so the author can see where each row came from
in the tree. Deep nesting chains with ' › ' (e.g. "Outer › Inner").

`step` children of `recipe` itself remain invisible — they were
silently skipped by the WO generator pre-19.0.18.8.0 anyway (only
operation nodes spawn fp.job.step rows). Restoring them here would
contradict that long-standing contract.

Edit/insert/reorder/remove endpoints unchanged: editing a nested
operation's name / description / tanks works (no parent change).
Drag-reorder within sub-process siblings still works. Drag across
sub-process boundaries isn't supported — opens the door for a Tree
Editor follow-up if needed, but the immediate "I can't see my
steps" complaint is resolved.

ENP-STEEL-BASIC on entech now shows all 16 operations in the Simple
Editor (was 10), with the 7 inside Steel Line tagged accordingly.

Tests: 7 new (TestSimpleRecipeFlatten) — flat recipes still work,
nested operations surface with correct path label, sub_process
nodes never appear as editor rows, step children of operations
stay hidden, deep-nested sub_processes chain path labels.

Module: fusion_plating 19.0.20.2.0 → 19.0.20.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-19 22:22:54 -04:00
parent 2645db40a2
commit 821e768b7e
6 changed files with 221 additions and 3 deletions

View File

@@ -63,12 +63,60 @@ class SimpleRecipeController(http.Controller):
def load(self, recipe_id):
recipe = request.env['fusion.plating.process.node'].browse(recipe_id)
recipe.check_access('read')
steps = recipe.child_ids.sorted('sequence')
# A recipe authored in the Tree Editor can have `sub_process`
# nodes that hold more operations underneath. The flat
# `recipe.child_ids` walk hid those — operators saw a partial
# recipe in the Simple Editor even though the work order
# generated the full list (bug surfaced on ENP-STEEL-BASIC,
# 2026-05-20).
#
# Match the WO generator: depth-first, collect every
# `operation` node, recurse into `recipe` / `sub_process`,
# skip `step` children (they're rendered as instructions
# within their parent operation). Each operation gets a
# `nested_under` label so the UI can tell the operator which
# sub-process the row came from.
flat_ops = self._flatten_recipe_operations(recipe)
return {
'recipe': self._recipe_payload(recipe),
'steps': [self._step_payload(s) for s in steps],
'steps': [
dict(self._step_payload(op), nested_under=path)
for op, path in flat_ops
],
}
def _flatten_recipe_operations(self, recipe):
"""Walk a recipe tree DFS, return [(operation_node, path_label)].
`path_label` is the name of the enclosing `sub_process` if the
operation lives inside one, else empty. Used by the Simple
Editor to render a "(in Steel Line)" hint next to the step.
"""
out = []
def _walk(node, path):
if node.node_type == 'operation':
out.append((node, path))
# Operations don't recurse — child `step` nodes are
# the operation's own instructions, not separate
# editor rows.
return
if node.node_type in ('recipe', 'sub_process'):
# Recipes themselves carry no path label; sub_process
# name becomes the path for nested children.
sub_path = (
path if node.node_type == 'recipe'
else (f"{path} {node.name}" if path else node.name)
)
for child in node.child_ids.sorted('sequence'):
_walk(child, sub_path)
# `step` nodes at the top level are legacy — flat 'step'
# children of a recipe were silently skipped by
# _generate_steps pre-19.0.18.8.0; we mirror that here.
_walk(recipe, '')
return out
def _recipe_payload(self, recipe):
return {
'id': recipe.id,