Odoo · Refactoring · Buenas prácticas
Todo proyecto Odoo con unos años tiene su módulo intocable: un write() sobrescrito de doscientas líneas, búsquedas dentro de bucles y APIs que se migraron de versión en versión sin entenderlas. La tentación es reescribirlo entero. La alternativa que funciona es menos heroica: fijar el comportamiento con tests y mejorarlo pieza a pieza.
Paso 0
Tests de caracterización antes de tocar nada
Un test de caracterización no comprueba lo que el código debería hacer, sino lo que hace hoy, bugs incluidos. Su único objetivo es avisarte si el refactor cambia el comportamiento. Si descubres un bug mientras los escribes, anótalo y arréglalo en un commit aparte: mezclar arreglos con refactor hace imposible revisar ninguno de los dos.
class TestTripLegacyBehaviour(TransactionCase):
def test_confirm_sets_vehicle_busy(self):
trip = self.env["fleet.trip"].create({
"name": "Valencia", "vehicle_id": self.vehicle.id,
})
trip.write({"state": "confirmed"})
# Comportamiento actual, aunque sea discutible: lo fijamos tal cual
self.assertEqual(self.vehicle.trip_status, "busy")
self.assertTrue(trip.confirmed_date)
Responsabilidades
Desmontar el write() que lo hace todo
El síntoma clásico del código legacy es un write() que mira qué claves vienen en vals y reacciona a cada una. Cada reacción suele tener un sitio mejor: un campo calculado almacenado, un método de acción explícito o una constraint.
# ❌ Antes: efectos secundarios escondidos en write()
def write(self, vals):
res = super().write(vals)
if vals.get("state") == "confirmed":
for trip in self:
trip.vehicle_id.trip_status = "busy"
trip.confirmed_date = fields.Datetime.now()
if "km_end" in vals:
for trip in self:
trip.distance = trip.km_end - trip.km_start
return res
# ✅ Después: cada responsabilidad en su mecanismo
distance = fields.Float(compute="_compute_distance", store=True)
@api.depends("km_start", "km_end")
def _compute_distance(self):
for trip in self:
trip.distance = trip.km_end - trip.km_start
def action_confirm(self):
self.write({"state": "confirmed", "confirmed_date": fields.Datetime.now()})
self.vehicle_id.write({"trip_status": "busy"})
Ahora el cálculo de distancia se recalcula solo y no depende de que alguien escriba km_end por un camino concreto, y la confirmación es un método con nombre que los botones, los tests y las acciones de servidor pueden llamar. Fiérate en que el test de caracterización hacía write({'state': ...}): si hay código externo que confirma así, toca buscarlo y cambiarlo a action_confirm() antes de quitar el comportamiento del write.
Rendimiento
Del bucle con search a una sola consulta
El segundo clásico: un campo calculado que hace un search_count por registro. Con 20 registros nadie lo nota; con 20.000, la lista tarda segundos. _read_group resuelve todo el lote en una consulta agregada.
# ❌ Una consulta por vehículo
def _compute_trip_count(self):
for vehicle in self:
vehicle.trip_count = self.env["fleet.trip"].search_count(
[("vehicle_id", "=", vehicle.id)]
)
# ✅ Una consulta para todo el recordset
def _compute_trip_count(self):
counts = dict(self.env["fleet.trip"]._read_group(
[("vehicle_id", "in", self.ids)], ["vehicle_id"], ["__count"],
))
for vehicle in self:
vehicle.trip_count = counts.get(vehicle, 0)
APIs antiguas
Lo que el código heredado arrastra de versiones anteriores
Muchos módulos han sobrevivido a varias migraciones aplicando el mínimo imprescindible. Estos son los restos más comunes y su equivalente actual en Odoo 19:
| Legacy | Odoo 19 |
|---|---|
name_get() | _compute_display_name() |
attrs="{'invisible': [...]}" en vistas | invisible="state != 'draft'" |
_sql_constraints = [...] | models.Constraint(...) |
read_group() con dicts | _read_group() con tuplas |
cr.execute() con % o f-strings | cr.execute(SQL(...)) |
@api.onchange que calcula valores | compute con store=True, readonly=False |
groups_id en usuarios | group_ids |
name_get, mover lógica del write y optimizar un compute en el mismo commit convierte cualquier regresión en una búsqueda a ciegas. Commits pequeños, cada uno con la suite en verde, permiten hacer git bisect si algo se rompe semanas después.
Resumen
Fija el comportamiento actual con tests de caracterización, saca los efectos secundarios del write() a computes y métodos de acción, sustituye los bucles con search por _read_group y moderniza las APIs antiguas de una en una. Refactorizar a pasos pequeños es más lento el primer día y mucho más rápido el resto del proyecto (ver también los antipatrones más comunes).