Se rendre au contenu

Héritage de vues en Odoo 19 : xpath, inherit_id et les 5 positions

Bloc 4 · Interface utilisateur — Article 3/4 Héritage de vues en Odoo 19 Étendre une vue existante sans la réécrire — avec xpath , inherit_id et les cinq…
26 avril 2026 par
Héritage de vues en Odoo 19 : xpath, inherit_id et les 5 positions
B.Mustapha

Bloc 4 · Interface utilisateur — Article 3/4

Héritage de vues en Odoo 19

Étendre une vue existante sans la réécrire — avec xpath, inherit_id et les cinq positions (after, before, inside, replace, attributes). Cas pratique : ajouter un onglet "Tickets" sur la fiche partenaire.

~14 minutes de lecture

Fiche res.partner avec onglet Tickets et bouton statistique ajoutés par héritage
Objectif — sur la form 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.

Prérequis

  • Module odooskills_helpdesk v19.0.1.9.0 (T17).
  • res.partner étendu avec ticket_ids, ticket_count et is_vip — ajoutés dès T13.
  • Bases XPath : //tag[@attribut='valeur'], //, /.

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 :

  1. <field name="inherit_id" ref="..."/> — l'external ID de la vue à étendre.
  2. Un ou plusieurs <xpath expr="..." position="..."/> dans arch.
  3. 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>
Convention de nommagename 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 injecter ton bloc par rapport au nœud ciblé par expr.

<field name="X"/> nœud ciblé par expr before insère juste AVANT after insère juste APRÈS inside ajoute COMME ENFANT replace REMPLACE le nœud attributes modifie readonly, invisible… Les 5 positions d'héritage de vue replace vide (sans contenu) = supprimer le nœud
PositionEffetCas 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. Le d final indique le format entier.
  • search_default_partner_id: id dans 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 :

  • string est 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">
Debug rapide — si une vue héritée "ne fait rien", active le mode développeur, ouvre la vue parente (Paramètres > Technique > Vues) et inspecte l'XML source : vérifie que l'élément ciblé porte bien l'attribut 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

  1. Cibler par @string au lieu de @name — le match échoue, la vue est ignorée ou plante.
  2. Oublier inherit_id — sans lui, Odoo croit que tu veux créer une nouvelle vue primaire et exige le nom complet, pas un patch.
  3. 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.
  4. 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.
  5. 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 pas ir.ui.view.
  6. 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

T13 — Héritage de modèles

_inherit, _inherits, mixins

T16 — Vues Form, List, Search

backend, menus, widgets

T17 — Kanban, Graph, Pivot

QWeb, progressbar, widgets

Prochain article — T19

Place aux wizards : TransientModel, formulaires d'assistant, action_apply, retour d'action depuis une méthode serveur.

Télécharger le guide technique Odoo 19 (PDF gratuit)
Vues Kanban, Graph et Pivot en Odoo 19 : QWeb, widgets et dashboards
Bloc 4 · Interface utilisateur — Article 2/4 Vues Kanban, Graph et Pivot en Odoo 19 Transformer ton module en tableau de bord avec une vue Kanban…