📋 What You Will Learn
- How to declare a computed field using
compute=and@api.depends - The difference between stored and non-stored computed fields
- How to make a computed field writable with
inverse= - Using
@api.depends_contextfor values that depend on the current user or language - Common patterns and pitfalls to avoid
What is a Computed Field?
A computed field is a field whose value is calculated by a Python method rather than entered directly by the user. You declare it like any normal field, but add a compute parameter pointing to the name of the method that fills it.
from odoo import models, fields, api
class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'
title = fields.Char(string='Title', required=True)
author_id = fields.Many2one('res.partner', string='Author')
display_name = fields.Char(
string='Display Name',
compute='_compute_display_name', # name of the method
)
@api.depends('title', 'author_id.name')
def _compute_display_name(self):
for record in self:
if record.author_id:
record.display_name = f"{record.title} — {record.author_id.name}"
else:
record.display_name = record.title
Key points about compute methods:
- The method always iterates over
self— even when only one record is involved. This is because Odoo calls compute methods on recordsets that may contain many records. - You assign to
record.field_nameinside the loop — you never return a value. - Every record in the loop must receive an assignment. If you skip a record, Odoo raises a
ValueError.
@api.depends Decorator
The @api.depends decorator tells Odoo which fields this computation depends on. Whenever any of the listed fields changes, Odoo marks the computed field as dirty and recalculates it (either immediately for stored fields, or on the next read for non-stored fields).
class LibraryLoan(models.Model):
_name = 'library.loan'
_description = 'Book Loan'
book_id = fields.Many2one('library.book', string='Book')
borrow_date = fields.Date(string='Borrow Date')
due_date = fields.Date(string='Due Date')
return_date = fields.Date(string='Return Date')
# Depends on two date fields — recomputes when either changes
@api.depends('due_date', 'return_date')
def _compute_days_overdue(self):
today = fields.Date.today()
for loan in self:
if loan.return_date:
# Already returned — not overdue
loan.days_overdue = 0
elif loan.due_date and loan.due_date < today:
loan.days_overdue = (today - loan.due_date).days
else:
loan.days_overdue = 0
days_overdue = fields.Integer(
string='Days Overdue',
compute='_compute_days_overdue',
)
Dot Notation in Depends
You can traverse relational fields using dot notation in @api.depends. This tells Odoo to recompute the field when the related field changes on the linked record:
@api.depends('author_id.name', 'author_id.country_id.name')
def _compute_author_info(self):
for book in self:
author = book.author_id
country = author.country_id.name or 'Unknown'
book.author_info = f"{author.name} ({country})" if author else ''
However, use deep dot notation sparingly — each additional level adds a dependency trigger that can cause unexpected recomputations across many records.
Stored vs Non-Stored Computed Fields
The store parameter controls whether the computed value is persisted in the database or calculated fresh on every read.
| Behaviour | Non-Stored (default) | Stored (store=True) |
|---|---|---|
| Database column | No | Yes |
| When computed | Every time the record is read | When a dependency changes (written to DB) |
| Searchable/filterable | No (by default) | Yes — like a normal field |
| Sortable in list views | No | Yes |
| Performance | CPU cost on every read | DB disk cost, but fast reads |
| Always up-to-date | Yes | Yes (triggered by depends) |
class LibraryBook(models.Model):
_name = 'library.book'
title = fields.Char(string='Title')
author_id = fields.Many2one('res.partner', string='Author')
# Non-stored: computed on every read, not searchable
display_name = fields.Char(
string='Display Name',
compute='_compute_display_name',
# store=False is the default — no need to write it
)
# Stored: has a DB column, searchable, sortable
search_name = fields.Char(
string='Search Name',
compute='_compute_search_name',
store=True, # saves to database
index=True, # good idea for fields you filter on
)
@api.depends('title', 'author_id.name')
def _compute_display_name(self):
for book in self:
book.display_name = f"{book.title} by {book.author_id.name or 'Unknown'}"
@api.depends('title', 'author_id.name')
def _compute_search_name(self):
for book in self:
book.search_name = f"{book.title} {book.author_id.name or ''}".strip().lower()
Use store=True when you need to search or sort by the computed field, when the computation is expensive and reads are frequent, or when you need to reference the field in a domain filter on a related model. For simple display-only values that are cheap to compute, leave store=False.
Inverse Functions (write-back)
By default, computed fields are read-only — you cannot write to them. Adding an inverse function makes the field writable. When the user (or code) sets a value on the computed field, Odoo calls the inverse function to translate that value back to the underlying fields.
class LibraryLoan(models.Model):
_name = 'library.loan'
borrow_date = fields.Date(string='Borrow Date', default=fields.Date.today)
loan_duration = fields.Integer(string='Duration (days)', default=14)
# Computed from borrow_date + loan_duration
# Inverse allows user to set due_date directly and back-calculate duration
due_date = fields.Date(
string='Due Date',
compute='_compute_due_date',
inverse='_inverse_due_date',
store=True,
)
@api.depends('borrow_date', 'loan_duration')
def _compute_due_date(self):
from datetime import timedelta
for loan in self:
if loan.borrow_date and loan.loan_duration:
loan.due_date = loan.borrow_date + timedelta(days=loan.loan_duration)
else:
loan.due_date = False
def _inverse_due_date(self):
from datetime import timedelta
for loan in self:
if loan.due_date and loan.borrow_date:
delta = loan.due_date - loan.borrow_date
loan.loan_duration = delta.days
else:
loan.loan_duration = 0
The inverse function has no decorator — it's a plain method. It receives self as a recordset, and reads self.field_name (the new value just set by the user) to update the source fields.
When you use both store=True and inverse, be careful: if the user directly sets the stored field to a value inconsistent with the dependencies, the next time a dependency changes the compute method will overwrite what the user set. Design your inverse and compute carefully to maintain a consistent state.
Computed Relational Fields
Computed fields can be any field type, including relational fields like Many2one, One2many, and Many2many. This is useful for building virtual relationships or aggregated collections.
class LibraryBook(models.Model):
_name = 'library.book'
loan_ids = fields.One2many('library.loan', 'book_id', string='All Loans')
# Computed Many2many — active loans only
active_loan_ids = fields.Many2many(
'library.loan',
string='Active Loans',
compute='_compute_active_loans',
)
# Computed Integer — count of overdue loans
overdue_loan_count = fields.Integer(
string='Overdue Loans',
compute='_compute_active_loans',
store=True,
)
@api.depends('loan_ids', 'loan_ids.state', 'loan_ids.days_overdue')
def _compute_active_loans(self):
for book in self:
active = book.loan_ids.filtered(lambda l: l.state == 'active')
book.active_loan_ids = active
book.overdue_loan_count = len(active.filtered(lambda l: l.days_overdue > 0))
# Computed Many2one — last borrower
last_borrower_id = fields.Many2one(
'res.partner',
string='Last Borrower',
compute='_compute_last_borrower',
store=True,
)
@api.depends('loan_ids.borrower_id', 'loan_ids.borrow_date')
def _compute_last_borrower(self):
for book in self:
last_loan = book.loan_ids.sorted('borrow_date', reverse=True)[:1]
book.last_borrower_id = last_loan.borrower_id if last_loan else False
@api.depends_context
@api.depends_context is used when the computed value depends on context variables rather than field values — most commonly the current user ('uid') or the user's language ('lang'). The cache is keyed per context value.
class LibraryBook(models.Model):
_name = 'library.book'
# This field is different for each user — it shows if the current user
# has this book in their reading list
is_in_my_list = fields.Boolean(
string='In My Reading List',
compute='_compute_is_in_my_list',
)
@api.depends_context('uid')
def _compute_is_in_my_list(self):
# self.env.uid is the current user's ID — available via context
my_list = self.env['library.reading.list'].search([
('user_id', '=', self.env.uid),
])
my_book_ids = my_list.mapped('book_id').ids
for book in self:
book.is_in_my_list = book.id in my_book_ids
Context-dependent fields cannot be stored because the value is per-user — storing one user's value would overwrite another's. Always leave store=False (the default) for fields that use @api.depends_context.
Common Patterns and Pitfalls
Missing Dependency — Stale Values
If a field used in the compute method is not listed in @api.depends, Odoo will not recompute the field when that field changes. The value will be stale:
# WRONG — penalty_amount is used but not listed in depends
# If penalty_per_day changes, full_cost will NOT update
@api.depends('days_overdue') # missing: 'penalty_per_day'
def _compute_full_cost(self):
for loan in self:
loan.full_cost = loan.days_overdue * loan.penalty_per_day
# CORRECT — both fields listed
@api.depends('days_overdue', 'penalty_per_day')
def _compute_full_cost(self):
for loan in self:
loan.full_cost = loan.days_overdue * loan.penalty_per_day
Circular Dependencies
If field A depends on field B and field B depends on field A, Odoo raises an odoo.exceptions.UserError at model load time. Break the cycle by introducing an intermediate field or restructuring the logic.
Always Assign a Value
Every record in the loop must receive a value. Using early continue without assigning causes a ValueError: cannot assign to the field error for skipped records:
# WRONG — records without a due_date are skipped and not assigned
def _compute_days_overdue(self):
today = fields.Date.today()
for loan in self:
if not loan.due_date:
continue # <-- this record is never assigned!
loan.days_overdue = (today - loan.due_date).days
# CORRECT — always assign, use a fallback for the missing-data case
def _compute_days_overdue(self):
today = fields.Date.today()
for loan in self:
if not loan.due_date:
loan.days_overdue = 0 # explicit fallback
else:
loan.days_overdue = max(0, (today - loan.due_date).days)
Performance: Batch Your Reads
Odoo's ORM prefetches fields automatically, but if you call external methods or search inside a compute loop, you may generate N queries for N records. Use mapped(), filtered(), and pre-fetch patterns instead:
# SLOW — search inside a loop → N database queries
def _compute_loan_count(self):
for book in self:
book.loan_count = self.env['library.loan'].search_count([
('book_id', '=', book.id)
])
# FAST — one search for all books, then distribute results
def _compute_loan_count(self):
# read_group returns aggregate data in a single query
data = self.env['library.loan'].read_group(
domain=[('book_id', 'in', self.ids)],
fields=['book_id'],
groupby=['book_id'],
)
loan_map = {row['book_id'][0]: row['book_id_count'] for row in data}
for book in self:
book.loan_count = loan_map.get(book.id, 0)
Summary
📋 Key Points
- Declare a computed field with
compute='_method_name'on the field definition - Decorate the compute method with
@api.depends('field1', 'field2.subfield') - Always iterate over
selfand assign to every record — never skip a record store=Truepersists the value to the database, making it searchable and sortableinverse='_method_name'makes the field writable by translating the value back to source fields@api.depends_context('uid')creates per-user cached computed fields — never stored- Missing a dependency causes stale values; a circular dependency raises an error at startup
- Use
read_groupand pre-fetching patterns to avoid N+1 queries in compute methods
FAQ
Not by default. You can add a search='_search_method_name' parameter to the field and define a search method that returns a domain — but it's complex and often slower than using store=True. For fields you plan to search or filter frequently, store=True is the practical choice.
Yes. Dot notation like @api.depends('partner_id.country_id.name') traverses relational fields across any model. Odoo tracks the dependency chain and will recompute your field when any field in the chain changes — even if the intermediate model is defined in another module.
Odoo adds the database column but leaves it empty (NULL) for existing records. It schedules a background recomputation job that fills in values for all existing records. For large tables this can take several minutes. If you need the field populated immediately for a migration, you may need a post-init hook or a manual _recompute_todo call.
This can happen during onchange handling in new (unsaved) records where related IDs do not yet exist. Your compute method must handle the case where relational fields return empty recordsets. Always guard with if record.related_field: or use or False / or 0 fallbacks for missing values.