Odoo Development

Context and Domain in Odoo — Developer Reference (Odoo 16–19)

A practical guide to Odoo domains (record filtering) and context (key–value state passing) — how they work, where they appear, and the syntax rules for Odoo 16 through 19. Verified for Odoo 18 & 19.

iWesabe Editorial TeamMarch 7, 20227 min read

Two concepts appear throughout Odoo's XML views, Python models, RPC calls, and JavaScript code: **domain** and **context**. A domain is a list of conditions used to filter records — the Odoo equivalent of a SQL `WHERE` clause. A context is a Python dictionary that carries state between the UI and server, controlling default values, view switches, grouping, and behaviour flags. Getting both right is essential for building reliable views, actions, and model methods.

Part 1 — Domain Syntax

A domain is a Python list of triples (tuples), where each triple has the form `(field_name, operator, value)`. Multiple triples are combined with `AND` logic by default. Prefix operators `'|'` (OR) and `'!'` (NOT) change the logic for the immediately following conditions.

python
# Basic domain — AND logic (default)
# Returns records where state is 'sale' AND partner_id is 7
domain = [('state', '=', 'sale'), ('partner_id', '=', 7)]

# OR logic — use '|' prefix before the two conditions it joins
# Returns records where state is 'sale' OR state is 'done'
domain = ['|', ('state', '=', 'sale'), ('state', '=', 'done')]

# NOT logic — use '!' prefix before the condition to negate
# Returns records where state is NOT 'cancel'
domain = ['!', ('state', '=', 'cancel')]

# Empty domain — matches all records
domain = []

Domain Operators Reference

Odoo domain operators — verified for Odoo 16–19
OperatorMeaningValue typeExample
=EqualScalar('state', '=', 'done')
!=Not equalScalar('state', '!=', 'cancel')
>Greater thanNumber / date('amount_total', '>', 0)
>=Greater than or equalNumber / date('date', '>=', '2026-01-01')
<Less thanNumber / date('qty', '<', 10)
<=Less than or equalNumber / date('date', '<=', '2026-12-31')
=?Equal or field is False/None (null-safe equal)Scalar or False('partner_id', '=?', False)
likeContains substring (case-sensitive)String('name', 'like', 'Odoo')
ilikeContains substring (case-insensitive)String('name', 'ilike', 'odoo')
=likeMatches pattern with % wildcard (case-sensitive)String with %('ref', '=like', 'INV%')
=ilikeMatches pattern with % wildcard (case-insensitive)String with %('ref', '=ilike', 'inv%')
inValue is in listList('state', 'in', ['sale', 'done'])
not inValue is not in listList('state', 'not in', ['draft', 'cancel'])
child_ofRecord or its children (hierarchical models)ID or list of IDs('category_id', 'child_of', 5)
parent_ofRecord or its parents (hierarchical models)ID or list of IDs('category_id', 'parent_of', 12)

Domain in XML Views

In XML views, domains are written as strings (the Python list is expressed as a string literal). Odoo evaluates the string in the context of the current record's field values. Use `uid` for the current user's ID and `context_today()` for today's date.

xml
<!-- Domain on a Many2one field — filters dropdown options -->
<field name="user_id"
       domain="[('groups_id', 'in', [ref('base.group_user')])]"/>

<!-- Domain on an action button — filters records opened by the action -->
<button name="action_view_invoices" type="object"
        domain="[('state', '=', 'posted'), ('partner_id', '=', active_id)]"/>

<!-- Domain referencing current user -->
<field name="assigned_to"
       domain="[('share', '=', False), ('id', '!=', uid)]"/>

<!-- Domain using context_today() for date comparison -->
<filter name="overdue"
        domain="[('date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>

Domain in Python (ORM search and search_count)

python
# search() — returns recordset matching domain
invoices = self.env['account.move'].search([
    ('state', '=', 'posted'),
    ('partner_id', '=', self.partner_id.id),
    ('invoice_date', '>=', '2026-01-01'),
], order='invoice_date desc', limit=10)

# search_count() — returns integer count
count = self.env['sale.order'].search_count([
    ('user_id', '=', self.env.uid),
    ('state', 'not in', ['cancel', 'draft']),
])

# filtered() — applies a domain-like condition to an existing recordset
# (uses a lambda, not a domain list — different API)
confirmed = orders.filtered(lambda o: o.state == 'sale')

# _search() / _domain — advanced: used inside compute methods
# to build dynamic domains programmatically
domain = [('company_id', '=', self.env.company.id)]
if self.partner_id:
    domain += [('partner_id', '=', self.partner_id.id)]

Part 2 — Context

The context is a Python dictionary (`dict`) passed along with every RPC call, action open, and wizard invocation. It carries user-session information (language, timezone, active record) and developer-set flags that modify how models, views, and actions behave. You can read it in Python with `self.env.context` and extend it with `self.with_context(...)`.

Standard Context Keys

Commonly used context keys in Odoo 16–19
KeyTypeSet byEffect
langstrUser profileActive language code — e.g. `'en_US'`, `'ar_001'`. Used for translations.
tzstrUser profileTimezone string — e.g. `'Asia/Riyadh'`. Used by date/datetime fields.
uidintSessionCurrent user ID. Available in domain expressions in XML views.
active_idintUI / actionID of the currently selected/active record. Passed when opening a wizard or related action.
active_idslist[int]UI / actionList of IDs of all selected records (multi-record actions).
active_modelstrUI / actionTechnical name of the model the action was triggered from — e.g. `'sale.order'`.
default_field_nameanyDeveloper / actionSets a default value for `field_name` when a new record is created in the opened view. Replace `field_name` with the actual field name.
no_createboolDeveloperWhen True on a Many2one field, disables the quick-create option in the dropdown.
no_openboolDeveloperWhen True on a Many2one field, disables the external link / open record option.
group_bylist[str]Developer / search filterList of field names to group records by in list/kanban views.
search_default_field_name1Action contextPre-applies a search filter for `field_name` when the action opens. Value must be `1` (truthy).

Context in XML Views and Actions

xml
<!-- Set default values when opening a new record form -->
<field name="partner_id"
       context="{'default_company_type': 'company', 'default_country_id': ref('base.sa')}"/>

<!-- Pre-apply a search filter when an action opens -->
<record id="action_sale_order_my_orders" model="ir.actions.act_window">
  <field name="name">My Sales Orders</field>
  <field name="res_model">sale.order</field>
  <field name="context">{
    'search_default_user_id': uid,
    'search_default_state_sale': 1
  }</field>
</record>

<!-- Group by partner when the action opens -->
<record id="action_invoices_grouped" model="ir.actions.act_window">
  <field name="name">Invoices by Partner</field>
  <field name="res_model">account.move</field>
  <field name="context">{'group_by': ['partner_id']}</field>
</record>

<!-- Disable quick-create on a Many2one field -->
<field name="product_id"
       context="{'no_create': True, 'no_open': True}"/>

Context in Python Model Methods

python
# Read context in a model method
def action_confirm(self):
    # Check if called from a specific flow
    if self.env.context.get('from_purchase'):
        # custom behaviour
        pass

# with_context() — extend context for a specific ORM call
# Does NOT modify self.env.context — returns a new recordset
records_ar = self.env['product.template'].with_context(lang='ar_001').search([])

# Common: set a flag to skip a compute or onchange in a loop
self.with_context(skip_onchange=True).write({'state': 'done'})

# Pass context when opening a wizard action programmatically
return {
    'type': 'ir.actions.act_window',
    'res_model': 'my.wizard',
    'view_mode': 'form',
    'target': 'new',
    'context': {
        'default_partner_id': self.partner_id.id,
        'default_amount': self.amount_total,
        'active_id': self.id,
        'active_model': self._name,
    },
}

Using Domain and Context Together

Fields, buttons, and actions can carry both a `domain` and a `context` simultaneously. The domain filters which records are shown or selectable; the context controls defaults and flags for records created or opened in the resulting view.

xml
<!-- Smart button — open related records filtered by domain,
     with a default set via context for any new record created there -->
<button name="action_view_picking"
        type="object"
        domain="[('sale_id', '=', active_id), ('state', '!=', 'cancel')]"
        context="{
            'default_sale_id': active_id,
            'default_partner_id': partner_id,
            'search_default_state_ready': 1
        }"
        string="Transfers"/>

Version Notes

Domain and context behaviour across Odoo versions
Odoo versionKey domain / context changes
Odoo 16Domain syntax unchanged. `attrs` deprecated (see companion D6 guide). `<tree>` → `<list>` rename. `context_today()` available in XML domains.
Odoo 17`attrs` removed (domain on fields unaffected). `parent_of` operator added for hierarchical models. Context propagation in actions unchanged.
Odoo 18No breaking changes to domain or context API. `=?` null-safe operator behaviour documented in ORM changelog.
Odoo 19No breaking changes. Domain and context behaviour stable. ORM performance improvements to `search()` for large datasets.

Need Help with Custom Odoo Development or Module Upgrades?

Our Odoo-certified developers handle custom domain logic, context-driven workflows, and module migrations from Odoo 15 to 17/18/19 — with full ZATCA and GOSI compliance verification.

WhatsApp

Frequently Asked Questions

What is the difference between a domain on a field and a domain on an action?
A domain on a relational field (`domain` attribute on a Many2one, Many2many, or One2many) filters the records shown in the dropdown or list when a user selects a related record — it controls what is *selectable*. A domain on an `ir.actions.act_window` filters the records loaded when the action opens a list or form view — it controls what is *shown*. Both use the same domain list syntax.
Can I use a computed field's value inside a domain?
Yes, in Python ORM calls (`search`, `search_count`) any stored or non-stored computed field can be used in a domain, as long as it is marked `store=True`. For non-stored computed fields, ORM will fetch the field value for each record and filter in Python — this works but is slower than a SQL-backed stored field. In XML view domain strings (for Many2one dropdowns or action domains), you can reference the computed field by name as long as it is stored.
How do I pass context from a Python action to a wizard?
Return an action dictionary from your model method and include a `context` key. Odoo merges this context with the current session context before opening the wizard view. Use `default_field_name` keys to pre-fill wizard fields, and `active_id` / `active_ids` to tell the wizard which records it was called from. Example: `return {'type': 'ir.actions.act_window', 'res_model': 'my.wizard', 'view_mode': 'form', 'target': 'new', 'context': {'default_partner_id': self.partner_id.id, 'active_id': self.id}}`.
What does with_context() do and does it modify the current environment?
`with_context()` returns a new recordset (and associated environment) with the given keys merged into the context. It does NOT mutate `self.env.context` — the original recordset is unchanged. This means `self.with_context(lang='ar_001').search([])` reads translations in Arabic without affecting any other code running in the same transaction. Always use `self.with_context(...)` on the recordset you want to apply the context to, and assign the result if you need it back.
How do I pre-apply a search filter when an action opens?
Add `search_default_: 1` to the action's `context`, where `` is the `name` attribute of a `` element in the search view. The value must be `1` (integer, truthy). Example: if you have `` in your search view, set `context="{'search_default_my_orders': 1}"` on the action to have it pre-applied when the list opens.
Can I use context keys to disable a @api.onchange in a write() call?
`@api.onchange` is a client-side event triggered by the browser UI — it does not fire during server-side `write()` calls. So there is nothing to disable. What developers sometimes want to skip is a `@api.depends` compute or a `@api.constrains` check. For a compute, mark it with `store=False` if you want it to run on-the-fly. For a constraint, use `self.with_context(skip_constraint=True).write(...)` and check `self.env.context.get('skip_constraint')` inside the constraint method — but use this sparingly, as bypassing constraints can break data integrity.
iWesabe Editorial Team

iWesabe Editorial Team

Practitioner insights on Odoo ERP, ZATCA compliance, and Saudi enterprise digital operations — written by iWesabe's consulting, finance, and engineering teams.

About iWesabe

Related Articles