Module 2 — Vues XML & OWL

La couche présentation d'Odoo : déclarer des formulaires, listes, kanbans en XML, et créer des composants OWL custom.

5h UI / Frontend

🎯 Objectifs

1. L'architecture UI Odoo

Pour qu'un modèle soit utilisable dans l'interface, il faut 3 briques XML :

  1. Vues (form, tree, kanban…) — déclarent comment afficher le modèle
  2. Action (ir.actions.act_window) — déclare quoi ouvrir (modèle + vues + filtres)
  3. Menu (ir.ui.menu) — déclare dans la barre de navigation
📌
XML ID = identifiant unique d'un record

Chaque record XML a un id appelé XML ID. Format : module_name.record_id. Permet de référencer un record depuis un autre fichier sans connaître son ID numérique. Exemple : base.user_admin.

2. Vue form

La vue formulaire — celle qu'on voit en édition. Structure type :

views/library_book_views.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>

  <record id="view_library_book_form" model="ir.ui.view">
    <field name="name">library.book.form</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
      <form string="Livre">
        <header>
          <button name="action_borrow" string="Emprunter"
                  type="object" class="btn-primary"
                  invisible="state != 'available'"/>
          <field name="state" widget="statusbar"
                 statusbar_visible="draft,available,borrowed"/>
        </header>

        <sheet>
          <div class="oe_title">
            <h1><field name="title" placeholder="Titre du livre"/></h1>
          </div>

          <group>
            <group string="Identification">
              <field name="isbn"/>
              <field name="author_id"/>
              <field name="publish_date"/>
            </group>
            <group string="Classement">
              <field name="tag_ids" widget="many2many_tags"/>
              <field name="category_id"/>
            </group>
          </group>

          <notebook>
            <page string="Description">
              <field name="description"/>
            </page>
            <page string="Réservations">
              <field name="reservation_ids">
                <list>
                  <field name="member_id"/>
                  <field name="date_start"/>
                  <field name="date_end"/>
                </list>
              </field>
            </page>
          </notebook>
        </sheet>

        <chatter/>   <!-- Odoo 19 : remplace les anciens div oe_chatter -->
      </form>
    </field>
  </record>

</odoo>
🆕
Nouveautés Odoo 19

1) Le tag <chatter/> remplace la div oe_chatter de la v17.
2) Les attributs attrs="..." dépréciés depuis la v17 sont totalement supprimés en v19. Utiliser invisible="state != 'draft'" directement.
3) La balise <tree> est officiellement renommée <list> (les deux fonctionnent encore mais list est recommandé).

3. Vue list (ex-tree)

<record id="view_library_book_list" model="ir.ui.view">
  <field name="name">library.book.list</field>
  <field name="model">library.book</field>
  <field name="arch" type="xml">
    <list string="Livres" multi_edit="1" sample="1">
      <field name="title"/>
      <field name="author_id"/>
      <field name="publish_date" optional="show"/>
      <field name="state" widget="badge"
             decoration-success="state == 'available'"
             decoration-warning="state == 'borrowed'"
             decoration-danger="state == 'lost'"/>
    </list>
  </field>
</record>

Attributs utiles :

4. Vue kanban

<record id="view_library_book_kanban" model="ir.ui.view">
  <field name="name">library.book.kanban</field>
  <field name="model">library.book</field>
  <field name="arch" type="xml">
    <kanban default_group_by="state" sample="1">
      <field name="author_id"/>
      <templates>
        <t t-name="kanban-box">
          <div class="oe_kanban_card">
            <strong><field name="title"/></strong>
            <div>par <field name="author_id"/></div>
            <div class="text-muted">
              ISBN: <field name="isbn"/>
            </div>
          </div>
        </t>
      </templates>
    </kanban>
  </field>
</record>

5. Vue search (filtres & group by)

<record id="view_library_book_search" model="ir.ui.view">
  <field name="name">library.book.search</field>
  <field name="model">library.book</field>
  <field name="arch" type="xml">
    <search>
      <field name="title"/>
      <field name="author_id"/>
      <field name="isbn"/>

      <filter name="available" string="Disponibles"
              domain="[('state', '=', 'available')]"/>
      <filter name="borrowed" string="Empruntés"
              domain="[('state', '=', 'borrowed')]"/>
      <separator/>
      <filter name="recent" string="Récents (30j)"
              domain="[('publish_date', '>=', (context_today() - relativedelta(days=30)).strftime('%Y-%m-%d'))]"/>

      <group expand="0" string="Grouper par">
        <filter name="group_state" string="État"
                context="{'group_by': 'state'}"/>
        <filter name="group_author" string="Auteur"
                context="{'group_by': 'author_id'}"/>
      </group>
    </search>
  </field>
</record>

6. Action & menu

<record id="action_library_book" model="ir.actions.act_window">
  <field name="name">Livres</field>
  <field name="res_model">library.book</field>
  <field name="view_mode">list,kanban,form</field>
  <field name="context">{'search_default_available': 1}</field>
  <field name="help" type="html">
    <p class="o_view_nocontent_smiling_face">
      Ajoutez votre premier livre
    </p>
  </field>
</record>

<menuitem id="menu_library_root" name="Bibliothèque" sequence="10"/>
<menuitem id="menu_library_book" name="Livres"
          parent="menu_library_root"
          action="action_library_book"
          sequence="10"/>

7. Héritage de vue (xpath)

Pour modifier une vue existante (la sienne ou celle d'un module standard), on utilise inherit_id + des opérations xpath.

<record id="view_partner_form_inherit_library" model="ir.ui.view">
  <field name="name">res.partner.form.library</field>
  <field name="model">res.partner</field>
  <field name="inherit_id" ref="base.view_partner_form"/>
  <field name="arch" type="xml">

    <!-- Ajouter un champ APRÈS un autre -->
    <xpath expr="//field[@name='vat']" position="after">
      <field name="library_card_id"/>
    </xpath>

    <!-- Ajouter une page dans le notebook -->
    <xpath expr="//notebook" position="inside">
      <page string="Bibliothèque">
        <field name="book_ids"/>
      </page>
    </xpath>

    <!-- Masquer un champ -->
    <xpath expr="//field[@name='website']" position="attributes">
      <attribute name="invisible">1</attribute>
    </xpath>

    <!-- Remplacer complètement un noeud -->
    <xpath expr="//field[@name='comment']" position="replace">
      <field name="library_notes"/>
    </xpath>

  </field>
</record>

Positions xpath disponibles : before, after, inside (à l'intérieur), replace, attributes.

💡
Trouver le bon xpath

En mode dev, ouvrez la vue à modifier dans Settings → Technical → Views → cliquez sur "Edit". Vous voyez l'XML complet et pouvez expérimenter le xpath en direct.

8. Le framework OWL (Odoo Web Library)

Depuis Odoo 17, le frontend est 100% OWL : un framework type Vue.js/React fait par Odoo. En Odoo 19, c'est OWL 2.

Anatomie d'un composant OWL

static/src/components/book_counter.js
import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";

class BookCounter extends Component {
    static template = "library.BookCounter";
    static props = {};

    setup() {
        this.orm = useService("orm");
        this.state = useState({ count: 0 });

        onWillStart(async () => {
            this.state.count = await this.orm.searchCount("library.book", []);
        });
    }
}

registry.category("actions").add("library_book_counter", BookCounter);
static/src/components/book_counter.xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
  <t t-name="library.BookCounter">
    <div class="o_book_counter">
      <h2>Total livres : <t t-esc="state.count"/></t>
    </div>
  </t>
</templates>

Pour qu'OWL trouve les fichiers, déclarer dans le __manifest__.py :

'assets': {
    'web.assets_backend': [
        'library/static/src/components/book_counter.js',
        'library/static/src/components/book_counter.xml',
    ],
},

Concepts OWL 2 essentiels

HookUsage
useState({...})État réactif (re-render auto)
useService("orm")Accès au service ORM (API JSON-RPC)
useService("notification")Toasts/notifications
useService("action")Lancer une action (ouvrir vue, wizard)
onWillStart(fn)Avant le 1er render (async OK)
onMounted(fn)Après l'insertion dans le DOM
useRef("name")Réf vers un élément DOM

Directives template (t-*)

<t t-esc="variable"/>                  <!-- texte échappé -->
<t t-out="htmlVariable"/>              <!-- HTML brut -->
<div t-if="state.count > 0">...</div>
<div t-elif="state.count === 0">...</div>
<div t-else="">...</div>

<t t-foreach="books" t-as="book" t-key="book.id">
  <li><t t-esc="book.title"/></li>
</t>

<button t-on-click="onBorrow">Emprunter</button>
<input t-model="state.searchTerm"/>     <!-- 2-way binding -->
<div t-att-class="{ active: state.isActive }">...</div>

9. Widgets standards (cheatsheet)

widget="..."Champ idéalEffet
statusbarSelectionBarre d'étapes en haut du formulaire
many2many_tagsMany2manyTags colorés en pillules
radioSelectionBoutons radio
handleInteger (sequence)Drag & drop pour réordonner
badgeChar / SelectionBadge coloré (en list)
monetaryMonetaryFormatage devise
emailCharLien mailto, validation visuelle
phoneCharLien tel:
urlCharLien cliquable
imageBinaryAffiche en image
signatureBinaryChamp signature dessinable
progressbarFloat / IntegerBarre de progression
percentageFloatAffichage %
htmlHtmlÉditeur riche

🏋️ Exercice de validation

Mission

  1. Pour le modèle library.book du module 1, créer 4 vues : form, list, kanban, search.
  2. Ajouter une action act_window + un menu racine "Bibliothèque" + un sous-menu "Livres".
  3. Le formulaire doit inclure : header avec statusbar, un bouton "Emprunter" visible uniquement si état = available, des groupes de champs, un notebook avec onglet "Description" et onglet "Tags".
  4. La vue list doit colorer le statut en vert/orange/rouge selon l'état.
  5. Hériter de res.partner pour ajouter un onglet "Mes livres" affichant tous les livres dont la personne est auteur.

🔗 Ressources

📋 Quiz de validation

Module 1 — ORM Module 3 — Avancé