res.partner native on ajoute, sans la
réécrire : un bouton statistique "Tickets" dans la boîte de boutons,
une case Client VIP et un onglet "Tickets" listant
les tickets ouverts du contact.Ce que tu vas apprendre
Le principe
inherit_id cible la vue parente, arch liste les retouches.
Les 5 positions
after, before, inside, replace,
attributes.
Cibler par @name
Pourquoi @string est un piège v19 — et quelles alternatives robustes.
Cas pratique
Onglet "Tickets" + bouton stat + champ VIP sur res.partner.
1. Le principe : patcher, pas cloner
Héritage de vue = patch XML déclaratif. Tu décris des retouches chirurgicales sur la vue parente, Odoo fusionne à la volée. Aucune duplication.
Trois ingrédients obligatoires :
<field name="inherit_id" ref="..."/>— l'external ID de la vue à étendre.- Un ou plusieurs
<xpath expr="..." position="..."/>dansarch. - Optionnel :
priority(défaut 16) pour contrôler l'ordre quand plusieurs modules héritent de la même vue.
Le squelette :
<record id="view_partner_form_inherit_helpdesk" model="ir.ui.view">
<field name="name">res.partner.form.inherit.odooskills.helpdesk</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<!-- 1 à N blocs <xpath> ici -->
</field>
</record>
name en
modèle.vue.inherit.module.objectif aide énormément au debug : quand Odoo
signale "Error in view X", tu sais immédiatement quel module est coupable.
2. Les cinq positions XPath expliquées visuellement
La vue parente est un arbre XML. position dit où injecter ton bloc
par rapport au nœud ciblé par expr.
| Position | Effet | Cas d'usage typique |
|---|---|---|
after |
Insère le contenu juste après le nœud ciblé. | Ajouter un champ à côté d'un autre. |
before |
Insère juste avant. | Placer un champ obligatoire en tête. |
inside |
Ajoute en dernier enfant du nœud ciblé. | Nouvel onglet dans un <notebook>, bouton dans
<div name="button_box">. |
replace |
Remplace le nœud entier. Vide = supprime. | Réécrire un bouton, cacher un champ existant (<xpath ... position="replace"/>). |
attributes |
Modifie/ajoute/supprime des attributs du nœud. | Rendre un champ readonly, changer un label, ajouter
invisible="state == 'done'". |
3. Cas pratique — onglet "Tickets" sur res.partner
Objectif : sur la fiche partenaire standard d'Odoo, afficher un onglet listant ses tickets et un bouton statistique en haut. On combine trois xpath dans une seule vue héritée.
Crée views/res_partner_views.xml :
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_inherit_helpdesk" model="ir.ui.view">
<field name="name">res.partner.form.inherit.odooskills.helpdesk</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<!-- 1. Bouton statistique dans la boîte de boutons -->
<xpath expr="//div[@name='button_box']" position="inside">
<button name="%(action_helpdesk_ticket)d"
type="action"
class="oe_stat_button"
icon="fa-life-ring"
context="{'search_default_partner_id': id,
'default_partner_id': id}">
<field name="ticket_count" widget="statinfo" string="Tickets"/>
</button>
</xpath>
<!-- 2. Case "Client VIP" après les étiquettes -->
<xpath expr="//field[@name='category_id']" position="after">
<field name="is_vip"/>
</xpath>
<!-- 3. Nouvel onglet "Tickets" dans le notebook -->
<xpath expr="//notebook" position="inside">
<page name="helpdesk_tickets" string="Tickets"
invisible="is_company == False and parent_id">
<field name="ticket_ids"
context="{'default_partner_id': id}">
<list decoration-danger="priority == '3'"
decoration-muted="state == 'done'">
<field name="reference"/>
<field name="name"/>
<field name="category_id" optional="show"/>
<field name="priority" widget="priority"/>
<field name="state"/>
<field name="create_date" optional="hide"/>
</list>
</field>
</page>
</xpath>
</field>
</record>
</odoo>
Puis déclare le fichier dans le manifest :
# __manifest__.py
'data': [
'security/ir.model.access.csv',
'data/ir_sequence.xml',
'views/helpdesk_ticket_category_views.xml',
'views/helpdesk_ticket_views.xml',
'views/res_partner_views.xml', # <-- NOUVEAU
'views/helpdesk_menus.xml',
],
Trois détails qui font la différence :
name="%(action_helpdesk_ticket)d"— Odoo résout l'external ID de l'action en son id numérique. Ledfinal indique le format entier.search_default_partner_id: iddans le contexte du bouton — en cliquant, l'action ouvre la liste déjà filtrée sur ce partenaire. L'expérience est native.invisible="is_company == False and parent_id"— sur la fiche d'un contact enfant (employé chez un client), on cache l'onglet : les tickets appartiennent à la société mère. Syntaxe v19, plus d'attrs.
4. Le piège mortel : @string vs @name
Neuf développeurs sur dix écrivent ça au premier module :
<!-- ❌ INTERDIT en Odoo 19 — @string n'est PAS un attribut XML ciblable -->
<xpath expr="//field[@string='Client']" position="after">
<xpath expr="//button[@string='Confirmer']" position="replace">
<xpath expr="//page[@string='Lignes']" position="inside">
Pourquoi ça casse :
stringest une propriété calculée côté Python (label traduit du champ), pas un attribut présent dans l'XML de la vue parente.- Le moteur XPath d'Odoo ne trouve rien — la vue héritée échoue silencieusement ou lance "Element not found in parent view".
- Pire : si tu traduis ton module en anglais,
@string='Client'cesserait de matcher même si c'était supporté. Dépendre d'un texte traduit = bombe à retardement.
La règle : toujours cibler par un identifiant stable. Dans l'ordre de robustesse :
<!-- ✅ PAR NAME (champs, pages, boutons, groupes nommés, div name=...) -->
<xpath expr="//field[@name='partner_id']" position="after">
<xpath expr="//button[@name='action_confirm']" position="replace">
<xpath expr="//page[@name='order_lines']" position="inside">
<xpath expr="//div[@name='button_box']" position="inside">
<!-- ✅ PAR FOR (pour les <label for="...">) -->
<xpath expr="//label[@for='partner_id']" position="after">
<!-- ✅ PAR POSITION (dernier recours : structure native Odoo stable) -->
<xpath expr="//sheet/group[1]" position="inside">
<xpath expr="//notebook/page[last()]" position="after">
name= que tu utilises dans ton expr.
Si le bouton natif n'a pas de name, passe par position ou par l'élément parent.
5. Le raccourci : tag direct sans <xpath>
Pour des patches simples, Odoo accepte une syntaxe compacte : tu réécris le tag directement, avec les attributs qui servent de sélecteur. Équivalent 100% fonctionnel.
<field name="arch" type="xml">
<!-- Équivalent de: <xpath expr="//field[@name='category_id']" position="after"> -->
<field name="category_id" position="after">
<field name="is_vip"/>
</field>
<!-- Équivalent de: <xpath expr="//notebook" position="inside"> -->
<notebook position="inside">
<page name="helpdesk_tickets" string="Tickets">
<field name="ticket_ids"/>
</page>
</notebook>
</field>
Quand l'utiliser — cible unique, pas d'axes XPath ([2],
last()…), pas de prédicat complexe. Dès qu'il y a ambiguïté (plusieurs
<field name='state'/> dans la vue, par exemple), reviens à <xpath>.
6. Bump de version et mise à jour
On monte le module à 19.0.1.10.0 puis on upgrade :
./odoo-bin -c config/odoo.conf \
-u odooskills_helpdesk \
-d odooskills_test \
--stop-after-init
Ouvre ensuite la fiche d'un partenaire qui a des tickets (on a semé TechCorp Algérie, Cabinet Medina, InfoSphere SARL en T16) : le bouton Tickets 2 apparaît en haut, la case VIP entre les étiquettes, et l'onglet Tickets liste les tickets ouverts. Trois xpath, une vue héritée, zéro duplication du code d'Odoo.
Les pièges d'héritage à connaître
- Cibler par
@stringau lieu de@name— le match échoue, la vue est ignorée ou plante. - Oublier
inherit_id— sans lui, Odoo croit que tu veux créer une nouvelle vue primaire et exige le nom complet, pas un patch. - Patcher un nœud absent sur certaines variantes — un bouton ajouté par un autre module payant n'existe pas en CE. Ton expr n'a pas de cible, ta vue plante sur les installs CE. Sécurise avec un fallback conditionnel ou une dépendance propre.
- Priorité manquante — deux modules qui modifient la même zone dans
un ordre arbitraire. Fixe
priority(défaut 16) pour forcer le passage après/avant. - Hériter d'une vue QWeb avec la syntaxe
ir.ui.view— pour un template website, on utilise<template id="..." inherit_id="website.xxx">avec la même logique xpath, mais pasir.ui.view. - Replace + texte brut — le moteur exige un élément XML, pas une chaîne
libre. Si tu veux juste supprimer un nœud,
<xpath ... position="replace"/>vide fait l'affaire.
Voir aussi dans cette série
_inherit, _inherits, mixins
backend, menus, widgets
QWeb, progressbar, widgets
Prochain article — T19
Place aux wizards : TransientModel,
formulaires d'assistant, action_apply, retour d'action depuis une méthode serveur.