DevTalents

Données XML dans Odoo : créer, modifier et supprimer des enregistrements

Mohsen HammediAgence Odoo DevTalents
  • 6 min de lecture
Données XML dans Odoo : créer, modifier et supprimer des enregistrements Billy, la mascotte de DevTalents, examine l'article à la loupe

Dans un module Odoo, les fichiers de données XML servent à créer les enregistrements dont le module a besoin : produits, séquences, modèles d'e-mail, groupes, actions planifiées. Ils servent aussi à modifier les données d'un autre module, à en supprimer, ou à appeler une méthode Python au moment de l'installation. Ce guide reprend chaque cas avec un exemple qui fonctionne, et les pièges que nous rencontrons le plus souvent en revue de code.

Où déclarer les fichiers de données

Un fichier XML n'est chargé que s'il est listé dans le __manifest__.py. La clé data contient ce qui est chargé à chaque installation et à chaque mise à jour du module ; la clé demo ne sert que si la base a été créée avec les données de démonstration.

__manifest__.py

{
    'name': 'My Module',
    'depends': ['product'],
    'data': [
        'security/ir.model.access.csv',
        'data/product_data.xml',
        'views/product_views.xml',
    ],
    'demo': [
        'demo/product_demo.xml',
    ],
}

L'ordre compte : Odoo charge les fichiers dans l'ordre de la liste. Un enregistrement qui en référence un autre doit venir après lui, sinon l'installation s'arrête sur une erreur « External ID not found ».

Créer des enregistrements avec <record>

La balise <record> prend le modèle et un identifiant externe (id). Odoo enregistre cet identifiant dans ir.model.data sous la forme nom_du_module.id : c'est lui qui permet de retrouver l'enregistrement à la mise à jour suivante, au lieu d'en créer un deuxième.

data/product_data.xml

<odoo>
    <record id="product_audit" model="product.product">
        <field name="name">Odoo audit</field>
        <field name="type">service</field>
        <field name="list_price" eval="950.0"/>
        <field name="categ_id" ref="product.product_category_all"/>
        <field name="sale_ok" eval="True"/>
    </record>
</odoo>

Trois façons de renseigner un champ :

  • Le texte de la balise pour les champs texte : <field name="name">Odoo audit</field> ;
  • ref pour un many2one, avec l'identifiant externe de la cible : ref="product.product_category_all" ;
  • eval pour tout ce qui doit être évalué en Python : nombres, booléens, dates, listes. Sans eval, True serait la chaîne « True ».

Pour un many2many ou un one2many, eval reçoit les commandes de l'ORM. (6, 0, ids) remplace toute la liste, (4, id) ajoute un lien sans toucher aux autres. Depuis Odoo 15, ces tuples ont un équivalent plus lisible en Python (Command.set, Command.link), mais les tuples restent la forme la plus courante dans les fichiers XML :

xml

<record id="partner_acme" model="res.partner">
    <field name="name">Acme</field>
    <field name="is_company" eval="True"/>
    <!-- replace the tags with this list -->
    <field name="category_id" eval="[(6, 0, [ref('my_module.tag_reseller'), ref('my_module.tag_france')])]"/>
</record>

Modifier un enregistrement d'un autre module

Pour modifier une donnée livrée par un autre module, reprenez son identifiant complet, préfixé par le nom du module d'origine. Seuls les champs que vous écrivez sont modifiés ; les autres gardent leur valeur. Pensez à mettre le module d'origine dans les depends du vôtre.

xml

<!-- in my_module: only the listed fields are written -->
<record id="product.product_category_all" model="product.category">
    <field name="name">All products</field>
</record>

Piège classique : si l'enregistrement d'origine est déclaré en noupdate="1" (c'est le cas de beaucoup de modèles d'e-mail et de séquences), votre modification s'applique à l'installation de votre module, puis elle est ignorée à chaque mise à jour. Pour forcer l'écriture, on lève le verrou le temps de la modification :

xml

<!-- unlock the record, write it, lock it again -->
<function name="write" model="ir.model.data">
    <function name="search" model="ir.model.data">
        <value eval="[('module', '=', 'sale'), ('name', '=', 'email_template_edi_sale')]"/>
    </function>
    <value eval="{'noupdate': False}"/>
</function>

<record id="sale.email_template_edi_sale" model="mail.template">
    <field name="subject">Your quotation {{ object.name }}</field>
</record>

<function name="write" model="ir.model.data">
    <function name="search" model="ir.model.data">
        <value eval="[('module', '=', 'sale'), ('name', '=', 'email_template_edi_sale')]"/>
    </function>
    <value eval="{'noupdate': True}"/>
</function>

À utiliser avec discernement : si l'utilisateur avait modifié ce modèle d'e-mail dans l'interface, sa version est écrasée.

noupdate : les données que l'utilisateur peut modifier

Les enregistrements placés dans <data noupdate="1"> sont créés à l'installation, puis plus jamais réécrits par une mise à jour du module. C'est ce qu'il faut pour une séquence, un modèle d'e-mail ou un paramètre que l'utilisateur va adapter : sans noupdate, chaque -u remettrait vos valeurs par défaut.

xml

<odoo>
    <data noupdate="1">
        <record id="seq_audit" model="ir.sequence">
            <field name="name">Audit</field>
            <field name="code">my_module.audit</field>
            <field name="prefix">AUD/%(year)s/</field>
            <field name="padding">4</field>
        </record>
    </data>
</odoo>

L'attribut forcecreate="False" sur un <record> va plus loin : si l'enregistrement a été supprimé, la mise à jour ne le recrée pas.

Supprimer des enregistrements avec <delete>

La balise <delete> supprime par identifiant externe ou par domaine :

xml

<!-- by external ID -->
<delete model="product.product" id="my_module.product_audit"/>

<!-- by domain -->
<delete model="product.product" search="[('default_code', '=', 'OLD-AUDIT')]"/>

Dans la pratique, nous supprimons rarement. Un produit, un partenaire ou une étiquette déjà utilisés dans des commandes ou des écritures comptables ne peuvent pas être supprimés, et l'installation échoue. Archiver est presque toujours plus sûr :

xml

<record id="product_audit" model="product.product">
    <field name="active" eval="False"/>
</record>

Appeler une méthode avec <function>

La balise <function> appelle une méthode du modèle pendant le chargement du fichier. C'est pratique pour une initialisation qu'un simple <record> ne sait pas faire. Déclarez la méthode avec @api.model, puisqu'elle est appelée sans enregistrement :

models/product.py

from odoo import api, models


class ProductProduct(models.Model):
    _inherit = 'product.product'

    @api.model
    def _dt_fill_default_codes(self):
        for product in self.search([('default_code', '=', False)]):
            product.default_code = 'P%05d' % product.id

    @api.model
    def _dt_set_price(self, xmlid, price):
        self.env.ref(xmlid).list_price = price

Les paramètres se passent avec des balises <value>, dans l'ordre de la signature. Comme pour les champs, eval transforme la valeur en objet Python :

xml

<!-- without parameters -->
<function model="product.product" name="_dt_fill_default_codes"/>

<!-- with parameters, in the order of the method signature -->
<function model="product.product" name="_dt_set_price">
    <value>my_module.product_audit</value>
    <value eval="1200.0"/>
</function>

Une fonction placée hors d'un bloc noupdate s'exécute à chaque mise à jour du module : écrivez-la pour qu'elle puisse tourner plusieurs fois sans dégât. Dans un bloc noupdate="1", elle ne s'exécute qu'à l'installation. Pour une logique plus lourde, ou qui doit se faire avant ou après l'installation, préférez les hooks de module.

Les erreurs les plus fréquentes

log

ValueError: External ID not found in the system: my_module.product_audit
  • External ID not found : le fichier qui définit l'identifiant est chargé après celui qui l'utilise, ou le module qui le contient manque dans depends.
  • Une modification qui « ne passe pas » à la mise à jour : l'enregistrement est en noupdate, chez vous ou dans le module d'origine.
  • Un doublon après mise à jour : l'id du record a changé entre deux versions. Odoo ne reconnaît plus l'enregistrement et en crée un nouveau.
  • Un booléen ou un nombre enregistré comme texte : il manque eval.

Pour retrouver l'identifiant externe d'un enregistrement existant, activez le mode développeur, ouvrez l'enregistrement et choisissez l'entrée des métadonnées (« View Metadata » en anglais) dans le menu de débogage, l'icône en forme d'insecte.

Pour aller plus loin

Des données à reprendre, un module qui doit évoluer proprement d'une version d'Odoo à l'autre : c'est le quotidien de notre équipe de développement. Développement Odoo sur mesure →

Un projet Odoo en tête ?

Parlez-nous-en : nous vous répondons sous 48 h avec une première analyse.

Parlons de votre projet
Billy, la mascotte de DevTalents, répond à un message sur son téléphone