Files
Odoo-Modules/Fusion Accounting/models/inter_company_rules.py
2026-02-22 01:22:18 -05:00

239 lines
8.8 KiB
Python

# Fusion Accounting - Inter-Company Invoice Synchronization
# Copyright (C) 2026 Nexa Systems Inc. (https://nexasystems.ca)
# Original implementation for the Fusion Accounting module.
#
# When an invoice is posted in Company A to a partner that IS Company B,
# automatically creates a matching bill in Company B (and vice-versa).
import logging
from odoo import api, fields, models, Command, _
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
class FusionInterCompanyRules(models.Model):
"""Extends res.company with inter-company invoice synchronization settings.
When enabled, posting an invoice in one company that targets a partner
linked to another company in the same database will automatically
generate the corresponding counter-document (bill ↔ invoice) in the
target company.
"""
_inherit = 'res.company'
# =====================================================================
# Configuration Fields
# =====================================================================
fusion_intercompany_invoice_enabled = fields.Boolean(
string="Inter-Company Invoice Sync",
default=False,
help="When enabled, posting an invoice/bill to a partner that "
"represents another company will automatically create the "
"corresponding counter-document in that company.",
)
fusion_intercompany_invoice_journal_id = fields.Many2one(
comodel_name='account.journal',
string="Inter-Company Journal",
domain="[('type', 'in', ('sale', 'purchase'))]",
help="Default journal used to create inter-company invoices/bills. "
"If empty, the system will pick the first appropriate journal "
"in the target company.",
)
# =====================================================================
# Helpers
# =====================================================================
def _fusion_get_intercompany_target(self, partner):
"""Return the company record linked to the given partner, if any.
A partner is considered an inter-company partner when it shares the
same ``company_id`` reference or the partner *is* the commercial
entity of another company in the system.
:param partner: res.partner record
:returns: res.company recordset (may be empty)
"""
self.ensure_one()
if not partner:
return self.env['res.company']
target_company = self.env['res.company'].sudo().search([
('partner_id', '=', partner.commercial_partner_id.id),
('id', '!=', self.id),
], limit=1)
return target_company
class FusionInterCompanyAccountMove(models.Model):
"""Extends account.move to trigger inter-company invoice creation on post."""
_inherit = 'account.move'
fusion_intercompany_move_id = fields.Many2one(
comodel_name='account.move',
string="Inter-Company Counter-Document",
copy=False,
readonly=True,
help="The matching invoice or bill that was auto-created in the "
"partner's company.",
)
fusion_intercompany_source_id = fields.Many2one(
comodel_name='account.move',
string="Inter-Company Source Document",
copy=False,
readonly=True,
help="The original invoice or bill in the originating company "
"that triggered the creation of this document.",
)
# ------------------------------------------------------------------
# Post Override
# ------------------------------------------------------------------
def _post(self, soft=True):
"""Override to trigger inter-company document creation after posting."""
posted = super()._post(soft=soft)
for move in posted:
move._fusion_trigger_intercompany_sync()
return posted
def _fusion_trigger_intercompany_sync(self):
"""Check conditions and create the inter-company counter-document."""
self.ensure_one()
# Only applies to customer invoices / vendor bills
if self.move_type not in ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'):
return
company = self.company_id
if not company.fusion_intercompany_invoice_enabled:
return
# Already has a counter-document
if self.fusion_intercompany_move_id:
return
partner = self.partner_id.commercial_partner_id
target_company = company._fusion_get_intercompany_target(partner)
if not target_company:
return
# Target company must also have the feature enabled
if not target_company.fusion_intercompany_invoice_enabled:
return
try:
self._create_intercompany_invoice(target_company)
except Exception as exc:
_logger.warning(
"Fusion Inter-Company: failed to create counter-document "
"for %s (id=%s): %s",
self.name, self.id, exc,
)
# ------------------------------------------------------------------
# Counter-Document Creation
# ------------------------------------------------------------------
_MOVE_TYPE_MAP = {
'out_invoice': 'in_invoice',
'out_refund': 'in_refund',
'in_invoice': 'out_invoice',
'in_refund': 'out_refund',
}
def _create_intercompany_invoice(self, target_company):
"""Create the counter-document in *target_company*.
Maps:
- Customer Invoice → Vendor Bill (and vice-versa)
- Customer Credit Note → Vendor Credit Note
Line items are copied with accounts resolved in the target company's
chart of accounts. Taxes are **not** copied to avoid cross-company
tax configuration issues; the target company's fiscal position and
default taxes will apply instead.
:param target_company: res.company record of the receiving company
"""
self.ensure_one()
target_move_type = self._MOVE_TYPE_MAP.get(self.move_type)
if not target_move_type:
return
# Determine journal in target company
journal = target_company.fusion_intercompany_invoice_journal_id
if not journal or journal.company_id != target_company:
journal_type = 'purchase' if target_move_type.startswith('in_') else 'sale'
journal = self.env['account.journal'].sudo().search([
('company_id', '=', target_company.id),
('type', '=', journal_type),
], limit=1)
if not journal:
_logger.warning(
"Fusion Inter-Company: no %s journal found in company %s",
'purchase' if target_move_type.startswith('in_') else 'sale',
target_company.name,
)
return
# Build the partner reference: the originating company's partner
source_partner = self.company_id.partner_id
# Prepare invoice line values
line_vals = []
for line in self.invoice_line_ids.filtered(lambda l: l.display_type == 'product'):
line_vals.append(Command.create({
'name': line.name or '/',
'quantity': line.quantity,
'price_unit': line.price_unit,
'discount': line.discount,
'product_id': line.product_id.id if line.product_id else False,
'product_uom_id': line.product_uom_id.id if line.product_uom_id else False,
'analytic_distribution': line.analytic_distribution,
}))
if not line_vals:
_logger.info(
"Fusion Inter-Company: no product lines to copy for %s (id=%s)",
self.name, self.id,
)
return
# Create the counter-document in sudo context of target company
move_vals = {
'move_type': target_move_type,
'journal_id': journal.id,
'company_id': target_company.id,
'partner_id': source_partner.id,
'invoice_date': self.invoice_date,
'date': self.date,
'ref': _("IC: %s", self.name),
'narration': self.narration,
'currency_id': self.currency_id.id,
'invoice_line_ids': line_vals,
'fusion_intercompany_source_id': self.id,
}
new_move = self.env['account.move'].sudo().with_company(
target_company
).create(move_vals)
# Link the two documents
self.sudo().write({
'fusion_intercompany_move_id': new_move.id,
})
_logger.info(
"Fusion Inter-Company: created %s (id=%s) in %s from %s (id=%s)",
new_move.name, new_move.id, target_company.name,
self.name, self.id,
)
return new_move