Domains filters in odoo 19 are one of those things in Odoo that you will encounter constantly once you start building or customizing anything. They show up in views, Python code, automated actions, and security rules. Once you understand how they work, a lot of Odoo development starts to click into place.
This guide walks through everything from the very basics all the way to record rules and multi company setups, written in plain and simple terms.
A domain is Odoo’s way of saying “only give me records that match these conditions.” It works a lot like the WHERE clause in a SQL query, but instead of writing SQL, you write it in Odoo’s own list based format.
Every domain is a list of tuples, and each tuple looks like this:
('field_name', 'operator', value)
The field name is the field you want to check on the model. The operator says how to compare. The value is what you are comparing against.
For example:
[('state', '=', 'draft')]
That tells Odoo to fetch only records where the state field equals draft. Simple and clean.
Domains can be used in many places. XML view definitions, Python ORM methods like search and search_count, window actions, record rules, and any place you need to filter down a set of records.
Basic Domain Filters
The most basic domain is just one tuple, or a few tuples grouped together. When you write multiple tuples in a single list, Odoo assumes you want all of them to be true at the same time. That is the default AND behavior.
So this:
[('state', '=', 'sale'), ('amount_total', '>', 2000)]
Returns only records where the state is sale AND the total amount is above 1000. Both conditions must match.
Here is the structure of each tuple broken down:
- field_name is the field on your model you want to filter on
- operator defines how the comparison should work
- value is the thing you are comparing against
Common Operators in Basic Domains
These are the operators you will use regularly when building domain filters:
| Operator | What It Checks | Example |
|---|---|---|
| = | Value is exactly equal | (‘state’, ‘=’, ‘sale’) |
| != | Value is not equal | (‘state’, ‘!=’, ‘cancel’) |
| > | Value is greater than | (‘amount_total’, ‘>’, 5000) |
| < | Value is less than | (‘qty’, ‘<‘, 10) |
| >= | Value is greater than or equal to | (‘date_order’, ‘>=’, ‘2025-01-01’) |
| <= | Value is less than or equal to | (‘date_order’, ‘<=’, ‘2025-01-31’) |
| in | Value is one of the items in a list | (‘state’, ‘in’, [‘draft’, ‘sent’]) |
| not in | Value is not any of the items in a list | (‘state’, ‘not in’, [‘cancel’]) |
| ilike | Text contains the string without caring about case | (‘name’, ‘ilike’, ‘customer’) |
These cover most of what you need day to day.
Complex Logical Operators
Basic domains only let you combine conditions with AND. But real business logic is often more complex than that. Sometimes you need OR. Sometimes you need to negate something entirely. That is what the complex logical operators are for.
Odoo provides three of them and all of them use prefix notation, meaning you place the operator before the conditions it applies to rather than between them.
The three operators are:
- | (pipe) for OR
- & (ampersand) for AND
- ! (exclamation mark) for NOT
OR Operator (|)
Use the pipe when you want records that match at least one of the conditions.
['|', ('state', '=', 'draft'), ('state', '=', 'sent')] This pulls orders where the state is draft OR sent.
AND Operator (&)
AND is already the default, but you need to write it out explicitly when you are mixing it with OR or NOT in the same domain.
['&', ('state', '=', 'sale'), ('partner_id.country_id.code', '=', 'US')] This returns confirmed orders from customers in the United States.
NOT Operator (!)
The exclamation mark inverts a condition. It matches everything that does not meet the condition.
['!', ('state', '=', 'cancel')] This excludes any record in the cancelled state.
Combining AND and OR Together
This is where the real power comes in. You can nest these operators to express more layered logic.
['|',
('amount_total', '>', 10000),
'&',
('state', '=', 'sale'),
('partner_id.is_company', '=', True)
]
Read this as: give me records where the amount is above 10,000, OR where the state is sale AND the customer is a company. The & governs the two conditions right after it, and the | wraps the whole thing.
Context Based Domains
The domains we have covered so far have fixed values baked into them. But sometimes you want the filter to adapt automatically based on who is using the system or what is currently active on the screen. Context based domains make this possible.
Odoo passes around a context dictionary throughout sessions. It holds information like the currently logged in user, the active company, and any default values set by actions or buttons. You can read from this context directly inside your domains.
In XML, you access context values like this:
<field name="partner_id"
domain="[('company_id', '=', context.get('company_id'))]"/>
Now the partner field only shows partners that belong to whichever company the user is currently working in, without hardcoding anything.
Using Default Values from Context
This pattern is especially common in wizards and popup forms where the parent record needs to pass some information down to a child form.
<field name="contact_id"
domain="[('parent_id', '=', context.get('default_partner_id'))]"/>
Context Based Domains in Python
When writing Python code, you access context through self.env.context.
company_id = self.env.context.get('company_id')
orders = self.env['sale.order'].search([
('company_id', '=', company_id)
]) Filtering by the Current User
One of the most common use cases is showing only the records that belong to the user who is currently logged in.
In XML:
<field name="task_id"
domain="[('user_id', '=', uid)]"/>
In Python:
<field name="task_id"
domain="[('user_id', '=', uid)]"/>
Passing Context from an Action
You can define context values inside an action and then reference them in a domain elsewhere.
Action definition:
<record id="action_my_tasks" model="ir.actions.act_window">
<field name="name">My Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">tree,form</field>
<field name="context">{'default_user_id': uid}</field>
</record>
Using that context value in a field domain:
<field name="user_id"
domain="[('id', '=', context.get('default_user_id'))]"/>
Multi Company Context Domains
When dealing with multi company setups, you usually want the user to see both records specific to their company and records that are shared across all companies.
<field name="product_id"
domain="['|',
('company_id', '=', False),
('company_id', '=', context.get('company_id'))
]"/>
Records with no company assigned are treated as global, so this approach covers both cases cleanly.
Dynamic Domains in Views
Context based domains are already flexible, but dynamic domains go one step further. They respond in real time as the user interacts with the form.
When a user picks a value in one field, the available options in another field update right away. The classic example is selecting a country and having the state dropdown refresh immediately to show only states from that country. No page reload, no button click. Just instant feedback.
In Odoo 19, you can achieve this through XML field references or through Python onchange methods depending on how complex your logic is.
A dynamic domain updates whenever a user selects or changes a field and another field depends on it. They are mainly used in form views, wizards, and one2many popup forms.
Setting Up Dynamic Domains in XML
For straightforward cases, Odoo can evaluate the domain directly in XML by looking at the value of another field on the same form.
<field name="country_id"/>
<field name="state_id"
domain="[('country_id', '=', country_id)]"/>
As soon as country_id is updated, Odoo recalculates the domain for state_id on its own. No Python required.
Dynamic Domains Using the onchange Decorator
When the logic is more involved, for example when several fields all affect what shows up in another field, you write an onchange method instead.
@api.onchange('category_id', 'company_id')
def _onchange_category_company(self):
domain = []
if self.category_id:
domain.append(('categ_id', '=', self.category_id.id))
if self.company_id:
domain += ['|',
('company_id', '=', False),
('company_id', '=', self.company_id.id)]
return {
'domain': {
'product_id': domain
}
}
This method fires every time category_id or company_id changes and rebuilds the product_id domain on the fly.
Dynamic Domains Inside Wizards
Wizards make heavy use of dynamic domains. This example shows how selecting a department instantly filters the employee options available in the wizard.
@api.onchange('department_id')
def _onchange_department_id(self):
return {
'domain': {
'employee_id': [('department_id', '=', self.department_id.id)]
}
}
Security Domains and Record Rules
Up until now everything we covered has been about what shows up on screen. But domains also play a major role in security, and this part matters a lot.
Record rules in Odoo use domains to control what data a user is actually allowed to access at the database level, not just what is displayed in the UI. These rules apply everywhere including form views, list views, search results, reports, and API calls. They cannot be bypassed through normal usage.
What a Record Rule Looks Like
A record rule is written in XML and attached to a specific model. The domain_force field holds the domain that Odoo applies automatically for every matching user.
<record id="rule_user_own_records" model="ir.rule">
<field name="name">Own Records Only</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
With this rule active, a user can only open or work with sale orders where they are the assigned user. Records belonging to other users simply do not appear for them.
You can combine conditions in record rule domains just like in any other domain:
['&',
('user_id', '=', user.id),
('state', '!=', 'cancel')
]
This lets the user access only their own records that are not in a cancelled state.
Special Variables Available in Record Rules
Inside record rule domains you have access to a few useful variables. user.id is the ID of the person currently logged in. user.company_id.id is their active company. uid is another way to reference the current user ID.
How Odoo Combines Multiple Record Rules
If more than one record rule applies to a model, Odoo combines them all using AND logic. Every rule must be satisfied for the user to get access. This is worth knowing when you design rules, because it is easy to accidentally lock users out of data they should be able to see if multiple rules layer too aggressively on top of each other.
Multi Company Domains
Multi company setups are a reality for many Odoo deployments. A single database can run several companies at once, and domains are what keep everything properly separated.
The challenge is balancing two needs. Some records should only ever be visible to users of a specific company. Others, like a shared product catalog, should be available to everyone. Domains let you handle both situations at the same time.
Restricting Records to the Current Company
The most straightforward approach just filters by the active company.
<field name="domain">[('company_id', '=', company_id)]</field> The Most Common Multi Company Pattern
This pattern appears all through Odoo’s own source code. It returns records that are either shared (no company assigned) or specifically belonging to the active company.
['|', ('company_id', '=', False), ('company_id', '=', self.env.company.id)] Records where company_id is False are treated as global and visible to everyone across the database.
Using This Pattern in a View
<field name="product_id"
domain="['|',
('company_id', '=', False),
('company_id', '=', company_id)
]"/>
This is what you would put on a product field to make sure users only see products that are available to them based on their company.
Locking Down Models with Multi Company Record Rules
You can enforce the same logic at the security layer using a record rule.
<record id="rule_multi_company" model="ir.rule">
<field name="name">Multi Company Access</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="domain_force">
['|',
('company_id', '=', False),
('company_id', '=', user.company_id.id)
]
</field>
</record>
This stops users from ever seeing partner records that belong to a different company.
Users Working Across Multiple Companies at Once
Odoo allows users to activate more than one company at a time. In those cases, you want to show records from all the companies the user has currently switched into, not just one.
[('company_id', 'in', self.env.context.get('allowed_company_ids'))] The context key allowed_company_ids holds the list of all currently active company IDs for that user session. This is especially important in accounting, inventory, and any reporting module that needs to pull data across multiple companies.
That covers the full picture of how domains work in Odoo 19. Start with basic filters, layer in logical operators when your conditions get more complex, bring in context and dynamic domains to make things responsive and smart, and use record rules to enforce proper data access at the security level. Once all of this clicks, a huge chunk of Odoo development becomes a lot more straightforward.
Ready to see Odoo 19 in action? Book a free demo with us today and let our experts walk you through everything your business needs.