# 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