Se rendre au contenu

Héritage des modèles Odoo 19 : _inherit, _inherits, AbstractModel

Bloc 3 · Framework ORM — Article 6/8 Héritage des modèles Odoo 19 Les trois formes d'héritage de l'ORM Odoo — _inherit pour étendre, _inherit + _name pour…
26 avril 2026 par
Héritage des modèles Odoo 19 : _inherit, _inherits, AbstractModel
B.Mustapha

Bloc 3 · Framework ORM — Article 6/8

Héritage des modèles Odoo 19

Les trois formes d'héritage de l'ORM Odoo — _inherit pour étendre, _inherit + _name pour copier, _inherits pour déléguer — expliquées sur un module helpdesk réel.

~17 minutes de lecture

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.

Prérequis
Le module fil rouge — Cet article fait passer 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. Extension classique _inherit = 'res.partner' Même modèle, même table Pas de _name → on étend res.partner reste res.partner ✓ Usage typique • Ajouter un champ à un modèle • Surcharger une méthode • Ajouter une contrainte • Modifier une vue Exemple T13 class ResPartner(models.Model): _inherit = 'res.partner' is_vip = fields.Boolean() → colonne is_vip ajoutée à la table res_partner 2. Prototype / Copie _inherit + _name différent Nouveau modèle, nouvelle table _name crée un modèle à part qui hérite des champs/méthodes ✓ Usage typique • Cloner mail.thread → autre modèle • Créer un modèle « comme mais pas » • Rare en pratique ⚠ Préférer _inherit simple Syntaxe class MyPartner(models.Model): _name = 'my.partner' _inherit = 'res.partner' → nouvelle table my_partner avec copie des champs 3. Délégation _inherits (avec S) Composition : « A contient B » Nouvelle table + Many2one vers B Les champs de B sont accessibles ✓ Usage typique • res.users délègue à res.partner • product.product → product.template • Spécialiser sans dupliquer ⚠ Complexe — à utiliser avec soin Syntaxe class VipContract(models.Model): _name = 'vip.contract' _inherits = {'res.partner': 'partner_id'} → table vip_contract + FK vers res_partner

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_vip et ticket_count à la table existante res_partner
  • ticket_ids étant un One2many, aucune colonne n'est créée — c'est la vue inverse de helpdesk_ticket.partner_id
  • Tous les modules qui utilisent res.partner (sale, purchase, account…) voient désormais ces nouveaux champs
Piège classique — Si tu mets _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 les vals avant
  • Capturer l'état avant le super() quand tu veux détecter une transition (ex : is_vip passe de False à True)
  • Utiliser @api.model_create_multi pour create — Odoo 19 passe toujours une liste de vals, 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 vs Model — Un 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).

Ordre important — Si deux mixins définissent le même champ avec des valeurs différentes, c'est la déclaration la plus à droite dans la liste qui l'emporte. Idem pour les surcharges de méthode — Python applique le MRO (Method Resolution Order) sur la liste _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.

À utiliser rarement — Le prototype duplique les données et casse la cohérence avec l'écosystème Odoo (sale, invoicing… pointent vers 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.name ou contract.email — Odoo redirige automatiquement vers contract.partner_id.name
  • À la création d'un vip.contract, Odoo crée automatiquement un res.partner associé si partner_id n'est pas fourni
  • La table vip_contract ne contient que partner_id, contract_ref, discount_pct — les autres champs restent sur res_partner

Exemples natifs Odoo qui utilisent _inherits

Modèle Délègue à Pourquoi
res.usersres.partnerUn utilisateur EST un partenaire (name, email, phone)
product.productproduct.templateUne variante EST un produit (nom, catégorie, taxes)
fleet.vehicleres.partnerUn véhicule lié à un contact fournisseur/conducteur
Règle de choix_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

T08 — Modèles de base

Model, TransientModel, AbstractModel

T11 — Relations entre modèles

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.

Télécharger le guide technique Odoo 19 (PDF gratuit)
Contraintes et champs calculés Odoo 19 : @api.depends, models.Constraint
Bloc 3 · Framework ORM — Article 5/8 Contraintes et champs calculés Odoo 19 Comment créer des champs dont la valeur se calcule automatiquement et poser des…