GuideOdoo ConfigurationMarch 13, 2026

Multi-Company Setup in Odoo 19:
Intercompany Rules & Consolidated Reporting

INTRODUCTION

Your Multi-Entity Business Deserves One Source of Truth—Not Five Disconnected Databases

A holding company with three subsidiaries. A manufacturer that sells through a separate retail entity. A services group spanning four countries with a shared back-office. Every one of these structures runs into the same problem: each legal entity needs its own books, but management needs a single consolidated view. The moment you create a second company in Odoo, you inherit a cascade of decisions—who sees what, how intercompany transactions flow, where shared data lives, and how the CFO gets a group-level P&L without spending three days in a spreadsheet.

Odoo 19 handles multi-company natively. Company creation, intercompany rules (auto SO/PO, auto invoice/bill), multi-company record rules, the branch concept, and consolidated financial reports are all built in. But native doesn't mean automatic. We've audited dozens of multi-entity Odoo deployments, and the same configuration gaps appear every time: record rules that leak data between entities, intercompany transactions that generate on one side but not the other, and chart of accounts misalignment that makes consolidated reporting impossible.

This guide covers the complete multi-company architecture in Odoo 19: company creation and hierarchy, intercompany rule configuration, shared vs. company-specific data, multi-company security, branch setup, inter-company journal entries, chart of accounts alignment, user access control, and consolidated financial reporting.

01

Company Creation, Hierarchy, and the Branch Concept in Odoo 19

Every multi-company setup starts with the hierarchy. Odoo 19 uses a parent-child structure where a holding company sits at the top and subsidiaries nest below it. The hierarchy determines data inheritance, default fiscal positions, and which users can switch between entities. Getting this wrong creates data isolation problems that surface months later during the first consolidation attempt.

Creating the Company Hierarchy

Navigate to Settings > Companies and create each legal entity. The critical field is parent_id—it establishes the group structure that drives consolidation and intercompany visibility:

Python — Programmatic company hierarchy setup
from odoo import api, SUPERUSER_ID


def create_company_hierarchy(cr):
    """Create a multi-company hierarchy programmatically."""
    env = api.Environment(cr, SUPERUSER_ID, {})
    Company = env["res.company"]

    # Create holding company (top-level)
    holding = Company.create({
        "name": "Acme Group Holdings",
        "country_id": env.ref("base.us").id,
        "currency_id": env.ref("base.USD").id,
    })

    # Create subsidiaries under the holding
    retail = Company.create({
        "name": "Acme Retail Inc.",
        "parent_id": holding.id,
        "country_id": env.ref("base.us").id,
        "currency_id": env.ref("base.USD").id,
    })

    manufacturing = Company.create({
        "name": "Acme Manufacturing GmbH",
        "parent_id": holding.id,
        "country_id": env.ref("base.de").id,
        "currency_id": env.ref("base.EUR").id,
    })

    services = Company.create({
        "name": "Acme Shared Services Ltd.",
        "parent_id": holding.id,
        "country_id": env.ref("base.gb").id,
        "currency_id": env.ref("base.GBP").id,
    })

    return holding, retail, manufacturing, services

Branches vs. Companies

Odoo 19 distinguishes between companies and branches. A branch is a child entity that shares the parent's chart of accounts, fiscal year, and tax configuration. A company is a fully independent legal entity with its own accounting setup. Use branches for regional offices within the same legal entity; use companies for separate legal entities that file independent tax returns.

AspectCompanyBranch
Chart of AccountsIndependentShared with parent
Tax ConfigurationIndependentShared with parent
Fiscal YearIndependentShared with parent
Intercompany RulesApplicableNot applicable (same entity)
Consolidated ReportingRequires eliminationNative aggregation

User Access Across Companies

Users must be explicitly granted access to each company. The company_ids field on res.users controls which entities appear in the company switcher. The company_id field sets the user's current (active) company context. Intercompany automations run under the triggering user's security context—if the user doesn't have access to the target company, the automation fails silently.

Dedicated Intercompany User

Create a dedicated service user (e.g., "Intercompany Bot") with access to all companies and the minimum required permissions in each entity. Assign this user as the intercompany automation actor in Settings > Companies > Intercompany. This avoids failures when individual employees change roles or lose access to a subsidiary. The bot user should have Billing access in all companies and be excluded from password rotation policies.

02

Intercompany Rules: Auto SO/PO and Auto Invoice/Bill Generation

Intercompany rules are the automation engine that eliminates manual double-entry between entities. When Company A sells to Company B, you don't want an accountant creating the sale order in A and then switching context to manually create the purchase order in B. Odoo 19 provides two intercompany rule types: Sales/Purchase Order synchronization and Invoice/Bill mirroring. Most multi-entity setups need both, configured per company pair.

Configuring Auto SO/PO Rules

The auto SO/PO rule is ideal for manufacturing and distribution groups. When a sale order is confirmed in Company A with Company B as the customer, Odoo automatically creates a matching purchase order in Company B:

Python — Intercompany SO/PO automation under the hood
# Simplified flow from inter_company_rules module
# File: odoo/addons/inter_company_rules/models/sale_order.py

from odoo import models, _
from odoo.exceptions import UserError


class SaleOrder(models.Model):
    _inherit = "sale.order"

    def action_confirm(self):
        """Override confirm to trigger intercompany PO creation."""
        res = super().action_confirm()
        for order in self:
            partner = order.partner_id
            dest_company = partner.company_id
            if not dest_company or dest_company == order.company_id:
                continue
            # Check if intercompany rules are enabled
            rule = dest_company.intercompany_transaction_type
            if rule in ("sale_purchase", "sale"):
                order._create_intercompany_purchase_order(
                    dest_company
                )
        return res

    def _create_intercompany_purchase_order(self, dest_company):
        """Generate PO in the destination company."""
        PurchaseOrder = self.env["purchase.order"].with_company(
            dest_company
        )
        po_vals = {
            "partner_id": self.company_id.partner_id.id,
            "company_id": dest_company.id,
            "origin": self.name,
            "order_line": [
                (0, 0, {
                    "product_id": line.product_id.id,
                    "product_qty": line.product_uom_qty,
                    "price_unit": line.price_unit,
                    "name": line.name,
                })
                for line in self.order_line
            ],
        }
        po = PurchaseOrder.sudo().create(po_vals)
        # Auto-confirm if configured
        if dest_company.intercompany_auto_validate:
            po.button_confirm()

Shared vs. Company-Specific Data

One of the most common configuration mistakes is misunderstanding which records are shared across companies and which are company-specific. Odoo 19 uses the company_id field to scope data. Records without a company_id are visible to all companies:

Record TypeShared / SpecificWhy It Matters
Products (product.template)Shared (no company_id)All entities see the same product catalog
Partners (res.partner)Shared by defaultCustomer/vendor records visible across entities
Chart of AccountsCompany-specificEach entity has its own account structure
Journal EntriesCompany-specificFinancial data is strictly isolated
PricelistsCompany-specificEach entity can have different pricing
WarehousesCompany-specificStock is isolated per legal entity
Product Variants and Company Scope

While product.template is shared, the accounting properties on products (income account, expense account, supplier taxes) are company-specific. They're stored as ir.property records scoped to each company. If you create a product in Company A and set its income account, Company B won't inherit that mapping. You must configure accounting properties for each product in each company, or use a server action to propagate defaults.

03

Multi-Company Record Rules: Preventing Data Leaks Between Entities

Record rules are the security backbone of any multi-company Odoo deployment. They control which records a user can see based on their current company context. Odoo 19 ships with default multi-company record rules for most models, but custom modules often forget to add them—creating data visibility holes where users in Company A can see Company B's invoices, sale orders, or inventory movements.

How Default Multi-Company Rules Work

XML — Standard multi-company record rule structure
<!-- Default multi-company rule for account.move (invoices) -->
<record id="account_move_comp_rule" model="ir.rule">
    <field name="name">Account Move: Multi-Company</field>
    <field name="model_id" ref="account.model_account_move"/>
    <field name="domain_force">
        ['|', ('company_id', '=', False),
              ('company_id', 'in', company_ids)]
    </field>
    <field name="global" eval="True"/>
</record>

<!-- Custom rule for a custom model -->
<record id="custom_request_comp_rule" model="ir.rule">
    <field name="name">Custom Request: Multi-Company</field>
    <field name="model_id" ref="model_custom_service_request"/>
    <field name="domain_force">
        ['|', ('company_id', '=', False),
              ('company_id', 'in', company_ids)]
    </field>
    <field name="global" eval="True"/>
</record>

The domain ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] is the standard pattern. It says: show records that either have no company (shared data) or belong to one of the user's allowed companies. The company_ids variable is automatically injected by Odoo's security engine and reflects the companies listed in the user's Allowed Companies field.

Auditing Record Rules Across Custom Modules

Python — Script to detect models missing multi-company rules
from odoo import api, SUPERUSER_ID


def audit_multi_company_rules(cr):
    """Find models with company_id but no multi-company rule."""
    env = api.Environment(cr, SUPERUSER_ID, {})
    IrModel = env["ir.model"]
    IrRule = env["ir.rule"]

    # Get all models that have a company_id field
    models_with_company = IrModel.search([]).filtered(
        lambda m: "company_id" in env[m.model]._fields
    )

    # Get all models that have at least one global rule
    # referencing company_ids
    rules = IrRule.search([("global", "=", True)])
    protected_models = set(rules.mapped("model_id.model"))

    # Find the gaps
    unprotected = []
    for model in models_with_company:
        if model.model not in protected_models:
            unprotected.append(model.model)

    if unprotected:
        print("Models with company_id but NO record rule:")
        for m in sorted(unprotected):
            print(f"  - {{m}}")
    else:
        print("All models with company_id have record rules.")

    return unprotected
Record Rules on Related Fields

If your custom model inherits company_id through a related field (e.g., company_id = fields.Many2one(related='order_id.company_id')), the multi-company rule still applies—but Odoo evaluates it against the stored value. If the related field is not stored (store=False), the record rule cannot filter on it efficiently and may cause performance issues or incorrect results. Always ensure company_id is stored on models that need multi-company isolation.

04

Consolidated Financial Reports: Chart Alignment and Group-Level P&L

Consolidated reporting is where all the upstream configuration either pays off or collapses. If your company hierarchy is clean, intercompany rules generate matching transactions on both sides, and your charts of accounts are aligned—consolidation is straightforward. If any of those pieces are wrong, your finance team spends three days every month reconciling spreadsheets instead of analyzing results.

Chart of Accounts Alignment Strategy

For consolidated reporting to work, equivalent accounts across entities must map to the same group-level line items. Odoo 19 provides account groups and account tags as native mapping tools. The cleanest approach depends on whether your entities share the same localization:

ScenarioStrategyEffort
All entities same countryShared chart of accountsLow — install same chart in each company
Multi-country, similar GAAPAccount tags for group mappingMedium — tag each account in each entity
Multi-country, different GAAPCustom group account mapping fieldHigh — requires custom module
XML — Account tags for cross-company consolidation mapping
<!-- Define group-level consolidation tags -->
<record id="tag_group_revenue" model="account.account.tag">
    <field name="name">GROUP:Revenue</field>
    <field name="applicability">accounts</field>
    <field name="country_id" eval="False"/>
</record>

<record id="tag_group_cogs" model="account.account.tag">
    <field name="name">GROUP:Cost of Goods Sold</field>
    <field name="applicability">accounts</field>
    <field name="country_id" eval="False"/>
</record>

<record id="tag_group_opex" model="account.account.tag">
    <field name="name">GROUP:Operating Expenses</field>
    <field name="applicability">accounts</field>
    <field name="country_id" eval="False"/>
</record>

<record id="tag_group_ic_receivable" model="account.account.tag">
    <field name="name">GROUP:IC Receivable</field>
    <field name="applicability">accounts</field>
    <field name="country_id" eval="False"/>
</record>

<record id="tag_group_ic_payable" model="account.account.tag">
    <field name="name">GROUP:IC Payable</field>
    <field name="applicability">accounts</field>
    <field name="country_id" eval="False"/>
</record>

<!-- Assign tags to US entity accounts -->
<function model="account.account"
         name="write"
         eval="[ref('l10n_us.400000'), {'tag_ids': [(4, ref('tag_group_revenue'))]}]"/>

<!-- Assign same tag to DE entity equivalent -->
<function model="account.account"
         name="write"
         eval="[ref('l10n_de_skr03.4000'), {'tag_ids': [(4, ref('tag_group_revenue'))]}]"/>

Running Consolidated Reports

Odoo 19's native accounting reports support multi-company selection. Navigate to Accounting > Reporting > Profit and Loss and select all entities in the company selector. Odoo merges data into a single report, aggregating by account code. When accounts share the same code across entities (shared chart), the aggregation is automatic. When codes differ (multi-country), the account tag approach groups them correctly in the report engine.

For more granular control, build a custom consolidation report using Odoo's account.report framework. This lets you define group-level line items that aggregate tagged accounts across all entities, apply elimination filters, and present the result with drill-down capability. The custom report approach is essential when your group structure includes sub-consolidations—for example, consolidating a European sub-group before rolling it into the global group.

Consolidated Trial Balance via SQL View

For finance teams that need ad-hoc analysis beyond Odoo's report builder, a SQL view provides direct access to consolidated data. This view groups by account tag rather than account code, automatically handling cross-country chart differences:

SQL — Consolidated trial balance view grouped by account tag
CREATE OR REPLACE VIEW v_consolidated_trial_balance AS
SELECT
    aat.name AS group_line,
    rc.name AS company_name,
    SUM(aml.debit) AS total_debit,
    SUM(aml.credit) AS total_credit,
    SUM(aml.balance) AS net_balance
FROM account_move_line aml
JOIN account_move am ON am.id = aml.move_id
JOIN account_account aa ON aa.id = aml.account_id
JOIN account_account_account_tag aatr
    ON aatr.account_account_id = aa.id
JOIN account_account_tag aat
    ON aat.id = aatr.account_account_tag_id
JOIN res_company rc ON rc.id = aml.company_id
WHERE am.state = 'posted'
  AND aat.name LIKE 'GROUP:%'
GROUP BY aat.name, rc.name
ORDER BY aat.name, rc.name;
Elimination Before Consolidation

The native multi-company report view does not automatically eliminate intercompany balances. It simply sums all transactions across selected companies. If Company A invoiced Company B for $100,000 in management fees, the consolidated P&L will overstate both revenue and expenses by $100,000. You must post elimination journal entries before running consolidated reports, or build a custom report that filters out intercompany transactions by partner or account tag.

05

Inter-Company Journal Entries: Elimination Workflow and Reconciliation

Inter-company journal entries serve two purposes: they record the financial impact of intercompany transactions within each entity, and they provide the basis for elimination entries during consolidation. Every intercompany invoice creates a receivable in one entity and a payable in the other. At consolidation time, these must net to zero—otherwise your group balance sheet is inflated.

Setting Up Intercompany Accounts and Journals

Each company needs dedicated intercompany accounts and a journal for recording intercompany transactions. These accounts must be marked as reconcilable so that balances can be matched and eliminated:

Python — Setting up intercompany accounts across all entities
from odoo import api, SUPERUSER_ID


def setup_intercompany_accounts(cr):
    """Create IC accounts and journals in all companies."""
    env = api.Environment(cr, SUPERUSER_ID, {})
    companies = env["res.company"].search([])

    for company in companies:
        AccountAccount = env["account.account"].with_company(company)
        AccountJournal = env["account.journal"].with_company(company)

        # Intercompany Receivable
        ic_recv = AccountAccount.create({
            "code": "131100",
            "name": "Intercompany Receivable",
            "account_type": "asset_receivable",
            "reconcile": True,  # CRITICAL for elimination
            "company_id": company.id,
        })

        # Intercompany Payable
        ic_pay = AccountAccount.create({
            "code": "231100",
            "name": "Intercompany Payable",
            "account_type": "liability_payable",
            "reconcile": True,
            "company_id": company.id,
        })

        # Intercompany Journal
        ic_journal = AccountJournal.create({
            "name": "Intercompany",
            "code": "ICJ",
            "type": "general",
            "company_id": company.id,
            "default_account_id": ic_recv.id,
        })

        print(
            f"{{company.name}}: IC Receivable {{ic_recv.code}}, "
            f"IC Payable {{ic_pay.code}}, "
            f"Journal {{ic_journal.code}}"
        )

How Intercompany Journal Entries Flow

When Company A posts a customer invoice to Company B, the following journal entries are created automatically (assuming intercompany rules are active):

EntityAccountDebitCredit
Company A131100 — IC Receivable$50,000
Company A400000 — Revenue$50,000
Company B (auto-generated)620000 — Operating Expenses$50,000
Company B (auto-generated)231100 — IC Payable$50,000

At consolidation, the elimination entry reverses the IC Receivable and IC Payable (they net to zero within the group) and removes the IC Revenue and IC Expense (the group didn't earn or spend anything externally). The net effect on consolidated financials: zero.

Monthly Elimination Workflow

The elimination workflow runs at each month-end close. The sequence matters—posting eliminations before all intercompany transactions are validated creates imbalances that take hours to trace:

  • Day 1–3 of close: Ensure all intercompany invoices and bills are posted in both entities. Run the intercompany reconciliation check to find mismatches.
  • Day 3: Register and synchronize all intercompany payments. Verify that IC receivable and IC payable accounts reconcile to zero for settled transactions.
  • Day 4: Run currency revaluation in all entities to align FX rates across the group.
  • Day 5: Post elimination journal entries for outstanding IC balances, IC revenue/expense, and unrealized intercompany profit on inventory.
  • Day 5: Generate consolidated P&L, Balance Sheet, and Cash Flow. Verify group-level figures against management expectations.
Reversal Entries for Clean Periods

Always create elimination entries with automatic reversal on the first day of the next period. This ensures each month starts clean. In Odoo 19, set the auto_reverse field to True and reverse_date to the first day of the next month when creating the elimination journal entry. Without reversal, your elimination entries accumulate and distort the next period's consolidated opening balances.

06

4 Multi-Company Configuration Mistakes That Break Your Group Reporting

1

Partner Records Not Linked to Their Company Entity

You create three companies and configure intercompany rules. When you post an invoice from Company A to Company B, nothing happens—no mirror bill, no error message, just silence. The intercompany engine checks whether the invoice partner's company_id points to another company in the group. If the partner record isn't linked to its corresponding res.company, the engine doesn't recognize it as an intercompany transaction.

Our Fix

Open Settings > Companies for each entity and verify the "Partner" field on the company form. This links the company to its res.partner record. Then verify that this partner record has company_id set correctly. Run a quick check: self.env['res.company'].search([]).mapped(lambda c: (c.name, c.partner_id.name, c.partner_id.company_id.name))—all three values should be consistent.

2

Intercompany Automation User Lacks Access to the Target Company

Intercompany invoice mirroring works perfectly for two months, then stops after an HR change. The user who originally configured the automation was reassigned and lost access to one subsidiary. Odoo's intercompany engine runs under the context of the user who posts the source document. When that user doesn't have the target company in their company_ids, the mirror document creation fails silently—no error, no log entry, just a missing bill in the target entity.

Our Fix

Use the dedicated intercompany bot user described in Step 01. Configure it as the automation actor so that individual user access changes never break the flow. Add a monthly automated check that verifies the bot user still has access to all companies: a scheduled action that runs env.user.company_ids against env['res.company'].search([]) and sends an alert if they don't match.

3

Custom Modules Without Multi-Company Record Rules

Your custom "Service Request" module works perfectly in a single-company setup. After enabling multi-company, users in Company A start seeing Company B's service requests in their list views. Worse, they can edit and even delete them. The module was built without a company_id field or, more commonly, it has the field but no corresponding ir.rule record to enforce visibility boundaries.

Our Fix

Run the audit script from Step 03 to identify all models with company_id but no record rule. For each gap, add an XML record rule following the standard pattern: ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]. Make it global (global="True") so it applies to all users regardless of group membership. Test by switching companies and verifying that list views only show records belonging to the current company context.

4

Chart of Accounts Misalignment Makes Consolidated Reports Useless

Company A (US) uses account 400000 for revenue. Company B (Germany) uses 4000 for the same thing. When you select both companies in the P&L report, Odoo shows two separate revenue lines instead of one. Your consolidated revenue is technically correct in total, but the line-item breakdown is meaningless—management can't compare revenue streams across entities because the same economic activity maps to different account codes.

Our Fix

Implement the account tag mapping from Step 04. Create a set of group-level tags (prefixed with "GROUP:") and assign them to equivalent accounts in each entity. Then build your consolidated reports filtering by tag rather than account code. For entities in the same country, the faster fix is reinstalling the same chart of accounts template—but only do this before go-live. After go-live, tag mapping is the only non-destructive option.

BUSINESS ROI

What a Properly Configured Multi-Company Setup Saves Your Finance Team

Multi-company configuration isn't an IT project—it's a finance efficiency project. The ROI compounds every month-end close:

5–8 daysFaster Month-End Close

Automated intercompany SO/PO and invoice mirroring eliminates manual double-entry. Elimination entries that took three days now take three hours with pre-configured accounts and reversal automation.

ZeroCross-Entity Data Leaks

Proper record rules ensure users only see data belonging to their assigned companies. Audit-ready access control that satisfies SOC 2 and GDPR data isolation requirements without custom development.

One-ClickConsolidated P&L

With chart alignment via account tags and automated elimination entries, your CFO gets a group-level P&L, Balance Sheet, and Cash Flow report by selecting all companies—no spreadsheet gymnastics required.

For a group with 4 entities, the manual approach to intercompany bookkeeping and consolidation typically consumes 60–80 hours of senior accountant time per month—double-entering invoices, cross-referencing IC balances in spreadsheets, manually creating elimination entries, and building consolidated reports outside of Odoo. At a blended cost of $85/hour, that's $5,100–$6,800/month or $61,000–$82,000/year in labor. A properly configured multi-company setup recovers 80% of that time from the first close cycle.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to multi-company setup in Odoo 19. Configure company hierarchy, intercompany rules, record rules, chart alignment, elimination entries, and consolidated financial reporting.

H2 Keywords

1. "Company Creation, Hierarchy, and the Branch Concept in Odoo 19"
2. "Intercompany Rules: Auto SO/PO and Auto Invoice/Bill Generation"
3. "Multi-Company Record Rules: Preventing Data Leaks Between Entities"
4. "Consolidated Financial Reports: Chart Alignment and Group-Level P&L"
5. "Inter-Company Journal Entries: Elimination Workflow and Reconciliation"
6. "4 Multi-Company Configuration Mistakes That Break Your Group Reporting"

Your Multi-Entity Group Deserves a Single Pane of Glass—Not a Folder of Spreadsheets

Every multi-company Odoo deployment that relies on manual intercompany bookkeeping is burning senior accountant hours on work that should be automated. Every month-end close that depends on spreadsheet consolidation is a month-end close that's vulnerable to human error, missed eliminations, and stale data.

If your Odoo multi-company setup still requires manual double-entry, spreadsheet consolidation, or ad-hoc record rule patches, that's a configuration problem with a permanent fix. We configure company hierarchies, implement intercompany rules, align charts of accounts, build elimination workflows, and deliver consolidated reporting that your CFO can trust on day one. The typical project takes 2–3 weeks and pays for itself in the very first month-end close.

Book a Free Multi-Company Audit