Ce que tu vas apprendre
Model
models.Model — le persistant, 90% des cas.
Wizard
TransientModel — éphémère, purgé par un cron.
Mixin
AbstractModel — code partagé, sans table.
Helpdesk
4 modèles testés, module fil rouge en marche.
- Avoir lu les articles T05 (première approche) et T06 (architecture)
- Un environnement Odoo 19 fonctionnel et un IDE configuré
- Connaître les bases de Python (classes, héritage)
odooskills_helpdesk.
Chaque article ajoute une pièce. Si tu suis toute la série, tu obtiens un module complet à la fin.
Les trois types de modèles, en un coup d'œil
Odoo fournit trois classes de base. Le choix dépend de ce que tu veux stocker et pendant combien de temps.
Règle simple : données durables → Model ; formulaire éphémère → TransientModel ; code partagé → AbstractModel.
1 — models.Model : le modèle persistant
C'est la classe que tu utiliseras dans 90% des cas. Elle crée une table PostgreSQL dédiée et chaque instance (un "record") est stockée durablement en base.
Caractéristiques
- Une table PostgreSQL est créée automatiquement à l'installation du module
- Les records persistent — ils survivent au redémarrage du serveur
- Toutes les fonctionnalités Odoo sont disponibles : vues, recherche, ACL, ORM
Exemple — le ticket helpdesk
On crée notre premier modèle dans models/helpdesk_ticket.py :
from odoo import models, fields
class HelpdeskTicket(models.Model):
"""Ticket de support — modèle persistant."""
_name = 'helpdesk.ticket'
_description = 'Ticket Helpdesk'
_inherit = ['mail.thread', 'mail.activity.mixin', 'odooskills.helpdesk.mixin']
name = fields.Char(string='Sujet', required=True, tracking=True)
description = fields.Text(string='Description')
category_id = fields.Many2one(
comodel_name='helpdesk.ticket.category',
string='Catégorie',
ondelete='restrict',
)
partner_id = fields.Many2one('res.partner', string='Client')
state = fields.Selection(
selection=[
('new', 'Nouveau'),
('in_progress', 'En cours'),
('done', 'Résolu'),
],
default='new',
required=True,
tracking=True,
)
Et un modèle compagnon pour les catégories dans models/helpdesk_ticket_category.py :
from odoo import models, fields
class HelpdeskTicketCategory(models.Model):
"""Catégorie de ticket — modèle persistant simple."""
_name = 'helpdesk.ticket.category'
_description = 'Catégorie de ticket helpdesk'
name = fields.Char(string='Nom', required=True)
color = fields.Integer(string='Couleur')
_name est en minuscules avec des points
(helpdesk.ticket), le nom de classe Python en PascalCase
(HelpdeskTicket). Odoo convertit _name en table PostgreSQL en
remplaçant les points par des underscores : helpdesk_ticket.
2 — models.TransientModel : le modèle temporaire
Un TransientModel a aussi une table PostgreSQL, mais celle-ci est purgée périodiquement par un cron (la base ne grossit pas indéfiniment). C'est la classe à utiliser pour tout ce qui est éphémère : wizards, dialogues, formulaires intermédiaires.
Caractéristiques
- Table PostgreSQL créée, mais vidée toutes les quelques heures par
ir.cron.vacuum - Pas adapté aux données métier — tout record peut disparaître
- Les wizards d'Odoo (popups avec boutons) sont presque tous des TransientModel
Exemple — wizard de clôture de ticket
On veut qu'un utilisateur puisse clôturer un ticket via une popup qui demande une
note de résolution. Pas besoin de persister le formulaire : on le crée en
TransientModel.
from odoo import models, fields
class HelpdeskTicketCloseWizard(models.TransientModel):
"""Wizard de clôture de ticket."""
_name = 'helpdesk.ticket.close.wizard'
_description = 'Clôture de ticket helpdesk (wizard)'
ticket_id = fields.Many2one(
comodel_name='helpdesk.ticket',
string='Ticket',
required=True,
)
resolution_note = fields.Text(string='Note de résolution')
def action_close(self):
self.ensure_one()
self.ticket_id.write({
'state': 'done',
'description': (self.ticket_id.description or '') +
'\n\nRésolution : ' + (self.resolution_note or ''),
})
return {'type': 'ir.actions.act_window_close'}
3 — models.AbstractModel : le mixin réutilisable
Un AbstractModel n'a aucune table en base. Il sert uniquement à partager du code — champs et méthodes — entre plusieurs modèles qui l'héritent.
Caractéristiques
- Pas de table PostgreSQL (pas de records non plus)
- Les modèles qui l'héritent reçoivent ses champs et ses méthodes
- Très utilisé dans Odoo :
mail.thread,mail.activity.mixin,portal.mixin... sont tous des AbstractModel
Exemple — mixin helpdesk
On veut ajouter des champs communs (active, priority) à plusieurs
modèles helpdesk sans dupliquer le code. Un mixin est parfait :
from odoo import models, fields
class HelpdeskMixin(models.AbstractModel):
"""Mixin réutilisable : pas de table en base, uniquement des
champs et méthodes injectés dans les modèles qui l'héritent."""
_name = 'odooskills.helpdesk.mixin'
_description = 'Mixin helpdesk — champs communs'
active = fields.Boolean(default=True)
priority = fields.Selection(
selection=[
('0', 'Normale'),
('1', 'Haute'),
('2', 'Urgente'),
],
default='0',
)
Dans helpdesk.ticket, on l'ajoute à _inherit :
class HelpdeskTicket(models.Model):
_name = 'helpdesk.ticket'
_inherit = ['mail.thread', 'mail.activity.mixin', 'odooskills.helpdesk.mixin']
# ticket hérite automatiquement des champs active et priority
\d helpdesk_ticket dans psql montre bien les colonnes
active et priority héritées du mixin, alors qu'aucune
table odooskills_helpdesk_mixin n'existe.
4 — L'ordre de chargement compte
Quand un modèle hérite d'un autre, celui-ci doit exister au moment
du chargement. L'ordre des imports dans models/__init__.py est donc
important :
# models/__init__.py
# 1. D'abord le mixin (il n'a pas de dépendance)
from . import helpdesk_mixin
# 2. Puis le modèle de catégorie (référencé par le ticket)
from . import helpdesk_ticket_category
# 3. Puis le ticket (dépend du mixin et de la catégorie)
from . import helpdesk_ticket
# 4. Enfin le wizard (dépend du ticket)
from . import helpdesk_ticket_close_wizard
Si tu importes helpdesk_ticket avant helpdesk_mixin,
tu obtiens cette erreur à l'installation :
TypeError: Model 'helpdesk.ticket' inherits from non-existing
model 'odooskills.helpdesk.mixin'.
5 — Manifest et droits d'accès
Le fichier __manifest__.py déclare le module et ses dépendances :
{
'name': 'OdooSkills Helpdesk',
'version': '19.0.1.0.0',
'category': 'Services/Helpdesk',
'summary': 'Module fil rouge du blog OdooSkills — tickets de support',
'author': 'OdooSkills',
'license': 'LGPL-3',
'depends': ['base', 'mail'],
'data': [
'security/ir.model.access.csv',
],
'installable': True,
'application': True,
}
Tout modèle Model ou TransientModel doit avoir
au moins une règle d'accès dans security/ir.model.access.csv. Sans ça,
Odoo refuse les opérations et log des warnings à l'installation :
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_helpdesk_ticket_user,helpdesk.ticket.user,model_helpdesk_ticket,base.group_user,1,1,1,1
access_helpdesk_ticket_category_user,helpdesk.ticket.category.user,model_helpdesk_ticket_category,base.group_user,1,1,1,1
access_helpdesk_ticket_close_wizard_user,helpdesk.ticket.close.wizard.user,model_helpdesk_ticket_close_wizard,base.group_user,1,1,1,1
6 — Installer et vérifier le module
On installe le module sur une base de test :
./odoo-bin -c config/odoo.conf -d odooskills_helpdesk_test \
-i odooskills_helpdesk --stop-after-init
Puis on vérifie que les tables sont bien créées :
psql -d odooskills_helpdesk_test -c "\dt helpdesk*"
Résultat attendu — 3 tables, pas 4 :
List of relations
Schema | Name | Type | Owner
--------+------------------------------+-------+-------
public | helpdesk_ticket | table | odoo
public | helpdesk_ticket_category | table | odoo
public | helpdesk_ticket_close_wizard | table | odoo
(3 rows)
Le mixin odooskills.helpdesk.mixin n'a volontairement
pas de table — c'est bien la preuve que c'est un AbstractModel.
Ses champs sont en revanche présents dans helpdesk_ticket :
psql -d odooskills_helpdesk_test -c "\d helpdesk_ticket" | grep -E 'active|priority'
priority | character varying |
active | boolean |
Les décorateurs @api.multi et @api.one
ont été supprimés depuis la v13, mais on les trouve encore dans
de vieux tutoriels. Ne les utilise jamais :
# ❌ INTERDIT — erreur à l'installation
@api.multi
def action_close(self):
...
# ✅ CORRECT — simplement la méthode, sans décorateur
def action_close(self):
...
En Odoo 19, self est toujours un
recordset. Si ta méthode ne doit traiter qu'un seul enregistrement à la fois,
ajoute self.ensure_one() en première ligne.
Récapitulatif
| Classe | Table PG ? | Durée de vie | Usage |
|---|---|---|---|
models.Model |
✅ Oui | Illimitée | Données métier (90% des cas) |
models.TransientModel |
✅ Oui, mais purgée | Quelques heures | Wizards, dialogues éphémères |
models.AbstractModel |
❌ Non | N/A | Mixins — partager champs et méthodes |
Ce qu'on a ajouté au module odooskills_helpdesk
helpdesk.ticket(Model) — 5 champs, héritera demail.threadhelpdesk.ticket.category(Model) — 2 champs pour classer les ticketshelpdesk.ticket.close.wizard(TransientModel) — dialogue de clôtureodooskills.helpdesk.mixin(AbstractModel) — champsactiveetpriority
Pour aller plus loin
- Article suivant (T09) — Les attributs de modèles :
_order,_rec_name,_sql_constraints,_check_company_autoet leurs usages avancés - Code source du module — disponible sur le dépôt GitHub d'OdooSkills, chaque article correspond à un commit
- Ressources officielles :
Télécharge le Guide Technique Odoo 19
Architecture, pièges v19, checklist premier module — tout dans un PDF gratuit.
Télécharger le guide← Gestion des bases de données Article suivant de la série : Attributs de modèles →