Desenvolvimento Odoo Python: guia completo para iniciantes e profissionais
De acordo com a Pesquisa de Desenvolvedores Python de 2025, mais de 47% dos desenvolvedores empresariais de Python trabalham com pelo menos uma estrutura de ERP, e Odoo domina esse segmento com 68% de participação de mercado entre ERPs de código aberto. Esteja você construindo seu primeiro módulo ou arquitetando uma implantação multiempresarial, compreender a camada Python do Odoo é a habilidade mais impactante que você pode desenvolver.
Este guia vai muito além das visões gerais de nível superficial. Examinaremos cada camada do back-end Python do Odoo — desde a magia da metaclasse do ORM até padrões avançados de controladores, desde a depuração de problemas de produção até a escrita de testes que realmente capturam regressões.
Principais conclusões
- O ORM do Odoo é um mapeador objeto-relacional completo construído em metaclasses que geram esquemas SQL automaticamente, gerenciam o cache e lidam com a segurança de várias empresas. Compreendê-lo profundamente é a diferença entre trabalhar com o Odoo e combatê-lo.
- Cinco categorias de campo cobrem todas as necessidades de modelagem de dados: básico (Char, Integer, Float, Boolean, Text, Html, Date, Datetime, Binary, Selection), relacional (Many2one, One2many, Many2many), campos computados, relacionados e monetários.
- Três mecanismos de herança permitem estender qualquer parte do Odoo sem bifurcação: herança de classe (_inherit), herança de protótipo (_inherit + _name) e herança de delegação (_inherits).
- Controladores lidam com HTTP usando decoradores (@http.route) com padrões de endpoint JSON-RPC e HTTP, modos de autenticação e configuração CORS.
- Testes automatizados com TransactionCase e HttpCase detectam regressões antes que elas cheguem à produção — busque mais de 80% de cobertura na lógica de negócios.
1. Configurando seu ambiente de desenvolvimento
Antes de escrever código, você precisa de um ambiente de desenvolvimento adequado. Aqui está a configuração 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
Crie um diretório custom_addons para seus módulos. Cada módulo reside em seu próprio subdiretório com uma estrutura específica que o Odoo espera.
2. Aprofundamento da arquitetura do módulo
Cada módulo Odoo segue uma estrutura de convenção sobre configuração. Compreender a função de cada componente é essencial:
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
O arquivo de manifesto
O __manifest__.py é o coração do seu módulo. Cada campo é importante:
# __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
},
}
Convenção de numeração de versão: {odoo_version}.{major}.{minor}.{patch} — o primeiro número sempre corresponde à série Odoo.
3. A API ORM: referência completa
O ORM do Odoo é muito mais poderoso do que a maioria dos desenvolvedores imagina. Ele lida automaticamente com geração de esquema, armazenamento em cache, controle de acesso e filtragem 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 %')
Aprofundamento dos tipos de campo
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)
Operações 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
Expressões de domínio
Domínios são a linguagem de consulta do Odoo. Domine-os:
# 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. Padrões de herança
Odoo fornece três mecanismos de herança que permitem estender qualquer módulo sem modificar seu código-fonte. A herança de classe adiciona campos e métodos aos modelos existentes. A herança de protótipo cria novos modelos baseados nos existentes com tabelas de banco de dados separadas. A herança de delegação vincula modelos por meio de composição, criando registros em ambas as tabelas automaticamente.
Herança de classe (mais comum)
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,
})
Herança do protótipo
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')
Herança de delegação
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. Padrões de lógica de negócios
Restrições
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 Onchange
@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.',
}
}
Fluxo de trabalho com máquina de estado
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 e endpoints 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 autenticação
| Modo | Descrição | Caso de uso |
|---|---|---|
| CÓDIGO0 | Requer sessão de login | Páginas internas, ações do usuário |
| CÓDIGO0 | Sessão opcional, pode ser anônima | Páginas do site |
| CÓDIGO0 | Sem sessão, sem ambiente | Verificações de integridade, endpoints estáticos |
| CÓDIGO0 | Chave API no cabeçalho | Integrações REST externas |
7. Segurança: ACL e regras de registro
Listas de controle de acesso (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
Regras 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 depuração
Usando o Odoo Shell
# 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 desempenho
# 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()
Problemas comuns de desempenho:
- Acessando campos relacionais em loops (consultas N+1) — use
read()oumapped() - Chamando
search()dentro de loops — agrupe suas pesquisas - Não usar
store=Trueem campos computados filtrados com frequência - Esquecer
sudo()causa verificações extras de direitos de acesso em todas as operações
9. Teste automatizado
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')
Execute testes com:
# 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. Padrões Avançados
Ações agendadas (Cron Jobs)
<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>
Padrões Multiempresas
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)])
Padrão Mixin para Lógica Reutilizável
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']
Exemplo do mundo real: Módulo de comissão completo
Para obter um exemplo de módulo de comissão completo e pronto para produção, visite módulos Odoo de código aberto da ECOSIRE onde publicamos conectores, utilitários e módulos de negócios seguindo todas as práticas recomendadas abordadas neste guia.
Nossa equipe de desenvolvimento Odoo construiu mais de 60 módulos Odoo usando exatamente esses padrões. Se você precisar de desenvolvimento personalizado em Odoo Python — desde a adição de um único campo até uma implementação completa de ERP — entre em contato com nossa equipe para uma consulta gratuita.
Perguntas frequentes
Qual versão do Python o Odoo 18 requer?
Odoo 18 requer Python 3.10 ou superior, sendo Python 3.12 a versão recomendada. Odoo 17 suporta Python 3.10+. Sempre verifique o requisitos.txt oficial para obter as dependências exatas necessárias para sua versão do Odoo.
Como faço para depurar consultas Odoo ORM?
Ative o log SQL configurando log_level como debug_sql em seu arquivo odoo.conf ou passe --log-sql na linha de comando. Você também pode usar self.env.cr.sql_log para rastrear consultas no código. Para criação de perfil de desempenho, use o decorador @profile integrado em odoo.tools.profiler.
Devo usar _inherit ou _inherits para estender modelos?
Use _inherit (herança de classe) 95% das vezes — ele adiciona campos e métodos à tabela de um modelo existente. Use _inherits (delegação) somente quando precisar de um modelo separado que acesse de forma transparente os campos de outro modelo, como criar um tipo de funcionário especializado que herde todos os campos hr.employee.
Como lidar com o isolamento de dados multiempresa?
Adicione um campo company_id Many2one ao seu modelo com um lambda padrão retornando self.env.company. Em seguida, crie uma ir.rule global com o domínio [('company_id', 'in', company_ids)]. Isso garante que os usuários vejam apenas os registros das empresas às quais têm acesso, que é o padrão multiempresa do Odoo.
Qual é a diferença entre @api.depends e @api.onchange?
@api.depends é acionado quando campos especificados mudam no banco de dados e é usado para campos computados armazenados — funciona tanto para alterações de interface do usuário quanto para alterações programáticas. @api.onchange é acionado apenas na UI quando um usuário modifica um campo e é usado para definir padrões ou mostrar avisos. Use @api.depends para integridade de dados e @api.onchange para melhorias de UX.
Como faço para testar meu módulo Odoo de forma eficaz?
Use TransactionCase para testes de unidade que testam a lógica de negócios — cada teste é executado em uma transação que é revertida. Use HttpCase para testar controladores e tours na web. Marque os testes com @tagged('post_install', '-at_install') para serem executados após a instalação do módulo. Procure pelo menos 80% de cobertura de código em métodos de lógica de negócios.
Próximas etapas
A construção de módulos Odoo de nível de produção requer habilidade técnica e conhecimento de domínio. Comece com herança de classe simples e depois avance para módulos personalizados completos. Para leitura adicional:
- Guia de desenvolvimento de módulo personalizado Odoo — Visão geral de nível superior do desenvolvimento de módulo
- Tutorial Odoo REST API — Integrações externas com Odoo
- Melhor Software ERP 2026 — Entendendo a posição do Odoo no mercado
Precisa de desenvolvedores especialistas em Odoo? A equipe de desenvolvimento Odoo da ECOSIRE entregou mais de 200 módulos personalizados para empresas em 40 países. Desde mecanismos de comissão até integrações completas de mercado, nossa equipe escreve códigos prontos para produção seguindo todos os padrões deste guia. Obtenha uma 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.
Artigos Relacionados
Segmentação de clientes baseada em IA: do RFM ao clustering preditivo
Saiba como a IA transforma a segmentação de clientes, desde a análise estática de RFM até o clustering preditivo dinâmico. Guia de implementação com dados Python, Odoo e ROI real.
IA para otimização da cadeia de suprimentos: visibilidade, previsão e automação
Transforme as operações da cadeia de suprimentos com IA: detecção de demanda, pontuação de risco de fornecedores, otimização de rotas, automação de armazéns e previsão de interrupções. Guia 2026.
Estratégia de comércio eletrônico B2B: construir um negócio online de atacado em 2026
Domine o comércio eletrônico B2B com estratégias de preços de atacado, gerenciamento de contas, condições de crédito, catálogos punchout e configuração do portal Odoo B2B.