Ad – 728×90
⚙️ Module Development

Odoo Basic Views — XML Views, XPath, QWeb, and Data Files

Odoo's user interface is defined entirely in XML. Every form, list, kanban board, and search bar your users interact with is a view record stored in the database and rendered by the OWL JS framework. This lesson covers how Odoo loads XML files, how to define the four essential view types, how to use XPath to inherit and extend existing views, how QWeb templates work for reports and portal pages, and how to manage data records and external IDs in your module.

⏱️ 50 min 🎯 Beginner 📅 Updated 2026

How Odoo Loads XML Files

When you install or upgrade a module, Odoo processes every XML file listed in __manifest__.py in order. Understanding this loading process helps you debug missing record errors and ensure correct load order.

The loading sequence:

Text
Module install/upgrade triggered
  ↓
Odoo reads __manifest__.py → gets ordered list of data files
  ↓
For each file (in order):
  1. lxml parses the XML → XMLSyntaxError if invalid
  2. Odoo walks each <record> element
  3. Resolves ref="..." to integer IDs
  4. Evaluates eval="..." Python expressions
  5. Creates or updates the ir.model.data row (XML ID registry)
  6. Creates or updates the target model row
  ↓
Module loaded

Python code showing what happens internally (simplified):

Python
import lxml.etree as etree

with open('views/book_views.xml', 'rb') as f:
    tree = etree.parse(f)          # Step 1 — parse XML
    root = tree.getroot()          # <odoo> element

for record_el in root.iter('record'):
    model_name = record_el.get('model')    # e.g. 'ir.ui.view'
    xml_id = record_el.get('id')           # e.g. 'view_book_form'
    # Odoo creates/updates the row, registers the XML ID

Common load-time errors:

ErrorCause
XMLSyntaxError: Opening and ending tag mismatchMissing or wrong closing tag
XMLSyntaxError: EntityRef: expecting ';'Unescaped & — use &amp;
XMLSyntaxError: attributes construct errorUnquoted attribute value
ValueError: External ID not found in the systemref="module.id" points to a record not yet loaded
KeyError: 'field_name'Field declared in view doesn't exist on the model

Load order is critical. Odoo processes manifest data files top-to-bottom. A file that ref=-references a record from a later file will fail. Safe order:

Python
'data': [
    'security/ir.model.access.csv',   # 1. security first (models must exist)
    'views/book_views.xml',           # 2. views
    'views/actions.xml',              # 3. actions (may reference views)
    'views/menus.xml',                # 4. menus (reference actions)
    'data/book_config.xml',           # 5. configuration data last
],

Data and Demo Files

Python
{
    'name': 'Library Management',
    'data': [
        'security/ir.model.access.csv',
        'views/book_views.xml',
        'views/menus.xml',
        'data/mail_template.xml',
        'report/book_report.xml',
    ],
    'demo': [
        'demo/demo_books.xml',
        'demo/demo_members.xml',
    ],
}

data/ — loaded on every module install AND every upgrade. Use for: views, menus, actions, security, email templates, sequences, default configuration.

demo/ — loaded ONLY when the database was created with demo data enabled. Use for: sample records for exploration and testing. Never rely on demo data for module functionality.

noupdate="1" — wrap records that users are expected to modify:

XML
<odoo>
    <!-- Updated on every upgrade (default) -->
    <record id="view_book_form" model="ir.ui.view">
        ...
    </record>

    <!-- Created once, never overwritten on upgrade -->
    <data noupdate="1">
        <record id="sequence_library_book" model="ir.sequence">
            <field name="name">Library Book Sequence</field>
            <field name="code">library.book</field>
            <field name="prefix">LIB/%(year)s/</field>
            <field name="padding">4</field>
        </record>
        <record id="mail_template_overdue" model="mail.template">
            <field name="name">Book Overdue Notice</field>
            ...
        </record>
    </data>
</odoo>

Use noupdate="1" for: email templates (users customise wording), sequences (users set numbering), default configuration values. Do NOT use it for: access rights, record rules, view definitions (those must update).

External IDs (XML IDs)

Every <record> element has an id attribute — its external identifier. Odoo stores these in ir.model.data and resolves them to database integer IDs at load time. This lets you reference records without hardcoding IDs that differ between installations.

XML
<!-- Define an external ID -->
<record id="action_library_book" model="ir.actions.act_window">
    <field name="name">Books</field>
    <field name="res_model">library.book</field>
    <field name="view_mode">list,form</field>
</record>

<!-- Reference within same module (no prefix needed) -->
<menuitem id="menu_library_books"
          name="Books"
          action="action_library_book"/>

<!-- Reference from another module (prefix required) -->
<menuitem id="menu_ext_books"
          action="library_management.action_library_book"/>

Python access:

Python
# Get the record object
action = self.env.ref('library_management.action_library_book')

# Get just the integer ID
action_id = self.env.ref('library_management.action_library_book').id

# Safe lookup (returns None if not found instead of raising)
action = self.env.ref('library_management.action_library_book', raise_if_not_found=False)

ref vs eval attributes:

XML
<!-- ref: resolves XML ID to integer ID, assigns to Many2one field -->
<field name="parent_id" ref="base.group_user"/>

<!-- eval: evaluates a Python expression at load time -->
<field name="active" eval="True"/>
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
<field name="date" eval="(datetime.date.today()).strftime('%Y-%m-%d')"/>

<delete> tag — remove a record during module upgrade:

XML
<delete id="old_menu_item" model="ir.ui.menu"/>
<delete model="ir.ui.menu" search="[('name', '=', 'Legacy Menu')]"/>

Naming conventions:

Record typeConventionExample
Form viewview_{model}_formview_library_book_form
List viewview_{model}_listview_library_book_list
Kanban viewview_{model}_kanbanview_library_book_kanban
Search viewview_{model}_searchview_library_book_search
Window actionaction_{model}action_library_book
Root menumenu_{module}_rootmenu_library_root
Sub-menumenu_{model}menu_library_books
Email templatemail_template_{name}mail_template_book_overdue

Odoo View Types Overview

All Odoo views are <record> elements with model="ir.ui.view". The arch field holds the view XML. The model field names which Odoo model the view belongs to.

View type<arch> tagPurpose
Form<form>View and edit one record
List<list> (was <tree>)Browse many records in a table
Kanban<kanban>Card-based board, ideal for pipelines
Search<search>Filters, group-by options, search panel
Pivot<pivot>Spreadsheet-style aggregation
Graph<graph>Bar, line, or pie chart
Calendar<calendar>Records plotted on a date/time calendar
Activity<activity>Activity schedule per record

A window action controls which views are available and in what order:

XML
<record id="action_library_book" model="ir.actions.act_window">
    <field name="name">Books</field>
    <field name="res_model">library.book</field>
    <field name="view_mode">list,kanban,form</field>
    <!-- Optional: set a default domain or context -->
    <field name="domain">[]</field>
    <field name="context">{'search_default_active': 1}</field>
</record>
Ad – 336×280

Form View

The form view is the most complex view — it's the full-screen editor for a single record. Annotated structure:

XML
<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="Book">

            <!-- HEADER: action buttons + state progression bar -->
            <header>
                <button name="action_confirm" string="Confirm"
                        type="object" class="oe_highlight"
                        invisible="state != 'draft'"/>
                <button name="action_return" string="Return"
                        type="object"
                        invisible="state != 'borrowed'"/>
                <button name="action_cancel" string="Cancel"
                        type="object"
                        invisible="state in ['cancelled', 'returned']"/>
                <field name="state" widget="statusbar"
                       statusbar_visible="draft,confirmed,borrowed,returned"/>
            </header>

            <!-- SHEET: main white content card -->
            <sheet>
                <!-- Title row -->
                <div class="oe_title">
                    <h1>
                        <field name="name" placeholder="Book title…"/>
                    </h1>
                </div>

                <!-- Two-column group -->
                <group>
                    <group string="Book Details" name="book_details">
                        <field name="author_id"/>
                        <field name="isbn"/>
                        <field name="publisher_id"/>
                        <field name="publish_date"/>
                    </group>
                    <group string="Library Info" name="library_info">
                        <field name="category_id"/>
                        <field name="available_copies"/>
                        <field name="price" widget="monetary"
                               options="{'currency_field': 'currency_id'}"/>
                        <field name="currency_id" invisible="1"/>
                    </group>
                </group>

                <!-- One2many embedded list (book copies/lines) -->
                <field name="copy_ids" string="Copies">
                    <list editable="bottom">
                        <field name="barcode"/>
                        <field name="location_id"/>
                        <field name="state"/>
                    </list>
                </field>

                <!-- Tabbed secondary content -->
                <notebook>
                    <page string="Description" name="description">
                        <field name="description" widget="html"/>
                    </page>
                    <page string="Tags" name="tags">
                        <field name="tag_ids" widget="many2many_tags"/>
                    </page>
                </notebook>
            </sheet>

            <!-- CHATTER: messages, activities, followers -->
            <!-- Requires mail.thread + mail.activity.mixin on the model -->
            <chatter/>
        </form>
    </field>
</record>

Conditional attributes — control visibility, editability, and required-ness based on field values:

XML
<!-- invisible: hide based on record state (client-side) -->
<field name="return_date" invisible="state != 'borrowed'"/>

<!-- readonly: make non-editable in certain states -->
<field name="author_id" readonly="state != 'draft'"/>

<!-- required: enforce a value in certain states -->
<field name="return_reason" required="state == 'cancelled'"/>

<!-- Combining: invisible on a button -->
<button name="action_borrow" string="Borrow"
        type="object"
        invisible="state != 'confirmed' or available_copies == 0"/>

Common widgets:

WidgetField typesEffect
statusbarSelection/Many2oneProgress bar showing stages
prioritySelection (0-3)Star rating
state_selectionSelectionKanban state traffic light (grey/green/red)
many2many_tagsMany2manyPill-style coloured tags
many2one_avatarMany2one (res.users)Avatar image next to name
htmlHtml/TextRich text WYSIWYG editor
monetaryFloatFormatted currency with currency symbol
handleIntegerDrag handle for manual reordering in lists
binaryBinaryFile upload/download button
color_pickerIntegerColor selector (0–11 Odoo palette)
badgeSelection/CharColoured badge instead of plain text

List View

XML
<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="Books"
              decoration-danger="available_copies == 0"
              decoration-warning="available_copies &lt; 3"
              decoration-muted="active == False"
              multi_edit="1"
              default_order="name asc">

            <field name="name"/>
            <field name="author_id"/>
            <field name="category_id" optional="show"/>
            <field name="available_copies"
                   decoration-danger="available_copies == 0"
                   decoration-warning="available_copies &lt; 3"
                   sum="Total Copies"/>
            <field name="price" sum="Total Value" optional="hide"/>
            <field name="state" widget="badge"
                   decoration-success="state == 'available'"
                   decoration-warning="state == 'borrowed'"
                   decoration-danger="state == 'lost'"/>
            <field name="publish_date" optional="hide"/>

            <!-- Inline action button -->
            <button name="action_quick_borrow" string="Borrow"
                    type="object" icon="fa-book"
                    invisible="state != 'available'"/>
        </list>
    </field>
</record>

Row decoration — color the entire row:

AttributeRow colour
decoration-dangerRed
decoration-warningYellow/amber
decoration-successGreen
decoration-infoBlue
decoration-mutedGrey/faded
decoration-bfBold
decoration-itItalic

The expression uses Python syntax. Field names reference the current row's values.

Key list attributes:

  • multi_edit="1" — select multiple rows, edit one field, apply the change to all selected rows
  • editable="bottom" or editable="top" — enable inline editing without opening a form view; new rows added at bottom or top
  • optional="show" — column is visible by default but user can hide it; optional="hide" — hidden by default
  • sum="Label" / avg="Label" on numeric <field> — shows aggregate in footer

Kanban View

XML
<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="category_id"
                class="o_kanban_small_column">

            <!-- Declare all fields used in the template -->
            <field name="name"/>
            <field name="author_id"/>
            <field name="category_id"/>
            <field name="available_copies"/>
            <field name="state"/>
            <field name="priority"/>
            <field name="kanban_state"/>
            <field name="color"/>

            <!-- Column-top progress bar (shows state distribution) -->
            <progressbar field="state"
                         colors='{"available": "success", "borrowed": "warning", "lost": "danger"}'/>

            <!-- Card template (Odoo 17+: t-name="card") -->
            <templates>
                <t t-name="card">
                    <div t-attf-class="oe_kanban_color_#{record.color.raw_value}">
                        <div class="oe_kanban_details">
                            <strong class="o_kanban_record_title">
                                <field name="name"/>
                            </strong>
                            <div class="o_kanban_record_body">
                                <span class="text-muted">
                                    <field name="author_id"/>
                                </span>
                            </div>
                            <div class="o_kanban_record_bottom">
                                <div class="oe_kanban_bottom_left">
                                    <field name="priority" widget="priority"/>
                                    <span t-if="record.available_copies.raw_value > 0"
                                          class="badge text-bg-success">
                                        <t t-esc="record.available_copies.value"/> available
                                    </span>
                                </div>
                                <div class="oe_kanban_bottom_right">
                                    <field name="kanban_state"
                                           widget="state_selection"/>
                                </div>
                            </div>
                        </div>
                    </div>
                </t>
            </templates>
        </kanban>
    </field>
</record>

Key concepts:

  • default_group_by="field_name" — groups cards into columns by a Many2one or Selection field
  • <progressbar field="..."> — coloured bar at the top of each column based on a field's distribution
  • t-name="card" — the card template (Odoo 17+). Older: t-name="kanban-box"
  • record.field_name.raw_value — the raw database value (integer, string, boolean)
  • record.field_name.value — the formatted display string
  • t-attf-class="literal #{expr}" — string template for dynamic CSS class names

Search View

The search view does not display data — it defines what appears in the search bar (filter buttons, group-by options, facets).

XML
<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 string="Search Books">

            <!-- Fields searched when user types in the search bar -->
            <field name="name" string="Title"/>
            <field name="author_id" string="Author"/>
            <field name="isbn"/>
            <field name="tag_ids" string="Tag"
                   filter_domain="[('tag_ids.name', 'ilike', self)]"/>

            <separator/>

            <!-- One-click filter buttons -->
            <filter string="Available" name="available"
                    domain="[('state', '=', 'available')]"/>
            <filter string="Borrowed" name="borrowed"
                    domain="[('state', '=', 'borrowed')]"/>
            <filter string="My Borrowings" name="my_books"
                    domain="[('borrower_id', '=', uid)]"/>

            <!-- Date filter (Odoo generates This Week / This Month etc.) -->
            <filter string="Published Date" name="published"
                    date="publish_date"/>

            <separator/>

            <!-- Group-by options -->
            <group expand="0" string="Group By">
                <filter string="Category" name="group_category"
                        context="{'group_by': 'category_id'}"/>
                <filter string="Author" name="group_author"
                        context="{'group_by': 'author_id'}"/>
                <filter string="State" name="group_state"
                        context="{'group_by': 'state'}"/>
                <filter string="Publish Month" name="group_month"
                        context="{'group_by': 'publish_date:month'}"/>
            </group>

            <!-- Search panel: sidebar with category filters -->
            <searchpanel>
                <field name="category_id" select="one"
                       icon="fa-book" string="Category"/>
                <field name="state" select="multi"
                       icon="fa-circle" string="Status"/>
            </searchpanel>

        </search>
    </field>
</record>

Key search view elements:

  • <field name="..."> — adds the field to full-text search. When user types in the bar and selects this field, Odoo searches using an ilike domain by default.
  • filter_domain with selfself is replaced with the typed value. Use for cross-field searches.
  • <filter domain="..."> — one-click button that applies a domain permanently
  • <filter date="field_name"> — Odoo auto-generates "This Week", "This Month", "This Quarter", "This Year" sub-filters
  • <group context="{'group_by': 'field'}"> — adds a group-by option; date fields support granularity: :day, :week, :month, :quarter, :year
  • <searchpanel> — left sidebar with select="one" (radio) or select="multi" (checkboxes)

Activating filters by default — add to the action's context:

XML
<field name="context">{'search_default_available': 1, 'search_default_group_category': 1}</field>
Ad – 336×280

XPath View Inheritance

XPath lets you modify an existing view from another module without editing the original. This is how Odoo's modular architecture works — a sales module can add a field to a base contact form without touching the contacts module.

XPath expression anatomy:

Text
    //    field    [@name='partner_id']
    ↑     ↑        ↑
    |     |        predicate — filter by attribute value
    |     element name to match
    // = search anywhere in the document
    /  = search only immediate children of root

Position values:

PositionWhat happens
afterNew XML inserted immediately after the matched element
beforeNew XML inserted immediately before the matched element
insideNew XML appended as last child of the matched element
replaceMatched element removed and replaced with new XML
attributesAttributes of the matched element modified (use <attribute> children)

Full example — extending the sale order form from a custom module:

XML
<record id="view_sale_order_form_inherit_library" model="ir.ui.view">
    <field name="name">sale.order.form.library</field>
    <field name="model">sale.order</field>
    <field name="inherit_id" ref="sale.view_order_form"/>
    <field name="arch" type="xml">

        <!-- Add a field after partner_id -->
        <xpath expr="//field[@name='partner_id']" position="after">
            <field name="library_card_id" string="Library Card"/>
        </xpath>

        <!-- Add a new page to the notebook -->
        <xpath expr="//notebook" position="inside">
            <page string="Library Info" name="library_info">
                <group>
                    <field name="borrowed_book_ids" widget="many2many_tags"/>
                    <field name="library_notes"/>
                </group>
            </page>
        </xpath>

        <!-- Make a field invisible -->
        <xpath expr="//field[@name='client_order_ref']" position="attributes">
            <attribute name="invisible">1</attribute>
        </xpath>

        <!-- Replace a field with a different one -->
        <xpath expr="//field[@name='payment_term_id']" position="replace">
            <field name="custom_payment_id" string="Custom Payment"/>
        </xpath>

        <!-- Add a button to the header -->
        <xpath expr="//header/button[last()]" position="after">
            <button name="action_library_sync" string="Sync Library"
                    type="object" icon="fa-refresh"/>
        </xpath>

    </field>
</record>

Shorthand syntax — for fields and buttons, you can use the element tag directly instead of a full XPath expression:

XML
<!-- Shorthand: fine for simple cases -->
<field name="partner_id" position="after">
    <field name="library_card_id"/>
</field>

<!-- Use full XPath when: -->
<!-- 1. The target is not a field (e.g. <group>, <notebook>, <page>, <div>) -->
<!-- 2. The field name appears multiple times in the view -->
<!-- 3. You need positional selectors like [last()] or [1] -->
💡
Always name your groups and pages

Always give your <group>, <page>, and <notebook> elements a name attribute. This makes them targetable with a stable XPath: //group[@name='billing'] instead of //sheet/group[2] (which breaks if any module adds a group earlier).

XML Validation

Well-formed vs valid:

  • Well-formed — follows XML syntax rules (tags close, attributes quoted, one root, correct nesting). Required — the module won't load if XML is malformed.
  • Valid — well-formed AND conforms to a schema defining which elements are allowed and where. Odoo validates view XML against RNG schema files in odoo/addons/base/views/.

Run Odoo with XML validation in development:

Bash
# Enables strict view validation (slower but catches schema violations)
python odoo-bin -c odoo.conf --dev=xml

# Upgrade a module with validation
python odoo-bin -c odoo.conf -u my_module --dev=xml

Check XML syntax locally before loading (catch errors fast without restarting Odoo):

Bash
# Install xmllint
sudo apt install libxml2-utils    # Ubuntu/Debian

# Validate a file (no output = valid; error shows file:line)
xmllint --noout views/book_views.xml

# Example error output:
# views/book_views.xml:23: parser error : Opening and ending tag mismatch

Common XML errors and fixes:

XML
<!-- ERROR: unescaped & in domain -->
<filter domain="[('amount', '>', 100) and ('state', '=', 'open')]"/>
<!-- FIX: use &amp; -->
<filter domain="[('amount', '&gt;', 100)]"/>

<!-- ERROR: self-closing tag not closed -->
<field name="name">
<!-- FIX: self-close or add closing tag -->
<field name="name"/>

<!-- ERROR: missing closing tag -->
<group>
    <field name="name"/>
<field name="email"/>   <!-- This is outside the group! -->
<!-- FIX -->
<group>
    <field name="name"/>
</group>
<field name="email"/>

QWeb — Odoo's XML Template Engine

QWeb is Odoo's template engine built on top of XML. It adds t-* directive attributes to standard HTML elements to produce dynamic output. QWeb is used for PDF reports, portal pages, website pages, and email templates — NOT for the main OWL-based UI views (form, list, kanban).

DirectivePurpose
t-esc="expr"Output HTML-escaped value — safe for user data
t-raw="expr"Output raw HTML — only for trusted content
t-if="condition"Render element only if condition is true
t-elif="condition"Else-if branch
t-elseElse branch
t-foreach="list" t-as="item"Loop; also gives item_index, item_size, item_first, item_last
t-call="template.xmlid"Include another template (like a function call)
t-set="var" t-value="expr"Assign a variable
t-att-href="expr"Dynamic attribute value
t-attf-class="literal #{expr}"String-template for mixed literal/dynamic attributes
t-field="record.fieldname"Render a field with Odoo formatting (currency, date, etc.)

The <t> element is an invisible wrapper — it applies directives without adding any element to the output:

XML
<!-- t-foreach without adding a wrapper div -->
<t t-foreach="order.line_ids" t-as="line">
    <tr>
        <td><t t-esc="line.product_id.name"/></td>
        <td><t t-esc="line.quantity"/></td>
        <td><t t-field="line.price_subtotal"/></td>
    </tr>
</t>

Minimal PDF report template:

XML
<template id="report_book_document">
    <t t-call="web.html_container">
        <t t-foreach="docs" t-as="doc">
            <div class="page">
                <div class="row">
                    <div class="col-6">
                        <h2><t t-esc="doc.name"/></h2>
                        <p>Author: <t t-field="doc.author_id"/></p>
                    </div>
                    <div class="col-6 text-end">
                        <p t-if="doc.isbn">ISBN: <t t-esc="doc.isbn"/></p>
                    </div>
                </div>
                <table class="table table-sm">
                    <thead>
                        <tr>
                            <th>Copy #</th>
                            <th>Location</th>
                            <th>Status</th>
                        </tr>
                    </thead>
                    <tbody>
                        <t t-foreach="doc.copy_ids" t-as="copy">
                            <tr>
                                <td><t t-esc="copy.barcode"/></td>
                                <td><t t-esc="copy.location_id.name"/></td>
                                <td><t t-esc="copy.state"/></td>
                            </tr>
                        </t>
                    </tbody>
                </table>
            </div>
        </t>
    </t>
</template>

Registering the report action:

XML
<record id="action_report_book" model="ir.actions.report">
    <field name="name">Book Card</field>
    <field name="model">library.book</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">library_management.report_book_document</field>
    <field name="binding_model_id" ref="model_library_book"/>
</record>
ℹ️
QWeb vs OWL templates

QWeb is separate from OWL's template syntax. OWL also uses t-* directives but operates on JavaScript objects in the browser. QWeb runs on the Python server side and produces an HTML string (for portal/website/email) or a PDF (for reports). Don't confuse the two.

📋 Key Points

  • Odoo loads XML files top-to-bottom in __manifest__.py order. A ref= pointing to a record in a later file causes "External ID not found" — always load security before views, views before actions, actions before menus.
  • data/ files load on every install+upgrade. demo/ files load only when demo data is enabled. Use noupdate="1" for records users are expected to modify.
  • External IDs let you reference records reliably with ref="module.id" in XML and self.env.ref() in Python, without hardcoding database integer IDs.
  • Odoo has 8+ view types: form (single record), list (table), kanban (cards), search (filters/group-by), plus pivot, graph, calendar, activity.
  • Form view: <header> for buttons+statusbar, <sheet> for content, <group> for columns, <notebook> for tabs, <chatter/> for messaging. Use invisible/readonly/required with Python expressions.
  • List view: decoration-* colors rows, sum/avg for footer aggregates, optional for user-toggleable columns, multi_edit for bulk changes.
  • Kanban view: group by Many2one column, <progressbar> for column status bars, card template in t-name="card".
  • Search view: <field> for full-text search, <filter> for quick filters, <group> for group-by options, <searchpanel> for sidebar categories.
  • XPath inheritance: <inherit_id ref="module.view_id"> + <xpath expr="..." position="..."> modifies any existing view without touching the original module.
  • QWeb: t-* directives on XML elements produce dynamic HTML/PDF output — used for reports, portal pages, website, and email templates (not for form/list/kanban UI).

FAQ

What is the difference between <tree> and <list> in Odoo views? +

They are the same view type. Odoo renamed <tree> to <list> in Odoo 17 for semantic clarity. Both tags still work in Odoo 17+. Any module you find using <tree> is targeting Odoo 16 or earlier, but the tag still parses correctly.

When should I use noupdate="1" on data records? +

Use it for records users will customise after installation — email templates (they'll change the wording), sequences (they'll change the numbering prefix), and default company settings. Avoid it for views, access rights, and server actions — those must be updatable so security fixes and feature changes apply correctly on module upgrade.

Why does my XPath fail even though the expression looks correct? +

Three common reasons: (1) the element doesn't have the attribute you're filtering on — the field might be named differently in the target view; (2) the element appears inside a sub-template that's rendered separately; (3) another module's inherited view changed the structure between the original view and your xpath. Use developer mode's "Edit View" feature (Settings > Technical > Views) to inspect the compiled arch before writing your XPath.

What is docs in a QWeb report template? +

docs is a standard variable automatically passed to every QWeb report template. It is a recordset of the records being printed — for example, if you print 3 sale orders, docs is a recordset of those 3 orders. The t-foreach="docs" t-as="doc" loop iterates over each one to produce one page per record.

Can I add a <searchpanel> to a list view? +

Yes — the search view defines the searchpanel and it works with both list and kanban views. It does not work with form, pivot, or graph views. The select="one" option gives radio buttons (filter by one value at a time); select="multi" gives checkboxes (filter by multiple values simultaneously).