Développement Odoo Python : Guide complet pour les débutants et les pros
Selon l'enquête 2025 sur les développeurs Python, plus de 47 % des développeurs Python d'entreprise travaillent avec au moins un framework ERP, et Odoo domine ce segment avec 68 % de part de marché parmi les ERP open source. Que vous construisiez votre premier module ou que vous conceviez un déploiement multi-entreprises, comprendre la couche Python d'Odoo est la compétence la plus importante que vous puissiez développer.
Ce guide va bien au-delà des aperçus superficiels. Nous passerons en revue chaque couche du backend Python d'Odoo - de la magie des métaclasses de l'ORM aux modèles de contrôleur avancés, du débogage des problèmes de production à l'écriture de tests qui détectent réellement les régressions.
Points clés à retenir
- L'ORM d'Odoo est un mappeur relationnel objet complet construit sur des métaclasses qui génèrent automatiquement des schémas SQL, gèrent la mise en cache et gèrent la sécurité multi-entreprises — le comprendre en profondeur fait la différence entre travailler avec Odoo et le combattre.
- Cinq catégories de champs couvrent tous les besoins de modélisation de données : champs de base (Char, Integer, Float, Boolean, Text, Html, Date, Datetime, Binary, Selection), relationnels (Many2one, One2many, Many2many), calculés, associés et monétaires.
- Trois mécanismes d'héritage vous permettent d'étendre n'importe quelle partie d'Odoo sans bifurquer : l'héritage de classe (_inherit), l'héritage de prototype (_inherit + _name) et l'héritage de délégation (_inherits).
- Les contrôleurs gèrent HTTP à l'aide de décorateurs (@http.route) avec des modèles de points de terminaison JSON-RPC et HTTP, des modes d'authentification et une configuration CORS.
- Les tests automatisés avec TransactionCase et HttpCase détectent les régressions avant qu'elles n'atteignent la production – visez une couverture de plus de 80 % sur la logique métier.
1. Configuration de votre environnement de développement
Avant d'écrire du code, vous avez besoin d'un environnement de développement approprié. Voici la configuration recommandée pour 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
Créez un répertoire custom_addons pour vos modules. Chaque module vit dans son propre sous-répertoire avec une structure spécifique attendue par Odoo.
2. Analyse approfondie de l'architecture des modules
Chaque module Odoo suit une structure de convention sur configuration. Comprendre le rôle de chaque composant est essentiel :
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
Le fichier manifeste
Le __manifest__.py est le cœur de votre module. Chaque domaine compte :
# __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
},
}
Convention de numérotation des versions : {odoo_version}.{major}.{minor}.{patch} — le premier numéro correspond toujours à la série Odoo.
3. L'API ORM : référence complète
L'ORM d'Odoo est bien plus puissant que la plupart des développeurs ne le pensent. Il gère automatiquement la génération de schémas, la mise en cache, le contrôle d’accès et le filtrage multi-entreprises.
Types de modèles
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 %')
Types de champs Analyse approfondie
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)
Opérations 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
Expressions de domaine
Les domaines sont le langage de requête d'Odoo. Maîtrisez-les :
# 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. Modèles d'héritage
Odoo fournit trois mécanismes d'héritage qui vous permettent d'étendre n'importe quel module sans modifier son code source. L'héritage de classe ajoute des champs et des méthodes aux modèles existants. L'héritage de prototype crée de nouveaux modèles basés sur ceux existants avec des tables de base de données distinctes. L'héritage de délégation lie les modèles via la composition, créant automatiquement des enregistrements dans les deux tables.
Héritage de classe (le plus courant)
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,
})
Héritage des prototypes
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')
Héritage de délégation
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. Modèles de logique métier
Contraintes
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éthodes de changement
@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.',
}
}
Workflow avec State Machine
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. Contrôleurs et points de terminaison 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'),
]
)
Modes d'authentification
| Mode | Descriptif | Cas d'utilisation |
|---|---|---|
auth='user' | Nécessite une session de connexion | Pages internes, actions utilisateur |
auth='public' | Session facultative, peut être anonyme | Pages du site Web |
auth='none' | Pas de session, pas d'environnement | Bilans de santé, points de terminaison statiques |
auth='api_key' | Clé API dans l'en-tête | Intégrations REST externes |
7. Sécurité : ACL et règles d'enregistrement
Listes de contrôle d'accès (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
Règles d'enregistrement (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. Techniques de débogage
Utilisation du shell 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
Journalisation
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
Profilage des performances
# 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()
Pièges courants en matière de performances :
- Accès aux champs relationnels dans les boucles (requêtes N+1) — utilisez plutôt
read()oumapped() - Appel de
search()dans des boucles - regroupez vos recherches - Ne pas utiliser
store=Truesur les champs calculés fréquemment filtrés - L'oubli de
sudo()entraîne des contrôles supplémentaires des droits d'accès à chaque opération
9. Tests automatisés
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')
Exécutez des tests avec :
# 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. Modèles avancés
Actions planifiées (tâches 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>
Modèles multi-entreprises
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)])
Modèle de mixage pour une logique réutilisable
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']
Exemple concret : module de commission complet
Pour un exemple de module de commission complet et prêt pour la production, visitez les modules Odoo open source d'ECOSIRE où nous publions des connecteurs, des utilitaires et des modules métier suivant toutes les meilleures pratiques couvertes dans ce guide.
Notre équipe de développement Odoo a construit plus de 60 modules Odoo en utilisant ces modèles exacts. Si vous avez besoin d'un développement Odoo Python personnalisé – d'un simple ajout de champ à une implémentation ERP complète – contactez notre équipe pour une consultation gratuite.
Questions fréquemment posées
De quelle version de Python Odoo 18 a-t-il besoin ?
Odoo 18 nécessite Python 3.10 ou supérieur, Python 3.12 étant la version recommandée. Odoo 17 prend en charge Python 3.10+. Vérifiez toujours le fichier exigences.txt officiel pour connaître les dépendances exactes nécessaires à votre version d'Odoo.
Comment déboguer les requêtes Odoo ORM ?
Activez la journalisation SQL en définissant log_level sur debug_sql dans votre fichier odoo.conf, ou transmettez --log-sql sur la ligne de commande. Vous pouvez également utiliser self.env.cr.sql_log pour tracer les requêtes dans le code. Pour le profilage des performances, utilisez le décorateur @profile intégré d'odoo.tools.profiler.
Dois-je utiliser _inherit ou _inherits pour étendre des modèles ?
Utilisez _inherit (héritage de classe) 95 % du temps : il ajoute des champs et des méthodes à la table d'un modèle existant. Utilisez _inherits (délégation) uniquement lorsque vous avez besoin d'un modèle distinct qui accède de manière transparente aux champs d'un autre modèle, par exemple en créant un type d'employé spécialisé qui hérite de tous les champs hr.employee.
Comment gérer l'isolation des données multi-entreprises ?
Ajoutez un champ company_id Many2one à votre modèle avec un lambda par défaut renvoyant self.env.company. Créez ensuite une ir.rule globale avec le domaine [('company_id', 'in', company_ids)]. Cela garantit que les utilisateurs ne voient que les enregistrements des entreprises auxquelles ils ont accès, ce qui est le modèle multi-entreprises standard d'Odoo.
Quelle est la différence entre @api.depends et @api.onchange ?
@api.depends se déclenche lorsque les champs spécifiés changent dans la base de données et est utilisé pour les champs calculés stockés — il fonctionne à la fois pour les modifications de l'interface utilisateur et du programme. @api.onchange se déclenche uniquement dans l'interface utilisateur lorsqu'un utilisateur modifie un champ et est utilisé pour définir les valeurs par défaut ou afficher des avertissements. Utilisez @api.depends pour l'intégrité des données et @api.onchange pour les améliorations UX.
Comment tester efficacement mon module Odoo ?
Utilisez TransactionCase pour les tests unitaires qui testent la logique métier : chaque test s'exécute dans une transaction qui est annulée. Utilisez HttpCase pour tester les contrôleurs et les visites Web. Marquez les tests avec @tagged('post_install', '-at_install') à exécuter après l'installation du module. Visez une couverture de code d'au moins 80 % sur les méthodes de logique métier.
Prochaines étapes
La création de modules Odoo de qualité production nécessite à la fois des compétences techniques et une expertise du domaine. Commencez par un simple héritage de classe, puis progressez vers des modules entièrement personnalisés. Pour aller plus loin :
- Guide de développement de modules personnalisés Odoo — Aperçu de niveau supérieur du développement de modules
- Tutoriel API REST Odoo — Intégrations externes avec Odoo
- Meilleur logiciel ERP 2026 — Comprendre la position d'Odoo sur le marché
Besoin de développeurs Odoo experts ? L'équipe de développement Odoo d'ECOSIRE a fourni plus de 200 modules personnalisés pour les entreprises dans 40 pays. Des moteurs de commission aux intégrations complètes du marché, notre équipe écrit du code prêt pour la production en suivant chaque modèle de ce guide. Obtenez une consultation gratuite.
Rédigé par
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.
Articles connexes
Segmentation client basée sur l'IA : du RFM au clustering prédictif
Découvrez comment l'IA transforme la segmentation client de l'analyse RFM statique au clustering prédictif dynamique. Guide d'implémentation avec Python, Odoo et données de retour sur investissement réel.
IA pour l'optimisation de la chaîne d'approvisionnement : visibilité, prédiction et automatisation
Transformez les opérations de la chaîne d'approvisionnement grâce à l'IA : détection de la demande, évaluation des risques des fournisseurs, optimisation des itinéraires, automatisation des entrepôts et prévision des perturbations. Guide 2026.
Stratégie de commerce électronique B2B : créer une entreprise de vente en gros en ligne en 2026
Maîtrisez le commerce électronique B2B avec des stratégies de prix de gros, de gestion des comptes, de conditions de crédit, de catalogues punchout et de configuration du portail Odoo B2B.