Skip to content

Odoo Computed Fields: store, precompute, depends — Complete Guide

DeployMonkey Team · March 22, 2026 14 min read

What Are Computed Fields?

Computed fields calculate their value dynamically from other fields. Instead of storing data, they derive it on the fly. Example: total = price × quantity. Odoo computes the field every time it is read (or stores it in the database if store=True).

Basic Computed Field

class SaleOrderLine(models.Model):
    _inherit = 'sale.order.line'

    margin_percent = fields.Float(
        string='Margin %',
        compute='_compute_margin_percent',
    )

    @api.depends('price_subtotal', 'purchase_price', 'product_uom_qty')
    def _compute_margin_percent(self):
        for line in self:
            cost = line.purchase_price * line.product_uom_qty
            if line.price_subtotal:
                line.margin_percent = ((line.price_subtotal - cost) / line.price_subtotal) * 100
            else:
                line.margin_percent = 0.0

store=True vs store=False

Aspectstore=False (default)store=True
Database columnNoYes
Computed whenEvery readWhen dependency changes
SearchableNo (unless search method)Yes
SortableNoYes
Group byNoYes
PerformanceCPU on every readDisk space, write overhead
Use whenSimple calculations, display onlyNeed search/sort/group, heavy computation
# Not stored (computed on every read)
full_name = fields.Char(compute='_compute_full_name')

# Stored (computed once, stored in DB, recomputed on dependency change)
full_name = fields.Char(compute='_compute_full_name', store=True)

@api.depends

# Tells Odoo WHEN to recompute the field

# Simple dependency
@api.depends('price', 'quantity')
def _compute_total(self):
    for rec in self:
        rec.total = rec.price * rec.quantity

# Related model dependency (dot notation)
@api.depends('partner_id.country_id.code')
def _compute_is_domestic(self):
    for rec in self:
        rec.is_domestic = rec.partner_id.country_id.code == 'US'

# One2many dependency
@api.depends('line_ids.price_subtotal')
def _compute_total_amount(self):
    for rec in self:
        rec.total_amount = sum(rec.line_ids.mapped('price_subtotal'))

# Multiple dependencies
@api.depends('state', 'date_deadline', 'user_id')
def _compute_priority(self):
    ...

precompute (Odoo 17+)

# precompute=True: compute the value BEFORE the record is inserted
# Useful for fields needed in SQL constraints or defaults

slug = fields.Char(
    compute='_compute_slug',
    store=True,
    precompute=True,  # Computed before INSERT
)

@api.depends('name')
def _compute_slug(self):
    for rec in self:
        rec.slug = rec.name.lower().replace(' ', '-') if rec.name else ''

precompute=True means the field is computed during create() before the SQL INSERT. Without it, stored computed fields are computed after INSERT (via a separate UPDATE). Precompute is needed when the value must exist at INSERT time (e.g., for UNIQUE constraints).

Inverse Methods

# Make a computed field editable
full_name = fields.Char(
    compute='_compute_full_name',
    inverse='_inverse_full_name',
    store=True,
)

@api.depends('first_name', 'last_name')
def _compute_full_name(self):
    for rec in self:
        rec.full_name = f"{rec.first_name or ''} {rec.last_name or ''}".strip()

def _inverse_full_name(self):
    for rec in self:
        parts = (rec.full_name or '').split(' ', 1)
        rec.first_name = parts[0]
        rec.last_name = parts[1] if len(parts) > 1 else ''

Related Fields

# Shortcut for computed fields that just read from a related record
# Equivalent to a computed field with store=True

country_code = fields.Char(
    related='partner_id.country_id.code',
    string='Country Code',
    store=True,  # Optional: store for searching
)

# This is equivalent to:
@api.depends('partner_id.country_id.code')
def _compute_country_code(self):
    for rec in self:
        rec.country_code = rec.partner_id.country_id.code

Search Method (for non-stored computed fields)

# Allow searching on non-stored computed fields
age = fields.Integer(
    compute='_compute_age',
    search='_search_age',
)

def _compute_age(self):
    today = fields.Date.today()
    for rec in self:
        if rec.birth_date:
            rec.age = (today - rec.birth_date).days // 365
        else:
            rec.age = 0

def _search_age(self, operator, value):
    """Convert age search to birth_date domain."""
    today = fields.Date.today()
    if operator == '>':
        date_limit = today - timedelta(days=value * 365)
        return [('birth_date', '<', date_limit)]
    elif operator == '<':
        date_limit = today - timedelta(days=value * 365)
        return [('birth_date', '>', date_limit)]
    return []

Performance Tips

  • Use store=True if you need to search, sort, or group by the field
  • Use store=False for simple display-only calculations
  • Avoid N+1: Use mapped() for batch operations, not per-record queries
  • Minimize depends: Only list actually used dependencies — extra ones cause unnecessary recomputation
  • Use precompute for fields needed at INSERT time
  • Stored computed fields are NOT recomputed by -u — need manual recompute if logic changes

Common Mistake: Stored Fields Not Updating After -u

# If you change a stored computed field's logic and run -u,
# existing records are NOT recomputed!

# Fix: force recompute in init() or post_init_hook:
def init(self):
    self.env['my.model'].search([])._compute_my_field()

# Or via shell:
# env['my.model'].search([])._compute_my_field()
# env.cr.commit()