Skip to content

10. Domain Filters - Controlling Data

What Is a Domain?

A domain is a filter condition that limits which records are shown or selectable.

Domains are written as a list of conditions: [('field', 'operator', 'value')]

A Day Without Domains

Imagine you're setting up a Sales Order. When you click the Customer dropdown, you see EVERY contact in the system - 50,000 records including vendors, shipping addresses, employees, and archived contacts. Good luck finding your customer!

Domains are the unsung heroes that make Odoo actually usable - they filter what appears in dropdowns, power your saved searches, and enforce who can see what data.

Where You See Domains Every Day

Where You See ItWhat's HappeningThe Domain Behind It
Customer dropdown shows only customersField domain filters contacts[('customer_rank', '>', 0)]
"My Orders" filter in searchSearch filter with domain[('user_id', '=', uid)]
Quotations menu shows only quotesWindow action domain[('state', 'in', ['draft', 'sent'])]
Salesperson only sees their salesRecord rule domain[('user_id', '=', user.id)]
Products dropdown shows only stockableDynamic field domain[('detailed_type', '=', 'product')]

How Domains Work

Domain Structure

A domain is a list of conditions (called "leaves") combined with logical operators.

Each condition is a tuple with 3 parts:

  1. Field name - The field to filter on (can use dot notation for related fields)
  2. Operator - How to compare (=, !=, in, like, etc.)
  3. Value - What to compare against
[('state', '=', 'draft'), ('amount', '>', 1000)]
   ↓         ↓       ↓
 field   operator  value

Default behavior: Multiple conditions are combined with AND (all must be true).

Reading Domains Step by Step

Simple domain: [('active', '=', True)] → "Show records where active equals True"

Multiple conditions: [('state', '=', 'sale'), ('amount', '>', 1000)] → "Show records where state is 'sale' AND amount is greater than 1000"

OR condition: ['|', ('state', '=', 'draft'), ('state', '=', 'sent')] → "Show records where state is 'draft' OR state is 'sent'"

Dot notation: [('partner_id.country_id.code', '=', 'IN')] → "Show records where the partner's country code is 'IN' (India)"

Domain Operators

OperatorMeaningExampleResult
=Equals[('state', '=', 'draft')]Records where state is draft
!=Not equals[('state', '!=', 'cancel')]Records not cancelled
>Greater than[('amount', '>', 1000)]Amount over 1000
<Less than[('amount', '<', 500)]Amount under 500
>=Greater or equal[('qty', '>=', 10)]Quantity 10 or more
<=Less or equal[('date', '<=', '2024-12-31')]Date on or before Dec 31
inIn list[('state', 'in', ['draft', 'sent'])]Draft OR sent
not inNot in list[('state', 'not in', ['cancel', 'done'])]Not cancelled or done
likeContains (case sensitive)[('name', 'like', 'Sales')]Name contains "Sales"
ilikeContains (case insensitive)[('email', 'ilike', '@gmail')]Gmail addresses
=likePattern match[('ref', '=like', 'SO%')]Starts with "SO"
=ilikePattern (case insensitive)[('name', '=ilike', '%ltd')]Ends with "ltd"
not likeDoes not contain (case sensitive)[('name', 'not like', 'Test')]Name doesn't contain "Test"
not ilikeDoes not contain (case insensitive)[('email', 'not ilike', 'spam')]Email without "spam"
=?Equals if value is set[('company_id', '=?', company_id)]Filter by company only if company_id has a value
child_ofHierarchical child[('categ_id', 'child_of', 5)]Category 5 and children
parent_ofHierarchical parent[('categ_id', 'parent_of', 10)]Parents of category 10
anyRelational subdomain[('order_line', 'any', [('product_id.type', '=', 'service')])]Orders with service products
not anyNegated subdomain[('order_line', 'not any', [('qty_delivered', '>', 0)])]Orders with no delivered lines

The "any" and "not any" Operators

These operators filter through relational fields with complex conditions:

The any operator:

  • Filter records where at least one related record matches a subdomain
  • Syntax: [('relation_field', 'any', [subdomain])]

The not any operator:

  • Filter records where no related records match a subdomain
  • Syntax: [('relation_field', 'not any', [subdomain])]
python
# Find sales orders with service products
[('order_line', 'any', [('product_id.type', '=', 'service')])]

# Find orders with NO delivered lines
[('order_line', 'not any', [('qty_delivered', '>', 0)])]

# Find partners with invoices to pay
[('partner_invoice_id', 'any', [('state', '=', 'posted')])]

Note

The traditional dot notation [('order_line.product_id.type', '=', 'service')] is equivalent to using any and still works.

The "=?" Conditional Operator

The =? operator is special - it only applies the filter if the value is truthy:

python
# Only filter by company if company_id is set
domain="[('company_id', '=?', company_id)]"

# This is equivalent to:
# If company_id is set: [('company_id', '=', company_id)]
# If company_id is False/empty: [] (no filter, show all)

Common use case: Field domains that should filter by company only when a company is selected.

Pattern Matching with Wildcards

The =like and =ilike operators use SQL LIKE syntax with wildcards:

WildcardMeaningExampleMatches
%Any characters (0 or more)[('ref', '=like', 'SO%')]SO001, SO123, SO
_Exactly one character[('code', '=like', 'A_')]A1, AB, AX (not A, A12)
%...%Contains anywhere[('name', '=ilike', '%sales%')]Sales Team, Pre-Sales, sales

Difference between like and =like:

  • like automatically adds % around your value → %value%
  • =like uses your value exactly as provided (you control the wildcards)
python
# These are equivalent:
[('name', 'like', 'Sales')]      # → matches '%Sales%'
[('name', '=like', '%Sales%')]   # → matches '%Sales%'

# But these are different:
[('ref', '=like', 'SO%')]        # Starts with SO
[('ref', 'like', 'SO')]          # Contains SO anywhere

Combining Conditions

AND, OR, NOT Logic (Prefix Notation)

Odoo uses prefix notation (Polish notation) where the operator comes BEFORE the conditions it applies to.

SymbolMeaningHow It Works
& (default)AND - both must be trueApplied to the NEXT TWO conditions
|OR - either can be trueApplied to the NEXT TWO conditions
!NOT - inverseApplied to the NEXT ONE condition

Understanding Prefix Notation

Key rule: The operator applies to the conditions that follow it.

Simple OR:

python
['|', ('state', '=', 'draft'), ('state', '=', 'sent')]
# → state is draft OR state is sent

Chained OR (A OR B OR C):

python
['|', '|', ('a', '=', 1), ('b', '=', 2), ('c', '=', 3)]
# → A OR B OR C

Mixed AND/OR (A AND (B OR C)):

python
['&', ('a', '=', 1), '|', ('b', '=', 2), ('c', '=', 3)]
# → A AND (B OR C)

Common Domain Patterns

1. Either/Or (A OR B):

python
['|', ('state', '=', 'draft'), ('state', '=', 'sent')]

2. Multiple OR (A OR B OR C):

python
['|', '|', ('state', '=', 'a'), ('state', '=', 'b'), ('state', '=', 'c')]
# Easier alternative using 'in':
[('state', 'in', ['a', 'b', 'c'])]

3. AND with OR ((A OR B) AND C):

python
['&', '|', ('type', '=', 'a'), ('type', '=', 'b'), ('active', '=', True)]

4. NOT condition:

python
['!', ('state', '=', 'cancel')]  # state is NOT cancel
# Easier alternative:
[('state', '!=', 'cancel')]

5. Complex: (A AND B) OR (C AND D):

python
['|', '&', ('a', '=', 1), ('b', '=', 2), '&', ('c', '=', 3), ('d', '=', 4)]

Where Domains Are Used

LocationPurposeExample
Field DefinitionLimit dropdown choicespartner_id domain: only show customers
Search FiltersPredefined searches"My Orders" filter
Action WindowsWhat records to showMenu shows only quotations
Record RulesSecurity restrictionsUsers see only their company's data
ReportsFilter report dataOnly confirmed invoices

Practical Examples

1. Show only companies (not individuals) in dropdown:

python
[('is_company', '=', True)]

2. Show only products in stock:

python
[('qty_available', '>', 0)]

3. Show orders from last 30 days:

python
[('date_order', '>=', (context_today() - relativedelta(days=30)).strftime('%Y-%m-%d'))]

4. Show only active, confirmed sales orders for current user:

python
[('active', '=', True), ('state', '=', 'sale'), ('user_id', '=', uid)]

Domain Variables in XML

When writing domains in XML views, you can use special variables:

VariableMeaningExample
uidCurrent user's ID[('user_id', '=', uid)]
userCurrent user record[('company_id', '=', user.company_id.id)]
company_idCurrent company ID[('company_id', '=', company_id)]
company_idsAll allowed company IDs[('company_id', 'in', company_ids)]
context_today()Today's date[('date', '=', context_today())]
selfCurrent field value (in filter_domain)[('categ_id', 'child_of', self)]
parent.fieldParent record's field value[('partner_id', '=', parent.partner_id)]

Dynamic Domains

Making Dropdowns Change Based on Selections

One of the most powerful features: make a field's domain depend on another field's value.

Scenario: On a Sales Order, you want the Product dropdown to only show products from the selected Pricelist's allowed categories.

In XML views, use field references:

xml
<field name="pricelist_id"/>
<field name="product_id" domain="[('categ_id', 'in', pricelist_id.item_ids.categ_id.ids)]"/>

Common dynamic domain patterns:

ScenarioDomain
Products matching selected category[('categ_id', '=', categ_id)]
Contacts from selected company[('parent_id', '=', company_partner_id)]
Bank accounts of selected partner[('partner_id', '=', partner_id)]
Employees in selected department[('department_id', '=', department_id)]

Common Mistakes to Avoid

Domain Pitfalls

❌ Wrong: Using = with a list

python
[('state', '=', ['draft', 'sent'])]  # WRONG!

✅ Correct: Use 'in' for multiple values

python
[('state', 'in', ['draft', 'sent'])]  # Correct

❌ Wrong: Forgetting prefix for OR

python
[('state', '=', 'draft'), '|', ('state', '=', 'sent')]  # WRONG!

✅ Correct: Put '|' before the conditions

python
['|', ('state', '=', 'draft'), ('state', '=', 'sent')]  # Correct

❌ Wrong: Comparing to None

python
[('partner_id', '=', None)]  # WRONG!

✅ Correct: Use False for empty/null

python
[('partner_id', '=', False)]  # Correct - finds records without partner

❌ Wrong: Uppercase operators

python
[('name', 'ILIKE', 'test')]  # Deprecated since Odoo 19

✅ Correct: Always lowercase operators

python
[('name', 'ilike', 'test')]  # Correct

Domain Troubleshooting

ProblemLikely CauseSolution
Dropdown shows nothingDomain too restrictive or typo in field nameTest with empty domain [] first, then add conditions one by one
Domain works in shell but not in viewMissing variable (uid, context_today, etc.)Check if you're using view-only variables in Python code
"Invalid field" errorField doesn't exist or wrong modelVerify field name in Developer Mode → View Fields
Domain ignoredDomain in wrong attribute or overriddenCheck inherited views that might override the domain
OR logic not workingPrefix notation errorRemember: | goes BEFORE the two conditions it joins

Test Your Knowledge

Q1: You want to filter Sales Orders to show only those that are EITHER in "Sales Order" status OR have a total amount greater than $10,000. Which domain is correct?

  • A) [('state', '=', 'sale'), ('amount_total', '>', 10000)]
  • B) ['|', ('state', '=', 'sale'), ('amount_total', '>', 10000)]
  • C) [('state', '=', 'sale'), '|', ('amount_total', '>', 10000)]
  • D) [('state', '=', 'sale') | ('amount_total', '>', 10000)]
Answer

Correct Answer: B

Odoo uses Polish (prefix) notation for OR operators. The '|' must come BEFORE the two conditions it joins. Without the '|' prefix, conditions are implicitly ANDed together. Option A would show orders that are BOTH in sale status AND over $10,000.

Q2: On a Sales Order form, you want the "Warehouse" dropdown to only show warehouses belonging to the same company as the order. What domain variable would you use?

  • A) [('company_id', '=', self.company_id.id)]
  • B) [('company_id', '=', context.get('company_id'))]
  • C) [('company_id', '=', uid)]
  • D) [('company_id', '=', company_id)]
Answer

Correct Answer: D

In view domains, you can reference other fields on the same record directly by their field name. company_id refers to the company selected on the current Sales Order form. This creates a dynamic domain that changes as the user changes the company field.

Q3: What does the domain operator 'ilike' do compared to '='?

  • A) It matches the exact value, but case-sensitive
  • B) It matches only integer values
  • C) It performs a case-insensitive partial match (contains) ✓
  • D) It matches the value at the beginning of the field only
Answer

Correct Answer: C

The 'ilike' operator performs a case-insensitive pattern match. [('name', 'ilike', 'john')] will match "John Smith", "JOHN DOE", and "johnny". The '=' operator requires an exact match. Use 'like' for case-sensitive partial matching.