Domain Syntax
A domain is a list of tuples: [('field', 'operator', 'value')]. Multiple tuples are combined with AND by default.
Operators
| Operator | Meaning | Example |
|---|---|---|
= | Equal | [('state', '=', 'draft')] |
!= | Not equal | [('state', '!=', 'done')] |
> | Greater than | [('amount', '>', 1000)] |
>= | Greater or equal | [('amount', '>=', 1000)] |
< | Less than | [('date', '<', '2026-01-01')] |
<= | Less or equal | [('quantity', '<=', 0)] |
in | In list | [('state', 'in', ['draft', 'sent'])] |
not in | Not in list | [('state', 'not in', ['done', 'cancel'])] |
like | Case-sensitive contains | [('name', 'like', 'Acme')] |
ilike | Case-insensitive contains | [('name', 'ilike', 'acme')] |
=like | Pattern match (case-sensitive) | [('email', '=like', '%@gmail.com')] |
=ilike | Pattern match (case-insensitive) | [('email', '=ilike', '%@Gmail.com')] |
child_of | Is child in hierarchy | [('category_id', 'child_of', parent_id)] |
parent_of | Is parent in hierarchy | [('category_id', 'parent_of', child_id)] |
Logic Operators
# AND (implicit — just list tuples)
[('state', '=', 'sale'), ('amount', '>', 1000)]
# Means: state = sale AND amount > 1000
# OR (prefix with '|')
['|', ('state', '=', 'draft'), ('state', '=', 'sent')]
# Means: state = draft OR state = sent
# NOT (prefix with '!')
['!', ('active', '=', True)]
# Means: NOT active = True (i.e., inactive records)
# Complex: (A AND B) OR (C AND D)
['|',
'&', ('state', '=', 'sale'), ('amount', '>', 1000),
'&', ('state', '=', 'draft'), ('is_urgent', '=', True),
]Relational Field Traversal
# Traverse Many2one relations with dot notation
[('partner_id.country_id.code', '=', 'US')]
[('order_id.partner_id.is_company', '=', True)]
[('category_id.parent_id.name', '=', 'Services')]Date Filters
from odoo import fields
from datetime import timedelta
today = fields.Date.today()
# This month
[('date', '>=', today.replace(day=1))]
# Last 30 days
[('date', '>=', today - timedelta(days=30))]
# This year
[('date', '>=', f'{today.year}-01-01')]
# Between dates
[('date', '>=', '2026-01-01'), ('date', '<=', '2026-03-31')]Common Patterns
# Active records only
[('active', '=', True)]
# Current company
[('company_id', '=', self.env.company.id)]
# Current user's records
[('user_id', '=', self.env.uid)]
# Non-empty field
[('email', '!=', False)]
# Records without a parent
[('parent_id', '=', False)]
# Overdue invoices
[('invoice_date_due', '<', fields.Date.today()),
('payment_state', '!=', 'paid')]
# Products in stock
[('qty_available', '>', 0)]
# Search by name (case-insensitive)
[('name', 'ilike', 'search term')]Using Domains in Python
# search()
records = self.env['sale.order'].search([
('state', '=', 'sale'),
('amount_total', '>', 1000),
], limit=10, order='date_order desc')
# search_count()
count = self.env['sale.order'].search_count([
('state', '=', 'sale'),
])
# Combining domains programmatically
from odoo.osv.expression import AND, OR
domain = AND([domain1, domain2])
domain = OR([domain1, domain2])Using Domains in XML
<!-- In search view filters -->
<filter name="overdue" string="Overdue"
domain="[('invoice_date_due', '<', context_today().strftime('%Y-%m-%d'))]"/>
<!-- In record rules -->
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<!-- In action context -->
<field name="domain">[('state', '=', 'draft')]</field>Tip: Use Claude Code for Complex Domains
Describe the filter in English: "Show me all invoices overdue by more than 30 days for customers in Europe." Claude Code generates the correct domain with proper OR/AND nesting and relational traversal.