# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. import logging from odoo import http from odoo.http import request _logger = logging.getLogger(__name__) class FpRecipeController(http.Controller): """JSON-RPC endpoints for the process recipe tree editor.""" # ------------------------------------------------------------------ # Read — full tree # ------------------------------------------------------------------ @http.route('/fp/recipe/tree', type='jsonrpc', auth='user') def get_tree(self, recipe_id): """Return the full nested tree for a recipe.""" Node = request.env['fusion.plating.process.node'] recipe = Node.browse(int(recipe_id)) if not recipe.exists(): return {'ok': False, 'error': f'Recipe {recipe_id} not found.'} return { 'ok': True, 'recipe': { 'id': recipe.id, 'name': recipe.name, 'code': recipe.code or '', 'version': recipe.version, 'process_type': recipe.process_type_id.name if recipe.process_type_id else '', }, 'tree': recipe.get_tree_data(), } # ------------------------------------------------------------------ # Create node # ------------------------------------------------------------------ @http.route('/fp/recipe/node/create', type='jsonrpc', auth='user') def create_node(self, parent_id, name, node_type='operation', vals=None): """Create a new child node under parent_id.""" Node = request.env['fusion.plating.process.node'] parent = Node.browse(int(parent_id)) if not parent.exists(): return {'ok': False, 'error': 'Parent node not found.'} # Determine next sequence max_seq = max((c.sequence for c in parent.child_ids), default=0) data = { 'name': name, 'node_type': node_type, 'parent_id': parent.id, 'sequence': max_seq + 10, } if vals: data.update(vals) try: new_node = Node.create(data) _logger.info('Recipe: created node %s (%s) under %s by uid %s', new_node.id, name, parent.id, request.env.uid) return {'ok': True, 'node_id': new_node.id} except Exception as exc: _logger.exception('Recipe create_node failed') return {'ok': False, 'error': str(exc)} # ------------------------------------------------------------------ # Update node # ------------------------------------------------------------------ @http.route('/fp/recipe/node/write', type='jsonrpc', auth='user') def write_node(self, node_id, vals): """Update fields on an existing node.""" Node = request.env['fusion.plating.process.node'] node = Node.browse(int(node_id)) if not node.exists(): return {'ok': False, 'error': 'Node not found.'} # Filter to allowed fields only allowed = { 'name', 'code', 'node_type', 'icon', 'color', 'process_type_id', 'work_center_id', 'description', 'notes', 'estimated_duration', 'auto_complete', 'customer_visible', 'is_manual', 'requires_signoff', 'opt_in_out', 'sequence', 'version', } safe_vals = {k: v for k, v in vals.items() if k in allowed} if not safe_vals: return {'ok': False, 'error': 'No valid fields to update.'} try: node.write(safe_vals) return {'ok': True} except Exception as exc: _logger.exception('Recipe write_node failed') return {'ok': False, 'error': str(exc)} # ------------------------------------------------------------------ # Delete node # ------------------------------------------------------------------ @http.route('/fp/recipe/node/unlink', type='jsonrpc', auth='user') def unlink_node(self, node_id): """Delete a node and all its children (cascade).""" Node = request.env['fusion.plating.process.node'] node = Node.browse(int(node_id)) if not node.exists(): return {'ok': False, 'error': 'Node not found.'} if node.node_type == 'recipe': return {'ok': False, 'error': 'Cannot delete a recipe root from the tree editor. Use the list view.'} try: name = node.name node.unlink() _logger.info('Recipe: deleted node %s (%s) by uid %s', node_id, name, request.env.uid) return {'ok': True} except Exception as exc: _logger.exception('Recipe unlink_node failed') return {'ok': False, 'error': str(exc)} # ------------------------------------------------------------------ # Reorder siblings # ------------------------------------------------------------------ @http.route('/fp/recipe/node/reorder', type='jsonrpc', auth='user') def reorder_nodes(self, node_ids): """Bulk-update sequence for an ordered list of sibling node IDs.""" Node = request.env['fusion.plating.process.node'] try: for idx, nid in enumerate(node_ids): Node.browse(int(nid)).write({'sequence': (idx + 1) * 10}) return {'ok': True} except Exception as exc: _logger.exception('Recipe reorder failed') return {'ok': False, 'error': str(exc)} # ------------------------------------------------------------------ # Move node to new parent # ------------------------------------------------------------------ @http.route('/fp/recipe/node/move', type='jsonrpc', auth='user') def move_node(self, node_id, new_parent_id): """Move a node to a new parent (drag between sub-trees).""" Node = request.env['fusion.plating.process.node'] node = Node.browse(int(node_id)) parent = Node.browse(int(new_parent_id)) if not node.exists() or not parent.exists(): return {'ok': False, 'error': 'Node or parent not found.'} # Prevent moving a recipe root if node.node_type == 'recipe': return {'ok': False, 'error': 'Cannot move a recipe root.'} # Prevent making a node its own descendant if f'/{node.id}/' in (parent.parent_path or ''): return {'ok': False, 'error': 'Cannot move a node under its own descendant.'} try: max_seq = max((c.sequence for c in parent.child_ids), default=0) node.write({ 'parent_id': parent.id, 'sequence': max_seq + 10, }) return {'ok': True} except Exception as exc: _logger.exception('Recipe move_node failed') return {'ok': False, 'error': str(exc)} # ------------------------------------------------------------------ # Duplicate recipe # ------------------------------------------------------------------ @http.route('/fp/recipe/duplicate', type='jsonrpc', auth='user') def duplicate_recipe(self, recipe_id): """Deep-copy an entire recipe tree.""" Node = request.env['fusion.plating.process.node'] recipe = Node.browse(int(recipe_id)) if not recipe.exists(): return {'ok': False, 'error': 'Recipe not found.'} if recipe.node_type != 'recipe': return {'ok': False, 'error': 'Can only duplicate recipe roots.'} try: new_recipe = recipe.copy() return {'ok': True, 'recipe_id': new_recipe.id} except Exception as exc: _logger.exception('Recipe duplicate failed') return {'ok': False, 'error': str(exc)}