Skip to content

Odoo ORM Cheat Sheet for Developers

DeployMonkey Team · March 11, 2026 9 min read

The Odoo ORM is powerful but dense. This cheat sheet condenses the most-used methods, patterns, and domain syntax into a single reference you can bookmark. Every snippet is runnable in the Odoo shell (odoo-bin shell) or inside a module method.

Environment and Recordsets

Everything in the Odoo ORM operates on recordsets — ordered collections of records of the same model, attached to an environment:

# env is available inside any model method as self.env
# Access a model
Partner = self.env['res.partner']

# env.user — current user record
# env.company — current company
# env.context — immutable dict of context values
# env.cr — raw psycopg2 cursor (use sparingly)

search — Find Records

# Basic search with a domain
partners = self.env['res.partner'].search([('is_company', '=', True)])

# With limit, offset, order
partners = self.env['res.partner'].search(
[('country_id.code', '=', 'US')],
limit=10,
offset=20,
order='name asc'
)

# Count without fetching records
count = self.env['res.partner'].search_count([('active', '=', True)])

# search_read — returns list of dicts, more efficient than search + read
rows = self.env['res.partner'].search_read(
[('is_company', '=', True)],
fields=['name', 'email', 'phone'],
limit=50
)

browse — Fetch by ID

# Single record
partner = self.env['res.partner'].browse(42)

# Multiple records (returns a recordset)
partners = self.env['res.partner'].browse([1, 2, 3])

# Check existence
if partner.exists():
print(partner.name)

create — Insert Records

# Single record
new_partner = self.env['res.partner'].create({
'name': 'Acme Corp',
'email': '[email protected]',
'is_company': True,
})

# Multiple records (Odoo 14+)
new_partners = self.env['res.partner'].create([
{'name': 'Alice', 'email': '[email protected]'},
{'name': 'Bob',   'email': '[email protected]'},
])

write — Update Records

# Update all records in a recordset at once
partners.write({'phone': '+1-800-ACME', 'active': True})

# Update a single field on one record
partner.name = 'Acme Corporation'  # triggers onchange, preferred for single records

# write() on an empty recordset is a no-op (safe)
self.env['res.partner'].browse([]).write({'name': 'x'})  # does nothing

unlink — Delete Records

# Delete all records in the recordset
old_logs = self.env['my.log'].search([('date', '<', '2024-01-01')])
old_logs.unlink()

# Always check for dependencies — Odoo raises UserError if restrict ondelete is triggered

filtered — In-Memory Filter

# Filter with a lambda
companies = partners.filtered(lambda p: p.is_company)

# Filter by field value (string shortcut)
active_partners = partners.filtered('active')

# Chain filters
us_companies = partners.filtered(
lambda p: p.is_company and p.country_id.code == 'US'
)

mapped — Extract or Transform Values

# Extract a field as a list
names = partners.mapped('name')  # ['Alice', 'Bob', ...]

# Traverse related fields
countries = partners.mapped('country_id')  # recordset of res.country

# Apply a function
totals = orders.mapped(lambda o: o.amount_total * 1.1)

sorted — Order a Recordset

# Sort by field name (ascending)
sorted_partners = partners.sorted('name')

# Descending
sorted_partners = partners.sorted('name', reverse=True)

# Sort by a lambda
sorted_orders = orders.sorted(key=lambda o: o.amount_total, reverse=True)

sudo — Bypass Access Rights

# Run as superuser (bypasses record rules and ACLs)
all_partners = self.env['res.partner'].sudo().search([])

# sudo() with a specific user
user = self.env['res.users'].browse(5)
record = self.env['sale.order'].sudo(user).browse(10)

# IMPORTANT: use sudo() sparingly — it bypasses all security checks

with_context — Pass Context Values

# Disable mail tracking for bulk writes
self.env['res.partner'].with_context(mail_notrack=True).write({'phone': '...'})

# Force a specific date for price computation
order = order.with_context(pricelist_date='2026-01-01')

# Set lang for translated field values
partner_fr = partner.with_context(lang='fr_FR')
print(partner_fr.name)  # returns French translation if available

# Pass arbitrary context to downstream methods
self.env['my.model'].with_context(my_flag=True).my_method()

Domain Syntax Reference

# Basic operators: =, !=, <, <=, >, >=, like, ilike, in, not in, child_of
[('name', 'ilike', 'acme')]          # case-insensitive contains
[('id', 'in', [1, 2, 3])]
[('partner_id', 'child_of', 7)]      # includes all children in hierarchy

# Logical operators: '&' (AND, default), '|' (OR), '!' (NOT)
# Prefix notation — operator comes before its two operands
['|', ('country_id.code', '=', 'US'), ('country_id.code', '=', 'CA')]
['&', ('is_company', '=', True), ('active', '=', True)]

# Negation
['!', ('active', '=', True)]         # same as [('active', '!=', True)]

# Nested
['|',
'&', ('is_company', '=', True), ('country_id.code', '=', 'US'),
('email', 'ilike', '@enterprise.com')
]

# Empty domain = no filter = all records
self.env['res.partner'].search([])  # returns everything (use with limit!)

Common Patterns

# Ensure singleton — raises if recordset has 0 or 2+ records
partner = partners.ensure_one()

# ids property — list of database IDs
id_list = partners.ids  # [1, 4, 7]

# Concatenate recordsets of the same model
all_partners = internal_partners | external_partners

# Intersection
shared = set_a & set_b

# Check membership
if partner in partners:
...

# _name — technical model name
print(self._name)  # 'res.partner'

Using the ORM on DeployMonkey Instances

DeployMonkey instances expose a full Odoo shell for debugging. Connect via the dashboard terminal or SSH and run:

odoo-bin shell -d your_database_name

All ORM operations above work directly in the shell — useful for one-off data fixes, debugging, or exploring data before writing a proper module. Pair with the Odoo debugging guide for a complete developer toolkit.

FAQ

What is the difference between search and browse?

search() queries the database using a domain filter and returns matching records. browse() returns a recordset for given IDs without querying — it trusts that the IDs exist. Use browse() when you already know the IDs.

Is it safe to use env.cr.execute() for raw SQL?

Use it only when the ORM cannot express the query efficiently. Always use parameterized queries (%s placeholders) to prevent SQL injection. Bypass the ORM cache with self.env.invalidate_all() after raw writes.

How do I avoid N+1 query problems in Odoo?

Use mapped() to prefetch related fields in bulk rather than accessing them in a loop. Also use search_read() to retrieve only needed columns. Odoo prefetches fields in batches automatically when you access them on a large recordset.

What does with_context(active_test=False) do?

By default, Odoo automatically adds ('active', '=', True) to domains on models with an active field. Setting active_test=False in context disables this filter, allowing you to search archived records.

Build Faster with a Reliable Hosting Platform

Great code needs great infrastructure. DeployMonkey hosts your Odoo modules with Git deployment, automatic backups, and monitoring — so you can focus on writing ORM queries, not managing servers. View plans or start free today.