Créer un module Odoo personnalisé est la compétence fondamentale de tout développeur Odoo. Ce guide vous emmène de la structure de fichiers jusqu'au déploiement en production, avec des exemples de code réels et des bonnes pratiques issues de projets clients réels au Québec.
- Prérequis et environnement de développement
- Structure d'un module Odoo : les fichiers obligatoires
- Le fichier __manifest__.py expliqué
- Créer un modèle (Model) avec ses champs
- Définir les vues : liste, formulaire, recherche
- Droits d'accès et sécurité
- Menus et actions de fenêtre
- Hériter d'un modèle existant
- Installer et tester son module
- Erreurs fréquentes et comment les éviter
1. Prérequis et environnement de développement
Avant d'écrire la moindre ligne de code, assurez-vous d'avoir un environnement fonctionnel. Pour Odoo 17, voici ce dont vous avez besoin :
- ✓Python 3.10+ — Odoo 17 requiert Python 3.10 minimum (python.org)
- ✓PostgreSQL 14+ — la base de données utilisée par Odoo
- ✓Odoo 17 Community ou Enterprise — cloné depuis github.com/odoo/odoo
- ✓Un IDE — VS Code avec l'extension Python, ou PyCharm
- ✓Un dossier addons personnalisé — séparé du core Odoo pour isoler vos développements
Utilisez un environnement virtuel Python (venv) dédié à chaque projet Odoo. Cela évite les conflits de dépendances entre projets et simplifie la maintenance à long terme.
2. Structure d'un module Odoo : les fichiers obligatoires
Un module Odoo est simplement un dossier Python avec une structure bien définie. Voici la structure minimale pour un module fonctionnel, que nous appellerons mon_module :
├── __init__.py # Rend le dossier importable en Python
├── __manifest__.py # Métadonnées du module (obligatoire)
├── models/
│ ├── __init__.py
│ └── mon_modele.py # Vos modèles de données
├── views/
│ └── mon_modele_views.xml
├── security/
│ ├── ir.model.access.csv
│ └── security.xml
└── data/ # Données de démonstration ou de configuration
└── demo.xml
3. Le fichier __manifest__.py expliqué
Le fichier __manifest__.py est le point d'entrée de votre module. Odoo le lit pour enregistrer votre module dans la liste des applications disponibles. Voici un manifest complet et commenté :
{
'name': 'Mon Module Personnalisé',
'version': '17.0.1.0.0', # Format : version_odoo.majeure.mineure.patch.révision
'category': 'Services',
'summary': 'Description courte du module (max 80 caractères)',
'author': 'Kuantiik',
'website': 'https://kuantiik.com',
'license': 'LGPL-3',
'depends': [
'base', # Dépendance minimale
'mail', # Pour les fonctionnalités de messagerie
'sale', # Si vous étendez le module Ventes
],
'data': [
'security/ir.model.access.csv',
'security/security.xml',
'views/mon_modele_views.xml',
],
'demo': ['data/demo.xml'],
'installable': True,
'application': True,
'auto_install': False,
}
L'ordre des fichiers dans la liste data est important. Les fichiers de sécurité doivent toujours être chargés avant les vues, sinon Odoo lèvera une erreur d'accès à l'installation.
4. Créer un modèle avec ses champs
Les modèles Odoo héritent de models.Model et définissent les tables de base de données via des champs Python. Odoo gère la création et la migration des tables automatiquement.
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class ProjetClient(models.Model):
_name = 'mon.module.projet'
_description = 'Projet Client'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date_debut desc'
name = fields.Char(string='Nom du projet', required=True, tracking=True)
client_id = fields.Many2one(comodel_name='res.partner', string='Client', required=True)
responsable_id = fields.Many2one(
comodel_name='res.users', string='Responsable',
default=lambda self: self.env.user
)
date_debut = fields.Date(string='Date de début', required=True)
date_fin = fields.Date(string='Date de fin prévue')
state = fields.Selection(
selection=[
('brouillon', 'Brouillon'),
('en_cours', 'En cours'),
('termine', 'Terminé'),
('annule', 'Annulé'),
],
default='brouillon', tracking=True
)
duree_jours = fields.Integer(string='Durée (jours)', compute='_compute_duree', store=True)
@api.depends('date_debut', 'date_fin')
def _compute_duree(self):
for record in self:
if record.date_debut and record.date_fin:
record.duree_jours = (record.date_fin - record.date_debut).days
else:
record.duree_jours = 0
@api.constrains('date_debut', 'date_fin')
def _check_dates(self):
for record in self:
if record.date_fin and record.date_debut > record.date_fin:
raise ValidationError("La date de fin doit être postérieure à la date de début.")
def action_demarrer(self):
self.write({'state': 'en_cours'})
def action_terminer(self):
self.write({'state': 'termine'})
5. Définir les vues : liste, formulaire, recherche
Les vues Odoo sont définies en XML. Voici les trois vues essentielles pour rendre votre modèle utilisable dans l'interface :
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- VUE LISTE -->
<record id="view_projet_list" model="ir.ui.view">
<field name="name">mon.module.projet.list</field>
<field name="model">mon.module.projet</field>
<field name="arch" type="xml">
<list string="Projets" decoration-success="state=='termine'">
<field name="name"/>
<field name="client_id"/>
<field name="responsable_id"/>
<field name="date_debut"/>
<field name="duree_jours"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<!-- VUE FORMULAIRE -->
<record id="view_projet_form" model="ir.ui.view">
<field name="name">mon.module.projet.form</field>
<field name="model">mon.module.projet</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_demarrer" string="Démarrer" type="object"
class="btn-primary" invisible="state != 'brouillon'"/>
<button name="action_terminer" string="Marquer terminé" type="object"
invisible="state != 'en_cours'"/>
<field name="state" widget="statusbar"
statusbar_visible="brouillon,en_cours,termine"/>
</header>
<sheet>
<group><group>
<field name="name"/>
<field name="client_id"/>
<field name="responsable_id"/>
</group><group>
<field name="date_debut"/>
<field name="date_fin"/>
<field name="duree_jours"/>
</group></group>
</sheet>
<chatter/>
</form>
</field>
</record>
</odoo>
6. Droits d'accès et sécurité
Sans fichier de droits d'accès, votre module s'installera mais personne ne pourra accéder aux données. Le fichier ir.model.access.csv définit qui peut lire, créer, modifier et supprimer les enregistrements.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_projet_user,projet.user,model_mon_module_projet,base.group_user,1,1,1,0
access_projet_manager,projet.manager,model_mon_module_projet,base.group_system,1,1,1,1
| Colonne | Valeur | Description |
|---|---|---|
model_id:id | model_mon_module_projet | Odoo convertit automatiquement les points en underscores |
perm_read | 1 ou 0 | Permission de lecture |
perm_write | 1 ou 0 | Permission de modification |
perm_create | 1 ou 0 | Permission de création |
perm_unlink | 1 ou 0 | Permission de suppression |
7. Menus et actions de fenêtre
<odoo>
<record id="action_projet_list" model="ir.actions.act_window">
<field name="name">Projets clients</field>
<field name="res_model">mon.module.projet</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_root" name="Mon Module" sequence="10"/>
<menuitem id="menu_projets" name="Projets" parent="menu_root"
action="action_projet_list" sequence="10"/>
</odoo>
8. Hériter d'un modèle existant
L'héritage est l'une des fonctionnalités les plus puissantes d'Odoo. Il vous permet d'ajouter des champs ou de modifier le comportement de n'importe quel modèle existant sans toucher au code source d'Odoo.
from odoo import models, fields
class ResPartnerExtend(models.Model):
_inherit = 'res.partner' # On hérite du modèle partenaire existant
numero_neq = fields.Char(
string='Numéro NEQ (Québec)',
help="Numéro d'entreprise du Québec (Registraire des entreprises)"
)
est_client_gouvernemental = fields.Boolean(string='Client gouvernemental')
La règle d'or du développement Odoo : n'éditez jamais le code source d'Odoo directement. Utilisez toujours l'héritage. C'est ce qui garantit que vos personnalisations survivront aux mises à jour.
9. Installer et tester son module
# Lancer Odoo avec votre dossier addons personnalisé
python odoo-bin \
--addons-path=addons,/chemin/vers/vos/modules \
--dev=all \
-d nom_de_votre_base \
-i mon_module
# Mettre à jour le module après modification
python odoo-bin \
--addons-path=addons,/chemin/vers/vos/modules \
-d nom_de_votre_base \
-u mon_module
Le flag --dev=all active le rechargement automatique des vues XML sans redémarrer le serveur. Activez-le aussi depuis l'interface : Paramètres → Mode développeur.
10. Erreurs fréquentes et comment les éviter
| Erreur | Cause probable | Solution |
|---|---|---|
AccessError | Fichier ir.model.access.csv manquant ou mal configuré | Vérifier que le modèle est listé et le fichier inclus dans le manifest |
ValueError: External ID not found | Référence à un ID externe inexistant dans un fichier XML | Vérifier l'orthographe et le module source de l'ID |
| Module invisible dans la liste | 'installable': False dans le manifest | Passer à True et mettre à jour la liste des apps |
| Champ calculé non mis à jour | Dépendances @api.depends incomplètes | Ajouter tous les champs dont dépend le calcul dans le décorateur |
UndefinedColumn | Colonne SQL non créée après ajout d'un champ | Relancer avec -u nom_module pour migrer le schéma |
Pour aller plus loin, consultez la documentation officielle Odoo 17 — tutoriels serveur et la référence complète de l'ORM Odoo. Pour les questions de sécurité et de règles d'accès avancées, la documentation sécurité Odoo est indispensable.
- → OWL vs QWeb en Odoo 17 : quand utiliser lequel
- → Odoo Community vs Enterprise : lequel choisir pour une PME québécoise ?
- → Combien de temps dure une implantation Odoo ?
- → Odoo pour les entreprises de services professionnels au Québec
Vous avez un besoin de développement Odoo sur mesure ?
Modules personnalisés, intégrations API, migration de données : notre équipe de développeurs Odoo certifiés au Québec vous accompagne de la conception au déploiement.
Discuter de votre projetCréer un module Odoo personnalisé est la compétence fondamentale de tout développeur Odoo. Ce guide vous emmène de la structure de fichiers jusqu'au déploiement en production, avec des exemples de code réels et des bonnes pratiques issues de projets clients réels au Québec.
- Prérequis et environnement de développement
- Structure d'un module Odoo : les fichiers obligatoires
- Le fichier __manifest__.py expliqué
- Créer un modèle (Model) avec ses champs
- Définir les vues : liste, formulaire, recherche
- Droits d'accès et sécurité
- Menus et actions de fenêtre
- Hériter d'un modèle existant
- Installer et tester son module
- Erreurs fréquentes et comment les éviter
1. Prérequis et environnement de développement
Avant d'écrire la moindre ligne de code, assurez-vous d'avoir un environnement fonctionnel. Pour Odoo 17, voici ce dont vous avez besoin :
- ✓Python 3.10+ — Odoo 17 requiert Python 3.10 minimum (python.org)
- ✓PostgreSQL 14+ — la base de données utilisée par Odoo
- ✓Odoo 17 Community ou Enterprise — cloné depuis github.com/odoo/odoo
- ✓Un IDE — VS Code avec l'extension Python, ou PyCharm
- ✓Un dossier addons personnalisé — séparé du core Odoo pour isoler vos développements
Utilisez un environnement virtuel Python (venv) dédié à chaque projet Odoo. Cela évite les conflits de dépendances entre projets et simplifie la maintenance à long terme.
2. Structure d'un module Odoo : les fichiers obligatoires
Un module Odoo est simplement un dossier Python avec une structure bien définie. Voici la structure minimale pour un module fonctionnel, que nous appellerons mon_module :
├── __init__.py # Rend le dossier importable en Python
├── __manifest__.py # Métadonnées du module (obligatoire)
├── models/
│ ├── __init__.py
│ └── mon_modele.py # Vos modèles de données
├── views/
│ └── mon_modele_views.xml
├── security/
│ ├── ir.model.access.csv
│ └── security.xml
└── data/ # Données de démonstration ou de configuration
└── demo.xml
3. Le fichier __manifest__.py expliqué
Le fichier __manifest__.py est le point d'entrée de votre module. Odoo le lit pour enregistrer votre module dans la liste des applications disponibles. Voici un manifest complet et commenté :
{
'name': 'Mon Module Personnalisé',
'version': '17.0.1.0.0', # Format : version_odoo.majeure.mineure.patch.révision
'category': 'Services',
'summary': 'Description courte du module (max 80 caractères)',
'author': 'Kuantiik',
'website': 'https://kuantiik.com',
'license': 'LGPL-3',
'depends': [
'base', # Dépendance minimale
'mail', # Pour les fonctionnalités de messagerie
'sale', # Si vous étendez le module Ventes
],
'data': [
'security/ir.model.access.csv',
'security/security.xml',
'views/mon_modele_views.xml',
],
'demo': ['data/demo.xml'],
'installable': True,
'application': True,
'auto_install': False,
}
L'ordre des fichiers dans la liste data est important. Les fichiers de sécurité doivent toujours être chargés avant les vues, sinon Odoo lèvera une erreur d'accès à l'installation.
4. Créer un modèle avec ses champs
Les modèles Odoo héritent de models.Model et définissent les tables de base de données via des champs Python. Odoo gère la création et la migration des tables automatiquement.
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class ProjetClient(models.Model):
_name = 'mon.module.projet'
_description = 'Projet Client'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date_debut desc'
name = fields.Char(string='Nom du projet', required=True, tracking=True)
client_id = fields.Many2one(comodel_name='res.partner', string='Client', required=True)
responsable_id = fields.Many2one(
comodel_name='res.users', string='Responsable',
default=lambda self: self.env.user
)
date_debut = fields.Date(string='Date de début', required=True)
date_fin = fields.Date(string='Date de fin prévue')
state = fields.Selection(
selection=[
('brouillon', 'Brouillon'),
('en_cours', 'En cours'),
('termine', 'Terminé'),
('annule', 'Annulé'),
],
default='brouillon', tracking=True
)
duree_jours = fields.Integer(string='Durée (jours)', compute='_compute_duree', store=True)
@api.depends('date_debut', 'date_fin')
def _compute_duree(self):
for record in self:
if record.date_debut and record.date_fin:
record.duree_jours = (record.date_fin - record.date_debut).days
else:
record.duree_jours = 0
@api.constrains('date_debut', 'date_fin')
def _check_dates(self):
for record in self:
if record.date_fin and record.date_debut > record.date_fin:
raise ValidationError("La date de fin doit être postérieure à la date de début.")
def action_demarrer(self):
self.write({'state': 'en_cours'})
def action_terminer(self):
self.write({'state': 'termine'})
5. Définir les vues : liste, formulaire, recherche
Les vues Odoo sont définies en XML. Voici les trois vues essentielles pour rendre votre modèle utilisable dans l'interface :
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- VUE LISTE -->
<record id="view_projet_list" model="ir.ui.view">
<field name="name">mon.module.projet.list</field>
<field name="model">mon.module.projet</field>
<field name="arch" type="xml">
<list string="Projets" decoration-success="state=='termine'">
<field name="name"/>
<field name="client_id"/>
<field name="responsable_id"/>
<field name="date_debut"/>
<field name="duree_jours"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<!-- VUE FORMULAIRE -->
<record id="view_projet_form" model="ir.ui.view">
<field name="name">mon.module.projet.form</field>
<field name="model">mon.module.projet</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_demarrer" string="Démarrer" type="object"
class="btn-primary" invisible="state != 'brouillon'"/>
<button name="action_terminer" string="Marquer terminé" type="object"
invisible="state != 'en_cours'"/>
<field name="state" widget="statusbar"
statusbar_visible="brouillon,en_cours,termine"/>
</header>
<sheet>
<group><group>
<field name="name"/>
<field name="client_id"/>
<field name="responsable_id"/>
</group><group>
<field name="date_debut"/>
<field name="date_fin"/>
<field name="duree_jours"/>
</group></group>
</sheet>
<chatter/>
</form>
</field>
</record>
</odoo>
6. Droits d'accès et sécurité
Sans fichier de droits d'accès, votre module s'installera mais personne ne pourra accéder aux données. Le fichier ir.model.access.csv définit qui peut lire, créer, modifier et supprimer les enregistrements.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_projet_user,projet.user,model_mon_module_projet,base.group_user,1,1,1,0
access_projet_manager,projet.manager,model_mon_module_projet,base.group_system,1,1,1,1
| Colonne | Valeur | Description |
|---|---|---|
model_id:id | model_mon_module_projet | Odoo convertit automatiquement les points en underscores |
perm_read | 1 ou 0 | Permission de lecture |
perm_write | 1 ou 0 | Permission de modification |
perm_create | 1 ou 0 | Permission de création |
perm_unlink | 1 ou 0 | Permission de suppression |
7. Menus et actions de fenêtre
<odoo>
<record id="action_projet_list" model="ir.actions.act_window">
<field name="name">Projets clients</field>
<field name="res_model">mon.module.projet</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_root" name="Mon Module" sequence="10"/>
<menuitem id="menu_projets" name="Projets" parent="menu_root"
action="action_projet_list" sequence="10"/>
</odoo>
8. Hériter d'un modèle existant
L'héritage est l'une des fonctionnalités les plus puissantes d'Odoo. Il vous permet d'ajouter des champs ou de modifier le comportement de n'importe quel modèle existant sans toucher au code source d'Odoo.
from odoo import models, fields
class ResPartnerExtend(models.Model):
_inherit = 'res.partner' # On hérite du modèle partenaire existant
numero_neq = fields.Char(
string='Numéro NEQ (Québec)',
help="Numéro d'entreprise du Québec (Registraire des entreprises)"
)
est_client_gouvernemental = fields.Boolean(string='Client gouvernemental')
La règle d'or du développement Odoo : n'éditez jamais le code source d'Odoo directement. Utilisez toujours l'héritage. C'est ce qui garantit que vos personnalisations survivront aux mises à jour.
9. Installer et tester son module
# Lancer Odoo avec votre dossier addons personnalisé
python odoo-bin \
--addons-path=addons,/chemin/vers/vos/modules \
--dev=all \
-d nom_de_votre_base \
-i mon_module
# Mettre à jour le module après modification
python odoo-bin \
--addons-path=addons,/chemin/vers/vos/modules \
-d nom_de_votre_base \
-u mon_module
Le flag --dev=all active le rechargement automatique des vues XML sans redémarrer le serveur. Activez-le aussi depuis l'interface : Paramètres → Mode développeur.
10. Erreurs fréquentes et comment les éviter
| Erreur | Cause probable | Solution |
|---|---|---|
AccessError | Fichier ir.model.access.csv manquant ou mal configuré | Vérifier que le modèle est listé et le fichier inclus dans le manifest |
ValueError: External ID not found | Référence à un ID externe inexistant dans un fichier XML | Vérifier l'orthographe et le module source de l'ID |
| Module invisible dans la liste | 'installable': False dans le manifest | Passer à True et mettre à jour la liste des apps |
| Champ calculé non mis à jour | Dépendances @api.depends incomplètes | Ajouter tous les champs dont dépend le calcul dans le décorateur |
UndefinedColumn | Colonne SQL non créée après ajout d'un champ | Relancer avec -u nom_module pour migrer le schéma |
Pour aller plus loin, consultez la documentation officielle Odoo 17 — tutoriels serveur et la référence complète de l'ORM Odoo. Pour les questions de sécurité et de règles d'accès avancées, la documentation sécurité Odoo est indispensable.
- → OWL vs QWeb en Odoo 17 : quand utiliser lequel
- → Odoo Community vs Enterprise : lequel choisir pour une PME québécoise ?
- → Combien de temps dure une implantation Odoo ?
- → Odoo pour les entreprises de services professionnels au Québec
Vous avez un besoin de développement Odoo sur mesure ?
Modules personnalisés, intégrations API, migration de données : notre équipe de développeurs Odoo certifiés au Québec vous accompagne de la conception au déploiement.
Discuter de votre projet