Multi-Entity Businesses Run on Intercompany Transactions—Most Odoo Setups Get Them Wrong
The moment a business grows beyond a single legal entity, every internal sale, shared service allocation, and management fee becomes an intercompany transaction. A warehouse subsidiary ships goods to the retail entity. A holding company charges management fees to its operating companies. A shared service center invoices IT costs across three subsidiaries in different countries.
In theory, Odoo 19 handles all of this. Multi-company support is built into the core. Intercompany rules can auto-generate mirror invoices. Payments can synchronize across entities. In practice, most implementations get the configuration half-right—invoices generate on one side but not the other, payments don't reconcile across companies, and month-end consolidation becomes a manual spreadsheet exercise that takes the finance team three days.
This guide walks you through the complete intercompany setup in Odoo 19: how to configure multi-company rules, how auto-invoicing and payment synchronization actually work, how to align charts of accounts for consolidation, how to handle elimination entries, and how to build cross-entity reports that your CFO will actually trust.
Setting Up the Multi-Company Foundation in Odoo 19
Before configuring intercompany rules, your multi-company hierarchy needs to be clean. Intercompany transactions depend on correct company relationships, shared user access, and properly scoped security rules. Skipping this foundation is the #1 reason intercompany setups fail silently.
Company Hierarchy and User Access
Every user involved in intercompany workflows needs access to both companies. The intercompany automation runs under the user's security context—if the user creating an invoice in Company A doesn't have access to Company B, the mirror document won't generate.
# Navigate to: Settings > Users & Companies > Users
# For each user involved in intercompany workflows:
1. Open the user record
2. Under "Allowed Companies", add BOTH entities
- Example: "Acme Holdings Ltd" + "Acme Retail Inc"
3. Set "Default Company" to their primary entity
4. Ensure they have "Billing" or "Invoicing" access
in BOTH companies
# Critical: The intercompany bot user (if configured)
# must have access to ALL companies in the group.Partner Records for Intercompany Entities
Each company in your group must exist as a partner (contact) record in the other company's database. This is how Odoo knows where to create the mirror invoice—it looks up the partner linked to the source company:
| Company | Partner Record In | Purpose |
|---|---|---|
| Acme Holdings Ltd | Acme Retail Inc | Vendor record for receiving mirror bills |
| Acme Retail Inc | Acme Holdings Ltd | Customer record for generating mirror invoices |
| Acme Shared Services | Both entities | Vendor record in both for cost allocations |
The partner record must be linked to the company via the company_id field on res.partner. If you create a partner manually without linking it to the corresponding res.company, Odoo's intercompany engine won't recognize it as a sister entity. Go to Settings > Companies, open the company, and verify the "Partner" field points to the correct contact.
Configuring Intercompany Rules: Auto-Invoice and Payment Synchronization
This is where the real automation lives. Odoo 19's intercompany module provides two core mechanisms: automatic invoice/bill generation and synchronized payments. When configured correctly, posting a customer invoice in Company A automatically creates a vendor bill in Company B—and paying that bill can trigger the corresponding payment registration on the other side.
Enabling Intercompany Transaction Rules
# Step 1: Enable the module
# Navigate to: Settings > General Settings
# Under "Companies" section, enable "Intercompany Transactions"
# Click Save and let the module install
# Step 2: Configure rules per company
# Navigate to: Settings > Companies > [Select Company]
# Open the "Intercompany" tab
# Available rule types:
# ┌─────────────────────────────────────────────────┐
# │ Rule Type │ What It Does │
# ├─────────────────────────────────────────────────┤
# │ Invoice/Bill │ Auto-creates mirror │
# │ │ invoice or bill in the │
# │ │ counterpart company │
# │ │ │
# │ Sales/Purchase │ Auto-creates SO/PO in the │
# │ Order │ counterpart company │
# │ │ │
# │ Synchronized │ Payment in one company │
# │ Payments │ auto-registers in the │
# │ │ other │
# └─────────────────────────────────────────────────┘Auto-Invoice Configuration
The auto-invoice rule is the most commonly used. When Company A posts a customer invoice to Company B (the intercompany partner), Odoo automatically creates a vendor bill in Company B with the matching amounts, tax mappings, and reference numbers.
# Simplified flow of account_inter_company_rules module
# File: odoo/addons/account_inter_company_rules/models/account_move.py
class AccountMove(models.Model):
_inherit = "account.move"
def _post(self, soft=True):
"""Override post to trigger intercompany invoice creation."""
posted = super()._post(soft=soft)
for move in posted:
if move._is_intercompany_transaction():
move._create_intercompany_invoice()
return posted
def _create_intercompany_invoice(self):
"""Create mirror invoice in the counterpart company."""
dest_company = self.partner_id.company_id
# Switch to destination company context
dest_move = self.with_company(dest_company).create(
self._prepare_intercompany_invoice_vals(dest_company)
)
# Link the two invoices for traceability
self.intercompany_move_id = dest_move.id
# Auto-validate if configured
if dest_company.intercompany_auto_validate:
dest_move._post(soft=False)Synchronized Payment Configuration
Payment synchronization closes the loop. Without it, your accountants must manually register payments in both companies every time an intercompany invoice is settled—doubling the reconciliation work:
# For each company in the intercompany group:
# Settings > Companies > [Company] > Intercompany tab
# 1. Check "Synchronize Payments"
# 2. Set the "Intercompany Payment Journal":
# - This journal must exist in BOTH companies
# - Typically a "Miscellaneous" or "Intercompany" journal
# - Use type "General" (not Bank or Cash)
#
# 3. Set the "Intercompany Receivable/Payable Accounts":
# - Receivable: e.g., 131100 - Intercompany Receivable
# - Payable: e.g., 231100 - Intercompany Payable
# - These accounts MUST be reconcilable
#
# Flow when payment is registered:
# Company A pays vendor bill to Company B
# > Odoo auto-creates payment receipt in Company B
# > Both intercompany receivable/payable are reconciled
# > The trail is fully linked via intercompany_payment_idThe "Auto-validate intercompany invoices" checkbox skips the draft stage in the receiving company. This is convenient for high-volume operations, but dangerous if your intercompany pricing has errors. We recommend leaving auto-validate OFF for the first month of any new intercompany relationship. Let your accountants review the mirror bills manually. Once you've confirmed the mappings are correct, turn it on.
Aligning Charts of Accounts Across Companies for Clean Consolidation
Intercompany invoices and payments work at the transaction level. Consolidation works at the account level. If Company A uses account 400000 for revenue and Company B uses 7000 for the same thing (because they're on different localization charts), your consolidated P&L will show two separate revenue lines that should be one. This is where most multi-entity Odoo setups break down at reporting time.
Strategy 1: Shared Chart of Accounts
The cleanest approach—all companies use the same chart of accounts. This works well when all entities operate in the same country or region:
# When creating a new company, assign the same chart template:
# Settings > Companies > Create
# Under "Accounting" tab:
# Chart of Accounts Package: select the SAME template
# as your parent company
# For existing companies that need alignment:
# Option A: Re-install the chart (WARNING: destructive)
# Option B: Manually map accounts (safer)
# Verify account alignment across companies:
from odoo import api, SUPERUSER_ID
env = api.Environment(cr, SUPERUSER_ID, {})
companies = env["res.company"].search([])
for company in companies:
accounts = env["account.account"].with_company(company).search([])
print(f"\n{{company.name}}: {{len(accounts)}} accounts")
for acc in accounts.filtered(lambda a: a.account_type == "income"):
print(f" {{acc.code}} - {{acc.name}}")Strategy 2: Account Mapping Table for Multi-Country Groups
When subsidiaries operate under different local GAAPs (e.g., a US parent with a German subsidiary), you need a mapping table that translates local accounts to group-level reporting accounts:
| Local Account (DE) | Local Name | Group Account | Group Name |
|---|---|---|---|
4000 | Umsatzerlöse | 400000 | Revenue — Goods |
6300 | Sonstige betriebliche Aufwendungen | 620000 | Other Operating Expenses |
1400 | Forderungen aus L&L | 131000 | Trade Receivables |
1600 | Verbindlichkeiten aus L&L | 210000 | Trade Payables |
from odoo import fields, models
class AccountAccount(models.Model):
_inherit = "account.account"
x_group_account_code = fields.Char(
string="Group Account Code",
help="Maps this local account to the group-level "
"chart for consolidated reporting.",
index=True,
)
x_group_account_name = fields.Char(
string="Group Account Name",
)
# Usage in consolidation reports:
# SELECT x_group_account_code, SUM(balance)
# FROM account_move_line
# JOIN account_account ON ...
# GROUP BY x_group_account_code Odoo 19 provides account.account.tag records that can serve as grouping labels across companies. Before building a custom mapping table, check if account tags meet your needs. Tags are natively supported in Odoo's financial reports and can be shared across companies in a multi-company setup. Assign the same tags to equivalent accounts in each entity, then build your consolidated report filtering by tag.
Elimination Entries: Removing Intercompany Balances from Consolidated Financials
Intercompany transactions create balances that exist only between entities in your group. When Company A invoices Company B for $50,000 in management fees, Company A shows $50,000 in revenue and Company B shows $50,000 in expenses. On a consolidated basis, these cancel out—the group didn't earn or spend anything. Elimination entries remove these internal balances so your consolidated financials reflect only external activity.
What Gets Eliminated
| Elimination Type | Company A Entry | Company B Entry | Consolidation Adjustment |
|---|---|---|---|
| Intercompany Revenue/Expense | Revenue $50,000 | Expense $50,000 | DR Revenue $50K / CR Expense $50K |
| Intercompany Receivable/Payable | AR $50,000 | AP $50,000 | DR AP $50K / CR AR $50K |
| Intercompany Inventory Profit | COGS $30K, Revenue $50K | Inventory $50K | Eliminate unrealized profit of $20K |
Automating Elimination Entries in Odoo 19
from odoo import fields, models, api
from odoo.exceptions import UserError
class IntercompanyElimination(models.TransientModel):
_name = "intercompany.elimination.wizard"
_description = "Generate Intercompany Elimination Entries"
date_from = fields.Date(required=True)
date_to = fields.Date(required=True)
elimination_journal_id = fields.Many2one(
"account.journal",
string="Elimination Journal",
required=True,
domain=[("type", "=", "general")],
)
company_ids = fields.Many2many(
"res.company",
string="Companies to Consolidate",
required=True,
)
def action_generate_eliminations(self):
"""Generate elimination entries for intercompany balances."""
self.ensure_one()
ic_accounts = self.env["account.account"].search([
("code", "like", "1311%"), # IC Receivable
])
ic_payable = self.env["account.account"].search([
("code", "like", "2311%"), # IC Payable
])
# Sum intercompany balances across companies
lines = self.env["account.move.line"].search([
("company_id", "in", self.company_ids.ids),
("account_id", "in", (ic_accounts | ic_payable).ids),
("date", ">=", self.date_from),
("date", "<=", self.date_to),
("parent_state", "=", "posted"),
])
total_debit = sum(lines.mapped("debit"))
total_credit = sum(lines.mapped("credit"))
net = total_debit - total_credit
if abs(net) < 0.01:
raise UserError("Intercompany balances already net to zero.")
# Create elimination journal entry
elim_move = self.env["account.move"].create({
"journal_id": self.elimination_journal_id.id,
"date": self.date_to,
"ref": f"IC Elimination {{self.date_from}} to {{self.date_to}}",
"line_ids": [
(0, 0, {
"account_id": ic_accounts[0].id,
"debit": 0,
"credit": abs(net) if net > 0 else 0,
"name": "Eliminate IC Receivable",
}),
(0, 0, {
"account_id": ic_payable[0].id,
"debit": abs(net) if net > 0 else 0,
"credit": 0,
"name": "Eliminate IC Payable",
}),
],
})
elim_move._post(soft=False)
return elim_moveRun elimination entries after all intercompany invoices and payments have been posted for the period. If Company A posts an invoice on March 31st but Company B's auto-generated bill isn't validated until April 2nd, your March elimination will be imbalanced. Set a hard cutoff: all intercompany transactions must be posted by the 3rd business day of the following month, eliminations run on the 4th.
Cross-Entity Reporting: Consolidated P&L, Balance Sheet, and Intercompany Reconciliation
With transactions flowing correctly and elimination entries in place, you need reports that bring everything together. Odoo 19's accounting reports support multi-company views natively, but the default configuration rarely gives you a clean consolidated view out of the box.
Consolidated Financial Reports
# Method 1: Native multi-company report view
# Accounting > Reporting > Profit and Loss
# In the report header, click the company selector
# Select ALL companies in your group
# Odoo merges the data into a single report
# Method 2: Custom SQL view for consolidated reporting
# Useful when you need group-level account mapping
CREATE OR REPLACE VIEW consolidated_trial_balance AS
SELECT
COALESCE(aa.x_group_account_code, aa.code) AS group_code,
COALESCE(aa.x_group_account_name, aa.name) AS group_name,
SUM(aml.debit) AS total_debit,
SUM(aml.credit) AS total_credit,
SUM(aml.balance) AS net_balance,
rc.name AS company_name
FROM account_move_line aml
JOIN account_account aa ON aa.id = aml.account_id
JOIN account_move am ON am.id = aml.move_id
JOIN res_company rc ON rc.id = aml.company_id
WHERE am.state = 'posted'
AND aml.date BETWEEN '2026-01-01' AND '2026-03-31'
GROUP BY
COALESCE(aa.x_group_account_code, aa.code),
COALESCE(aa.x_group_account_name, aa.name),
rc.name
ORDER BY group_code;Intercompany Reconciliation Report
Before running eliminations each month, you need to verify that intercompany balances match across entities. If Company A shows a $50,000 receivable from Company B, Company B should show a $50,000 payable to Company A. Discrepancies mean a transaction was posted in one company but not the other:
-- Find intercompany balance mismatches
WITH ic_balances AS (
SELECT
rc.name AS source_company,
rp.name AS counterparty,
aa.account_type,
SUM(aml.balance) AS balance
FROM account_move_line aml
JOIN account_account aa ON aa.id = aml.account_id
JOIN account_move am ON am.id = aml.move_id
JOIN res_company rc ON rc.id = aml.company_id
JOIN res_partner rp ON rp.id = aml.partner_id
WHERE am.state = 'posted'
AND aa.code LIKE '1311%' OR aa.code LIKE '2311%'
AND rp.company_id IS NOT NULL -- intercompany partner
GROUP BY rc.name, rp.name, aa.account_type
)
SELECT
a.source_company,
a.counterparty,
a.balance AS "A owes/owed",
b.balance AS "B owes/owed",
a.balance + COALESCE(b.balance, 0) AS discrepancy
FROM ic_balances a
LEFT JOIN ic_balances b
ON a.source_company = b.counterparty
AND a.counterparty = b.source_company
WHERE ABS(a.balance + COALESCE(b.balance, 0)) > 0.01
ORDER BY ABS(a.balance + COALESCE(b.balance, 0)) DESC;Schedule the intercompany reconciliation query as a server action with a cron job that runs on the 1st of each month. If discrepancies exceed a threshold (e.g., $100), it sends an email to the finance team with the list of unmatched balances. Catching mismatches early prevents the month-end scramble.
3 Intercompany Configuration Mistakes That Break Month-End Close
Intercompany Accounts That Aren't Marked as Reconcilable
You configure intercompany receivable and payable accounts, transactions flow correctly, and invoices generate on both sides. But when your accountant tries to reconcile the intercompany balances at month-end, the reconciliation wizard shows no matching entries. The accounts exist, the entries are posted, but Odoo won't let you match them. The reason: the accounts weren't created with the "Allow Reconciliation" flag enabled.
Open each intercompany account (receivable and payable) in Accounting > Configuration > Chart of Accounts. Check the "Allow Reconciliation" box. This must be done in every company that participates in intercompany transactions. Without this flag, journal items on these accounts cannot be matched against each other, making automated reconciliation impossible.
Tax Mapping Mismatches on Auto-Generated Mirror Invoices
Company A invoices Company B with 20% VAT. The auto-generated mirror bill in Company B also shows 20% VAT. But Company B is in a different tax jurisdiction where intercompany services are subject to reverse charge—meaning Company B should self-assess the tax, not receive it from Company A. The mirror bill now has the wrong tax treatment, and your VAT returns in both countries are incorrect.
Configure fiscal positions for each intercompany partner. In Company B, create a fiscal position called "Intercompany - Reverse Charge" that maps the incoming 20% VAT to the correct reverse charge tax. Assign this fiscal position to Company A's partner record in Company B. The auto-generated mirror bill will apply the fiscal position automatically, remapping taxes to the correct local treatment.
Currency Conversion Timing Creates Permanent Elimination Differences
Company A (USD) invoices Company B (EUR) for $100,000 on March 15th. Odoo converts this at the March 15th exchange rate: €92,500. By month-end (March 31st), the rate has moved. Company A's books show $100,000 receivable. Company B's books show €92,500 payable, which at the March 31st rate equals $101,200. Your elimination entries are $1,200 out of balance—and this difference grows every month.
Run Odoo's currency revaluation wizard (Accounting > Periodic Processing > Currency Revaluation) in both companies before generating elimination entries. This creates unrealized exchange gain/loss entries that bring both sides to the same rate. The elimination entry then nets cleanly. Include the exchange difference accounts in your consolidation mapping so they appear on the consolidated P&L under "Foreign Exchange Gains/Losses."
What Automated Intercompany Transactions Save Your Finance Team
Intercompany automation isn't an accounting convenience—it's a structural time savings that compounds every month:
Automated mirror invoices and synchronized payments eliminate the manual creation and matching of intercompany entries that typically consume the first week of close.
When invoices and payments auto-generate on both sides, intercompany balances match by design. Your team stops hunting for mismatches in spreadsheets.
Every intercompany transaction is linked via intercompany_move_id. Auditors can trace any elimination entry back to the originating invoice in seconds, not hours.
For a group with 5 entities processing 200 intercompany transactions per month, the manual approach requires roughly 40 hours of accountant time each month for entry creation, cross-referencing, and reconciliation. At a blended cost of $75/hour, that's $3,000/month or $36,000/year in labor—recovered entirely by a one-time Odoo configuration project.
Optimization Metadata
Complete guide to configuring intercompany transactions in Odoo 19. Set up auto-invoicing, payment sync, chart alignment, elimination entries, and consolidated reporting.
1. "Setting Up the Multi-Company Foundation in Odoo 19"
2. "Configuring Intercompany Rules: Auto-Invoice and Payment Synchronization"
3. "Aligning Charts of Accounts Across Companies for Clean Consolidation"
4. "Elimination Entries: Removing Intercompany Balances from Consolidated Financials"
5. "3 Intercompany Configuration Mistakes That Break Month-End Close"