Desarrollo de Módulos Personalizados en Odoo 18
Odoo permite extender su funcionalidad mediante módulos personalizados. Odoo 18 introduce cambios importantes en la API, el sistema de herencia y las vistas web. Este artículo guía el proceso completo de desarrollo: desde la estructura básica del manifiesto hasta la creación de modelos, vistas, reglas de seguridad y pruebas automatizadas.
1. Estructura de un Módulo Odoo
Todo módulo de Odoo sigue una estructura de directorios estandarizada con carpetas para modelos, vistas, seguridad, datos y tests. La estructura mínima incluye los archivos __init__.py y __manifest__.py en la raíz, y subdirectorios organizados por responsabilidad.
2. El Archivo __manifest__.py
El manifiesto define los metadatos del módulo y sus dependencias. En Odoo 18, el campo assets se usa para declarar archivos JavaScript, CSS y XML del frontend. Los campos depends son críticos ya que Odoo cargará los módulos dependientes antes que el tuyo, garantizando que las clases y tablas existan.
3. Definición de Modelos
Los modelos en Odoo 18 se definen heredando de models.Model. Cada modelo representa una tabla en la base de datos con campos de不同类型 (Char, Integer, Float, Many2one, One2many, Many2many, Selection, Boolean, Text, Datetime, Date, Binary).
Ejemplo completo de modelo con workflow, campos compute y validaciones:
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class PedidoPersonalizado(models.Model):
_name = 'pedido.personalizado'
_description = 'Pedido Personalizado'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
name = fields.Char(
string='Referencia', required=True, copy=False,
readonly=True, default='Nuevo'
)
partner_id = fields.Many2one(
comodel_name='res.partner', string='Cliente',
required=True, tracking=True
)
user_id = fields.Many2one(
comodel_name='res.users', string='Vendedor',
default=lambda self: self.env.user, tracking=True
)
state = fields.Selection([
('borrador', 'Borrador'),
('aprobado', 'Aprobado'),
('confirmado', 'Confirmado'),
('cancelado', 'Cancelado'),
], string='Estado', default='borrador', tracking=True, copy=False)
line_ids = fields.One2many(
comodel_name='pedido.personalizado.linea',
inverse_name='pedido_id', string='Líneas'
)
amount_total = fields.Monetary(
string='Total', compute='_compute_amount_total',
store=True, currency_field='currency_id'
)
currency_id = fields.Many2one(
comodel_name='res.currency',
default=lambda self: self.env.company.currency_id
)
note = fields.Text(string='Notas')
@api.depends('line_ids.subtotal')
def _compute_amount_total(self):
for rec in self:
rec.amount_total = sum(line.subtotal for line in rec.line_ids)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', 'Nuevo') == 'Nuevo':
vals['name'] = self.env['ir.sequence'].next_by_code(
'pedido.personalizado'
) or 'Nuevo'
return super().create(vals_list)
def action_aprobar(self):
self.ensure_one()
if self.state != 'borrador':
raise ValidationError('Solo se pueden aprobar borradores.')
self.state = 'aprobado'
self.message_post(body='Pedido aprobado por %s' % self.env.user.name)
def action_confirmar(self):
self.ensure_one()
if self.state != 'aprobado':
raise ValidationError('Debe estar aprobado para confirmar.')
self.state = 'confirmado'
def action_cancelar(self):
self.ensure_one()
if self.state == 'confirmado':
raise ValidationError('No se puede cancelar un pedido confirmado.')
self.state = 'cancelado'
4. Modelos con Herencia (Inheritance)
Odoo soporta tres tipos de herencia. El más común es _inherit para agregar campos a un modelo existente sin modificar la tabla original. La herencia por delegación (_inherits) crea una relación de tabla extendida que permite agregar funcionalidad sin tocar el modelo base.
Ejemplo de herencia para agregar campos a res.partner:
class ResPartner(models.Model):
_inherit = 'res.partner'
es_cliente_frecuente = fields.Boolean(
string='Cliente Frecuente', default=False
)
nivel_descuento = fields.Selection([
('0', 'Sin descuento'), ('5', '5%'),
('10', '10%'), ('15', '15%'),
], string='Nivel de Descuento', default='0')
fecha_ultimo_pedido = fields.Datetime(string='Fecha Último Pedido')
5. Definición de Vistas XML
Las vistas definen la interfaz de usuario. Odoo 18 usa el sistema de herencia de vistas para extender vistas existentes con xpath. Los campos invisible, readonly y required ahora usan expresiones Python directamente en lugar del sistema attrs anterior.
Ejemplo de vista de formulario con header, sheet, notebook y chatter:
<record id="view_pedido_personalizado_form" model="ir.ui.view">
<field name="name">pedido.personalizado.form</field>
<field name="model">pedido.personalizado</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_aprobar" string="Aprobar"
type="object" class="btn-primary"
invisible="state != 'borrador'" />
<button name="action_confirmar" string="Confirmar"
type="object" class="btn-success"
invisible="state != 'aprobado'" />
<field name="state" widget="statusbar"
statusbar_visible="borrador,aprobado,confirmado" />
</header>
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="Referencia" /></h1>
</div>
<group>
<group>
<field name="partner_id" />
<field name="user_id" />
</group>
<group>
<field name="amount_total" widget="monetary" />
</group>
</group>
<notebook>
<page string="Líneas" name="lineas">
<field name="line_ids">
<tree editable="bottom">
<field name="producto_id" />
<field name="cantidad" />
<field name="precio_unitario" />
<field name="subtotal" widget="monetary" />
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" />
<field name="activity_ids" />
<field name="message_ids" />
</div>
</form>
</field>
</record>
6. Reglas de Seguridad y Accesos
El archivo ir.model.access.csv define los permisos CRUD por grupo. Las reglas de registro (record rules) controlan qué filas puede ver cada usuario usando dominios del ORM. Configura permisos separados para usuarios normales y managers para garantizar la seguridad de los datos.
7. Métodos Compute, Onchange y Constrains
Los campos compute se recalculan automáticamente cuando cambian sus dependencias declaradas con @api.depends. Los métodos onchange se ejecutan en el formulario cuando cambia un campo. Los constrains validan datos antes de guardar y lanzan ValidationError si la validación falla.
8. Usando el ORM de Odoo para Búsquedas
El ORM de Odoo ofrece múltiples métodos para buscar, crear, escribir y eliminar registros. Usa search con dominios, search_read para lectura eficiente, search_fetch para muchos registros, y write para escritura masiva. Los dominios soportan operadores como '=', '!=', '>', '<', 'in', 'like', y combinaciones con '|' y '&'.
9. Wizard (Transient Models)
Los wizards son formularios temporales que no guardan datos permanentemente. Se definen heredando de models.TransientModel y se ejecutan como acciones de ventana. Son ideales para procesos como cancelaciones con motivo, exportaciones con filtros, y confirmaciones con observaciones.
10. Pruebas Unitarias
Las pruebas aseguran que tu módulo funciona correctamente. Odoo usa unittest con fixtures de base de datos. Crea una clase que herede de TransactionCase, define setUp con los datos de prueba necesarios, y escribe métodos que validen cada funcionalidad del módulo.
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
class TestPedidoPersonalizado(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({
'name': 'Cliente Test', 'email': '[email protected]',
})
self.producto = self.env['product.product'].create({
'name': 'Producto Test', 'list_price': 100.0,
})
def test_crear_pedido(self):
pedido = self.env['pedido.personalizado'].create({
'partner_id': self.partner.id,
'line_ids': [(0, 0, {
'producto_id': self.producto.id,
'cantidad': 5, 'precio_unitario': 100.0,
})],
})
self.assertEqual(pedido.state, 'borrador')
self.assertEqual(pedido.amount_total, 500.0)
11. Desarrollo de Componentes OWL (Frontend)
Odoo 18 usa el framework OWL para la interfaz web. Los componentes se definen en archivos JavaScript usando Component, useState y useService. Los templates se definen en archivos XML con la sintaxis t-esc, t-if, t-foreach y t-on-click para manejar eventos.
12. Integración con el Sistema de Email
Odoo integra un sistema de email potente. Los emails se envían automáticamente al usar mail.thread. Configura templates de email en XML con campos dinámicos usando la sintaxis object.nombre_campo para personalizar los mensajes enviados a clientes y usuarios.
Conclusión
El desarrollo de módulos en Odoo 18 combina la potencia del ORM de Python con un sistema de vistas declarativo. Dominar los patrones de herencia, los campos compute y las reglas de seguridad te permite crear soluciones empresariales robustas. Las pruebas unitarias son esenciales para mantener la calidad del código a medida que tu módulo evoluciona.
13. Uso del ORM para Operaciones Complejas
El ORM de Odoo soporta operaciones complejas que van mas alla de las busquedas simples. Los grupos (GROUP BY) se implementan con read_group() que retorna conteos, sumas, promedios y otros agregados. Los dominios con expresiones SQL permiten ejecutar consultas personalizadas sin salir del ORM.
Para operaciones de escritura masiva, usa write() en un recordset que contenga multiples registros. Esto ejecuta un solo UPDATE en la base de datos en lugar de multiples UPDATE individuales, lo que es significativamente mas eficiente para miles de registros.
Los recordset operations como filtered(), mapped(), y sorted() permiten manipular colecciones de registros de forma funcional. filtered() selecciona registros segun un predicado, mapped() extrae un campo de cada registro, y sorted() ordena los registros segun un criterio.
Para transacciones complejas que modifican multiples modelos, usa savepoint() para crear puntos de restauracion que permiten revertir cambios parciales si ocurre un error. Esto es especialmente util en wizards que ejecutan multiples operaciones.
14. Manejo de Errores y Excepciones
Odoo define multiples tipos de excepciones que se deben usar segun el contexto. UserError es la mas comun y muestra un mensaje de error al usuario. ValidationError se usa para violaciones de reglas de negocio. AccessError para problemas de permisos, y MissingError cuando un registro no existe.
Los try/except en metodos de modelo deben ser especificos y manejar cada tipo de error por separado. Un error de base de datos (psycopg2.Error) requiere una respuesta diferente a un error de validacion de negocio (ValidationError).
Para errores que ocurren en cron jobs, asegurate de que el metodo tiene un manejo de errores robusto que no detenga la ejecucion de otros cron jobs. Usa try/except para capturar errores y registrarlos en el log, pero permite que el cron job continue con el siguiente registro.
Los logs de errores deben incluir suficiente contexto para diagnosticar el problema: el ID del registro afectado, los valores de los campos relevantes, y el stack trace completo. Este nivel de detalle facilita enormemente la resolucion de problemas en produccion.
15. Patrones de Diseno en Odoo
El patron Repository encapsula las consultas de base de datos en clases dedicadas que centralizan la logica de acceso a datos. Esto mejora la mantenibilidad y permite reutilizar consultas entre multiples modelos sin duplicar codigo.
El patron Service separa la logica de negocio de los modelos ORM. Los servicios se definen como clases que reciben modelos como dependencias y ejecutan operaciones complejas que involucran multiples modelos. Este patron es ideal para procesos como la facturacion, la conciliacion bancaria, o la generacion de reportes.
El patron Event permite desacoplar componentes mediante la publicacion y suscripcion de eventos. Cuando un modelo ejecuta una accion, publica un evento que otros componentes pueden escuchar y responder sin modificar el modelo original. Esto facilita la extension de funcionalidades.
El patron Strategy permite seleccionar algoritmos o comportamientos en tiempo de ejecucion. Por ejemplo, puedes definir multiples estrategias de calculo de precios y seleccionar la adecuada segun la configuracion del pedido. Esto evita condicionales complejos en el codigo.
16. Integracion con el Sistema de Chatter
El chatter es el sistema de comunicaciones integrado en cada formulario de Odoo. Permite a los usuarios enviar mensajes, asignar actividades, y seguir registros especificos. Para que tu modelo soporte el chatter, hereda de mail.thread y mail.activity.mixin en la definicion del modelo.
Los mensajes del chatter se envian con message_post() que acepta un body (texto plano o HTML), subtype_mail (para controlar que usuarios reciben la notificacion), y partner_ids (para mencionar usuarios especificos). Los mensajes pueden incluir archivos adjuntos usando attachment_ids.
Las actividades del chatter (mail.activity) son tareas asignadas a usuarios especificos con una fecha de vencimiento. Se crean con activity_schedule() y se completan con activity_done(). Las actividades aparecen en el calendario del usuario y generan notificaciones automaticas.
El follow/unfollow de registros permite a los usuarios suscribirse a las notificaciones de cambios en registros especificos. Los followers reciben notificaciones por email cuando otros usuarios publican mensajes en el chatter del registro.
Para personalizar las notificaciones del chatter, usa los subtipos de mensaje que controlan que usuarios reciben cada tipo de notificacion. Los subtipos se definen en XML y se asocian a los mensajes con el parametro subtype_mail de message_post().
17. Uso de QWeb para Templates
QWeb es el motor de templates de Odoo que se usa para generar HTML dinamico. Los templates QWeb se definen en archivos XML y se renderizan con el metodo _render_qweb() del modelo. QWeb soporta directivas como t-if, t-foreach, t-set, t-call, y t-esc.
Los templates QWeb son especialmente utiles para generar reportes personalizados, emails dinamicos, y vistas kanban. Cada template tiene un ID unico que se usa para referenciarlo desde el codigo Python o desde otros templates.
Para templates de reportes, usa el motor de reportes de Odoo que combina QWeb con datos del modelo para generar PDFs personalizados. Los reportes se definen en XML con el modelo ir.actions.report y se asocian a un modelo especifico.
Los templates heredados usan xpath para modificar templates existentes sin duplicar codigo. Esto permite agregar o modificar elementos de templates de Odoo estandar desde modulos personalizados. La herencia de templates es mas flexible que la herencia de vistas XML.
18. Uso de Decoradores del ORM
Odoo ofrece multiples decoradores que facilitan la definicion de modelos y campos. @api.depends define las dependencias de campos compute. @api.onchange define metodos que se ejecutan al cambiar un campo en el formulario. @api.constrains define validaciones que se ejecutan al guardar un registro.
@api.model indica que un metodo se ejecuta sobre el modelo en lugar de sobre un registro especifico. Este decorador es util para metodos factory o para metodos que buscan registros sin tener un recordset predefinido.
@api.constrains acepta una lista de campos que activan la validacion. Cuando cualquier campo de la lista cambia, el metodo constrains se ejecuta y verifica que la condicion se cumple. Si la condicion falla, se lanza ValidationError con un mensaje descriptivo.
@api.depends_multi (nuevo en Odoo 18) permite definir campos compute que dependen de campos en otros modelos. Esto es util para campos compute que necesitan datos de registros relacionados sin usar related (que ejecuta una consulta adicional por cada registro).
Los decoradores de cache como ormcache y ormcache_context permiten almacenar resultados de funciones en Redis para evitar ejecuciones repetidas. El cache se invalida automaticamente cuando los datos subyacentes cambian, garantizando la consistencia.
19. Manejo de Transacciones y Bloqueos
Odoo ejecuta cada peticion HTTP dentro de una transaccion de base de datos que se confirma automaticamente al finalizar la peticion. Si ocurre un error, la transaccion se revierte y todos los cambios se deshacen. Este comportamiento garantiza la consistencia de los datos.
Para operaciones largas que no deben bloquear a otros usuarios, usa savepoints que permiten revertir cambios parciales sin afectar la transaccion completa. Los savepoints son especialmente utiles en wizards que ejecutan multiples operaciones y necesitan deshacer todos los cambios si una operacion falla.
Los bloqueos de fila (FOR UPDATE) se usan para evitar condiciones de carrera cuando multiples usuarios intentan modificar el mismo registro. Odoo aplica bloqueos automaticamente al ejecutar write() o unlink(), pero para operaciones complejas puedes usar cr.execute() con FOR UPDATE para bloquear registros manualmente.
Para evitar deadlocks, siempre adquiere los bloqueos en el mismo orden. Si tu modulo modifica registros en multiples tablas, asegurate de que todos los metodos adquieren los bloqueos en el mismo orden para prevenir situaciones donde dos transacciones esperan mutuamente la liberacion de bloqueos.
20. Gestion de Relaciones Many2many
Los campos Many2many en Odoo almacenan las relaciones en una tabla intermedia con dos foreign keys. La tabla intermedia se crea automaticamente cuando defines un campo Many2many, pero puedes personalizarla agregando campos adicionales como fecha de asignacion o notas.
Para crear un campo Many2many personalizado, hereda de la tabla intermedia y agrega los campos adicionales. Usa el parametro relation para especificar el nombre de la tabla intermedia, column1 para la primera foreign key, y column2 para la segunda foreign key.
Las operaciones en campos Many2many incluyen: link para agregar una relacion, unlink para eliminar una relacion, y write para reemplazar todas las relaciones. Estas operaciones se ejecutan con los comandos (4, id) para link, (3, id) para unlink, y (6, 0, [ids]) para reemplazar.
Los dominios de busqueda que usan campos Many2many requieren la sintaxis relacional correcta. Para buscar registros que tienen una relacion especifica, usa ('many2many_field.name', '=', valor) que busca en el campo de la tabla relacionada a traves de la tabla intermedia.
21. Uso de Contextos y Environment
El contexto (context) de Odoo es un diccionario de Python que se pasa entre metodos y contiene informacion sobre la sesion actual, el idioma, la zona horaria, y parametros personalizados. El contexto se usa para personalizar el comportamiento de los metodos segun las necesidades del usuario.
Los keys del contexto mas comunes incluyen: lang (idioma del usuario), tz (zona horaria), uid (ID del usuario actual), active_id (ID del registro activo en el formulario), y active_model (modelo del registro activo). Estos keys se pasan automaticamente por Odoo y no necesitan ser configurados manualmente.
El environment (env) es un objeto que encapsula la sesion actual de Odoo incluyendo el usuario, la empresa, la base de datos, y el contexto. El env se accede con self.env y proporciona metodos para buscar registros, crear registros, y acceder a configuraciones del sistema.
Para ejecutar operaciones con privilegios de administrador, usa self.env.sudo() que crea un environment con el usuario superadministrador. Esto es util para operaciones de sistema que requieren permisos elevados pero no deben exponerse al usuario final.