Files
Odoo-Modules/fusion_inventory/models/inter_company_transfer.py
gsinghpal e9cf75ee48 changes
2026-03-14 12:04:20 -04:00

317 lines
13 KiB
Python

# -*- 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,
})