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.
- Avoir lu T10 — Champs non-relationnels
- Le module
odooskills_helpdeskinstallé (version 19.0.1.2.0 minimum)
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é.
La règle fondamentale des relations Odoo
Avant d'entrer dans le détail de chaque type, une règle à retenir :
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 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',
)
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',
)
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
],
})
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_commentavec sa FKticket_id - La table de liaison
helpdesk_ticket_helpdesk_ticket_tag_rel - Les colonnes
user_idsurhelpdesk_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.