Initial commit
This commit is contained in:
238
Fusion Accounting/models/inter_company_rules.py
Normal file
238
Fusion Accounting/models/inter_company_rules.py
Normal file
@@ -0,0 +1,238 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user