Desarrollo de Odoo Python: guía completa para principiantes y profesionales
Según la Encuesta de desarrolladores de Python de 2025, más del 47 % de los desarrolladores empresariales de Python trabajan con al menos un marco ERP, y Odoo domina ese segmento con una participación de mercado del 68 % entre los ERP de código abierto. Ya sea que esté creando su primer módulo o diseñando una implementación para varias empresas, comprender la capa Python de Odoo es la habilidad más impactante que puede desarrollar.
Esta guía va mucho más allá de las descripciones generales a nivel superficial. Recorreremos cada capa del backend Python de Odoo, desde la magia de metaclase del ORM hasta los patrones de controlador avanzados, desde la depuración de problemas de producción hasta la escritura de pruebas que realmente detecten regresiones.
Conclusiones clave
- ORM de Odoo es un mapeador relacional de objetos completo construido sobre metaclases que generan automáticamente esquemas SQL, administran el almacenamiento en caché y manejan la seguridad de varias empresas; comprenderlo profundamente es la diferencia entre trabajar con Odoo y luchar contra él.
- Cinco categorías de campos cubren todas las necesidades de modelado de datos: campos básicos (carácter, entero, flotante, booleano, texto, HTML, fecha, fecha y hora, binario, selección), relacionales (Many2one, One2many, Many2many), calculados, relacionados y monetarios.
- Tres mecanismos de herencia te permiten extender cualquier parte de Odoo sin bifurcar: herencia de clases (_inherit), herencia de prototipos (_inherit + _name) y herencia de delegación (_inherits).
- Los controladores manejan HTTP usando decoradores (@http.route) con patrones de punto final JSON-RPC y HTTP, modos de autenticación y configuración CORS.
- Las pruebas automatizadas con TransactionCase y HttpCase detectan regresiones antes de que lleguen a producción: apunte a una cobertura superior al 80 % en lógica empresarial.
1. Configurando su entorno de desarrollo
Antes de escribir código, necesita un entorno de desarrollo adecuado. Aquí está la configuración recomendada para 2026:
# Install system dependencies (Ubuntu/Debian)
# sudo apt install python3.12 python3.12-venv python3.12-dev
# sudo apt install postgresql-17 libpq-dev libxml2-dev libxslt1-dev
# Clone Odoo source (enterprise requires license)
# git clone https://github.com/odoo/odoo.git --branch 18.0 --depth 1
# git clone https://github.com/odoo/enterprise.git --branch 18.0 --depth 1
# Create virtual environment
# python3.12 -m venv odoo-venv
# source odoo-venv/bin/activate
# pip install -r odoo/requirements.txt
# Create database and start Odoo
# createdb mydb
# python odoo/odoo-bin -d mydb --addons-path=odoo/addons,enterprise,custom_addons
Cree un directorio custom_addons para sus módulos. Cada módulo vive en su propio subdirectorio con una estructura específica que espera Odoo.
2. Análisis profundo de la arquitectura del módulo
Cada módulo de Odoo sigue una estructura de convención sobre configuración. Comprender el papel de cada componente es esencial:
my_module/
├── __init__.py # Python package initializer
├── __manifest__.py # Module metadata and dependencies
├── models/
│ ├── __init__.py
│ ├── sale_order.py # Business logic models
│ └── res_partner.py # Partner extensions
├── views/
│ ├── sale_order_views.xml # Form, tree, search views
│ └── menu.xml # Menu items and actions
├── controllers/
│ ├── __init__.py
│ └── main.py # HTTP controllers
├── security/
│ ├── ir.model.access.csv # ACL rules
│ └── security.xml # Record rules and groups
├── data/
│ ├── data.xml # Default data
│ └── demo.xml # Demo data (dev only)
├── wizards/
│ ├── __init__.py
│ └── mass_update.py # TransientModel wizards
├── report/
│ ├── report_invoice.xml # QWeb report templates
│ └── report.py # Report parsers
├── static/
│ ├── description/
│ │ └── icon.png # Module icon (128x128)
│ └── src/
│ ├── js/ # OWL components
│ └── xml/ # OWL templates
└── tests/
├── __init__.py
└── test_sale_order.py # Automated tests
El archivo de manifiesto
El __manifest__.py es el corazón de su módulo. Cada campo importa:
# __manifest__.py
{
'name': 'Sales Commission Engine',
'version': '18.0.1.0.0', # Odoo version.major.minor.patch
'category': 'Sales/Commission',
'summary': 'Automated commission calculations for sales teams',
'description': """
Long description with RST formatting.
Supports multi-tier commission plans.
""",
'author': 'ECOSIRE Private Limited',
'website': 'https://ecosire.com',
'license': 'LGPL-3',
'depends': ['sale', 'account', 'hr'], # Required modules
'data': [
'security/ir.model.access.csv',
'views/commission_plan_views.xml',
'views/menu.xml',
'data/commission_data.xml',
],
'demo': ['data/demo.xml'],
'installable': True,
'application': True, # Shows in Apps menu
'auto_install': False,
'external_dependencies': {
'python': ['pandas'], # pip packages
},
}
Convención de numeración de versiones: {odoo_version}.{major}.{minor}.{patch} — el primer número siempre coincide con la serie de Odoo.
3. La API ORM: referencia completa
El ORM de Odoo es mucho más poderoso de lo que la mayoría de los desarrolladores creen. Maneja automáticamente la generación de esquemas, el almacenamiento en caché, el control de acceso y el filtrado multiempresa.
Tipos de modelo
from odoo import models, fields, api
# Persistent model — stored in database
class CommissionPlan(models.Model):
_name = 'commission.plan'
_description = 'Commission Plan'
_order = 'sequence, name'
_rec_name = 'name'
name = fields.Char(string='Plan Name', required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
# Transient model — auto-cleaned after ~1 hour
class CommissionWizard(models.TransientModel):
_name = 'commission.calculate.wizard'
_description = 'Calculate Commissions'
date_from = fields.Date(required=True)
date_to = fields.Date(required=True)
# Abstract model — no database table, only for inheritance
class CommissionMixin(models.AbstractModel):
_name = 'commission.mixin'
_description = 'Commission Mixin'
commission_rate = fields.Float(string='Commission %')
Tipos de campo Análisis profundo
class SaleCommission(models.Model):
_name = 'sale.commission'
_description = 'Sale Commission Record'
# Basic fields
name = fields.Char(string='Reference', required=True, copy=False,
default=lambda self: self.env['ir.sequence'].next_by_code('sale.commission'))
amount = fields.Float(string='Amount', digits=(16, 2))
percentage = fields.Float(string='Rate %', digits=(5, 2))
is_paid = fields.Boolean(string='Paid', default=False)
notes = fields.Text(string='Internal Notes')
description = fields.Html(string='Description', sanitize=True)
date = fields.Date(string='Commission Date', default=fields.Date.today)
create_datetime = fields.Datetime(string='Created', default=fields.Datetime.now)
document = fields.Binary(string='Attachment')
document_name = fields.Char(string='File Name')
# Selection field
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('paid', 'Paid'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', tracking=True)
# Relational fields
salesperson_id = fields.Many2one('res.users', string='Salesperson',
required=True, ondelete='restrict')
order_id = fields.Many2one('sale.order', string='Sale Order')
line_ids = fields.One2many('sale.commission.line', 'commission_id', string='Lines')
tag_ids = fields.Many2many('sale.commission.tag', string='Tags')
# Computed field with store and search
total_amount = fields.Monetary(string='Total', compute='_compute_total',
store=True, currency_field='currency_id')
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
@api.depends('line_ids.amount')
def _compute_total(self):
for record in self:
record.total_amount = sum(record.line_ids.mapped('amount'))
# Related field (shortcut for computed)
salesperson_email = fields.Char(related='salesperson_id.email', store=True)
Operaciones CRUD
# CREATE — returns new recordset
commission = self.env['sale.commission'].create({
'salesperson_id': self.env.user.id,
'order_id': order.id,
'state': 'draft',
'line_ids': [
(0, 0, {'product_id': product.id, 'amount': 150.00}), # Create
],
})
# Batch create (much faster than loop)
vals_list = [{'name': f'COM-{i}', 'amount': i * 10} for i in range(100)]
records = self.env['sale.commission'].create(vals_list)
# READ — browse by IDs
commission = self.env['sale.commission'].browse(42)
commissions = self.env['sale.commission'].browse([42, 43, 44])
# SEARCH — returns recordset matching domain
pending = self.env['sale.commission'].search([
('state', '=', 'confirmed'),
('date', '>=', '2026-01-01'),
('salesperson_id.department_id.name', '=', 'Sales'),
], order='date desc', limit=50)
# search_count — just the count
count = self.env['sale.commission'].search_count([('state', '=', 'draft')])
# search_read — search + read in one query (optimal for API calls)
data = self.env['sale.commission'].search_read(
[('state', '=', 'confirmed')],
fields=['name', 'amount', 'salesperson_id'],
limit=20, offset=0, order='amount desc'
)
# UPDATE
commission.write({'state': 'paid', 'notes': 'Paid on March 2026'})
# Batch update (all records get same values)
pending.write({'state': 'confirmed'})
# DELETE
old_drafts.unlink()
# SPECIAL: (0, 0, vals) create, (1, id, vals) update, (2, id, 0) delete
# (3, id, 0) unlink M2M, (4, id, 0) link M2M, (5, 0, 0) clear M2M
# (6, 0, [ids]) replace M2M
Expresiones de dominio
Los dominios son el lenguaje de consulta de Odoo. Domínalos:
# Basic operators: =, !=, >, >=, <, <=, like, ilike, in, not in
# Special operators: =like (SQL LIKE), =ilike (case-insensitive LIKE)
# child_of, parent_of — for hierarchical models
domain = [
'|', # OR (next two leaves)
('state', '=', 'confirmed'),
'&', # AND (next two leaves)
('state', '=', 'draft'),
('amount', '>', 1000),
('salesperson_id', '!=', False), # Implicit AND with above
('date', '>=', '2026-01-01'),
('tag_ids.name', 'in', ['priority', 'vip']), # Dot notation for relations
]
# Negation
domain = [('state', 'not in', ['cancelled', 'paid'])]
# NULL checks
domain = [('notes', '!=', False)] # Has notes
domain = [('notes', '=', False)] # No notes
4. Patrones de herencia
Odoo proporciona tres mecanismos de herencia que le permiten extender cualquier módulo sin modificar su código fuente. La herencia de clases agrega campos y métodos a los modelos existentes. La herencia de prototipos crea nuevos modelos basados en los existentes con tablas de bases de datos separadas. La herencia de delegación vincula los modelos a través de la composición, creando registros en ambas tablas automáticamente.
Herencia de clases (más común)
class SaleOrderInherit(models.Model):
_inherit = 'sale.order' # Extend existing model
# Add new fields
commission_plan_id = fields.Many2one('commission.plan', string='Commission Plan')
commission_amount = fields.Monetary(compute='_compute_commission')
@api.depends('amount_total', 'commission_plan_id.rate')
def _compute_commission(self):
for order in self:
rate = order.commission_plan_id.rate or 0
order.commission_amount = order.amount_total * rate / 100
# Override existing method
def action_confirm(self):
res = super().action_confirm() # Always call super()
for order in self:
if order.commission_plan_id:
order._create_commission_record()
return res
def _create_commission_record(self):
self.ensure_one()
self.env['sale.commission'].create({
'order_id': self.id,
'salesperson_id': self.user_id.id,
'amount': self.commission_amount,
})
Herencia del prototipo
class ProjectCommission(models.Model):
_name = 'project.commission'
_inherit = 'sale.commission' # Copy structure to new model
_description = 'Project-based Commission'
# Has all fields from sale.commission PLUS its own table
project_id = fields.Many2one('project.project', required=True)
milestone_id = fields.Many2one('project.milestone')
Herencia de delegación
class CommissionEmployee(models.Model):
_name = 'commission.employee'
_inherits = {'hr.employee': 'employee_id'} # Delegation
_description = 'Commission-eligible Employee'
employee_id = fields.Many2one('hr.employee', required=True, ondelete='cascade')
commission_plan_id = fields.Many2one('commission.plan')
lifetime_earnings = fields.Monetary()
# Can access employee fields directly: record.name, record.department_id
5. Patrones de lógica empresarial
Restricciones
from odoo.exceptions import ValidationError
class CommissionPlan(models.Model):
_name = 'commission.plan'
rate = fields.Float(required=True)
name = fields.Char(required=True)
# Python constraint
@api.constrains('rate')
def _check_rate(self):
for plan in self:
if not 0 <= plan.rate <= 100:
raise ValidationError(
f"Commission rate must be between 0 and 100. Got: {plan.rate}"
)
# SQL constraint (faster, database-level)
_sql_constraints = [
('name_unique', 'UNIQUE(name, company_id)',
'Commission plan name must be unique per company.'),
('rate_positive', 'CHECK(rate >= 0)',
'Commission rate must be positive.'),
]
Métodos de cambio
@api.onchange('commission_plan_id')
def _onchange_plan(self):
"""Triggered in UI when field changes. Sets defaults and warnings."""
if self.commission_plan_id:
self.percentage = self.commission_plan_id.rate
if self.commission_plan_id.rate > 30:
return {
'warning': {
'title': 'High Commission Rate',
'message': 'This plan has a rate above 30%. Please confirm with management.',
}
}
Flujo de trabajo con máquina de estados
class SaleCommission(models.Model):
_name = 'sale.commission'
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('approved', 'Approved'),
('paid', 'Paid'),
('cancelled', 'Cancelled'),
], default='draft', tracking=True)
def action_confirm(self):
for rec in self.filtered(lambda r: r.state == 'draft'):
rec.state = 'confirmed'
rec.message_post(body="Commission confirmed.")
def action_approve(self):
self.filtered(lambda r: r.state == 'confirmed').write({
'state': 'approved',
'approved_by': self.env.user.id,
'approved_date': fields.Datetime.now(),
})
def action_pay(self):
for rec in self.filtered(lambda r: r.state == 'approved'):
rec._process_payment()
rec.state = 'paid'
def action_cancel(self):
allowed = self.filtered(lambda r: r.state in ('draft', 'confirmed'))
allowed.write({'state': 'cancelled'})
6. Controladores y puntos finales HTTP
from odoo import http
from odoo.http import request, Response
import json
class CommissionController(http.Controller):
# JSON-RPC endpoint (used by Odoo's internal JS framework)
@http.route('/commission/calculate', type='json', auth='user', methods=['POST'])
def calculate_commission(self, order_id, **kwargs):
order = request.env['sale.order'].browse(order_id)
if not order.exists():
return {'error': 'Order not found'}
commission = order.commission_amount
return {'order_id': order_id, 'commission': commission}
# HTTP endpoint (REST-style, for external integrations)
@http.route('/api/v1/commissions', type='http', auth='api_key',
methods=['GET'], csrf=False)
def list_commissions(self, **kwargs):
domain = [('state', '!=', 'cancelled')]
if kwargs.get('salesperson_id'):
domain.append(('salesperson_id', '=', int(kwargs['salesperson_id'])))
commissions = request.env['sale.commission'].search_read(
domain,
fields=['name', 'amount', 'state', 'date'],
limit=int(kwargs.get('limit', 50)),
offset=int(kwargs.get('offset', 0)),
)
return Response(
json.dumps({'data': commissions, 'count': len(commissions)}),
content_type='application/json',
status=200,
)
# File download endpoint
@http.route('/commission/report/<int:commission_id>', type='http', auth='user')
def download_report(self, commission_id, **kwargs):
commission = request.env['sale.commission'].browse(commission_id)
pdf = request.env['ir.actions.report']._render_qweb_pdf(
'my_module.commission_report', commission.ids
)
return request.make_response(
pdf[0],
headers=[
('Content-Type', 'application/pdf'),
('Content-Disposition', f'attachment; filename=commission_{commission_id}.pdf'),
]
)
Modos de autenticación
| Modo | Descripción | Caso de uso |
|---|---|---|
| CÓDIGO0 | Requiere sesión de inicio de sesión | Páginas internas, acciones de usuario |
| CÓDIGO0 | Sesión opcional, puede ser anónima | Páginas del sitio web |
| CÓDIGO0 | Sin sesión, sin entorno | Comprobaciones de estado, puntos finales estáticos |
| CÓDIGO0 | Clave API en el encabezado | Integraciones REST externas |
7. Seguridad: ACL y reglas de registro
Listas 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_commission_user,commission.user,model_sale_commission,sales_team.group_sale_salesman,1,0,0,0
access_commission_manager,commission.manager,model_sale_commission,sales_team.group_sale_manager,1,1,1,0
access_commission_admin,commission.admin,model_sale_commission,base.group_system,1,1,1,1
Reglas de registro (security.xml)
<record id="commission_own_rule" model="ir.rule">
<field name="name">Salesperson sees own commissions</field>
<field name="model_id" ref="model_sale_commission"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="domain_force">[('salesperson_id', '=', user.id)]</field>
</record>
<record id="commission_company_rule" model="ir.rule">
<field name="name">Company-level commission isolation</field>
<field name="model_id" ref="model_sale_commission"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
8. Técnicas de depuración
Usando el shell de Odoo
# Start interactive shell with full ORM access
python odoo-bin shell -d mydb --addons-path=addons,custom_addons
# In shell:
>>> orders = env['sale.order'].search([('state', '=', 'sale')], limit=5)
>>> for o in orders:
... print(f"{o.name}: {o.amount_total} - {o.partner_id.name}")
>>> env.cr.commit() # Commit changes made in shell
Registro
import logging
_logger = logging.getLogger(__name__)
class SaleCommission(models.Model):
_name = 'sale.commission'
def action_confirm(self):
_logger.info("Confirming commission %s for user %s", self.name, self.env.user.name)
try:
self._validate_commission()
except Exception as e:
_logger.exception("Failed to confirm commission %s: %s", self.name, e)
raise
Perfil de rendimiento
# Enable SQL logging in odoo.conf:
# log_level = debug_sql
# Or use the profiler decorator
from odoo.tools.profiler import profile
@profile('/tmp/commission_profile')
def action_bulk_calculate(self):
# This will generate a profile dump you can analyze
for order in self.env['sale.order'].search([]):
order._calculate_commission()
Errores comunes de rendimiento:
- Acceder a campos relacionales en bucles (consultas N+1): use
read()omapped()en su lugar - Llamar a
search()dentro de bucles: agrupar sus búsquedas - No usar
store=Trueen campos calculados filtrados con frecuencia - Olvidar
sudo()provoca comprobaciones adicionales de derechos de acceso en cada operación
9. Pruebas automatizadas
from odoo.tests.common import TransactionCase, HttpCase, tagged
from odoo.exceptions import ValidationError
@tagged('post_install', '-at_install')
class TestCommission(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env['res.partner'].create({'name': 'Test Customer'})
cls.user = cls.env['res.users'].create({
'name': 'Test Salesperson',
'login': 'test_sales',
'groups_id': [(4, cls.env.ref('sales_team.group_sale_salesman').id)],
})
cls.plan = cls.env['commission.plan'].create({
'name': 'Standard 10%',
'rate': 10.0,
})
def test_commission_calculation(self):
"""Commission amount should be 10% of order total."""
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'user_id': self.user.id,
'commission_plan_id': self.plan.id,
})
# Add order line...
self.assertAlmostEqual(order.commission_amount, 100.0, places=2)
def test_rate_constraint(self):
"""Rates above 100 should raise ValidationError."""
with self.assertRaises(ValidationError):
self.env['commission.plan'].create({
'name': 'Invalid Plan',
'rate': 150.0,
})
def test_state_machine(self):
"""Commission should follow draft -> confirmed -> paid flow."""
commission = self.env['sale.commission'].create({
'salesperson_id': self.user.id,
'amount': 500,
})
self.assertEqual(commission.state, 'draft')
commission.action_confirm()
self.assertEqual(commission.state, 'confirmed')
# HTTP/Tour tests
@tagged('post_install', '-at_install')
class TestCommissionTour(HttpCase):
def test_commission_dashboard(self):
"""Test that the commission dashboard loads correctly."""
self.start_tour('/web#action=commission.action_dashboard',
'commission_dashboard_tour', login='admin')
Ejecute pruebas con:
# Run all tests for a module
python odoo-bin -d testdb -i my_module --test-enable --stop-after-init
# Run specific test class
python odoo-bin -d testdb --test-tags /my_module:TestCommission --stop-after-init
# Run with coverage
coverage run odoo-bin -d testdb -i my_module --test-enable --stop-after-init
coverage report --include="custom_addons/my_module/*"
10. Patrones avanzados
Acciones programadas (tareas cron)
<record id="ir_cron_monthly_commission" model="ir.cron">
<field name="name">Calculate Monthly Commissions</field>
<field name="model_id" ref="model_sale_commission"/>
<field name="state">code</field>
<field name="code">model._cron_calculate_monthly()</field>
<field name="interval_number">1</field>
<field name="interval_type">months</field>
<field name="numbercall">-1</field>
</record>
Patrones multiempresa
class CommissionPlan(models.Model):
_name = 'commission.plan'
company_id = fields.Many2one('res.company', required=True,
default=lambda self: self.env.company)
# Use sudo() carefully — only when crossing company boundaries is intended
def get_global_plans(self):
return self.sudo().search([('global_plan', '=', True)])
Patrón Mixin para lógica reutilizable
class CommissionMixin(models.AbstractModel):
_name = 'commission.mixin'
_description = 'Adds commission tracking to any model'
commission_ids = fields.One2many('sale.commission', 'res_id')
commission_total = fields.Monetary(compute='_compute_commission_total', store=True)
@api.depends('commission_ids.amount')
def _compute_commission_total(self):
for record in self:
record.commission_total = sum(record.commission_ids.mapped('amount'))
# Apply to any model via class inheritance
class SaleOrder(models.Model):
_inherit = ['sale.order', 'commission.mixin']
Ejemplo del mundo real: módulo de comisión completo
Para obtener un ejemplo de módulo de comisión completo y listo para producción, visite módulos Odoo de código abierto de ECOSIRE donde publicamos conectores, utilidades y módulos comerciales siguiendo todas las mejores prácticas cubiertas en esta guía.
Nuestro equipo de desarrollo de Odoo ha creado más de 60 módulos de Odoo utilizando estos patrones exactos. Si necesita un desarrollo personalizado de Odoo Python, desde la adición de un solo campo hasta una implementación completa de ERP, comuníquese con nuestro equipo para una consulta gratuita.
Preguntas frecuentes
¿Qué versión de Python requiere Odoo 18?
Odoo 18 requiere Python 3.10 o superior, siendo Python 3.12 la versión recomendada. Odoo 17 es compatible con Python 3.10+. Siempre consulte el archivo de requisitos oficial.txt para conocer las dependencias exactas necesarias para su versión de Odoo.
¿Cómo depuro consultas ORM de Odoo?
Habilite el registro SQL configurando log_level en debug_sql en su archivo odoo.conf, o pase --log-sql en la línea de comando. También puede utilizar self.env.cr.sql_log para rastrear consultas en el código. Para crear perfiles de rendimiento, utilice el decorador @profile integrado de odoo.tools.profiler.
¿Debo usar _inherit o _inherits para ampliar modelos?
Utilice _inherit (herencia de clases) el 95% del tiempo: agrega campos y métodos a la tabla de un modelo existente. Utilice _inherits (delegación) solo cuando necesite un modelo independiente que acceda de forma transparente a los campos de otro modelo, como crear un tipo de empleado especializado que herede todos los campos hr.employee.
¿Cómo manejo el aislamiento de datos de varias empresas?
Agregue un campo company_id Many2one a su modelo con una lambda predeterminada que devuelva self.env.company. Luego cree una ir.rule global con el dominio [('company_id', 'in', company_ids)]. Esto garantiza que los usuarios solo vean registros de las empresas a las que tienen acceso, que es el patrón multiempresa estándar de Odoo.
¿Cuál es la diferencia entre @api.depends y @api.onchange?
@api.depends se activa cuando los campos especificados cambian en la base de datos y se usa para campos calculados almacenados; funciona tanto para la interfaz de usuario como para cambios programáticos. @api.onchange se activa solo en la interfaz de usuario cuando un usuario modifica un campo y se usa para establecer valores predeterminados o mostrar advertencias. Utilice @api.depends para la integridad de los datos y @api.onchange para mejoras de UX.
¿Cómo pruebo mi módulo Odoo de manera efectiva?
Utilice TransactionCase para pruebas unitarias que prueben la lógica empresarial: cada prueba se ejecuta en una transacción que se revierte. Utilice HttpCase para probar controladores y recorridos web. Etiquete las pruebas con @tagged('post_install', '-at_install') para ejecutarlas después de la instalación del módulo. Trate de alcanzar al menos un 80 % de cobertura de código en métodos de lógica empresarial.
Próximos pasos
La creación de módulos Odoo de nivel de producción requiere tanto habilidad técnica como experiencia en el dominio. Comience con una herencia de clases simple y luego avance a módulos personalizados completos. Para más lecturas:
- Guía de desarrollo de módulos personalizados de Odoo — Descripción general de nivel superior del desarrollo de módulos
- Tutorial de API REST de Odoo — Integraciones externas con Odoo
- Mejor software ERP 2026 — Comprender la posición de Odoo en el mercado
¿Necesita desarrolladores expertos en Odoo? El equipo de desarrollo de Odoo de ECOSIRE ha entregado más de 200 módulos personalizados para empresas en 40 países. Desde motores de comisiones hasta integraciones completas del mercado, nuestro equipo escribe código listo para producción siguiendo todos los patrones de esta guía. Obtenga una consulta gratuita.
Escrito por
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
Artículos relacionados
Segmentación de clientes impulsada por IA: del RFM a la agrupación predictiva
Descubra cómo la IA transforma la segmentación de clientes desde el análisis RFM estático hasta la agrupación predictiva dinámica. Guía de implementación con Python, Odoo y datos reales de ROI.
IA para la optimización de la cadena de suministro: visibilidad, predicción y automatización
Transforme las operaciones de la cadena de suministro con IA: detección de demanda, calificación de riesgos de proveedores, optimización de rutas, automatización de almacenes y predicción de interrupciones. Guía 2026.
Estrategia de comercio electrónico B2B: cree un negocio mayorista en línea en 2026
Domine el comercio electrónico B2B con estrategias de precios mayoristas, gestión de cuentas, condiciones de crédito, catálogos perforados y configuración del portal Odoo B2B.