changes
This commit is contained in:
316
fusion_inventory/models/inter_company_transfer.py
Normal file
316
fusion_inventory/models/inter_company_transfer.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# -*- 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,
|
||||
})
|
||||
Reference in New Issue
Block a user