# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import logging from odoo import models, fields, api from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class FusionInterCompanyTransfer(models.Model): _name = 'fusion.inter.company.transfer' _description = 'Inter-Company Inventory Transfer' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc' _rec_name = 'display_name' display_name = fields.Char(compute='_compute_display_name') config_id = fields.Many2one( 'fusion.sync.config', string='Remote Instance', required=True, ondelete='restrict', domain=[('state', '=', 'connected')]) product_id = fields.Many2one( 'product.product', string='Product', required=True, ondelete='restrict') product_mapping_id = fields.Many2one( 'fusion.product.sync.mapping', string='Product Mapping', compute='_compute_product_mapping', store=True) quantity = fields.Float(string='Quantity', required=True, default=1.0) state = fields.Selection([ ('draft', 'Draft'), ('requested', 'Requested'), ('so_created', 'Remote SO Created'), ('po_created', 'Local PO Created'), ('invoiced', 'Invoiced'), ('transferred', 'Transferred'), ('done', 'Done'), ('error', 'Error'), ('cancelled', 'Cancelled'), ], string='Status', default='draft', required=True, tracking=True, index=True) remote_so_id = fields.Integer(string='Remote SO ID', readonly=True) remote_so_name = fields.Char(string='Remote SO Reference', readonly=True) local_po_id = fields.Many2one( 'purchase.order', string='Local PO', readonly=True) remote_invoice_id = fields.Integer(string='Remote Invoice ID', readonly=True) local_bill_id = fields.Many2one( 'account.move', string='Local Vendor Bill', readonly=True) task_id = fields.Many2one( 'fusion.technician.task', string='Delivery Task', readonly=True) warehouse_inventory_id = fields.Many2one( 'fusion.warehouse.inventory', string='Warehouse Record', readonly=True) error_step = fields.Char(string='Failed Step', readonly=True) requested_by = fields.Many2one('res.users', string='Requested By', default=lambda self: self.env.uid) notes = fields.Text(string='Notes') @api.depends('product_id', 'config_id') def _compute_display_name(self): for rec in self: product = rec.product_id.name or 'No Product' remote = rec.config_id.name or 'No Remote' rec.display_name = f'{product} <- {remote}' @api.depends('product_id', 'config_id') def _compute_product_mapping(self): Mapping = self.env['fusion.product.sync.mapping'] for rec in self: if rec.product_id and rec.config_id: mapping = Mapping.search([ ('config_id', '=', rec.config_id.id), ('local_product_id', '=', rec.product_id.product_tmpl_id.id), ], limit=1) rec.product_mapping_id = mapping.id if mapping else False else: rec.product_mapping_id = False # ── Manual Step-by-Step Actions (preserved) ── def action_request(self): for rec in self.filtered(lambda r: r.state == 'draft'): if not rec.product_mapping_id: raise UserError( f'No sync mapping found for {rec.product_id.name} ' f'on {rec.config_id.name}. Sync products first.') rec.state = 'requested' def action_create_remote_so(self): for rec in self.filtered(lambda r: r.state == 'requested'): try: partner_name = ( rec.config_id.local_company_name or rec.env.company.name) remote_so_id, so_name = rec.config_id._create_remote_sale_order( rec.product_mapping_id, rec.quantity, partner_name) rec.write({ 'remote_so_id': remote_so_id, 'remote_so_name': so_name, 'state': 'so_created', }) _logger.info('Remote SO %s created for transfer %s', so_name, rec.display_name) except Exception as e: raise UserError(f'Failed to create remote SO: {e}') def action_create_local_po(self): for rec in self.filtered(lambda r: r.state == 'so_created'): partner = rec.config_id.remote_partner_id if not partner: partner = self.env['res.partner'].search([ ('name', 'ilike', rec.config_id.name), ], limit=1) if not partner: partner = self.env['res.partner'].create({ 'name': rec.config_id.name, 'supplier_rank': 1, }) po = self.env['purchase.order'].create({ 'partner_id': partner.id, 'order_line': [(0, 0, { 'product_id': rec.product_id.id, 'product_qty': rec.quantity, 'price_unit': rec.product_id.standard_price, })], 'origin': f'ICT from {rec.config_id.name} (SO {rec.remote_so_name or rec.remote_so_id})', }) po.button_confirm() rec.write({ 'local_po_id': po.id, 'state': 'po_created', }) _logger.info('Local PO %s created for transfer %s', po.name, rec.display_name) def action_create_invoice(self): for rec in self.filtered(lambda r: r.state == 'po_created'): try: remote_inv_id = rec.config_id._create_remote_invoice(rec.remote_so_id) rec.write({ 'remote_invoice_id': remote_inv_id or 0, 'state': 'invoiced', }) except Exception as e: _logger.warning('Remote invoice creation failed: %s', e) rec.state = 'invoiced' def action_create_vendor_bill(self): """Create a local vendor bill from the PO (draft, for accounting review).""" for rec in self.filtered(lambda r: r.state == 'invoiced' and r.local_po_id): if rec.local_bill_id: continue action = rec.local_po_id.action_create_invoice() if action and action.get('res_id'): rec.local_bill_id = action['res_id'] else: bills = self.env['account.move'].search([ ('purchase_order_count', '>', 0), ('ref', 'ilike', rec.local_po_id.name), ], limit=1, order='id desc') if bills: rec.local_bill_id = bills.id def action_create_delivery_task(self): for rec in self.filtered(lambda r: r.state in ('po_created', 'invoiced')): try: task = self.env['fusion.technician.task'].create({ 'name': f'Warehouse Transfer: {rec.product_id.name} x{rec.quantity}', 'task_type': 'delivery', 'description': ( f'Transfer {rec.quantity}x {rec.product_id.name} ' f'from {rec.config_id.name} shared warehouse.\n' f'Remote SO: {rec.remote_so_name or rec.remote_so_id}\n' f'Local PO: {rec.local_po_id.name if rec.local_po_id else "N/A"}' ), }) rec.task_id = task.id except Exception as e: _logger.warning('Task creation failed (fusion_tasks may not be configured): %s', e) def action_mark_transferred(self): for rec in self.filtered(lambda r: r.state == 'invoiced'): if rec.warehouse_inventory_id: rec.warehouse_inventory_id.action_mark_transferred() rec.state = 'transferred' def action_complete(self): for rec in self.filtered(lambda r: r.state == 'transferred'): rec.state = 'done' def action_cancel(self): for rec in self.filtered(lambda r: r.state not in ('done', 'cancelled')): if rec.warehouse_inventory_id: rec.warehouse_inventory_id.action_release() rec.state = 'cancelled' # ── One-Click Automated Transfer ── def action_execute_transfer(self): """Execute all transfer steps in one click: 1. Create SO on remote 2. Create Invoice on remote 3. Create PO locally 4. Confirm PO 5. Create Vendor Bill (draft) 6. Mark done """ for rec in self: if rec.state not in ('draft', 'requested'): continue if not rec.product_mapping_id: rec.write({ 'state': 'error', 'error_step': 'validation', 'notes': (rec.notes or '') + '\nNo sync mapping found. Sync products first.', }) continue config = rec.config_id partner_name = config.local_company_name or rec.env.company.name # Step 1: Remote SO try: remote_so_id, so_name = config._create_remote_sale_order( rec.product_mapping_id, rec.quantity, partner_name) rec.write({ 'remote_so_id': remote_so_id, 'remote_so_name': so_name, 'state': 'so_created', }) rec.message_post(body=f'Remote SO {so_name} created.') except Exception as e: rec.write({ 'state': 'error', 'error_step': 'remote_so', 'notes': (rec.notes or '') + f'\nFailed at Remote SO: {e}', }) _logger.error('ICT %s: Remote SO failed: %s', rec.id, e) continue # Step 2: Remote Invoice try: remote_inv_id = config._create_remote_invoice(remote_so_id) rec.write({ 'remote_invoice_id': remote_inv_id or 0, }) rec.message_post(body=f'Remote Invoice created (ID: {remote_inv_id}).') except Exception as e: _logger.warning('ICT %s: Remote Invoice failed (non-fatal): %s', rec.id, e) rec.message_post(body=f'Remote Invoice skipped: {e}') # Step 3: Local PO try: partner = config.remote_partner_id if not partner: partner = self.env['res.partner'].search([ ('name', 'ilike', config.name), ], limit=1) if not partner: partner = self.env['res.partner'].create({ 'name': config.name, 'supplier_rank': 1, }) po = self.env['purchase.order'].create({ 'partner_id': partner.id, 'order_line': [(0, 0, { 'product_id': rec.product_id.id, 'product_qty': rec.quantity, 'price_unit': rec.product_id.standard_price, })], 'origin': f'ICT from {config.name} (SO {rec.remote_so_name or rec.remote_so_id})', }) po.button_confirm() rec.write({ 'local_po_id': po.id, 'state': 'po_created', }) rec.message_post(body=f'Local PO {po.name} created and confirmed.') except Exception as e: rec.write({ 'state': 'error', 'error_step': 'local_po', 'notes': (rec.notes or '') + f'\nFailed at Local PO: {e}', }) _logger.error('ICT %s: Local PO failed: %s', rec.id, e) continue # Step 4: Local Vendor Bill (draft) try: action = po.action_create_invoice() if action and action.get('res_id'): rec.local_bill_id = action['res_id'] rec.message_post(body='Local Vendor Bill created (draft).') except Exception as e: _logger.warning('ICT %s: Vendor bill creation failed (non-fatal): %s', rec.id, e) # Step 5: Done rec.write({ 'state': 'done', }) rec.message_post(body='Transfer completed automatically.') _logger.info('ICT %s: One-click transfer completed', rec.id) def action_retry(self): """Reset an errored transfer back to draft for retry.""" for rec in self.filtered(lambda r: r.state == 'error'): rec.write({ 'state': 'draft', 'error_step': False, })