Odoo · Arquitectura · SOLID
Los principios SOLID no son teoría de laboratorio: son la brújula para construir módulos Odoo mantenibles, extensibles y libres de deuda técnica.
Contexto
¿Por qué SOLID en Odoo?
Odoo ya trae un framework de extensión potente: _inherit, mixins (mail.thread, mail.activity.mixin), wizards transitorios y un ORM declarativo. Pero sin criterio arquitectónico, es fácil acabar con modelos de 800 líneas, lógica duplicada en tres sitios y módulos imposibles de actualizar entre versiones.
«Extender sin romper» es exactamente lo que SOLID propone — y Odoo lo hace posible si lo aplicas con intención.
S
S Single Responsibility — Una razón de cambio
Cada modelo, servicio o wizard debe tener una sola responsabilidad. Si un método calcula precios, envía emails y actualiza inventario, tienes tres razones para modificarlo.
❌ Mal
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
res = super().action_confirm()
self._send_whatsapp()
self._update_crm_score()
self._create_project_tasks()
return res
✅ Bien
# sale_whatsapp/models/sale_order.py
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
res = super().action_confirm()
self._send_whatsapp()
return res
# project_sale/models/sale_order.py
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
res = super().action_confirm()
self._create_project_tasks()
return res
Un módulo por integración. Cada uno sobrescribe action_confirm llamando a super(). Odoo encadena las herencias automáticamente.
O
O Open/Closed — Abierto a extensión, cerrado a modificación
_inherit es la herramienta natural del principio Open/Closed.Nunca modifiques código del core de Odoo. Extiende con _inherit o crea módulos puente:
class ResPartner(models.Model):
_inherit = 'res.partner'
birth_date = fields.Date(string='Birth Date')
age = fields.Integer(compute='_compute_age', store=True)
@api.depends('birth_date')
def _compute_age(self):
today = fields.Date.today()
for partner in self:
if partner.birth_date:
partner.age = today.year - partner.birth_date.year
else:
partner.age = 0
Si mañana necesitas otro campo, añades otro módulo que herede res.partner. El módulo original no se toca.
L
L Liskov Substitution — Las subclases no rompen el contrato
Si heredas un modelo y sobrescribes un método, el comportamiento debe ser compatible con lo que el padre promete. No cambies la semántica de retorno ni relajes validaciones que el core espera.
Regla práctica en Odoo
super().action_confirm()siempre al inicio o al final — nunca lo omitas- No lances excepciones en estados donde el padre no las lanza
- Si añades validaciones (
@api.constrains), documenta por qué - Los métodos
create/writedeben aceptar los mismosvalsque el padre
I
I Interface Segregation — No obligues a implementar lo que no usas
En Odoo esto se traduce en mixins selectivos y modelos abstractos pequeños:
# ❌ Heredar mail.thread en un modelo que no necesita chatter
class ProductLabel(models.Model):
_name = 'product.label'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char()
# ✅ Solo los mixins que aportan valor
class DiagnosticTest(models.Model):
_name = 'diagnostic.test'
_inherit = ['mail.thread'] # Sí necesita historial
_description = 'Diagnostic Test'
Tampoco crees métodos «comodín» de 15 parámetros opcionales. Prefiere métodos pequeños con responsabilidad clara o wizards para flujos complejos.
D
D Dependency Inversion — Depende de abstracciones, no de implementaciones
En Odoo el equivalente es usar self.env['modelo'] en lugar de importar clases directamente, y delegar integraciones externas a servicios inyectables:
class PaymentGateway(models.AbstractModel):
_name = 'payment.gateway'
_description = 'Payment Gateway Interface'
def charge(self, amount, token):
raise NotImplementedError
class PaymentGatewayStripe(models.Model):
_name = 'payment.gateway.stripe'
_inherit = 'payment.gateway'
def charge(self, amount, token):
# Lógica Stripe
...
El modelo de negocio llama a self.env['payment.gateway'] sin saber si detrás hay Stripe, Redsys o un mock en tests.
Patrones
SOLID en la práctica diaria
| Situación | Patrón SOLID + Odoo |
|---|---|
| Lógica de negocio compleja | Extraer a métodos privados o modelos abstract |
| Integración externa (API, SMS) | Modelo dedicado + inyección vía env |
| Flujo multi-paso | Wizard (TransientModel) con SRP |
| Extender funcionalidad estándar | _inherit en módulo separado (OCP) |
| Evitar lógica en controladores | Controlador delgado → método del modelo |
| Evitar lógica en QWeb | Datos preparados en Python, plantilla tonta |
Antipatrones
Señales de alarma en tu código
- Modelo con más de 300–400 líneas → probable violación de SRP
sudo()esparcido sin justificación → seguridad y DIP rotos- Copiar-pegar bloques entre módulos → falta de abstracción
- XML con
if/elsecomplejos → lógica que debería estar en Python - Módulo «misc» o «utils» que crece sin control → cajón de sastre
💡 Consejo
Resumen
SOLID no contradice el estilo Odoo: lo refuerza. Un módulo por responsabilidad, herencia con super(), mixins solo cuando aportan, y dependencias resueltas vía env. El resultado son addons que sobreviven a actualizaciones de versión y equipos que no tienen miedo de tocar el código.