From 9d7b7daf5a4fbde1118e62deefa90a5518efa525 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 23 Apr 2026 08:16:05 -0400 Subject: [PATCH] feat(plating): tree-editor import supports insert-before position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The import feature appended every imported node to the end of the target recipe. That's wrong for the common case — General Processing has Shipping as its last operation, so importing an Electroless Nickel pack should land BEFORE Shipping, not after it. The user would otherwise have to click Move Up dozens of times. Controller: /fp/recipe/node/import_children now accepts insert_before_id: null/missing → append at end (default, unchanged) 0 → insert at the start → insert right before that top-level child Implementation reorders target's top-level children in one pass after Phase 1 creates the copies (placeholder sequence=0). Phase 2 splits existing vs. new, finds the anchor index in the existing list, and reassigns sequences 10/20/30/... across the merged list. Collisions on the old max_seq-based append strategy are eliminated. JS: state.importInsertBefore drives a new "Insert:" dropdown in the toolbar with options: — At the end — (default) — At the start — Before Smoke on entech (3-case): insert-before-middle, insert-at-start, insert-at-end all produce the expected ordering. fusion_plating → 19.0.7.2.0 Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/fusion_plating/__manifest__.py | 2 +- .../controllers/recipe_controller.py | 69 +++++++++++++++---- .../static/src/js/recipe_tree_editor.js | 25 +++++++ .../static/src/xml/recipe_tree_editor.xml | 11 +++ 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index 83279546..2f1e7b68 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating', - 'version': '19.0.7.1.0', + 'version': '19.0.7.2.0', 'category': 'Manufacturing/Plating', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'description': """ diff --git a/fusion_plating/fusion_plating/controllers/recipe_controller.py b/fusion_plating/fusion_plating/controllers/recipe_controller.py index 27cd1bf9..f0d73d3d 100644 --- a/fusion_plating/fusion_plating/controllers/recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/recipe_controller.py @@ -203,13 +203,24 @@ class FpRecipeController(http.Controller): # ------------------------------------------------------------------ @http.route('/fp/recipe/node/import_children', type='jsonrpc', auth='user') def import_children(self, source_recipe_id, target_parent_id, - dedupe_by_name=True): + dedupe_by_name=True, insert_before_id=None): """Copy every top-level child of `source_recipe_id` under `target_parent_id`, preserving the sub-tree structure. - dedupe_by_name: when True, skip any immediate child whose name - already exists under target_parent_id. Useful for importing a - preset without clobbering custom tweaks already made. + Args: + source_recipe_id: recipe to copy children from. + target_parent_id: parent to drop the copied sub-trees under. + dedupe_by_name: when True, skip any immediate child whose name + already exists under target_parent_id. Useful for importing + a preset without clobbering custom tweaks already made. + insert_before_id: controls WHERE the imported nodes land in + the target's top-level ordering. + * None / missing → append to the end (default). + * 0 → insert at the start. + * → insert right before that child id. + Needed because "General Processing" has Shipping as the + LAST operation — importing a plating pack belongs between + Scheduling and Shipping, not after Shipping. Returns: {ok, imported_count, skipped_count} """ @@ -230,7 +241,6 @@ class FpRecipeController(http.Controller): imported = 0 skipped = 0 - max_seq = max((c.sequence for c in target.child_ids), default=0) def _copy_subtree(src_node, new_parent, base_seq): """Deep-copy src_node under new_parent, recursing children. @@ -240,12 +250,6 @@ class FpRecipeController(http.Controller): * strip child_ids (we recurse ourselves) * strip parent_path (Odoo recomputes from parent_id) * force parent_id + sequence to our target values - - The previous copy()-based version sometimes produced a - flattened tree because copy() on a _parent_store model can - leave parent_id pointed at the original source when the - override in copy_vals collides with the field's copy= flag. - copy_data() returns a plain dict — safer. """ [vals] = src_node.copy_data() vals.pop('child_ids', None) @@ -257,17 +261,54 @@ class FpRecipeController(http.Controller): _copy_subtree(child, new_node, i * 10) return new_node - for i, child in enumerate(source.child_ids.sorted('sequence'), 1): + # Phase 1 — create every copied top-level child, tracking their + # ids so we can separate them from the original children when + # reordering below. + new_top_level_ids = [] + for child in source.child_ids.sorted('sequence'): key = (child.name or '').strip().lower() if dedupe_by_name and key and key in existing_names: skipped += 1 continue - max_seq += 10 - _copy_subtree(child, target, max_seq) + # Placeholder sequence; Phase 2 reassigns all top-level seqs. + new_node = _copy_subtree(child, target, 0) + new_top_level_ids.append(new_node.id) imported += 1 if key: existing_names.add(key) + # Phase 2 — compute the final top-level ordering and reassign + # sequences so imported nodes land at the requested position + # instead of always appearing after every existing child. + target.invalidate_recordset(['child_ids']) + all_top = list(target.child_ids.sorted('sequence')) + existing_top = [c for c in all_top if c.id not in new_top_level_ids] + new_nodes = [c for c in all_top if c.id in new_top_level_ids] + + # Resolve the insertion anchor in the EXISTING list. + anchor_idx = len(existing_top) # default: at the end + if insert_before_id is not None: + try: + before_id = int(insert_before_id) + except (TypeError, ValueError): + before_id = None + if before_id == 0: + anchor_idx = 0 + elif before_id: + for idx, node in enumerate(existing_top): + if node.id == before_id: + anchor_idx = idx + break + + final_order = ( + existing_top[:anchor_idx] + + new_nodes + + existing_top[anchor_idx:] + ) + for i, node in enumerate(final_order, 1): + if node.sequence != i * 10: + node.sequence = i * 10 + return { 'ok': True, 'imported_count': imported, diff --git a/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js b/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js index 31fa09d9..fccbb59b 100644 --- a/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js @@ -120,6 +120,11 @@ export class RecipeTreeEditor extends Component { importRecipeOptions: [], importRecipeId: null, importDedupe: true, + // Insertion anchor for the imported nodes. Values: + // "" → append at the end (default) + // "0" → insert at the start + // "" → insert right before that existing top-level child + importInsertBefore: "", importing: false, }); @@ -472,11 +477,21 @@ export class RecipeTreeEditor extends Component { } if (!this._recipeId) return; this.state.importing = true; + // Map the UI anchor to the controller's insert_before_id contract: + // "" → null (append at end) + // "0" → 0 (insert at start) + // "" → int (insert before that top-level child) + const rawBefore = this.state.importInsertBefore; + const insertBeforeId = + rawBefore === "" || rawBefore == null + ? null + : parseInt(rawBefore, 10); try { const result = await rpc("/fp/recipe/node/import_children", { source_recipe_id: parseInt(this.state.importRecipeId, 10), target_parent_id: this._recipeId, dedupe_by_name: !!this.state.importDedupe, + insert_before_id: insertBeforeId, }); if (result && result.ok) { this.notification.add( @@ -485,6 +500,7 @@ export class RecipeTreeEditor extends Component { ); this.state.importOpen = false; this.state.importRecipeId = null; + this.state.importInsertBefore = ""; await this.loadTree(); } else { this.notification.add(result?.error || "Import failed.", { type: "warning" }); @@ -496,6 +512,15 @@ export class RecipeTreeEditor extends Component { } } + // Convenience getter for the XML: the list of top-level children + // currently under the open recipe, for the "Insert before" dropdown. + // Falls back to [] on an unloaded tree. + get topLevelChildren() { + const root = this.state.tree; + if (!root || !root.children) return []; + return root.children; + } + // ---- Navigation --------------------------------------------------------- onBackToList() { diff --git a/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml b/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml index b8426c1f..770e9422 100644 --- a/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml @@ -207,6 +207,17 @@