Se rendre au contenu

Vues Form, List et Search en Odoo 19 : actions, menus et widgets

Bloc 4 · Interface utilisateur — Article 1/4 Vues Form, List et Search en Odoo 19 Donner enfin un backend à ton module helpdesk — vue liste, formulaire avec…
26 avril 2026 par
Vues Form, List et Search en Odoo 19 : actions, menus et widgets
B.Mustapha

Bloc 4 · Interface utilisateur — Article 1/4

Vues Form, List et Search en Odoo 19

Donner enfin un backend à ton module helpdesk — vue liste, formulaire avec statusbar, barre de recherche, filtres, menus et action act_window, sans aucun attribut attrs (v17 is dead).

~18 minutes de lecture

Ce que tu vas apprendre

Architecture

Comment ir.ui.view, ir.actions.act_window et ir.ui.menu s'articulent.

Vue List

<list> (ex-tree), decoration-*, widgets colonnes.

Vue Form

Header, sheet, notebook, statusbar, boutons d'action, chatter.

Vue Search

Filtres, group by, search_default_*, recherche multi-champs.

Prérequis

  • Module odooskills_helpdesk v19.0.1.7.0 (T15).
  • Sécurité du modèle déjà posée (security/ir.model.access.csv) — sinon le menu s'affiche mais les records sont invisibles.
  • Connaître les champs et relations du modèle (T10/T11).

1. L'architecture Menu → Action → Vue

Le backend Odoo suit une chaîne simple : l'utilisateur clique sur un menu, le menu déclenche une action, l'action ouvre un ou plusieurs vues sur un modèle donné.

ir.ui.menu "Tickets" Entrée visible dans la barre action ir.actions.act_window res_model, view_mode… Décrit quoi afficher view_id / view_mode ir.ui.view (list) ir.ui.view (form) ir.ui.view (search) Un modèle peut avoir plusieurs vues d'un même type : Odoo choisit la plus prioritaire. La vue search est unique par action, partagée entre list/form/kanban.
⚠ Rappel Odoo 19 : la balise <tree> a été renommée en <list>, et view_mode utilise list,form (pas tree,form). Tout comme attrs et states sont supprimés — on utilise invisible="state == 'done'" directement.

2. La vue <list>

Tableau scrollable, tri, multi-sélection, exports. C'est la porte d'entrée vers les enregistrements. Pour notre modèle helpdesk.ticket :

<record id="view_helpdesk_ticket_list" model="ir.ui.view">
    <field name="name">helpdesk.ticket.list</field>
    <field name="model">helpdesk.ticket</field>
    <field name="arch" type="xml">
        <list string="Tickets"
              decoration-danger="is_overdue"
              decoration-success="state == 'done'"
              decoration-muted="state == 'done'">
            <field name="reference"/>
            <field name="name"/>
            <field name="partner_id"/>
            <field name="category_id"/>
            <field name="user_id" widget="many2one_avatar_user"/>
            <field name="tag_ids" widget="many2many_tags"
                   options="{'color_field': 'color'}"/>
            <field name="deadline"/>
            <field name="is_overdue" invisible="1"/>
            <field name="state" widget="badge"
                   decoration-info="state == 'new'"
                   decoration-warning="state == 'in_progress'"
                   decoration-success="state == 'done'"/>
        </list>
    </field>
</record>
Vue liste des tickets avec références, tags et badges d'état
Vue liste rendue — références auto HLP/2026/NNNNN, lignes rouges pour les tickets en retard, lignes vertes pour les résolus, badges colorés sur state.

Points notables :

  • decoration-* colorise les lignes selon une expression Python — danger (rouge), warning (orange), success (vert), info, muted, primary.
  • invisible="1" — on charge is_overdue dans la vue pour pouvoir l'évaluer dans decoration-danger, sans l'afficher comme colonne.
  • widget="badge" affiche la selection comme pastille colorée — très lisible sur un statut.
  • widget="many2one_avatar_user" montre l'avatar utilisateur — UI Odoo moderne, disponible sur tout Many2one vers res.users.

3. La vue <form>

Écran de détail d'un enregistrement — la vue la plus riche, structurée en trois zones : header (actions + statusbar), sheet (contenu édité), chatter (historique + messages).

<form string="Ticket">
    <header>
        <button name="action_start" type="object" string="Démarrer"
                class="oe_highlight" invisible="state != 'new'"/>
        <button name="action_resolve" type="object" string="Résoudre"
                class="oe_highlight" invisible="state != 'in_progress'"/>
        <field name="state" widget="statusbar"
               statusbar_visible="new,in_progress,done"/>
    </header>
    <sheet>
        <div class="oe_title">
            <label for="name" string="Sujet"/>
            <h1><field name="name" placeholder="Ex : Imprimante bloquée"/></h1>
        </div>
        <group>
            <group>
                <field name="partner_id"/>
                <field name="category_id" options="{'no_create': True}"/>
                <field name="user_id" widget="many2one_avatar_user"/>
                <field name="channel"/>
            </group>
            <group>
                <field name="deadline"/>
                <field name="resolved_at" readonly="1" invisible="state != 'done'"/>
                <field name="sla_hours"/>
                <field name="sla_status" widget="badge"
                       decoration-success="sla_status == 'ok'"
                       decoration-danger="sla_status == 'breach'"/>
            </group>
        </group>
        <notebook>
            <page string="Description" name="description">
                <field name="description"/>
            </page>
            <page string="Tags" name="tags">
                <field name="tag_ids" widget="many2many_tags"/>
            </page>
            <page string="Résolution" name="resolution"
                  invisible="state != 'done'">
                <field name="resolution_note"/>
                <field name="hours_spent"/>
            </page>
        </notebook>
    </sheet>
    <chatter/>
</form>
Vue formulaire du ticket helpdesk avec statusbar, notebook et chatter
Form rendue — bouton Démarrer (bleu, masqué si state != 'new'), statusbar en haut, deux colonnes de champs, notebook Description/Tags/Commentaires, chatter à droite avec les messages auto postés par create() (T15).

Les cinq mécaniques à retenir :

  • Boutons headertype="object" appelle une méthode Python (nos action_start / action_resolve de T15). class="oe_highlight" les rend bleus.
  • widget="statusbar" transforme le Selection state en flux visuel. statusbar_visible limite les étapes affichées.
  • Expressions v19 directes : invisible="state != 'new'" remplace l'ancien attrs="{'invisible': [('state','!=','new')]}".
  • Deux <group> imbriqués produisent la mise en colonnes gauche/droite — c'est la grille Odoo native.
  • <chatter/> (v19) remplace l'ancien bloc oe_chatter. Ajoute automatiquement followers, activités, messages — à condition que le modèle hérite de mail.thread.

4. La vue <search>

Souvent négligée, c'est pourtant la différence entre un module jouable et un module pro. Trois briques : champs de recherche, filtres prédéfinis, groupements.

<search string="Tickets">
    <!-- Champs : ce que tape l'utilisateur dans la barre -->
    <field name="name" string="Sujet/Référence"
           filter_domain="['|', ('name', 'ilike', self),
                                ('reference', 'ilike', self)]"/>
    <field name="partner_id"/>
    <field name="category_id"/>
    <field name="tag_ids"/>

    <!-- Filtres : cases à cocher rapides -->
    <filter name="filter_mine" string="Mes tickets"
            domain="[('user_id', '=', uid)]"/>
    <filter name="filter_unassigned" string="Non assignés"
            domain="[('user_id', '=', False)]"/>
    <separator/>
    <filter name="filter_new" string="Nouveaux"
            domain="[('state', '=', 'new')]"/>
    <filter name="filter_overdue" string="En retard"
            domain="[('is_overdue', '=', True)]"/>

    <!-- Groupements -->
    <separator/>
    <filter name="group_state" string="Statut"
            context="{'group_by': 'state'}"/>
    <filter name="group_category" string="Catégorie"
            context="{'group_by': 'category_id'}"/>
    <filter name="group_user" string="Assigné à"
            context="{'group_by': 'user_id'}"/>
</search>
Dropdown de recherche avec filtres Mes tickets, Nouveaux, En retard et regroupements
Dropdown de recherche ouvert — colonne Filtres (Mes tickets, Non assignés, Nouveaux, En cours, En retard, Urgents), colonne Regrouper par (Statut, Catégorie, Assigné à, Canal) et Favoris pour sauvegarder une recherche.
Liste de tickets regroupée par statut Nouveau, En cours, Résolu
Le filtre Statut (context="{'group_by': 'state'}") activé — les tickets sont regroupés et chaque groupe est pliable.

Trois pépites à connaître :

  • filter_domain remplace le domaine auto — indispensable pour les recherches sur plusieurs champs à la fois (ici sujet OU référence). self désigne la chaîne saisie par l'utilisateur.
  • uid (pas user.id !) est disponible dans les domaines des vues search — utilisateur courant, sans eval Python.
  • context="{'group_by': 'champ'}" transforme un filtre en groupement — même syntaxe, effet différent. Plusieurs group_by activés s'empilent.

5. L'action ir.actions.act_window

L'action fait le lien entre un menu et un modèle, et décide des vues à ouvrir.

<record id="action_helpdesk_ticket" model="ir.actions.act_window">
    <field name="name">Tickets</field>
    <field name="res_model">helpdesk.ticket</field>
    <field name="view_mode">list,form</field>
    <field name="search_view_id" ref="view_helpdesk_ticket_search"/>
    <field name="context">{'search_default_filter_new': 1}</field>
    <field name="help" type="html">
        <p class="o_view_nocontent_smiling_face">Créer un premier ticket</p>
        <p>Enregistre les demandes de support, assigne-les, suis les SLA.</p>
    </field>
</record>
ChampRôle
view_modeOrdre des vues. La première (list) s'ouvre par défaut.
search_view_idVue search à utiliser (sinon Odoo prend la plus prioritaire sur le modèle).
contextsearch_default_<name>: 1 active automatiquement le filtre <name> à l'ouverture.
helpÉcran vide — message quand aucun enregistrement. HTML autorisé.
domain (optionnel)Filtre statique toujours appliqué. Ex: [('active','=',True)].

6. Les menus ir.ui.menu

Trois niveaux suffisent pour un module bien structuré : racine (menu principal), catégorie (fonctionnel ou configuration), entrée cliquable (liée à une action).

<menuitem id="menu_helpdesk_root"
          name="Helpdesk"
          sequence="90"
          web_icon="odooskills_helpdesk,static/description/icon.png"/>

<menuitem id="menu_helpdesk_tickets"
          name="Tickets"
          parent="menu_helpdesk_root"
          action="action_helpdesk_ticket"
          sequence="10"/>

<menuitem id="menu_helpdesk_config"
          name="Configuration"
          parent="menu_helpdesk_root"
          sequence="90"/>

<menuitem id="menu_helpdesk_categories"
          name="Catégories"
          parent="menu_helpdesk_config"
          action="action_helpdesk_ticket_category"
          sequence="10"/>
Liste des catégories hiérarchiques avec nom complet Support / Matériel / Imprimante
Menu Configuration > Catégories — la colonne complete_name du T14 affiche le chemin entier Support / Matériel / Imprimante.

Règles d'or :

  • Un menu racine sans action est juste un conteneur — l'utilisateur ne peut pas cliquer dessus.
  • L'attribut web_icon accepte module_name,chemin/icon.png — obligatoire pour apparaître dans l'App Switcher.
  • sequence pilote l'ordre d'affichage — 10, 20, 30… laissent de la marge pour insérer.
  • Ranger la configuration sous un sous-menu "Configuration" est la convention Odoo.

7. Manifest mis à jour

'data': [
    'security/ir.model.access.csv',
    'data/ir_sequence.xml',
    'views/helpdesk_ticket_category_views.xml',
    'views/helpdesk_ticket_views.xml',
    'views/helpdesk_menus.xml',              # ← toujours après les actions !
],
Ordre de chargement : un menu référençant une action doit être chargé après le fichier qui définit cette action. Sinon : External ID not found: action_helpdesk_ticket à l'install.

Upgrade et test visuel

./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init

# Puis démarre le serveur normal et va sur http://localhost:8069
# Dans l'App Switcher : le menu "Helpdesk" doit apparaître avec son icône.
# Clique "Tickets" → tu tombes sur la liste filtrée "Nouveaux" (search_default).

Les pièges Odoo 19 à éviter

  1. Utiliser <tree> — renommé <list> en v19. Et view_mode="tree,form" lève l'erreur « View types not defined tree ».
  2. Écrire attrs="{'invisible': [(...)]}" — supprimé. Utiliser directement invisible="condition".
  3. Oublier search_view_id sur l'action — Odoo prend une vue search auto-générée, tes filtres n'apparaissent pas.
  4. Charger les menus avant les actions dans data — External ID introuvable à l'install.
  5. Référencer user.id dans un domain — utiliser uid (clé spéciale du contexte search).

Voir aussi dans cette série

T13 — Héritage des modèles

_inherit, mixins, mail.thread

T14 — Hiérarchie de modèles

parent_id, _parent_store

T15 — Méthodes de modèle

create, write, actions métier

Prochain article — T17

On passe aux vues Kanban : templates QWeb, colonnes draggables, widgets visuels, Graph et Pivot pour les dashboards.

Télécharger le guide technique Odoo 19 (PDF gratuit)
Méthodes de modèle Odoo 19 : create, write, unlink et @api.model_create_multi
Bloc 3 · Framework ORM — Article 8/8 Méthodes de modèle Odoo 19 Surcharger create , write , unlink proprement — @api.model_create_multi , @api.