ORM Methods
Examples target Odoo 18 and later unless marked. Tabs show the Odoo 16 and 17 versions where the API differs.
Use this page as a quick reference for the Odoo ORM methods you call most: creating, searching, updating and deleting records, building domains, working with recordsets and the environment, and customising display names and name search.
Quick CRUD
The four calls below cover the basic life cycle of a record: create() adds one, search() finds records, write() changes them and unlink() deletes them. The sections under CRUD in detail cover single and multiple records for each call.
# Create a record
self.env['model.name'].create({'field': value})# Read records
records = self.env['model.name'].search([('field', '=', value)])# Update records
record.write({'field': new_value})# Delete records
record.unlink()Advanced usage
These patterns build on the basic methods for cases where a single condition or call is not enough.
Combine domains with OR
A domain is a list of conditions joined by AND by default. Put the '|' operator before two conditions to join them with OR, as in the example below, where field1 must match and either field2 or field3 must match.
records = self.env['model.name'].search([
('field1', '=', value1),
'|', ('field2', '!=', value2), ('field3', 'in', [1, 2, 3]),
])CRUD in detail
These sections repeat the four CRUD calls from Quick CRUD and add the single-record and multi-record forms.
Create records
Call create() with a dictionary to add one record, or with a list of dictionaries to add several records in one call, which is faster than creating them in a loop.
# Create a single record
self.env['model.name'].create({
'field1': value1,
'field2': value2,
})
# Create multiple records
self.env['model.name'].create([
{'field1': value1, 'field2': value2},
{'field1': value3, 'field2': value4},
])Read records
Use search() to find records by domain, browse() to load records from known IDs, read() to get field values as dictionaries, and attribute access to read a single field value.
# Search records
records = self.env['model.name'].search([('field', '=', value)])
# Browse by ID
record = self.env['model.name'].browse(id)
# Read specific fields
values = record.read(['field1', 'field2'])
# Get field value
value = record.field_nameUpdate records
Call write() on a record or a recordset with a dictionary of new values. On a recordset, one call updates every record in it.
# Update single record
record.write({
'field1': new_value1,
'field2': new_value2,
})
# Update multiple records
records.write({
'field1': new_value1,
'field2': new_value2,
})Delete records
Call unlink() on a record or a recordset to delete it permanently. Deleting cannot be undone, so check the recordset first.
# Delete single record
record.unlink()
# Delete multiple records
records.unlink()Search methods
Use search() with a domain, a list of conditions, to find records. The sections below cover the domain syntax and the operators you can use.
Search domain
A search domain is a list of (field, operator, value) tuples passed to search(). Conditions are joined with AND unless you add '|' (OR), '&' (AND) or '!' (NOT) in front of them, which Odoo reads in prefix notation.
# Basic search
records = self.env['model.name'].search([
('field1', '=', value1),
('field2', '!=', value2),
])
# Complex conditions
records = self.env['model.name'].search([
'|', # OR
'&', # AND
('field1', '=', value1),
('field2', '!=', value2),
('field3', 'in', [value3, value4]),
])Search operators
Use these operators in the second position of a domain tuple. Use ilike for case-insensitive text matching and in to match any value in a list.
# Common operators
'=' # equals
'!=' # not equals
'>' # greater than
'>=' # greater than or equal
'<' # less than
'<=' # less than or equal
'like' # pattern matching
'ilike' # case-insensitive pattern matching
'in' # in list
'not in' # not in listRecordset operations
A recordset is a collection of records. You can filter, map and sort it in Python without extra database queries.
Filtering
filtered() keeps the records for which a Python function returns true, filtered_domain() keeps the records that match a domain without querying the database again, and mapped() returns the values of a field (or a recordset for a relational field) across all records. Use these on a recordset you already loaded instead of running a new search().
# Filter records
filtered_records = records.filtered(lambda r: r.field > value)
# Filter by domain
filtered_records = records.filtered_domain([('field', '=', value)])
# Map to field values
values = records.mapped('field_name')Sorting
sorted() returns a new recordset ordered by a field name or a key function. Pass reverse=True for descending order.
# Sort by field
sorted_records = records.sorted('field_name')
sorted_records = records.sorted(key=lambda r: r.field_name)
# Reverse sort
reversed_records = records.sorted('field_name', reverse=True)Environment methods
The environment (self.env) holds the current user, company and context, and lets you run code as a different user or company.
Context
The context is a dictionary of flags that travels with the environment. Read it with self.env.context, use with_context() to get a copy of the recordset with extra keys, and use with_company() to run code as a different company.
# Get context
context = self.env.context
# Update context
self.with_context(key=value)
self.with_context(dict(key=value))
# Switch company
self.with_company(company_id)User
self.env.user is the user running the code. Use it to read the user's companies and to test group membership with has_group() before you allow an action.
# Get current user
user = self.env.user
# Get user companies
companies = self.env.user.company_ids
# Check access rights
self.env.user.has_group('group_name')Common patterns
These are the ORM customisations you meet most often when extending a model.
Display name
To change how a record is shown in many2one fields and breadcrumbs, set the computed display_name field. Odoo 17 deprecates name_get() in favour of display_name, and Odoo 18 removes name_get(), so override _compute_display_name in Odoo 17 and later and name_get() in Odoo 16 and earlier.
@api.depends('code', 'name')
def _compute_display_name(self):
for record in self:
record.display_name = f"{record.code} - {record.name}"Name search
Name search runs when a user types in a many2one field. To search more than the name field, such as a code, override the hook for your version: name_search (or _name_search) in Odoo 16, _name_search in Odoo 17 and _search_display_name in Odoo 18 and later. Starting in Odoo 18, _search_display_name returns a domain and name_search is no longer the hook to override.
@api.model
def _search_display_name(self, operator, value):
if operator in ('ilike', 'like', '=', '=like', '=ilike') and value:
return ['|', ('name', operator, value), ('code', operator, value)]
return super()._search_display_name(operator, value)Default get
default_get() returns the initial values of a new record's fields. Override it to set defaults that depend on the context or on other data, and call super() first to keep the standard defaults.
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
defaults['field'] = self.env.context.get('default_field', False)
return defaultsRelated
- Computed fields for
@api.dependsand compute methods. - API decorators for
@api.model,@api.dependsand the other decorators used above. - Inheritance in Odoo for extending models with
_inherit. - Security rules for access rights and record rules.
- Smart suggestions in VS Code and in PyCharm to complete ORM calls and field names in the editor.
- Python snippets in Assista IDE to insert common ORM code.