Este artículo actualmente está disponible solo en inglés. La traducción estará disponible próximamente.
By the end of this recipe, your Odoo 19 instance will produce a fully customized balance sheet matching your country's statutory format (US GAAP, IFRS, UK FRS 102, India Schedule III) with hierarchical totals, comparative period columns, and multi-currency consolidation. Skill required: developer-accountant hybrid comfortable with XML and Odoo's report engine. Time required: 3 hours setup, 30 minutes per quarterly comparison. ECOSIRE has built statutory balance sheets for clients in 15+ jurisdictions, and the recipe below is the playbook.
The trap most teams fall into: trying to use Odoo's stock balance sheet for statutory filing. The default ships with an IFRS-leaning structure that's wrong for US GAAP (where Inventory comes after Receivables, not before) and very wrong for India Schedule III (which requires Equity at the top, not Assets). The recipe below builds a fully custom report that matches whatever standard your auditor expects.
What you will need
- Odoo version: 17, 18, or 19 with
account_reports(Enterprise) oraccount_financial_report(OCA Community). - Skill: comfortable with XML, basic accounting (knowing what goes on a balance sheet).
- Existing chart of accounts with account codes following a logical hierarchy (e.g., 1xxx = Assets, 2xxx = Liabilities, 3xxx = Equity).
- Time: 3 hours.
Step-by-step
1. Define the report structure
Sketch your target layout on paper first:
ASSETS
Current Assets
Cash and Cash Equivalents
Marketable Securities
Accounts Receivable, net
Inventory
Prepaid Expenses
Total Current Assets
Non-Current Assets
Property, Plant and Equipment, net
Goodwill
Intangible Assets
Investments
Total Non-Current Assets
TOTAL ASSETS
LIABILITIES AND EQUITY
Current Liabilities
Accounts Payable
Accrued Liabilities
Short-term Debt
Current Portion of Long-term Debt
Total Current Liabilities
Non-Current Liabilities
Long-term Debt
Deferred Tax Liability
Total Non-Current Liabilities
Equity
Common Stock
Retained Earnings
Other Comprehensive Income
Total Equity
TOTAL LIABILITIES AND EQUITY
Map each leaf node to a range of account codes.
2. Create a custom report XML
In your custom module, create report/balance_sheet_us_gaap.xml:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="balance_sheet_us_gaap" model="account.report">
<field name="name">Balance Sheet (US GAAP)</field>
<field name="filter_period_comparison">1</field>
<field name="filter_growth_comparison">1</field>
<field name="filter_journals">1</field>
<field name="filter_unfold_all">1</field>
<field name="filter_hierarchy">1</field>
<field name="root_report_id" ref="account_reports.balance_sheet"/>
<field name="column_ids" eval="[(0,0,{'name': 'Balance', 'expression_label': 'balance'})]"/>
</record>
<!-- ASSETS -->
<record id="bs_assets" model="account.report.line">
<field name="report_id" ref="balance_sheet_us_gaap"/>
<field name="name">ASSETS</field>
<field name="sequence">10</field>
<field name="hierarchy_level">0</field>
</record>
<record id="bs_current_assets" model="account.report.line">
<field name="report_id" ref="balance_sheet_us_gaap"/>
<field name="parent_id" ref="bs_assets"/>
<field name="name">Current Assets</field>
<field name="sequence">20</field>
<field name="hierarchy_level">1</field>
</record>
<record id="bs_cash" model="account.report.line">
<field name="report_id" ref="balance_sheet_us_gaap"/>
<field name="parent_id" ref="bs_current_assets"/>
<field name="name">Cash and Cash Equivalents</field>
<field name="sequence">30</field>
<field name="hierarchy_level">2</field>
<field name="expression_ids" eval="[(0,0,{
'label': 'balance',
'engine': 'domain',
'formula': [('account_id.account_type','=','asset_cash')],
'subformula': 'sum',
})]"/>
</record>
<record id="bs_ar" model="account.report.line">
<field name="report_id" ref="balance_sheet_us_gaap"/>
<field name="parent_id" ref="bs_current_assets"/>
<field name="name">Accounts Receivable, net</field>
<field name="sequence">40</field>
<field name="hierarchy_level">2</field>
<field name="expression_ids" eval="[(0,0,{
'label': 'balance',
'engine': 'domain',
'formula': [('account_id.account_type','=','asset_receivable')],
'subformula': 'sum',
})]"/>
</record>
<!-- ... continue for inventory, PPE, payables, debt, equity ... -->
<record id="bs_total_assets" model="account.report.line">
<field name="report_id" ref="balance_sheet_us_gaap"/>
<field name="parent_id" ref="bs_assets"/>
<field name="name">TOTAL ASSETS</field>
<field name="sequence">200</field>
<field name="hierarchy_level">1</field>
<field name="expression_ids" eval="[(0,0,{
'label': 'balance',
'engine': 'aggregation',
'formula': 'bs_current_assets.balance + bs_noncurrent_assets.balance',
})]"/>
</record>
</odoo>
The key XML constructs: engine='domain' for direct account-domain lookups, engine='aggregation' for sums of other lines.
3. Add the Equity computation with retained earnings
Retained earnings is the accumulated net income from prior periods plus current year-to-date income. Compute it explicitly:
<record id="bs_retained_earnings" model="account.report.line">
<field name="report_id" ref="balance_sheet_us_gaap"/>
<field name="parent_id" ref="bs_equity"/>
<field name="name">Retained Earnings</field>
<field name="sequence">370</field>
<field name="hierarchy_level">2</field>
<field name="expression_ids" eval="[
(0,0,{
'label': 'balance',
'engine': 'domain',
'formula': [('account_id.account_type','in',['equity','equity_unaffected'])],
'subformula': 'sum',
})
]"/>
</record>
The trick: Odoo automatically includes the current period's net income in equity_unaffected if you have closed prior periods. Confirm by checking Settings > Companies > Accounting > "Lock Date for Non-Advisers".
Verification: TOTAL ASSETS = TOTAL LIABILITIES + EQUITY exactly, to the cent.
4. Add comparison columns
In the main report record, add columns for prior period and prior year:
<field name="column_ids" eval="[
(0,0,{'name': 'Current Period', 'expression_label': 'balance'}),
(0,0,{'name': 'Prior Period', 'expression_label': 'balance', 'date_scope': 'previous_period'}),
(0,0,{'name': 'Prior Year', 'expression_label': 'balance', 'date_scope': 'previous_year'}),
]"/>
Verification: the report renders three columns side-by-side.
5. Configure multi-company consolidation
For a group with subsidiaries, add a column per company plus a consolidation total:
<field name="column_ids" eval="[
(0,0,{'name': 'Parent Co.', 'expression_label': 'balance', 'company_id_filter': company_parent}),
(0,0,{'name': 'Sub Co. A', 'expression_label': 'balance', 'company_id_filter': company_a}),
(0,0,{'name': 'Sub Co. B', 'expression_label': 'balance', 'company_id_filter': company_b}),
(0,0,{'name': 'Eliminations', 'expression_label': 'balance', 'company_id_filter': elim_company}),
(0,0,{'name': 'Consolidated', 'expression_label': 'balance', 'aggregate': 'sum'}),
]"/>
For complex inter-company eliminations, use the Enterprise Consolidation module instead of doing it in the report.
Verification: parent + subs + elims = consolidated total.
6. Add country-specific overrides
For India Schedule III, the order is reversed (Equity & Liabilities first). Inherit the report:
<record id="balance_sheet_india_sch3" model="account.report">
<field name="name">Balance Sheet (India Schedule III)</field>
<field name="root_report_id" ref="balance_sheet_us_gaap"/>
<!-- override the structure -->
</record>
<record id="bs_india_equity_liab" model="account.report.line">
<field name="report_id" ref="balance_sheet_india_sch3"/>
<field name="name">EQUITY AND LIABILITIES</field>
<field name="sequence">5</field>
<!-- Schedule III mandates this header first -->
</record>
Verification: India statutory filing accepts the PDF without comment.
7. Lock fiscal periods
Before issuing the report to auditors, lock the period: Settings > Accounting > Lock Date. This prevents anyone from posting to the closed period. Verification: trying to post a JE to the locked period raises a clear error.
8. Schedule monthly export
Use the same cron pattern as the cash flow recipe to email the PDF to your CFO and auditor on the 5th of every month. Include a one-page commentary section auto-generated from the growth_comparison percentages.
Verification: the email arrives with PDF attached.
Common mistakes
- Computing Total Equity manually. Always derive it from account types so the BS balances automatically.
- Hardcoding account codes in domain filters. Use
account_typeinstead — survives chart-of-accounts restructures. - Forgetting to handle minority interest. For consolidated reports with non-100%-owned subsidiaries, add a Minority Interest line in equity.
- Not testing fiscal year transitions. Run the report on December 31 and January 1 — make sure beginning equity carries cleanly.
- Ignoring forex revaluation. Foreign-subsidiary balances must be translated; assets+liabilities at period-end rate, equity at historical rate, with CTA balancing the difference.
Going further
Branch-level balance sheet: filter by analytic_account_id for branch-specific BS within a single legal entity. Useful for franchise operators or retail chains where each store needs its own BS for performance reviews. Add a branch dimension column and produce an internal P&L per branch alongside.
Comparative consolidation: side-by-side Q1/Q2/Q3/Q4 + YTD columns for board packs. The Financial Reports engine supports up to 16 comparison columns natively. For full-year quarterly view: Q1, Q2, Q3, Q4, YTD, with Q-over-Q variance percentage as additional column.
Drill-down to JE: every line on Odoo's BS is clickable to the underlying journal entries. Useful during audit walkthroughs — auditor clicks Total Receivables, sees the constituent invoices, ages them, samples a few for confirmations. Make sure your team knows how to use this; saves days of audit prep.
Live KPI tile: pair this with how to build an Odoo dashboard tile to surface "Total Assets" on the executive dashboard. Add side-by-side ratios: current ratio (CA/CL), quick ratio ((CA-Inv)/CL), debt-to-equity (Total Debt / Total Equity), interest coverage. Each ratio is a single DAX measure once the BS lines exist.
Footnotes section: regulated jurisdictions require footnotes (commitments, contingencies, related-party transactions). Build a separate "Footnotes" tab on the report with customizable narrative blocks pulled from account.note records.
Period locking with audit log: pair statutory reports with formal period locking and an audit log of who locked what when. Settings > Accounting > "Lock Date for All Users" prevents back-posting. Combine with mail.activity so the close team gets a checklist of pre-lock validations.
XBRL export: jurisdictions like UK/Ireland require XBRL filing. Add an export option that maps your BS lines to the appropriate XBRL taxonomy. Saves the £500-£2000 per year that companies pay for separate XBRL tools.
Restatement handling: when prior periods need adjustment (post-audit), Odoo Enterprise has a "Period Adjustment" wizard that creates the restating entries with a clear audit trail. Your custom BS report should respect the period lock and show before/after restatement columns when present.
For statutory reporting setups across multiple jurisdictions including IFRS-to-GAAP reconciliations, ECOSIRE accounting services deliver fully audited templates. Pair this with how to build a cash flow statement.
Frequently Asked Questions
Why does my BS not balance by a few cents?
Almost always rounding. Use the "Round Globally" tax method on company config. Otherwise per-line rounding accumulates pennies of difference.
Can I add custom KPIs at the bottom?
Yes — add report lines after Total Equity using engine='aggregation' with formulas like bs_current_assets.balance / bs_current_liabilities.balance for current ratio.
How do I handle foreign-currency transactions in BS?
Tag the relevant accounts with currency_id set to the foreign currency. The BS report shows balances in company currency by default; foreign-currency-balance can be added as a separate column.
What about India Companies Act 2013 schedule changes?
Schedule III amendments require disclosing aging buckets for receivables and trade payables. Add aging-bucket lines that filter account_move_line.date_maturity.
For multi-jurisdiction statutory reporting including audit-firm-specific formats, ECOSIRE accounting services handle the configuration. Or read how to build an AR/AP aging report for the working-capital deep-dive.
Escrito por
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transforme su negocio con Odoo ERP
Implementación, personalización y soporte experto de Odoo para optimizar sus operaciones.
Artículos relacionados
Cómo agregar un botón personalizado a una vista de formulario de Odoo (2026)
Agregue botones de acción personalizados a las vistas de formulario de Odoo 19: método de acción de Python, herencia de vistas, visibilidad condicional, cuadros de diálogo de confirmación. Probado en producción.
Cómo agregar un campo personalizado en Odoo sin Studio (2026)
Agregue campos personalizados a través de un módulo personalizado en Odoo 19: herencia de modelo, extensión de vista, campos calculados, decisiones de tienda/no tienda. Código primero, controlado por versiones.
Cómo agregar un informe personalizado en Odoo usando un diseño externo
Cree un informe PDF con su marca en Odoo 19 usando web.external_layout: plantilla QWeb, formato de papel, enlace de acción. Con logotipo impreso + anulaciones de pie de página.