Odoo · Campos · ORM
Los campos compute son la herramienta más usada — y más mal configurada — en módulos Odoo. Un @api.depends incompleto o un store=True mal planteado provoca valores obsoletos, búsquedas rotas y tests intermitentes.
Tipos
Compute, related y almacenados
| Tipo | Definición | Se guarda en BD | Buscable / agrupable |
|---|---|---|---|
fields.Char() normal | Valor directo | Sí | Sí |
compute sin store | Calculado al leer | No | No |
compute + store=True | Calculado y persistido | Sí | Sí (con índice si hace falta) |
related='campo' | Alias de otro campo | Opcional (store=True) | Si está almacenado |
Sin
store=True, el valor se recalcula en cada lectura. Constore=True, Odoo lo escribe en PostgreSQL y lo invalida cuando cambian las dependencias declaradas en@api.depends.
Básico
Anatomía de un campo compute
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
line_count = fields.Integer(
string='Line Count',
compute='_compute_line_count',
store=True,
)
@api.depends('order_line')
def _compute_line_count(self):
for order in self:
order.line_count = len(order.order_line)
Reglas mínimas:
- El método compute se llama
_compute_<nombre_campo> - Asigna todos los registros del recordset (
for rec in self) @api.dependsdebe listar cada campo leído dentro del método- Con
store=True, añade dependencias de campos en registros relacionados con notación punto
store=True
¿Cuándo almacenar el resultado?
Usa store=True si…
- Filtras o agrupas por el campo en vistas o
search - El cálculo es costoso y se lee a menudo
- Exportas o reportas sobre ese valor
- Necesitas índice SQL para rendimiento
Evita store=True si…
- El valor cambia constantemente (fecha/hora actual)
- Depende de datos externos no rastreables
- Es un simple
relatedsin lógica extra - El campo casi nunca se busca ni agrupa
# Depende de campos del partner vinculado
@api.depends('partner_id', 'partner_id.country_id', 'partner_id.country_id.code')
def _compute_is_export(self):
for order in self:
code = order.partner_id.country_id.code
order.is_export = code and code != 'ES'
related='partner_id.email', store=True en lugar de un compute manual. Es más claro y el ORM optimiza la invalidación.
depends
@api.depends — la fuente de la verdad
Odoo solo recalcula un campo almacenado cuando cambia un campo listado en depends. Si lees line.price_subtotal pero solo declaras order_line, el total puede quedar desactualizado al cambiar el precio de una línea.
| Lees en el compute | Declara en depends |
|---|---|
order.order_line | 'order_line' |
line.product_uom_qty | 'order_line.product_uom_qty' |
partner.country_id.code | 'partner_id.country_id.code' |
| Campo de otro compute almacenado | Incluye ese campo o sus dependencias |
# ❌ Trampa: falta dependencia profunda
@api.depends('order_line')
def _compute_amount_custom(self):
for order in self:
order.amount_custom = sum(order.order_line.mapped('price_subtotal'))
# ✅ Correcto
@api.depends('order_line.price_subtotal')
def _compute_amount_custom(self):
...
Ciclos
Ciclos de dependencia
Dos campos almacenados que dependen uno del otro generan un ciclo. Odoo puede fallar al instalar el módulo o entrar en recomputes infinitos.
# ❌ Ciclo: total depende de discount, discount depende de total
total = fields.Float(compute='_compute_total', store=True)
discount = fields.Float(compute='_compute_discount', store=True)
@api.depends('discount')
def _compute_total(self): ...
@api.depends('total')
def _compute_discount(self): ...
Solución habitual: un solo campo almacenado «fuente» y el otro sin store, o extraer la lógica a un método privado sin dependencia circular.
depends incompleto, no un bug de la vista.Trampas
Cuatro trampas que ves en producción
| Síntoma | Causa probable | Arreglo |
|---|---|---|
| Valor correcto en form, mal en lista | Compute sin store + caché del navegador | store=True o forzar recomputo |
No se actualiza tras write en relación | Falta ruta en depends | Añadir line_id.campo |
search devuelve registros «viejos» | Campo almacenado desactualizado | records._compute_campo() o fix depends + -u |
| Tests flaky | Orden de creación / flush pendiente | flush_recordset() o asserts tras write |
⚠️ Nunca escribas un campo compute
inverse, un write({{'mi_campo_compute': valor}}) se ignora o falla. Si el usuario debe editarlo, define inverse='_inverse_mi_campo' o usa un campo normal con @api.onchange.Avanzado
inverse, compute_sudo y prefetch
display_total = fields.Monetary(
compute='_compute_display_total',
inverse='_inverse_display_total',
store=True,
)
@api.depends('order_line.price_total')
def _compute_display_total(self):
for order in self:
order.display_total = sum(order.order_line.mapped('price_total'))
def _inverse_display_total(self):
# Distribuir manualmente el total editado en las líneas
for order in self:
...
compute_sudo=True recalcula con permisos de superusuario — útil para campos que leen datos restringidos (p. ej. coste en producto), pero úsalo con criterio: puede exponer datos en vistas si no aplicas groups=.
En bucles sobre muchos registros, evita search dentro del compute. Prefiere mapped, read_group o precargar con depends bien definidos.
Checklist
Antes de hacer merge
- ¿Necesito buscar/agrupar por este campo? →
store=True - ¿
dependscubre todos los campos leídos, incluidos los de relaciones? - ¿Hay ciclo con otro compute almacenado?
- ¿El usuario edita el valor? →
inverseo campo no compute - ¿Test que cree, modifique relación y verifique el valor recalculado?
Resumen
Los campos compute son potentes cuando @api.depends es exhaustivo y store=True se reserva para campos buscables o costosos. La mayoría de «bugs de vista» son dependencias incompletas o ciclos entre campos almacenados. Declara el grafo completo, testea tras write en relaciones y evita escribir computes sin inverse.