fix(simple-editor): also surface step children of operations

Follow-up to 821e768b. The previous fix flattened sub_process nodes
so all 16 operations of ENP-STEEL-BASIC became visible — but the
Tree Editor also shows the 26 `step` nodes that live under each
operation ("Ready For Blast / Blast", "Soak Clean / Electroclean /
Primary Rinse", etc.). The Simple Editor still hid those, so author
+ Tree Editor still disagreed by 26 rows.

New `_flatten_recipe_nodes(recipe)` helper walks DFS and surfaces
BOTH operations and their step children. Each operation is followed
immediately by its step children in sequence order so the editor
renders them as a contiguous block:

  10. Ready For Steel Line
  11. Cleaner                            [Steel Line]
     ↳ Soak Clean (S-3)                  [Steel Line › Cleaner]
     ↳ Electroclean (S-3)                [Steel Line › Cleaner]
     ↳ Primary Rinse (S-4)               [Steel Line › Cleaner]
  15. Acid Dip (S-5)                     [Steel Line]
     ↳ Primary Rinse (S-6)               [Steel Line › Acid Dip (S-5)]
     ...

Payload additions on each step:
- `node_type`: 'operation' | 'step'
- `is_substep`: True for steps (renders indented)
- `nested_under`: chained path (sub-process › operation for substeps,
  sub-process for nested operations, '' for top-level operations)

UI: substep rows are indented 2.5rem, smaller font, no drag handle,
no numeric position. The "↳" indent glyph and a "[parent operation]"
chip make the parent-child relationship obvious. Substeps are not
draggable to keep the existing reorder semantics simple — Tree Editor
remains the home for structural changes.

Legacy `_flatten_recipe_operations` helper retained for back-compat
(it now delegates by filtering `node.node_type == 'operation'` from
the full walk).

ENP-STEEL-BASIC on entech: Simple Editor now shows 42 rows (was 10
before 821e768b, was 16 after 821e768b) — matches what the Tree
Editor displays exactly.

Tests: 10 total (was 7), 3 new cover the substep surfacing, path
chaining, and is_substep / node_type flags on the payload.

Module: fusion_plating 19.0.20.3.0 → 19.0.20.4.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-19 22:30:00 -04:00
parent 821e768b7e
commit 2142a66bc0
5 changed files with 164 additions and 46 deletions

View File

@@ -63,56 +63,89 @@ class SimpleRecipeController(http.Controller):
def load(self, recipe_id):
recipe = request.env['fusion.plating.process.node'].browse(recipe_id)
recipe.check_access('read')
# 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).
# Tree-Editor-authored recipes carry FOUR node levels:
# recipe → sub_process → operation → step
# The Tree Editor shows all of them. The Simple Editor used to
# only show direct children of the recipe — so for
# ENP-STEEL-BASIC (1 sub_process + 16 operations + 26 step
# nodes), authors saw 10 rows out of 43. Work-order generation
# walked the full tree and emitted operations as fp.job.step
# rows with step-nodes folded in as instruction text.
#
# 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)
# We now walk the full tree depth-first and surface EVERY
# operation and step node, in traversal order, each tagged
# with:
# - `nested_under`: chained sub-process path ("Steel Line",
# "Steel Line Cleaner", etc.)
# - `node_type`: 'operation' or 'step'
# - `is_substep`: True for `step` nodes (renders indented)
#
# The Simple Editor's drag/insert/reorder semantics still
# treat operations as headline rows; substeps are read-only
# by default in the UI but their fields can be edited via the
# existing step_write endpoint (which doesn't care about
# node_type).
flat_nodes = self._flatten_recipe_nodes(recipe)
return {
'recipe': self._recipe_payload(recipe),
'steps': [
dict(self._step_payload(op), nested_under=path)
for op, path in flat_ops
dict(self._step_payload(node),
nested_under=path,
node_type=node.node_type,
is_substep=(node.node_type == 'step'))
for node, path in flat_nodes
],
}
def _flatten_recipe_operations(self, recipe):
"""Walk a recipe tree DFS, return [(operation_node, path_label)].
"""Legacy helper — returns ONLY operations.
`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.
Kept for back-compat with callers and tests that asked for the
operations-only view. Most paths should now use
``_flatten_recipe_nodes`` which also surfaces step children.
"""
return [
(n, p) for n, p in self._flatten_recipe_nodes(recipe)
if n.node_type == 'operation'
]
def _flatten_recipe_nodes(self, recipe):
"""Walk the recipe DFS, return [(node, path_label)].
Surfaces both `operation` and `step` nodes. The traversal order
matches what the Tree Editor displays:
recipe → recurse → operation (emit) → its step children (emit)
recipe → recurse → sub_process → recurse → operation → steps
Step children are emitted IMMEDIATELY after their parent
operation so the editor can render them as a contiguous block.
"""
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.
# Emit step children right after the operation so the
# editor sees: [Op, step, step, NextOp, step, ...].
# The path label for a substep names its parent
# operation, chained from the sub-process if present.
sub_path = (
f"{path} {node.name}" if path else node.name
)
for child in node.child_ids.sorted('sequence'):
if child.node_type == 'step':
out.append((child, sub_path))
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.
# `step` nodes that are direct children of a recipe (rare,
# legacy seed data) are silently dropped — _generate_steps
# has always skipped them.
_walk(recipe, '')
return out