Ad – 728×90
🔧 Backend Development

Odoo Computed Fields – @api.depends and Stored vs Non-Stored

Computed fields derive their value dynamically from other fields rather than being stored by users directly. They are one of the most powerful tools in Odoo backend development — letting you expose calculated data, enforce formatting, and build reactive UIs without writing view logic. This page covers everything you need to know about computed fields in Odoo 19: the @api.depends decorator, the store parameter, inverse functions for write-back, and context-dependent calculations.

⏱️ 22 min 🎯 Intermediate 📅 Updated 2026

📋 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_context for 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.

Python
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_name inside 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).

Python
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:

Python
@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.

Ad – 336×280

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.

BehaviourNon-Stored (default)Stored (store=True)
Database columnNoYes
When computedEvery time the record is readWhen a dependency changes (written to DB)
Searchable/filterableNo (by default)Yes — like a normal field
Sortable in list viewsNoYes
PerformanceCPU cost on every readDB disk cost, but fast reads
Always up-to-dateYesYes (triggered by depends)
Python
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()
💡
When to use store=True

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.

Python
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.

⚠️
Inverse and store=True

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.

Python
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.

Python
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
⚠️
Never use store=True with depends_context

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:

Python
# 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:

Python
# 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:

Python
# 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 self and assign to every record — never skip a record
  • store=True persists the value to the database, making it searchable and sortable
  • inverse='_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_group and pre-fetching patterns to avoid N+1 queries in compute methods

FAQ

Can I search on a non-stored computed field? +

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.

Does @api.depends work across modules (on fields from other models)? +

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.

What happens to stored computed fields when I install the module on an existing database? +

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.

Why does my compute method run with an empty self (no records)? +

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.