Introduction
Odoo, l'ERP open-source leader, repose sur un système modulaire ultra-flexible qui permet d'étendre ses fonctionnalités sans toucher au core. En 2026, avec Odoo 18+, développer un module personnalisé est essentiel pour adapter l'ERP aux besoins métier complexes comme la gestion de projets multi-équipes avec tracking temps réel et facturation automatisée.
Ce tutoriel avancé vous guide dans la création du module gestion_projets, intégrant : un modèle Projet hérité de mail.thread, des relations one2many vers tâches, un champ computed pour le total d'heures, une vue Kanban dynamique, un wizard pour assigner des tâches en masse, et un contrôleur REST sécurisé. Chaque étape inclut du code complet, fonctionnel et optimisé pour la production.
Pourquoi c'est crucial ? Les modules custom réduisent les coûts de 70% vs. apps tierces, boostent les perfs (ORM OWL) et assurent la souveraineté des données. Prêt à transformer vos specs en module scalable ? (128 mots)
Prérequis
- Odoo 18+ installé en mode développeur (
--dev=all) - PostgreSQL 15+ avec base de test
- Python 3.10+ et wkhtmltopdf pour reports
- Connaissances avancées : ORM Odoo, OWL framework, héritage modèles
- Éditeur VS Code avec extensions Python/XML/Odoo
- Accès shell Odoo pour
odoo-bin scaffold
Générer la structure du module
cd /path/to/your/odoo/addons
./odoo-bin scaffold gestion_projets .
# Éditez __manifest__.py pour activer le module aprèsLa commande scaffold génère l'arborescence standard : models/, views/, security/, data/, wizards/. Cela évite les erreurs de structure courantes. Activez en mode dev via Apps > Mettre à jour la liste des apps, puis installez 'gestion_projets'.
Configurer le manifest principal
Le fichier __manifest__.py définit les métadonnées, dépendances et auto-install. Pour advanced, ajoutez 'assets': {'web.assets_backend': [...]}' pour JS custom et 'external_dependencies': {'python': ['pandas']}` si besoin.
Définir __manifest__.py complet
{
'name': 'Gestion Projets Avancée',
'version': '18.0.1.0.0',
'category': 'Services/Project',
'summary': 'Module avancé pour projets avec tracking heures et wizards',
'description': """
Gestion complète des projets :
- Modèles hérités
- Computed fields
- Wizards one2many
- API REST
""",
'author': 'Learni Dev',
'website': 'https://learni-group.com',
'depends': ['base', 'mail', 'project'],
'data': [
'security/ir.model.access.csv',
'views/projet_views.xml',
'views/projet_menu.xml',
'wizards/assign_task_wizard.xml',
],
'demo': ['data/demo.xml'],
'installable': True,
'auto_install': False,
'application': True,
}Ce manifest dépend de 'project' pour héritage, charge les vues/menus en ordre (security d'abord), et marque comme application pour icône menu. Piège : ordre des data[] faux = crash XML. Version '18.0.1.0.0' suit convention Odoo.
Créer le modèle Projet avec computed
from odoo import models, fields, api
class Projet(models.Model):
_name = 'gestion.projet'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Projet Avancé'
name = fields.Char('Nom Projet', required=True, tracking=True)
client_id = fields.Many2one('res.partner', string='Client', required=True)
total_heures = fields.Float('Total Heures', compute='_compute_total_heures', store=True)
task_ids = fields.One2many('project.task', 'projet_id', string='Tâches')
@api.depends('task_ids.heures_prevues')
def _compute_total_heures(self):
for record in self:
record.total_heures = sum(task.heures_prevues for task in record.task_ids)
def action_view_tasks(self):
return {
'type': 'ir.actions.act_window',
'name': 'Tâches',
'res_model': 'project.task',
'view_mode': 'tree,form',
'domain': [('projet_id', '=', self.id)],
}Héritage mail.thread active discussions/chatter. Computed total_heures stocké pour perfs (store=True), dépend de one2many vers project.task (custom field ajouté via _inherit plus bas). Action button ouvre vue filtrée. Piège : sans @api.depends, recompute foireux.
Étendre project.task pour intégration
Analogie : Comme greffer une branche sur un arbre existant, on hérite project.task pour ajouter projet_id et heures_prevues sans casser les vues standard.
Hériter et étendre project.task
from odoo import models, fields
class ProjectTask(models.Model):
_inherit = 'project.task'
projet_id = fields.Many2one('gestion.projet', string='Projet Parent')
heures_prevues = fields.Float('Heures Prévue', default=8.0)
def _compute_project_id(self):
for task in self:
if not task.projet_id:
task.projet_id = self.env['gestion.projet'].search([('name', 'ilike', task.project_id.name)], limit=1)Extension minimaliste : ajoute champs liés. _compute_project_id auto-assign via search (optimisé limit=1). Utile pour migration données existantes. Piège : héritage sans _inherit = duplication modèles.
Définir les vues XML (tree, form, kanban)
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_projet_tree" model="ir.ui.view">
<field name="name">gestion.projet.tree</field>
<field name="model">gestion.projet</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="client_id"/>
<field name="total_heures"/>
<button name="action_view_tasks" type="object" icon="fa-list"/>
</tree>
</field>
</record>
<record id="view_projet_form" model="ir.ui.view">
<field name="name">gestion.projet.form</field>
<field name="model">gestion.projet</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="client_id"/>
<field name="total_heures" readonly="1"/>
</group>
<notebook>
<page string="Tâches">
<field name="task_ids">
<tree editable="bottom">
<field name="name"/>
<field name="heures_prevues"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_projet_kanban" model="ir.ui.view">
<field name="name">gestion.projet.kanban</field>
<field name="model">gestion.projet</field>
<field name="arch" type="xml">
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card">
<div class="oe_kanban_details">
<strong><field name="name"/></strong>
<field name="total_heures"/>h
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>Vues multi-mode : tree avec button action, form avec notebook one2many editable, kanban OWL-ready. readonly=1 sur computed évite edits manuels. Piège : sans
Sécurité : droits d'accès CSV
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_gestion_projet_user,gestion.projet.user,model_gestion_projet,project.group_project_user,1,0,0,0
access_gestion_projet_manager,gestion.projet.manager,model_gestion_projet,project.group_project_manager,1,1,1,1
access_project_task_projet,project.task.projet,model_project_task,project.group_project_user,1,1,1,1CSV définit accès granulaires : user lit-only, manager full CRUD. Lie à groupes 'project'. Piège : sans ligne, 403 errors partout. Charge avant vues.
Créer wizard d'assignation tâches
from odoo import models, fields, api
class AssignTaskWizard(models.TransientModel):
_name = 'assign.task.wizard'
_description = 'Assigner Tâches à Projet'
projet_id = fields.Many2one('gestion.projet', required=True)
task_ids = fields.Many2many('project.task', string='Tâches à Assigner')
def action_assign(self):
self.task_ids.write({'projet_id': self.projet_id.id})
return {'type': 'ir.actions.act_window_close'}
# Action XML appelée depuis vue
Wizard TransientModel pour actions batch. Many2many sélectionne tâches, write() assigne en masse. Retour close_window. Advanced : ajoutez @api.model pour context domain.
Vues wizard et menu XML
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_assign_task_wizard_form" model="ir.ui.view">
<field name="name">assign.task.wizard.form</field>
<field name="model">assign.task.wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="projet_id"/>
<field name="task_ids" widget="many2many_tags"/>
</group>
<footer>
<button name="action_assign" type="object" string="Assigner" class="btn-primary"/>
</footer>
</form>
</field>
</record>
<record id="action_assign_task_wizard" model="ir.actions.act_window">
<field name="name">Assigner Tâches</field>
<field name="res_model">assign.task.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>Form wizard avec tags widget, button footer-primary. Action popup (target=new). Appelez via dans vue Projet.
Ajouter menu principal
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_gestion_projets_root" name="Gestion Projets" sequence="10"/>
<menuitem id="menu_projets" name="Projets" parent="menu_gestion_projets_root" action="action_projets" sequence="10"/>
</odoo>
<!-- action_projets généré par scaffold, ou ajoutez :
<record id="action_projets" model="ir.actions.act_window">
<field name="name">Projets</field>
<field name="res_model">gestion.projet</field>
<field name="view_mode">tree,form,kanban</field>
</record> -->Menu imbriqué sous root custom. Sequence contrôle ordre. Action multi-view. Piège : sans parent, menu orphelin.
Bonnes pratiques
- Héritage préférentiel : Toujours étendre modèles existants (ex. project.task) vs. nouveaux pour compatibilité upgrades.
- Computed store=True : Pour index/search, mais surveillez triggers DB (max 5 dépends).
- Wizards Transient : Limitez à <500 records pour éviter timeouts.
- Sécurité granulaire : Groupes Odoo standards + record_rules pour RIB.
- Tests unitaires : Ajoutez tests/ avec
self.assertEqual(projet.total_heures, 40).
Erreurs courantes à éviter
- Ordre data[] faux : Security après views = accès denied. Toujours security first.
- @api.depends incomplet : Computed non-triggeré sur one2many → valeurs obsolètes.
- XML sans namespace : sans
= parse fail silencieux. - Mémoire leaks wizards : Many2many sans domain = chargement 10k records crash.
- Pas de tracking : Champs sans tracking=True perdent historique chatter.
Pour aller plus loin
- Docs officielles : Odoo 18 Dev Guide
- OWL pour vues JS custom
- Reports QWeb avancés
- API externe avec controllers