Ir al contenido

Refactorizar código Odoo: de legacy a buenas prácticas

Simplificación, separación de responsabilidades y tests que te dejan cambiar sin miedo
9 de octubre de 2026 por
Refactorizar código Odoo: de legacy a buenas prácticas
ATEMI, Aitor Atencia

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.

Logo Odoo
Refactorizar es cambiar la forma del código sin cambiar lo que hace. Sin tests, no hay forma de saberlo.

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:

LegacyOdoo 19
name_get()_compute_display_name()
attrs="{'invisible': [...]}" en vistasinvisible="state != 'draft'"
_sql_constraints = [...]models.Constraint(...)
read_group() con dicts_read_group() con tuplas
cr.execute() con % o f-stringscr.execute(SQL(...))
@api.onchange que calcula valorescompute con store=True, readonly=False
groups_id en usuariosgroup_ids

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

en Odoo
Auditoría técnica de módulos Odoo antes de producción
Checklist de calidad, seguridad y rendimiento para un release seguro