239 lines
8.8 KiB
Python
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
|