How to Add Filters in Accounting Report Odoo 19: Complete Tutorial
The redesigned account_reports module, which powers Odoo 19's Accounting Reports engine, is one of the framework's most potent but least documented expansion points. It powers all financial reports including the Tax Report, Balance Sheet, and Profit and Loss Statement. While the engine comes with built-in filters for date range, journals, analytical accounts, partners, and fiscal positions, practical applications nearly always require additional filters. This complete step by step guide walks through adding a custom filter using a fully functional invoice_tag_report_filter module as a real example.
What You'll Learn:
- How to create an invoice.tag master-data model for categorising journal entries
- How to add a Many2one field to account.move linking invoices to tags
- How to extend account.report with a boolean filter toggle and options initialisation
- How to override _get_options_domain using the new Domain class
- How to build OWL frontend components with registerCustomComponent and XML templates
Understanding the Account Reports Architecture
Odoo 19's account_reports module introduces a well-thought-out extension architecture. The report engine dynamically discovers methods following the pattern _init_options_<suffix> and calls them automatically. The new odoo.fields.Domain class provides a safe and readable way to write filter criteria. The OWL frontend offers clear extension points through registerCustomComponent and t-inherit. All these components work together in the invoice_tag_report_filter module demonstrated in this tutorial.
Master-Data Model
The invoice.tag model stores tag names with active flag, color index, and notes. Managed from Accounting > Configuration with proper access rights for users and managers.
Account.move Extension
A Many2one field on account.move links each invoice to an invoice.tag. Uses ondelete set null and tracking=True for auditability.
Account.report Extension
Adds a boolean toggle per report, _init_options_invoice_tag for options initialisation, and _get_options_domain override using the Domain class.
OWL Frontend
A JS component extending AccountReportFilters and an XML template with MultiRecordSelector widget, patched into the filter bar via t-inherit-mode extension.
Module Structure Overview
The complete module follows a clean directory structure. Every component has a specific role in the filter pipeline:
| File | Purpose |
|---|---|
| __manifest__.py | Module declaration with dependencies and assets registration |
| models/invoice_tag.py | invoice.tag master-data model definition |
| models/account_move.py | Adds invoice_tag_id Many2one to account.move |
| models/account_report.py | Filter toggle, options init, and domain override |
| views/invoice_tag_views.xml | Form, list, action, and menu for invoice.tag |
| views/account_move_views.xml | Injects tag field into invoice form |
| views/account_report_views.xml | Exposes filter toggle on report configuration form |
| security/ir.model.access.csv | Read/write access rules for invoice.tag |
| static/src/account_report/account_report_filter.js | OWL component registration |
| static/src/account_report/account_report_filter.xml | OWL template with dropdown and filter bar patch |
Step 1: Creating the Module Manifest
The __manifest__.py is straightforward but has two critical sections: the data list and the assets block. The security CSV must come first in the data list so access rights exist before any demo data loading. The assets block registers OWL components into the backend bundle without this the JS file is ignored.
{
'name': "Invoice Tag Report Filter",
'summary': """
Adds an Invoice Tag field on account.move and a corresponding
filter on Accounting Reports (P&L, Balance Sheet, etc.).
""",
'description': """
* New model : invoice.tag
* New field : account.move.invoice_tag_id (Many2one to invoice.tag)
* New filter : Filter Invoice Tag on account.report
Enable the filter by checking 'Filter Invoice Tag' on the
Accounting Report form view.
""",
'author': "Braincuber",
'category': 'Accounting',
'version': '19.0.1.0.0',
'depends': ['base', 'web', 'account',
'account_reports', 'account_accountant'],
'data': [
'security/ir.model.access.csv',
'views/invoice_tag_views.xml',
'views/account_move_views.xml',
'views/account_report_views.xml',
],
'assets': {
'web.assets_backend': [
'invoice_tag_report_filter/static/src/account_report/'
'account_report_filter.xml',
'invoice_tag_report_filter/static/src/account_report/'
'account_report_filter.js',
],
},
'license': 'LGPL-3',
}
The manifest depends on account_reports and account_accountant in addition to base and account. The assets block uses web.assets_backend to inject the OWL component files into the accounting report bundle.
Step 2: The invoice.tag Master-Data Model
Every meaningful filter needs something to filter by. The invoice.tag model is a lean master-data model that accountants can manage from the Accounting > Configuration menu. It stores tag names, an active flag for soft-delete, a color index for visual identification, and notes for internal reference.
from odoo import models, fields
class InvoiceTag(models.Model):
"""
Simple master-data model used to categorise
journal entries / invoices.
Example tags: "Project-A", "Intercompany",
"Recurring", etc.
"""
_name = 'invoice.tag'
_description = 'Invoice Tag'
_order = 'name'
name = fields.Char(string='Tag Name', required=True)
active = fields.Boolean(default=True)
color = fields.Integer(string='Color Index')
note = fields.Text(string='Notes')
_sql_constraints = [
('name_uniq', 'unique(name)',
'Invoice Tag name must be unique.'),
]
Key Design Decisions
The active field gives accountants a soft-delete capability via the standard Odoo archive mechanism. The color field is consumed by the color_picker widget in the form view. The SQL constraint enforces uniqueness at the database level, preventing duplicates from concurrent saves. The _order = name ensures dropdowns and list views are always alphabetically sorted.
Access Rights Configuration
The security CSV grants read access to accounting users and full CRUD to accounting managers. This follows Odoo's convention: users can see and pick tags; only managers can create, rename, or delete them. Granting write to ordinary users would create a maintenance headache as every accountant starts inventing their own tags.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_invoice_tag_user,invoice.tag user,model_invoice_tag,account.group_account_user,1,0,0,0
access_invoice_tag_manager,invoice.tag manager,model_invoice_tag,account.group_account_manager,1,1,1,1
Step 3: Adding the Tag Field to account.move
The Many2one field linking invoices to tags is added via a simple model extension. Two attributes deserve special attention: ondelete set null ensures that deleting a tag does not break thousands of invoices, and tracking=True records every tag change in the chatter for audit purposes.
from odoo import models, fields
class AccountMove(models.Model):
_inherit = 'account.move'
invoice_tag_id = fields.Many2one(
comodel_name='invoice.tag',
string='Account Tag',
ondelete='set null',
tracking=True,
help="Categorise this invoice / journal entry "
"with a custom tag for reporting purposes.",
)
The corresponding view injection places the tag field just after the ref field in the standard invoice header. The no_create_edit option prevents users from creating or editing tags inline from the invoice form, keeping tag management controlled through the dedicated configuration menu.
account.move.form.inherit.invoice.tag
account.move
Step 4: Extending account.report with Filter Logic
This is the heart of the module. The account_report.py file does three things: adds a boolean toggle to account.report, initialises the filter option via the naming convention hook, and extends the domain query to filter by selected tags.
The Boolean Toggle
The filter_invoice_tag field is a per-report feature flag. An administrator can enable it on any accounting report via the report configuration form. When True, the invoice-tag dropdown appears in the filter bar. When False, the filter is completely ignored with no UI, no extra SQL, and no performance overhead.
class InheritAccountReport(models.Model):
_inherit = 'account.report'
filter_invoice_tag = fields.Boolean(
string="Filter Invoice Tag")
def _init_options_invoice_tag(
self, options, previous_options=None):
"""
Called automatically by
account.report._init_options() because the
method name follows the pattern
_init_options_.
"""
if not self.filter_invoice_tag:
return
options['filter_invoice_tag'] = True
options['invoice_tag_ids'] = (
previous_options.get('invoice_tag_ids')
if previous_options else []
) or []
Understanding the Options Pipeline
Odoo 19's account.report._init_options() dynamically discovers all methods whose names follow the pattern _init_options_<suffix> and calls them in sequence. You never need to override _init_options() itself. The guard clause returns immediately if the feature flag is disabled, ensuring zero overhead for reports without the filter.
The Domain Override
When a user selects invoice tags and refreshes the report, Odoo calls _get_options_domain() to build the final WHERE clause. We override this method to inject our tag condition. Odoo 19 uses the new odoo.fields.Domain class with the & operator for cleaner merging compared to the older osv.expression.AND.
def _get_options_domain(self, options, date_scope):
self.ensure_one()
domain = Domain([
('display_type', 'not in',
('line_section', 'line_note')),
('company_id', 'in', company_ids),
])
# Invoice Tag filter
if (options.get('filter_invoice_tag')
and options.get('invoice_tag_ids')):
domain &= Domain([
('move_id.invoice_tag_id', 'in',
options['invoice_tag_ids']),
])
# Standard sub-domains
domain &= Domain(
self._get_options_journals_domain(options))
domain &= Domain(
self._get_options_date_domain(options,
date_scope))
domain &= Domain(
self._get_options_partner_domain(options))
domain &= Domain(
self._get_options_all_entries_domain(options))
domain &= Domain(
self._get_options_unreconciled_domain(options))
domain &= Domain(
self._get_options_fiscal_position_domain(
options))
domain &= Domain(
self._get_options_account_type_domain(options))
return domain
Domain Path Traversal
The path move_id.invoice_tag_id traverses from account.move.line to account.move to invoice.tag. Odoo's ORM resolves this join automatically with no raw SQL needed. The guard condition if options.get('filter_invoice_tag') and options.get('invoice_tag_ids') means the filter only applies when both the feature is enabled and at least one tag is selected — an empty selection shows everything.
The view injection that exposes the toggle on the report configuration form places the filter_invoice_tag field after the existing filter_partner field:
account.report.form.inherit.invoice.tag
account.report
Step 5: Building the OWL Frontend
Odoo 19's accounting report UI is built in OWL, not legacy QWeb. Adding a UI filter requires two files: a JS component class and an XML template with the dropdown widget and the filter bar patch.
The JavaScript Component
The JS component extends AccountReportFilters to inherit all state management methods, particularly getMultiRecordSelectorProps which the XML template uses to wire up the tag selector. The registerCustomComponent call is the official extension point provided by account_reports.
/** @odoo-module **/
import { AccountReport } from "@account_reports/components/account_report/account_report";
import { AccountReportFilters } from "@account_reports/components/account_report/filters/filters";
export class AccountReportFilterInvoiceTag
extends AccountReportFilters {
static template = "invoice_tag_report_filter.AccountReportFilterInvoiceTag";
setup() {
super.setup();
}
}
AccountReport.registerCustomComponent(
AccountReportFilterInvoiceTag
);
The XML Template
The XML contains two templates. The first defines the standalone dropdown widget using the built-in MultiRecordSelector OWL component, which creates a many2many-style tag selector. The second template patches the standard filter bar using t-inherit-mode extension to add our dropdown only when the server has set the filter_invoice_tag flag in options.
Understanding t-inherit-mode extension
The t-inherit-mode extension directive is crucial. Without it, the template would replace the filter bar entirely instead of patching it. The conditional t-if ensures the dropdown only shows up when the server-side flag is active, keeping the JS and Python layers in sync.
Testing the Module
Once the module is installed, navigate to Accounting > Reports > Profit and Loss. The Invoice Tag dropdown should appear in the filter bar. Select a tag and verify the report figures change accordingly. The same filter will be available on the Balance Sheet, Tax Report, and any other report where the administrator enables it from the report configuration form.
You can modify this pattern to fit any categorisation axis that your client's accounting team requires, whether cost center, project, product line, geographic region, or any other dimension not covered by the standard Odoo filter set. In under an hour, you can extend the module to support any new master-data model.
Frequently Asked Questions
What naming convention does Odoo 19 use for filter options initialisation?
Odoo 19's account.report._init_options() dynamically discovers methods following the pattern _init_options_<suffix> and calls them automatically. This allows extension without overriding the parent method, keeping the integration clean and maintainable.
How does the Domain class override merge custom filters with existing report filters?
The _get_options_domain override uses the Domain class with the & operator to combine the custom tag domain with domains from journals, dates, partners, and fiscal positions. This preserves all existing logic while injecting the new filter condition.
What does AccountReport.registerCustomComponent() do in the OWL frontend?
It is the official extension point provided by account_reports to mount a custom filter component alongside the built-in filter bar. It tells the report controller to render the component without patching the parent component directly.
Why is the no_create_edit option set on the invoice_tag_id field?
To prevent users from creating or editing tags inline from the invoice form. Tag management is kept controlled through the dedicated configuration menu, avoiding accidental tag proliferation and ensuring consistent naming conventions.
What is the significance of t-inherit-mode extension in the XML template?
The t-inherit-mode extension directive patches the existing filter bar template by adding content at specific xpath positions instead of replacing the entire template. This preserves all built-in filters while injecting the custom dropdown only when the server-side flag is active.
Need Help with Odoo Custom Module Development?
Our Odoo development experts can help you build custom accounting report filters, extend the account_reports module, create OWL frontend components, and optimize your Odoo instance for your specific business workflows.
About the author
Founder & Odoo Practice Lead, Braincuber Technologies
Founder of Braincuber. Has scoped and shipped 500+ Odoo implementations for US mid-market and global brands. Takes every founder call personally — no SDR layer between buyers and the people building the system.
