Se rendre au contenu

Méthodes de modèle Odoo 19 : create, write, unlink et @api.model_create_multi

Bloc 3 · Framework ORM — Article 8/8 Méthodes de modèle Odoo 19 Surcharger create , write , unlink proprement — @api.model_create_multi , @api.
26 avril 2026 par
Méthodes de modèle Odoo 19 : create, write, unlink et @api.model_create_multi
B.Mustapha

Bloc 3 · Framework ORM — Article 8/8

Méthodes de modèle Odoo 19

Surcharger create, write, unlink proprement — @api.model_create_multi, @api.model, méthodes métier action_* — sur le module helpdesk.

~15 minutes de lecture

Ce que tu vas apprendre

create batch

Surcharger create avec @api.model_create_multi — la forme canonique en v19.

write protégé

Figer un enregistrement dans un état final sauf champs whitelistés.

unlink sûr

Empêcher la suppression d'un record dans un état non éligible.

Actions métier

Méthodes action_* appelables depuis les boutons XML, ensure_one(), @api.model.

Prérequis

  • Module odooskills_helpdesk v19.0.1.6.0 de T14 installé.
  • Les bases ORM : modèles (T08), champs (T10), contraintes (T12).
  • Avoir déjà utilisé super() en Python.

1. Le cycle CRUD de l'ORM Odoo

Chaque modèle hérite de models.Model qui expose quatre opérations fondamentales :

create() Insertion — reçoit vals_list @api.model_create_multi read() / search() Lecture — rarement surchargée préférer un compute write() Mise à jour — reçoit vals (dict) signature standard unlink() Suppression — pas d'arguments protéger les états finaux Règle d'or : toujours appeler super() — et respecter la signature exacte. Omettre super() = court-circuiter l'ORM (workflow, tracking, compute stored, logs…) — bug garanti.

2. Surcharger create avec @api.model_create_multi

Historiquement, create() recevait un seul dictionnaire. Depuis Odoo 13+, la forme batch est recommandée, et en Odoo 19 c'est la seule forme acceptée pour une nouvelle surcharge : create(vals_list) — une liste de dicts.

from odoo import api, models, fields


class HelpdeskTicket(models.Model):
    _inherit = 'helpdesk.ticket'

    @api.model_create_multi
    def create(self, vals_list):
        """Génère automatiquement une référence si absente."""
        for vals in vals_list:
            if not vals.get('reference'):
                vals['reference'] = self.env['ir.sequence'].sudo().next_by_code(
                    'helpdesk.ticket'
                ) or '/'
        tickets = super().create(vals_list)
        for ticket in tickets:
            ticket.message_post(body=f"Ticket créé — {ticket.reference}")
        return tickets

Points clés :

  • Décorateur obligatoire — sans @api.model_create_multi, Odoo émet un deprecation warning à l'install et refuse carrément la classe dans certains cas.
  • Signature (self, vals_list)self est vide ici, vals_list est une liste de dicts, une par record à créer.
  • super().create(vals_list) — passe la main à l'ORM qui retourne le recordset des tickets créés (plusieurs d'un coup si batch).
  • sudo() sur ir.sequence — un utilisateur lambda n'a pas accès en écriture aux séquences. C'est l'un des rares cas légitimes de sudo().
Ne reviens pas à l'ancienne forme. Écrire create(vals) sans le décorateur @api.model_create_multi coupe les créations par lot (ex. import, copy_multi, onchange), crée des incohérences et déclenche un warning au chargement du module.

3. Déclarer la séquence ir.sequence

Pour que next_by_code('helpdesk.ticket') retourne HLP/2026/00001, la séquence doit exister. On la crée en data XML (chargée à l'install) :

<!-- data/ir_sequence.xml -->
<odoo noupdate="1">
    <record id="seq_helpdesk_ticket" model="ir.sequence">
        <field name="name">Helpdesk Ticket Reference</field>
        <field name="code">helpdesk.ticket</field>
        <field name="prefix">HLP/%(year)s/</field>
        <field name="padding">5</field>
        <field name="number_increment">1</field>
        <field name="number_next">1</field>
    </record>
</odoo>

Et dans le manifest :

'data': [
    'security/ir.model.access.csv',
    'data/ir_sequence.xml',        # ← nouveau
],

Le noupdate="1" est crucial : on protège la séquence d'un reset à chaque upgrade du module (sinon le compteur repartirait à 1 en écrasant les valeurs courantes).

4. write : figer un état final

Pattern très fréquent : un ticket résolu ne doit plus être modifié — sauf la note de résolution et les tags. On surcharge write :

from odoo.exceptions import ValidationError


class HelpdeskTicket(models.Model):
    _inherit = 'helpdesk.ticket'

    _EDITABLE_WHEN_DONE = {'resolution_note', 'tag_ids', 'message_follower_ids'}

    def write(self, vals):
        protected = set(vals) - self._EDITABLE_WHEN_DONE
        if protected:
            for ticket in self:
                if ticket.state == 'done':
                    raise ValidationError(
                        f"Ticket {ticket.reference} résolu : seuls "
                        f"{', '.join(sorted(self._EDITABLE_WHEN_DONE))} "
                        "restent modifiables."
                    )
        if vals.get('state') == 'done':
            vals.setdefault('resolved_at', fields.Datetime.now())
        return super().write(vals)

Observations :

  • Signature (self, vals) — un seul dict, pas une liste. La forme batch n'existe pas pour write : self est déjà un recordset de N éléments, vals s'applique à chacun.
  • Boucle for ticket in self — nécessaire car les records du recordset peuvent être dans des états différents.
  • Whitelist plutôt que blacklist — on liste les champs autorisés, pas les interdits. Safer (un nouveau champ est protégé par défaut).
  • vals.setdefault plutôt que vals[...] = ... — si l'appelant fournit déjà resolved_at, on respecte sa valeur.

5. unlink : protéger les états critiques

On refuse la suppression d'un ticket en cours ou résolu — seul un ticket Nouveau est éligible. L'ORM appelle unlink sans arguments :

def unlink(self):
    for ticket in self:
        if ticket.state != 'new':
            raise ValidationError(
                f"Ticket {ticket.reference} ({ticket.state}) : "
                "seuls les tickets 'Nouveau' peuvent être supprimés."
            )
    return super().unlink()

Deux fautes fréquentes à éviter :

  • Oublier super().unlink() — le record resterait en base, ses dépendances seraient orphelines.
  • Retourner autre chose que le résultat de super() — certains callers attendent un bool, d'autres un nombre. Retourne toujours tel quel.

6. Les décorateurs @api à connaître

Décorateur self Quand l'utiliser
@api.model Recordset vide Méthodes qui ne dépendent pas d'un record précis : usines, recherches globales, default_get, name_search…
@api.model_create_multi Recordset vide Unique forme pour surcharger create en Odoo 19.
@api.depends('f1', 'f2') Recordset concerné Champs calculés (voir T12).
@api.constrains('f') Recordset à valider Contraintes Python — levé au create/write.
@api.onchange('f') Record virtuel (form) UI seulement — recalculs côté client avant save. Pas persisté.
Aucun décorateur Recordset complet Méthode d'instance standard : actions métier, action_*, helpers.

7. Méthodes métier : la convention action_*

Tout ce qui est déclenché par un bouton XML ou par une action serveur porte le préfixe action_ : action_confirm, action_cancel, action_done. Pratique pour grep, et ça signale l'intention.

def action_start(self):
    """Bascule le ticket en 'En cours'."""
    self.ensure_one()
    if self.state != 'new':
        raise ValidationError("Seul un ticket 'Nouveau' peut être démarré.")
    self.write({'state': 'in_progress'})
    return True


def action_resolve(self):
    """Clôture le ticket."""
    self.ensure_one()
    if self.state == 'done':
        return True
    self.write({'state': 'done'})
    return True


@api.model
def count_open_tickets(self):
    """Méthode globale — @api.model car ne dépend pas d'un record."""
    return self.search_count([('state', '!=', 'done')])

Trois idiomes à intégrer :

  • self.ensure_one() — garantit que la méthode est appelée sur un seul record. Lève ValueError sinon. À utiliser dans toutes les actions de boutons single-record.
  • Déléguer à write plutôt qu'accéder aux colonnes directement : self.state = 'done' fonctionne aussi, mais self.write({...}) applique proprement toutes les hooks (tracking, compute, contraintes).
  • Retourner True ou un dict d'action client — pour un bouton simple, True suffit. Pour ouvrir une vue, retourner un dict ir.actions.act_window.

Upgrade du module et test rapide

./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init

# Dans un shell Odoo : créer un ticket — la référence doit être auto-générée
./odoo-bin shell -c config/odoo.conf -d ta_base
>>> t = env['helpdesk.ticket'].create({'name': 'Test T15'})
>>> t.reference            # → 'HLP/2026/00001'
>>> t.message_ids.mapped('body')   # → ['Ticket créé — HLP/2026/00001']
>>> t.action_start()
>>> t.state                # → 'in_progress'
>>> t.action_resolve()
>>> t.resolved_at          # → datetime auto-rempli par write()
>>> t.unlink()             # → ValidationError : état 'done'

Les 5 pièges à éviter

  1. Oublier super() — casse tracking, mail.thread, compute stored.
  2. Surcharger create sans @api.model_create_multi — deprecated en v19.
  3. Modifier vals après super() — inutile, le record est déjà créé. Tout prétraitement doit être fait avant super().
  4. Ne pas faire ensure_one() sur une action de bouton — si l'action est déclenchée sur plusieurs records, le code casse à la première incohérence.
  5. Utiliser sudo() à la légère — seulement pour ir.sequence, ir.config_parameter, ir.model.access. Jamais pour bypasser des droits métier.

Voir aussi dans cette série

T12 — Contraintes et champs calculés

@api.depends, @api.constrains

T13 — Héritage des modèles

_inherit, _inherits, mixins

T14 — Hiérarchie de modèles

parent_id, _parent_store

Fin du Bloc 3 — Framework ORM 🎉

Tu maîtrises maintenant modèles, champs, relations, contraintes, héritage, hiérarchies et méthodes CRUD. Le Bloc 4 attaque les vues : Form, List, Kanban, Search, menus et actions.

Télécharger le guide technique Odoo 19 (PDF gratuit)
Hiérarchie de modèles Odoo 19 : parent_id, child_ids, _parent_store
Bloc 3 · Framework ORM — Article 7/8 Hiérarchie de modèles Odoo 19 Structurer tes données en arborescence avec parent_id , child_ids , _parent_store et…