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.0store=True vs store=False
| Aspect | store=False (default) | store=True |
|---|---|---|
| Database column | No | Yes |
| Computed when | Every read | When dependency changes |
| Searchable | No (unless search method) | Yes |
| Sortable | No | Yes |
| Group by | No | Yes |
| Performance | CPU on every read | Disk space, write overhead |
| Use when | Simple calculations, display only | Need 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.codeSearch 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()