Se rendre au contenu

Relations entre modèles Odoo 19 : Many2one, One2many, Many2many

Bloc 3 · Framework ORM — Article 4/8 Relations entre modèles Odoo 19 : Many2one, One2many, Many2many Comment relier les modèles entre eux.
26 avril 2026 par
Relations entre modèles Odoo 19 : Many2one, One2many, Many2many
B.Mustapha

Bloc 3 · Framework ORM — Article 4/8

Relations entre modèles Odoo 19 : Many2one, One2many, Many2many

Comment relier les modèles entre eux. On explore les trois types de relations d'Odoo ORM, ce qu'ils génèrent en PostgreSQL, et les pièges à éviter — le tout sur le module odooskills_helpdesk.

~15 minutes de lecture

Ce que tu vas apprendre

Many2one

FK réelle en base, ondelete, domain, index.

One2many

Relation inverse, pas de colonne PG, inverse_name obligatoire.

Many2many

Table de liaison auto, colonne dans les deux sens, domain.

Prérequis
Le module fil rouge — Cet article fait passer odooskills_helpdesk de 19.0.1.2.0 à 19.0.1.3.0. On ajoute deux nouveaux modèles (helpdesk.ticket.tag, helpdesk.ticket.comment) et trois champs relationnels au ticket.

Schéma entité-relation — ce qu'on va construire

Chaque flèche indique qui porte la clé étrangère et quel type de relation Odoo est utilisé.

helpdesk.ticket user_id Many2one → res.users tag_ids Many2many → ticket.tag comment_ids One2many → ticket.comment category_id Many2one → ticket.category partner_id Many2one → res.partner res.users (natif Odoo) Many2one FK: user_id helpdesk.ticket.tag name (Char) color (Integer) Many2many helpdesk_ticket_tag_rel (table de liaison auto-créée) helpdesk_ticket_id | helpdesk_ticket_tag_id helpdesk.ticket.comment ticket_id (Many2one, FK réelle) One2many inverse de ticket_id Many2one (FK colonne PG) Many2many (table liaison) One2many (vue inverse, pas de colonne) La FK est toujours déclarée sur le modèle Many2one (côté "many") — jamais sur le One2many.

La règle fondamentale des relations Odoo

Avant d'entrer dans le détail de chaque type, une règle à retenir :

La FK est toujours déclarée sur le modèle "many".
Un Many2one crée une vraie colonne en base (la FK).
Un One2many ne crée aucune colonne — c'est juste une vue Python sur la FK du modèle enfant.
Un Many2many crée une table de liaison séparée.

Conséquence pratique : pour un One2many, le modèle enfant doit forcément avoir un champ Many2one pointant vers le parent. C'est l'inverse_name.

1. Many2one — clé étrangère

C'est la relation la plus courante. Un ticket est assigné à un seul utilisateur. Côté PostgreSQL, c'est une simple colonne INTEGER (l'id du record lié) avec une contrainte FOREIGN KEY.

user_id = fields.Many2one(
    comodel_name='res.users',   # modèle cible — OBLIGATOIRE
    string='Assigné à',
    tracking=True,
    ondelete='set null',        # comportement si l'utilisateur est supprimé
    index=True,                 # FK souvent filtrée → index recommandé
)

Le paramètre ondelete

Il définit ce qu'Odoo fait au record parent quand le record lié est supprimé :

ondelete Comportement Quand l'utiliser
'set null' La FK passe à False Relation optionnelle — ticket sans assigné OK
'restrict' Bloque la suppression (erreur) Intégrité forte — empêcher la suppression si lié
'cascade' Supprime aussi le record courant Enfant sans sens sans parent (commentaire → ticket)

Le paramètre domain

Filtre les valeurs proposées dans le widget Many2one des vues :

# N'affiche que les utilisateurs internes (pas les portails/public)
user_id = fields.Many2one(
    comodel_name='res.users',
    string='Assigné à',
    domain=[('share', '=', False)],
    ondelete='set null',
)
Domain Python vs XML — Le domain déclaré dans le champ Python s'applique partout (vues, ORM). On peut aussi le surcharger dans la vue XML pour un contexte particulier, sans toucher au modèle.

2. One2many — relation inverse

Un ticket peut avoir plusieurs commentaires. Du point de vue du ticket, c'est un One2many. Mais la FK réelle est sur le commentaire, pas sur le ticket.

Étape 1 — Le modèle enfant (la FK réelle)

# helpdesk_ticket_comment.py
class HelpdeskTicketComment(models.Model):
    _name = 'helpdesk.ticket.comment'
    _description = 'Commentaire ticket helpdesk'
    _order = 'create_date desc'

    # C'est ICI que se trouve la vraie FK PostgreSQL
    ticket_id = fields.Many2one(
        comodel_name='helpdesk.ticket',
        string='Ticket',
        required=True,
        ondelete='cascade',   # si le ticket est supprimé, les commentaires aussi
        index=True,
    )
    body = fields.Text(string='Commentaire', required=True)
    author_id = fields.Many2one(
        comodel_name='res.users',
        string='Auteur',
        default=lambda self: self.env.user,
        ondelete='set null',
    )

Étape 2 — La relation inverse sur le parent

# Dans helpdesk_ticket.py
comment_ids = fields.One2many(
    comodel_name='helpdesk.ticket.comment',  # modèle enfant
    inverse_name='ticket_id',                # NOM du Many2one dans l'enfant
    string='Commentaires',
)
Erreur classique — Oublier inverse_name ou se tromper dans son nom (doit correspondre exactement à l'attribut Python dans le modèle enfant, pas au string ni au nom de colonne PG). Odoo lève une ValueError au chargement du registry.

Ce que One2many ne fait PAS

  • Il ne crée aucune colonne en base sur helpdesk_ticket
  • Il ne peut pas être required (n'a pas de sens — la liste peut être vide)
  • Il n'a pas de paramètre ondelete (c'est le Many2one enfant qui le gère)

3. Many2many — table de liaison

Un ticket peut avoir plusieurs tags ; un tag peut être sur plusieurs tickets. Ni le ticket ni le tag ne porte de FK — Odoo crée automatiquement une table de liaison avec les deux FK.

Le modèle tag

# helpdesk_ticket_tag.py
class HelpdeskTicketTag(models.Model):
    _name = 'helpdesk.ticket.tag'
    _description = 'Tag de ticket helpdesk'
    _order = 'name'

    name  = fields.Char(string='Nom', required=True)
    color = fields.Integer(string='Couleur', default=0)

Le champ Many2many sur le ticket

# Dans helpdesk_ticket.py
tag_ids = fields.Many2many(
    comodel_name='helpdesk.ticket.tag',
    string='Tags',
)
# → Odoo crée automatiquement la table : helpdesk_ticket_helpdesk_ticket_tag_rel
# avec deux colonnes : helpdesk_ticket_id  et  helpdesk_ticket_tag_id

Nommer la table de liaison manuellement

Par défaut Odoo génère un nom à partir des deux modèles. Si le nom généré dépasse 63 caractères (limite PG) ou si tu veux le contrôler :

tag_ids = fields.Many2many(
    comodel_name='helpdesk.ticket.tag',
    relation='helpdesk_ticket_tag_rel',  # nom exact de la table de liaison
    column1='ticket_id',                 # colonne FK vers le modèle courant
    column2='tag_id',                    # colonne FK vers le modèle cible
    string='Tags',
)
Quand nommer la table ? — Toujours quand les deux modèles ont une seconde relation M2m entre eux (sinon conflit de nom de table). Et systématiquement si le nom généré dépasse ~55 caractères, pour éviter les troncatures silencieuses de PostgreSQL.

Many2many dans les deux sens

On peut aussi exposer la relation depuis le modèle tag pour voir quels tickets ont ce tag :

# Dans helpdesk_ticket_tag.py — la même table de liaison, sens inverse
ticket_ids = fields.Many2many(
    comodel_name='helpdesk.ticket',
    relation='helpdesk_ticket_helpdesk_ticket_tag_rel',  # même table !
    column1='helpdesk_ticket_tag_id',
    column2='helpdesk_ticket_id',
    string='Tickets',
)

Les nouveaux champs dans helpdesk_ticket.py

Extrait des champs relationnels ajoutés à T11 :

    # ── Champs relationnels ajoutés (T11) ────────────────────────────────────

    # Many2one : FK vers res.users
    user_id = fields.Many2one(
        comodel_name='res.users',
        string='Assigné à',
        tracking=True,
        ondelete='set null',
        index=True,
    )

    # Many2many : table de liaison avec helpdesk.ticket.tag
    tag_ids = fields.Many2many(
        comodel_name='helpdesk.ticket.tag',
        string='Tags',
    )

    # One2many : inverse de helpdesk.ticket.comment.ticket_id
    comment_ids = fields.One2many(
        comodel_name='helpdesk.ticket.comment',
        inverse_name='ticket_id',
        string='Commentaires',
    )

Lire et écrire des champs relationnels en ORM

Lire

ticket = self.env['helpdesk.ticket'].browse(1)

# Many2one → retourne un recordset (1 record ou vide)
print(ticket.user_id.name)       # "Alice Dupont"
print(ticket.category_id.id)     # 3

# One2many → retourne un recordset (0..N records)
for comment in ticket.comment_ids:
    print(comment.body)

# Many2many → retourne un recordset (0..N records)
for tag in ticket.tag_ids:
    print(tag.name)

Écrire — commandes spéciales M2m / O2m

Pour écrire dans des champs One2many ou Many2many, Odoo utilise des commandes numériques :

ticket.write({
    # Many2one : passer l'id directement
    'user_id': 5,

    # Many2many : commandes de liaison
    'tag_ids': [
        (4, tag_id),      # (4, id) → ajouter un lien existant
        (3, tag_id),      # (3, id) → supprimer un lien (sans supprimer le record)
        (5, 0, 0),        # (5) → vider tous les liens
        (6, 0, [1, 2]),   # (6, 0, ids) → remplacer par cette liste exacte
    ],

    # One2many : commandes de création/modification/suppression
    'comment_ids': [
        (0, 0, {'body': 'Nouveau commentaire'}),   # (0) → créer
        (1, comment_id, {'body': 'Modifié'}),       # (1, id) → modifier
        (2, comment_id, 0),                         # (2, id) → supprimer
    ],
})
Raccourci create — Lors d'un create(), Odoo accepte aussi une liste de dicts pour les O2m/M2m : 'comment_ids': [{'body': 'Premier commentaire'}] (équivaut à la commande (0, 0, {...})).

Récapitulatif — choisir le bon type de relation

Situation Type Odoo PG généré
Un ticket appartient à une catégorie Many2one Colonne FK category_id INTEGER
Une catégorie a plusieurs tickets (vue inverse) One2many Rien — lit la FK du ticket
Un ticket a plusieurs tags ; un tag sur plusieurs tickets Many2many Table de liaison *_rel
Un ticket a plusieurs commentaires enfants One2many + Many2one sur l'enfant FK sur le commentaire, pas sur le ticket

Upgrade du module

On passe en 19.0.1.3.0. Après le -u, Odoo crée :

  • La table helpdesk_ticket_tag
  • La table helpdesk_ticket_comment avec sa FK ticket_id
  • La table de liaison helpdesk_ticket_helpdesk_ticket_tag_rel
  • Les colonnes user_id sur helpdesk_ticket
./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init

# Vérifier les nouvelles tables
# psql -d ta_base -c "\dt helpdesk*"

Prochain article — T12

On aborde les contraintes et champs calculés : @api.depends, @api.onchange, models.Constraint et les computed fields stockés ou non.

Télécharger le guide technique Odoo 19 (PDF gratuit)
Champs non-relationnels Odoo 19 : Char, Float, Date, Html, Monetary…
Bloc 3 · Framework ORM — Article 3/8 Champs non-relationnels Odoo 19 : Char, Float, Date, Html, Monetary… Odoo propose 12 types de champs scalaires .