Ce que tu vas apprendre
Extension
Ajouter des champs à res.partner avec _inherit (même modèle, même table).
Prototype
Copier un modèle avec _inherit + _name pour créer un nouveau modèle basé sur un existant.
Délégation
Composer avec _inherits (au pluriel) — un modèle qui « contient » un autre.
Mixin
Réutiliser un AbstractModel sur plusieurs modèles — pattern SLA.
- Avoir lu T12 — Contraintes et champs calculés
- Connaître Model / TransientModel / AbstractModel (T08)
- Le module
odooskills_helpdeskinstallé (version 19.0.1.4.0 minimum)
odooskills_helpdesk de 19.0.1.4.0 à 19.0.1.5.0.
On étend res.partner avec un champ is_vip et on crée
un mixin SLA (odooskills.sla.mixin) appliqué aux tickets.
Les trois formes d'héritage Odoo
1. _inherit — étendre un modèle existant
C'est la forme d'héritage la plus courante en Odoo — 95% des modules
custom ne font que ça. On déclare une classe qui pointe vers un modèle existant
(via _inherit, sans _name) et on y ajoute
champs, méthodes ou contraintes. Odoo fusionne les déclarations au chargement du registre.
from odoo import api, models, fields
class ResPartner(models.Model):
"""Extension de res.partner — pas de _name, on étend l'existant."""
_inherit = 'res.partner'
is_vip = fields.Boolean(
string='Client VIP',
default=False,
help="Clients prioritaires — SLA réduit, assignation dédiée.",
)
ticket_ids = fields.One2many(
comodel_name='helpdesk.ticket',
inverse_name='partner_id',
string='Tickets',
)
ticket_count = fields.Integer(
string='Nb tickets',
compute='_compute_ticket_count',
)
@api.depends('ticket_ids')
def _compute_ticket_count(self):
for partner in self:
partner.ticket_count = len(partner.ticket_ids)
Ce qui se passe en base
- Odoo ajoute les colonnes
is_vipetticket_countà la table existanteres_partner ticket_idsétant un One2many, aucune colonne n'est créée — c'est la vue inverse dehelpdesk_ticket.partner_id- Tous les modules qui utilisent
res.partner(sale, purchase, account…) voient désormais ces nouveaux champs
_name = 'res.partner'
en plus de _inherit = 'res.partner', tu bascules en
héritage prototype et Odoo va essayer de créer une nouvelle table.
Pour étendre : uniquement _inherit, jamais _name.
2. Surcharger une méthode avec super()
Le même _inherit permet de surcharger les méthodes du modèle
parent. On appelle super() pour déclencher le comportement hérité, puis on
ajoute la logique spécifique.
class ResPartner(models.Model):
_inherit = 'res.partner'
is_vip = fields.Boolean(default=False)
@api.model_create_multi
def create(self, vals_list):
"""Surcharge : log la création des partenaires VIP."""
# 1. Toujours appeler super() pour déclencher la logique parente
partners = super().create(vals_list)
# 2. Ajouter la logique spécifique après
for partner in partners:
if partner.is_vip:
partner.message_post(body="Nouveau client VIP enregistré.")
return partners
def write(self, vals):
"""Surcharge : quand un partenaire devient VIP, notifier."""
was_vip = {p.id: p.is_vip for p in self}
result = super().write(vals)
for partner in self:
if not was_vip[partner.id] and partner.is_vip:
partner.message_post(body="Client promu VIP.")
return result
Règles d'or des surcharges
- Toujours appeler
super()— sinon la logique native (mail.thread, chatter, calculs, etc.) est court-circuitée - Placer le
super()au début pour create (besoin du record créé) et à la fin si tu veux modifier lesvalsavant - Capturer l'état avant le
super()quand tu veux détecter une transition (ex :is_vippasse de False à True) - Utiliser
@api.model_create_multipourcreate— Odoo 19 passe toujours une liste devals, pas un dict
3. AbstractModel — le pattern Mixin
Un AbstractModel est un modèle sans table en base. Il ne
sert qu'à transporter des champs et des méthodes réutilisables, qu'on injecte dans
d'autres modèles via _inherit. C'est l'équivalent des mixins
en Python.
from odoo import api, models, fields
class OdooskillsSlaMixin(models.AbstractModel):
"""Mixin SLA — aucune table, uniquement des champs/méthodes injectés."""
_name = 'odooskills.sla.mixin'
_description = 'Mixin SLA — délai d\'intervention'
sla_hours = fields.Integer(string='SLA (heures)', default=48)
sla_deadline = fields.Datetime(
string='Échéance SLA',
compute='_compute_sla_deadline',
store=True,
)
sla_status = fields.Selection(
selection=[
('ok', 'Dans les temps'),
('warning', 'Proche de l\'échéance'),
('breach', 'SLA dépassé'),
],
string='Statut SLA',
compute='_compute_sla_status',
store=True,
)
@api.depends('sla_hours', 'create_date')
def _compute_sla_deadline(self):
for rec in self:
if rec.create_date and rec.sla_hours:
rec.sla_deadline = fields.Datetime.add(
rec.create_date, hours=rec.sla_hours,
)
else:
rec.sla_deadline = False
@api.depends('sla_deadline')
def _compute_sla_status(self):
now = fields.Datetime.now()
for rec in self:
if not rec.sla_deadline:
rec.sla_status = 'ok'
continue
remaining = (rec.sla_deadline - now).total_seconds() / 3600.0
if remaining < 0:
rec.sla_status = 'breach'
elif remaining < 4:
rec.sla_status = 'warning'
else:
rec.sla_status = 'ok'
Appliquer le mixin sur un modèle réel
class HelpdeskTicket(models.Model):
_name = 'helpdesk.ticket'
_inherit = [
'mail.thread',
'mail.activity.mixin',
'odooskills.helpdesk.mixin',
'odooskills.sla.mixin', # ← on injecte le SLA ici
]
_description = 'Ticket Helpdesk'
# ...champs propres au ticket...
Résultat : helpdesk.ticket a désormais les champs sla_hours,
sla_deadline, sla_status, et les méthodes
_compute_sla_* — sans avoir besoin de les copier. Si demain on crée un
modèle service.request, il suffit d'ajouter
'odooskills.sla.mixin' dans son _inherit.
AbstractModel n'a pas de
table PostgreSQL propre. Ses champs sont copiés dans la table de chaque
modèle qui l'hérite. C'est ce qui rend le pattern si léger.
4. _inherit comme liste — multi-mixins
_inherit accepte une string OU une liste de modèles
parents. Quand c'est une liste, les champs et méthodes sont fusionnés dans l'ordre,
et les conflits se résolvent du dernier au premier
(le plus à droite gagne).
class HelpdeskTicket(models.Model):
_name = 'helpdesk.ticket'
_inherit = [
'mail.thread', # chatter, tracking, subtypes
'mail.activity.mixin', # activités planifiées
'odooskills.helpdesk.mixin', # active + priority (notre mixin)
'odooskills.sla.mixin', # sla_hours + sla_status
]
À l'upgrade, Odoo crée dans helpdesk_ticket toutes les colonnes issues
de ces 4 mixins, plus les champs propres au ticket. Aucune table
odooskills_sla_mixin n'est créée (AbstractModel).
_inherit.
5. _inherit + _name — héritage prototype
Quand tu déclares _inherit et _name avec une
valeur différente, Odoo crée un nouveau modèle qui copie
les champs et méthodes du parent, mais sur une table séparée.
class VipPartner(models.Model):
"""Modèle séparé, copie de res.partner, avec logique propre."""
_name = 'vip.partner'
_inherit = 'res.partner' # copie les champs/méthodes
_description = 'Client VIP dédié'
vip_since = fields.Date(string='VIP depuis')
dedicated_manager_id = fields.Many2one('res.users')
Résultat : une table vip_partner est créée avec une copie
de toutes les colonnes de res_partner, plus les deux champs spécifiques.
Les deux modèles sont totalement indépendants ensuite — modifier un
partenaire dans res.partner n'affecte pas vip.partner.
res.partner, pas vip.partner). Dans 99% des cas,
préfère _inherit simple + un champ is_vip.
6. _inherits — héritage par délégation
Noter le S final : _inherits (avec S) est un mécanisme
totalement différent. Il permet à un modèle de « contenir » un autre
via une Many2one, en exposant transparent les champs du modèle délégué.
class VipContract(models.Model):
_name = 'vip.contract'
_description = 'Contrat VIP lié à un partenaire'
_inherits = {'res.partner': 'partner_id'} # délégation
partner_id = fields.Many2one(
'res.partner',
required=True,
ondelete='cascade',
)
contract_ref = fields.Char(required=True)
discount_pct = fields.Float()
Conséquences magiques de _inherits :
- Tu peux faire
contract.nameoucontract.email— Odoo redirige automatiquement verscontract.partner_id.name - À la création d'un
vip.contract, Odoo crée automatiquement unres.partnerassocié sipartner_idn'est pas fourni - La table
vip_contractne contient quepartner_id,contract_ref,discount_pct— les autres champs restent surres_partner
Exemples natifs Odoo qui utilisent _inherits
| Modèle | Délègue à | Pourquoi |
|---|---|---|
res.users | res.partner | Un utilisateur EST un partenaire (name, email, phone) |
product.product | product.template | Une variante EST un produit (nom, catégorie, taxes) |
fleet.vehicle | res.partner | Un véhicule lié à un contact fournisseur/conducteur |
_inherits est puissant mais
piégeur (cascades, ownership, cycles). Utilise-le uniquement quand la
relation est une vraie composition métier (users/partner, product/template).
Pour du simple « ajouter un champ », reste sur _inherit.
Les trois héritages en un tableau
| Critère | _inherit seul |
_inherit + _name |
_inherits |
|---|---|---|---|
| Table PG | Même que le parent | Nouvelle table (copie) | Nouvelle table + FK |
| Champs hérités | Fusionnés | Copiés | Accessibles via FK (délégation) |
| Méthodes hérités | Oui (surcharge possible) | Oui | Oui (via le record délégué) |
| Nom à utiliser | Identique au parent | Nouveau (_name) |
Nouveau (_name) |
| Fréquence d'usage | 95% | < 1% | ~4% (cas spécifiques) |
| Exemple Odoo natif | Tous les modules métier | Rare — ex : mail.thread → certains templates |
res.users, product.product |
Récapitulatif — fichiers T13
models/res_partner.py — extension classique
from odoo import api, models, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
is_vip = fields.Boolean(string='Client VIP', default=False)
ticket_ids = fields.One2many(
comodel_name='helpdesk.ticket',
inverse_name='partner_id',
string='Tickets',
)
ticket_count = fields.Integer(
string='Nb tickets',
compute='_compute_ticket_count',
)
@api.depends('ticket_ids')
def _compute_ticket_count(self):
for partner in self:
partner.ticket_count = len(partner.ticket_ids)
models/sla_mixin.py — mixin réutilisable
class OdooskillsSlaMixin(models.AbstractModel):
_name = 'odooskills.sla.mixin'
_description = 'Mixin SLA — délai d\'intervention'
sla_hours = fields.Integer(default=48)
sla_deadline = fields.Datetime(compute='_compute_sla_deadline', store=True)
sla_status = fields.Selection([
('ok', 'Dans les temps'),
('warning', 'Proche de l\'échéance'),
('breach', 'SLA dépassé'),
], compute='_compute_sla_status', store=True)
# ...compute methods (voir section 3)...
models/helpdesk_ticket.py — application du mixin
class HelpdeskTicket(models.Model):
_name = 'helpdesk.ticket'
_inherit = [
'mail.thread',
'mail.activity.mixin',
'odooskills.helpdesk.mixin',
'odooskills.sla.mixin', # ← nouveau en T13
]
# ...champs existants...
models/__init__.py
from . import helpdesk_mixin
from . import sla_mixin # ← nouveau
from . import helpdesk_ticket_category
from . import helpdesk_ticket_tag
from . import helpdesk_ticket
from . import helpdesk_ticket_comment
from . import helpdesk_ticket_close_wizard
from . import res_partner # ← nouveau
Upgrade du module
./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init
# Vérifier les nouvelles colonnes
psql -d ta_base -c "\d res_partner" | grep is_vip
psql -d ta_base -c "\d helpdesk_ticket" | grep sla_
Tu dois voir is_vip | boolean dans res_partner et trois
colonnes sla_hours / sla_deadline / sla_status dans
helpdesk_ticket. Aucune table odooskills_sla_mixin
n'est créée — c'est bien un AbstractModel.
Voir aussi dans cette série
Model, TransientModel, AbstractModel
Many2one, One2many, Many2many
T12 — Contraintes et champs calculés
@api.depends, models.Constraint
Prochain article — T14
On voit la hiérarchie de modèles :
parent_id, child_ids, _parent_store,
arborescence et requêtes récursives — appliqués aux catégories de tickets.