Cómo crear módulos Odoo personalizados: una guía para desarrolladores sobre OWL, ORM y herencia

Guía para desarrolladores para crear módulos Odoo personalizados. Cubre la estructura del módulo, el marco OWL, la herencia ORM, las vistas, las reglas de seguridad, las pruebas y las pautas de OCA.

E

ECOSIRE Research and Development Team

Equipo ECOSIRE

19 de febrero de 20267 min de lectura1.4k Palabras

Cómo crear módulos Odoo personalizados: una guía para desarrolladores sobre OWL, ORM y herencia

Cuando los 82 módulos oficiales y los más de 40.000 módulos comunitarios de Odoo no cubren los requisitos exactos de su negocio, el desarrollo de módulos personalizados llena el vacío. Esta guía cubre los fundamentos de la creación de módulos de Odoo en 2026, incluida la estructura del módulo, el marco de interfaz OWL, los patrones ORM, los mecanismos de herencia y las mejores prácticas alineadas con las pautas de OCA (Asociación Comunitaria de Odoo).

¿Qué es un módulo Odoo personalizado?

Un módulo personalizado de Odoo es un paquete autónomo de lógica de backend de Python, definiciones de vistas XML, componentes de frontend de JavaScript y reglas de seguridad que amplía la funcionalidad de Odoo. Los módulos pueden agregar características completamente nuevas, modificar el comportamiento existente o integrar Odoo con sistemas externos. Cada pieza de Odoo en sí es un módulo, lo que hace que la arquitectura sea inherentemente extensible.

Estructura del directorio del módulo

Un módulo bien organizado sigue esta estructura estándar:

my_custom_module/
├── __init__.py              # Python package init
├── __manifest__.py          # Module metadata and dependencies
├── models/
│   ├── __init__.py
│   └── my_model.py          # Business logic and data models
├── views/
│   ├── my_model_views.xml   # Form, tree, and kanban views
│   └── menu.xml             # Menu items and actions
├── security/
│   ├── ir.model.access.csv  # Access control list
│   └── security.xml         # Record rules and groups
├── data/
│   └── data.xml             # Default data records
├── static/
│   └── src/
│       ├── js/              # OWL components
│       ├── css/             # Stylesheets
│       └── xml/             # QWeb templates
├── wizard/                  # Transient models for wizards
├── reports/                 # QWeb report templates
└── tests/                   # Unit tests

El archivo de manifiesto

El archivo __manifest__.py define la identidad de su módulo:

{
    'name': 'My Custom Module',
    'version': '18.0.1.0.0',
    'category': 'Custom',
    'summary': 'Short description of what the module does',
    'description': """
        Long description with details about
        features and configuration.
    """,
    'author': 'ECOSIRE',
    'website': 'https://ecosire.com',
    'license': 'LGPL-3',
    'depends': ['base', 'sale', 'stock'],
    'data': [
        'security/ir.model.access.csv',
        'views/my_model_views.xml',
        'views/menu.xml',
        'data/data.xml',
    ],
    'assets': {
        'web.assets_backend': [
            'my_custom_module/static/src/js/**/*',
            'my_custom_module/static/src/css/**/*',
            'my_custom_module/static/src/xml/**/*',
        ],
    },
    'installable': True,
    'application': False,
    'auto_install': False,
}

Convención de versión: {odoo_version}.{major}.{minor}.{patch} (por ejemplo, 18.0.1.0.0).

Trabajando con el ORM

El Mapeo Relacional de Objetos (ORM) de Odoo es la base de todo el desarrollo backend. Los modelos se asignan a tablas de bases de datos y el ORM proporciona operaciones CRUD, campos calculados, restricciones y gestión del flujo de trabajo.

Definiendo un modelo

from odoo import models, fields, api

class ProjectTask(models.Model):
    _name = 'my_module.task'
    _description = 'Project Task'
    _order = 'priority desc, create_date desc'

    name = fields.Char(string='Task Name', required=True)
    description = fields.Html(string='Description')
    state = fields.Selection([
        ('draft', 'Draft'),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
        ('cancelled', 'Cancelled'),
    ], default='draft', tracking=True)
    assigned_to = fields.Many2one('res.users', string='Assigned To')
    deadline = fields.Date(string='Deadline')
    priority = fields.Selection([
        ('0', 'Normal'),
        ('1', 'Important'),
        ('2', 'Urgent'),
    ], default='0')
    tag_ids = fields.Many2many('my_module.tag', string='Tags')
    progress = fields.Float(compute='_compute_progress', store=True)

Referencia de tipos de campo

| Tipo de campo | Tipo de pitón | Caso de uso | |---|---|---| | Carbón | cadena | Texto breve (nombre, referencia) | | Texto | cadena | Texto largo y sin formato | | HTML | cadena | Contenido de texto enriquecido | | Entero | entero | Números enteros | | Flotador | flotador | Números decimales | | Booleano | booleano | Banderas de verdadero/falso | | Fecha | fecha | Fecha sin hora | | Fecha y hora | fechahora | Fecha con hora | | Selección | cadena | Opciones desplegables | | Muchos2uno | entero | Enlace a un registro | | Uno2muchos | lista | Reverso de Many2one | | Muchos2muchos | lista | Enlace a múltiples registros | | Binario | bytes | Archivos adjuntos |

Campos y restricciones calculados

@api.depends('subtask_ids.state')
def _compute_progress(self):
    for task in self:
        total = len(task.subtask_ids)
        done = len(task.subtask_ids.filtered(
            lambda t: t.state == 'done'
        ))
        task.progress = (done / total * 100) if total else 0

@api.constrains('deadline')
def _check_deadline(self):
    for task in self:
        if task.deadline and task.deadline < fields.Date.today():
            raise ValidationError(
                "Deadline cannot be in the past."
            )

Mecanismos de herencia

Odoo proporciona tres tipos de herencia, cada uno con un propósito diferente:

1. Herencia de clase (extensión)

Amplíe un modelo existente agregando campos o reemplazando métodos. Este es el patrón más común.

class SaleOrderExtend(models.Model):
    _inherit = 'sale.order'

    custom_reference = fields.Char(string='Custom Ref')
    approved_by = fields.Many2one('res.users')

    def action_confirm(self):
        # Add custom logic before standard confirmation
        for order in self:
            if order.amount_total > 10000 and not order.approved_by:
                raise UserError("Orders over $10,000 require approval.")
        return super().action_confirm()

2. Herencia del prototipo

Cree un nuevo modelo que copie todos los campos y métodos de un modelo existente.

class CustomPartner(models.Model):
    _name = 'my_module.partner'
    _inherit = 'res.partner'   # Copies structure
    _description = 'Custom Partner'

3. Herencia de delegación

Cree un nuevo modelo que delega a un modelo existente a través de un enlace Many2one. Los campos del modelo principal aparecen en el modelo secundario de forma transparente.

class LibraryMember(models.Model):
    _name = 'library.member'
    _inherits = {'res.partner': 'partner_id'}

    partner_id = fields.Many2one('res.partner', required=True,
                                  ondelete='cascade')
    membership_date = fields.Date()
    member_number = fields.Char()

El marco OWL (frontend)

Odoo 18 utiliza OWL (Odoo Web Library) como marco de interfaz. OWL es un marco basado en componentes similar a React o Vue pero diseñado específicamente para las necesidades de Odoo.

Componente OWL básico

/** @odoo-module */

import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";

class TaskDashboard extends Component {
    static template = "my_module.TaskDashboard";

    setup() {
        this.state = useState({
            tasks: [],
            filter: 'all',
        });
        this.loadTasks();
    }

    async loadTasks() {
        this.state.tasks = await this.env.services.orm.searchRead(
            "my_module.task",
            [["state", "!=", "cancelled"]],
            ["name", "state", "assigned_to", "deadline"]
        );
    }

    get filteredTasks() {
        if (this.state.filter === 'all') return this.state.tasks;
        return this.state.tasks.filter(
            t => t.state === this.state.filter
        );
    }
}

Plantilla QWeb (XML)

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="my_module.TaskDashboard">
        <div class="o_task_dashboard">
            <div class="task-filters">
                <button t-on-click="() => state.filter = 'all'">All</button>
                <button t-on-click="() => state.filter = 'in_progress'">
                    In Progress
                </button>
            </div>
            <div class="task-list">
                <t t-foreach="filteredTasks" t-as="task" t-key="task.id">
                    <div class="task-card">
                        <span t-esc="task.name"/>
                    </div>
                </t>
            </div>
        </div>
    </t>
</templates>

Configuración de seguridad

Cada modelo necesita reglas de acceso explícitas. Sin ellos, ningún usuario puede acceder a los datos del modelo.

Lista de control de acceso (ir.model.access.csv)

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_task_user,task.user,model_my_module_task,base.group_user,1,1,1,0
access_task_manager,task.manager,model_my_module_task,my_module.group_manager,1,1,1,1

Reglas de registro (security.xml)

<record id="task_own_rule" model="ir.rule">
    <field name="name">Own Tasks Only</field>
    <field name="model_id" ref="model_my_module_task"/>
    <field name="domain_force">
        [('assigned_to', '=', user.id)]
    </field>
    <field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>

Probando su módulo

Odoo admite pruebas unitarias de Python utilizando el marco unittest con clases de prueba específicas de Odoo:

from odoo.tests.common import TransactionCase

class TestTask(TransactionCase):
    def setUp(self):
        super().setUp()
        self.task = self.env['my_module.task'].create({
            'name': 'Test Task',
            'state': 'draft',
        })

    def test_task_creation(self):
        self.assertEqual(self.task.state, 'draft')
        self.assertEqual(self.task.progress, 0)

    def test_deadline_constraint(self):
        with self.assertRaises(ValidationError):
            self.task.write({
                'deadline': fields.Date.subtract(
                    fields.Date.today(), days=1
                ),
            })

Ejecute pruebas con: odoo-bin -d test_db --test-enable -i my_custom_module --stop-after-init

Preguntas frecuentes

P: ¿Cuánto tiempo lleva construir un módulo Odoo personalizado? Los módulos simples (campos nuevos, vistas básicas) tardan entre 1 y 3 días. Los módulos moderados (nuevos modelos, flujos de trabajo, informes) tardan entre 1 y 3 semanas. Los módulos complejos (sistemas multimodelo, integraciones externas, componentes OWL personalizados) tardan entre 4 y 12 semanas. Para las empresas que necesitan módulos personalizados pero que carecen de desarrolladores internos de Odoo, contrate a un desarrollador experimentado de Odoo de nuestro equipo.

P: ¿Debo modificar el código principal de Odoo o crear un módulo separado? Cree siempre un módulo independiente. La modificación del código central interrumpe la capacidad de actualización y crea conflictos de fusión durante las actualizaciones de versiones. Utilice la herencia para ampliar los modelos y vistas existentes desde su módulo personalizado.

P: ¿Cuáles son las pautas de OCA? La Odoo Community Association (OCA) publica estándares de codificación para la calidad de los módulos, incluidas convenciones de nomenclatura, requisitos de documentación, expectativas de cobertura de pruebas y reglas de estilo de código. Seguir las pautas de OCA garantiza que su módulo sea mantenible y compatible con el ecosistema comunitario más amplio.

Obtener ayuda profesional

La creación de módulos personalizados de Odoo requiere experiencia en Python, JavaScript, PostgreSQL y las convenciones marco de Odoo. Ya sea que necesite una extensión de campo simple o un sistema complejo de múltiples módulos, los servicios de personalización de Odoo de ECOSIRE ofrecen módulos listos para producción creados según los estándares OCA.

Contáctenos para analizar los requisitos de su módulo personalizado y obtener una estimación de desarrollo.

Compartir:
E

Escrito por

ECOSIRE Research and Development Team

Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.

Chatea en whatsapp