Views — XML-Based UI (Part 1: Basic Views)

Series: Odoo 18 Development for Python Developers Target Audience: Python developers who are new to web development and want to learn Odoo Prerequisites: Lessons 1–5 completed, library.book model with fields, relations, computed fields, and business methods


How Odoo Renders UI — The Full Pipeline

After five lessons of pure Python, it’s time to give our module a face. In Odoo, the UI is defined in XML, not HTML. Let’s understand why and how.

The Pipeline: From Click to Screen

When a user interacts with Odoo, here’s the complete chain:

User clicks a Menu Item
        │
        ▼
Menu triggers a Window Action
        │
        ▼
Action tells the web client: "Show model X with view types [list, form]"
        │
        ▼
Web client asks the server: "Give me the list view for model X"
        │
        ▼
Server finds the view XML, processes it, and sends it as JSON
        │
        ▼
Web client (JavaScript/OWL) renders the view in the browser
        │
        ▼
User sees the page!

As a developer, you define:

  1. Views (XML) — What the page looks like
  2. Actions (XML) — What happens when something is clicked
  3. Menus (XML) — Where the user clicks

The web client handles all the rendering. You never write HTML, CSS, or JavaScript for standard views.

Where XML Files Live

All view XML files go in the views/ directory of your module, and they’re registered in __manifest__.py:

# In __manifest__.py
'data': [
    'security/library_security.xml',
    'security/ir.model.access.csv',
    'views/book_views.xml',          # Views for library.book
    'views/author_views.xml',        # Views for library.author
    'views/tag_views.xml',           # Views for library.tag
    'views/menu_views.xml',          # Menu structure (load last!)
],

Order matters! Views must be loaded before menus that reference them. Security files should be loaded before views (because views may use group-based visibility).


Understanding XML Data Files

Before diving into views, let’s understand the XML syntax that Odoo uses for all data files.

The Basic Structure

Every Odoo XML data file follows this structure:

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <!-- Your data records go here -->
</odoo>

The tag is the root element. Everything inside is processed by Odoo’s data loader.

The Tag

The tag creates or updates a record in a specific model:

<record id="unique_xml_id" model="target.model">
    <field name="field_name">value</field>
    <field name="another_field">another value</field>
</record>

Attributes:

  • id — A unique identifier (called XML ID or External ID). Format: module_name.id (the module prefix is added automatically)
  • model — The Odoo model where this record will be created

Example — creating a view:

<record id="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">
        <!-- View definition goes here -->
    </field>
</record>

What’s happening:

  • We’re creating a record in the ir.ui.view model (where all views are stored)
  • id="library_book_form" — The XML ID we can use to reference this view later
  • — The view’s internal name
  • — Which model this view is for
  • — The view’s architecture — the actual XML that defines the UI

The ref Attribute

When a field references another record, you use the ref attribute:

<field name="view_id" ref="library_book_form"/>
<!-- This sets view_id to the record with XML ID 'library_book_form' -->

The eval Attribute

For Python expressions:

<field name="domain" eval="[('state', '=', 'available')]"/>
<field name="context" eval="{'default_state': 'draft'}"/>
<field name="sequence" eval="10"/>

Now let’s use this knowledge to build actual views.


Form View: Building the Book Detail Page

The form view is what users see when they open a single record — like a detail page for editing a book.

Basic Form View Structure

<record id="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">

            <!-- The form content goes here -->

        </form>
    </field>
</record>

The Element

Inside

, the element creates the white card that holds the main content:

<form string="Book">
    <sheet>
        <!-- Main content inside the white card -->
    </sheet>
</form>

Without , the form looks plain. With it, you get the standard Odoo card layout with proper padding and styling.

The Element — Layout Columns

is how you organize fields into columns:

<sheet>
    <!-- A single group: fields stack vertically, label on left, value on right -->
    <group>
        <field name="name"/>
        <field name="isbn"/>
        <field name="author_id"/>
    </group>
</sheet>

Nested groups create columns:

<sheet>
    <!-- Outer group splits into two columns -->
    <group>
        <!-- Left column -->
        <group string="Basic Info">
            <field name="name"/>
            <field name="isbn"/>
            <field name="author_id"/>
        </group>

        <!-- Right column -->
        <group string="Details">
            <field name="pages"/>
            <field name="price"/>
            <field name="rating"/>
        </group>
    </group>
</sheet>

This renders as a two-column layout — very common in Odoo forms:

┌─────────────────────────────────────────────────────┐
│  Basic Info                │  Details                │
│  Title:    [The Hobbit   ] │  Pages:   [310        ] │
│  ISBN:     [978-054...   ] │  Price:   [14.99      ] │
│  Author:   [J.R.R. Tol. ] │  Rating:  [4.70       ] │
└─────────────────────────────────────────────────────┘

The and Elements — Tabs

For forms with lots of fields, use tabs:

<sheet>
    <group>
        <!-- Main fields (always visible) -->
        <group>
            <field name="name"/>
            <field name="author_id"/>
        </group>
        <group>
            <field name="state"/>
            <field name="price"/>
        </group>
    </group>

    <!-- Tabbed sections -->
    <notebook>
        <page string="Description" name="page_description">
            <field name="synopsis"/>
            <field name="description"/>
        </page>

        <page string="Publication Details" name="page_publication">
            <group>
                <group>
                    <field name="isbn"/>
                    <field name="pages"/>
                    <field name="cover_type"/>
                </group>
                <group>
                    <field name="publisher_id"/>
                    <field name="date_published"/>
                    <field name="date_added"/>
                </group>
            </group>
        </page>

        <page string="Tags" name="page_tags">
            <field name="tag_ids"/>
        </page>

        <page string="Internal Notes" name="page_notes">
            <field name="notes" placeholder="Add internal notes here..."/>
        </page>
    </notebook>
</sheet>

What this creates:

┌─────────────────────────────────────────────────────┐
│  Title:    [The Hobbit   ]   State:   [Available  ] │
│  Author:   [J.R.R. Tolk. ]   Price:   [14.99     ] │
├─────────────────────────────────────────────────────┤
│ [Description] [Publication Details] [Tags] [Notes]  │
│                                                     │
│  Synopsis: A hobbit goes on an unexpected           │
│  adventure with a group of dwarves...               │
│                                                     │
│  Full Description:                                  │
│  [Rich text editor with formatting buttons]         │
└─────────────────────────────────────────────────────┘

The Image and Title Area

Most Odoo forms show an image on the left and a title at the top:

<sheet>
    <field name="cover_image" widget="image" class="oe_avatar"
           options="{'preview_image': 'cover_image', 'size': [90, 90]}"/>

    <div class="oe_title">
        <label for="name"/>
        <h1>
            <field name="name" placeholder="Book Title"/>
        </h1>
    </div>

    <!-- Rest of the form... -->
</sheet>

What’s happening:

  • widget="image" — Renders the Binary/Image field as an image
  • class="oe_avatar" — Positions the image on the left, like a profile picture
  • — Styles the title area
  • — Makes the book name appear as a large heading

  • placeholder — Shows gray text when the field is empty

Field Attributes in XML

You can control field behavior directly in the view XML:

<!-- Basic field -->
<field name="name"/>

<!-- With placeholder text -->
<field name="isbn" placeholder="e.g., 978-0547928227"/>

<!-- Readonly based on condition (Odoo 18 syntax) -->
<field name="isbn" readonly="state != 'draft'"/>
<!-- ISBN is editable only when state is 'draft' -->

<!-- Invisible based on condition -->
<field name="date_published" invisible="state == 'draft'"/>
<!-- Hide published date for draft books -->

<!-- Required based on condition -->
<field name="isbn" required="state == 'available'"/>
<!-- ISBN is required only for available books -->

<!-- With a specific widget -->
<field name="tag_ids" widget="many2many_tags"/>
<!-- Shows tags as colored chips instead of a list -->

<!-- With options -->
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<!-- Tags with colors from the 'color' field -->

Dynamic Visibility in Odoo 18

Odoo 18 simplified how dynamic visibility works. Instead of the old attrs syntax, you write conditions directly:

<!-- Odoo 18 syntax (new and cleaner): -->
<field name="isbn" readonly="state != 'draft'"/>
<field name="price" invisible="state == 'lost'"/>
<group invisible="state == 'draft'" string="Publication">
    <field name="date_published"/>
</group>

<!-- Old Odoo 16/17 syntax (deprecated in 18):
<field name="isbn" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
Don't use this in Odoo 18! -->

The new syntax is much more readable. The expression is evaluated in JavaScript on the client side, so it updates in real-time as the user changes fields.


Form View: Buttons, Statusbar, and Header

The
Element

The header section sits above the sheet and typically contains:

  • A status bar showing the record’s state
  • Action buttons for state transitions
<form string="Book">
    <header>
        <!-- Action buttons -->
        <button name="action_mark_available"
                type="object"
                string="Mark Available"
                class="btn-primary"
                invisible="state != 'draft'"/>

        <button name="action_mark_borrowed"
                type="object"
                string="Borrow"
                invisible="state != 'available'"/>

        <button name="action_mark_returned"
                type="object"
                string="Return"
                class="btn-primary"
                invisible="state != 'borrowed'"/>

        <button name="action_mark_lost"
                type="object"
                string="Mark as Lost"
                class="btn-danger"
                invisible="state not in ('available', 'borrowed')"/>

        <!-- Status bar -->
        <field name="state" widget="statusbar"
               statusbar_visible="draft,available,borrowed"/>
    </header>

    <sheet>
        <!-- ... form content ... -->
    </sheet>
</form>

Button Attributes Explained

<button name="action_mark_available"
        type="object"
        string="Mark Available"
        class="btn-primary"
        invisible="state != 'draft'"
        confirm="Are you sure you want to make this book available?"/>
Attribute Value Meaning
name action_mark_available The Python method to call on the model
type object Call a Python method (vs action for a window action)
string Mark Available The button text
class btn-primary CSS class — btn-primary (blue), btn-danger (red), or nothing (gray)
invisible state != 'draft' Only show this button when state is ‘draft’
confirm Are you sure... Show a confirmation dialog before executing

How it connects to Python:

When the user clicks the “Mark Available” button:

  1. The web client sends a request to the server: “Call action_mark_available on library.book record X”
  2. Odoo calls self.action_mark_available() where self is the current record
  3. The method we wrote in Lesson 5 runs: self.state = 'available'
  4. The web client refreshes the form

The Statusbar Widget

<field name="state" widget="statusbar"
       statusbar_visible="draft,available,borrowed"/>

This renders a visual pipeline at the top of the form:

  ● Draft  ──────►  ○ Available  ──────►  ○ Borrowed
  • widget="statusbar" — Renders as a horizontal pipeline
  • statusbar_visible — Which states to show as steps (others are hidden but still selectable)
  • The current state is highlighted with a filled circle
  • Users can click on a state to jump to it (if clickable is not disabled)

List (Tree) View: The Book Table

The list view (historically called “tree view” in Odoo) shows records as a table — like a spreadsheet.

Basic List View

<record id="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">
            <field name="name"/>
            <field name="author_id"/>
            <field name="isbn"/>
            <field name="pages"/>
            <field name="price" widget="monetary"/>
            <field name="state"
                   decoration-success="state == 'available'"
                   decoration-info="state == 'borrowed'"
                   decoration-danger="state == 'lost'"
                   decoration-muted="state == 'draft'"
                   widget="badge"/>
            <field name="tag_ids" widget="many2many_tags"
                   options="{'color_field': 'color'}"/>
        </list>
    </field>
</record>

List View Features

Column decorations — color-code rows or cells:

<!-- Color entire rows based on conditions -->
<list decoration-danger="state == 'lost'"
      decoration-success="state == 'available'"
      decoration-muted="active == False">
    <field name="name"/>
    <!-- ... -->
</list>
Decoration Color Use for
decoration-success Green Positive states (available, done, paid)
decoration-info Blue Informational (in progress, borrowed)
decoration-warning Orange/Yellow Warnings (expiring soon, low stock)
decoration-danger Red Negative states (lost, overdue, failed)
decoration-muted Gray Inactive or secondary items
decoration-bf Bold Emphasis (not a color, just bold)

Editable list — edit inline without opening the form:

<list editable="bottom">
    <!-- 'bottom' = new rows added at the bottom -->
    <!-- 'top' = new rows added at the top -->
    <field name="name"/>
    <field name="pages"/>
    <field name="price"/>
</list>

Optional columns — user can choose which columns to show:

<list>
    <field name="name"/>
    <field name="author_id"/>
    <field name="isbn" optional="show"/>      <!-- Shown by default, can be hidden -->
    <field name="pages" optional="hide"/>     <!-- Hidden by default, can be shown -->
    <field name="price" optional="show"/>
    <field name="rating" optional="hide"/>
</list>

Column width:

<field name="name" width="300px"/>
<field name="isbn" width="150px"/>

Search View: Filters, Group By, and Search Fields

The search view defines what appears in the search bar above list and kanban views — the search fields, predefined filters, and group-by options.

Basic Search View

<record id="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">

            <!-- Search fields: what the user can type to search -->
            <field name="name" string="Title"/>
            <field name="isbn"/>
            <field name="author_id"/>
            <field name="publisher_id"/>
            <field name="tag_ids"/>

            <!-- Separator between search fields and filters -->
            <separator/>

            <!-- Predefined filters: clickable buttons -->
            <filter name="filter_available"
                    string="Available"
                    domain="[('state', '=', 'available')]"/>

            <filter name="filter_borrowed"
                    string="Borrowed"
                    domain="[('state', '=', 'borrowed')]"/>

            <filter name="filter_draft"
                    string="Draft"
                    domain="[('state', '=', 'draft')]"/>

            <separator/>

            <filter name="filter_long_books"
                    string="Long Books (500+ pages)"
                    domain="[('pages', '>=', 500)]"/>

            <filter name="filter_archived"
                    string="Archived"
                    domain="[('active', '=', False)]"/>

            <!-- Group By options -->
            <separator/>

            <group expand="0" string="Group By">
                <filter name="group_author"
                        string="Author"
                        context="{'group_by': 'author_id'}"/>

                <filter name="group_state"
                        string="Status"
                        context="{'group_by': 'state'}"/>

                <filter name="group_publisher"
                        string="Publisher"
                        context="{'group_by': 'publisher_id'}"/>

                <filter name="group_cover_type"
                        string="Cover Type"
                        context="{'group_by': 'cover_type'}"/>

                <filter name="group_published_month"
                        string="Published Month"
                        context="{'group_by': 'date_published:month'}"/>
            </group>

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

Search Fields Explained

<field name="name" string="Title"/>

When the user types in the search bar, Odoo searches across all entries. The name field matches the model field, and string customizes the label shown in the search dropdown.

For relational fields, Odoo automatically searches the related model:

<field name="author_id"/>
<!-- Typing "Tolkien" will search library.author records matching "Tolkien" -->

<!-- You can customize which fields to search in the related model: -->
<field name="author_id"
       filter_domain="['|', ('author_id.name', 'ilike', self), ('author_id.email', 'ilike', self)]"/>
<!-- 'self' is replaced by whatever the user typed -->

Filter Buttons Explained

<filter name="filter_available"
        string="Available"
        domain="[('state', '=', 'available')]"/>
  • name — Technical name (used for referencing in code or as default filters)
  • string — The label the user sees
  • domain — The filter condition applied when clicked

Multiple filters can be active at once:

  • Filters in the same group are combined with OR (click both “Available” and “Borrowed” = show both)
  • Filters in different groups (separated by ) are combined with AND

Group By Explained

<filter name="group_author"
        string="Author"
        context="{'group_by': 'author_id'}"/>

This adds a “Group By” option. When selected, the list view groups records by the specified field, showing subtotals and collapsible sections.

Date grouping with granularity:

<filter name="group_published_month"
        string="Published Month"
        context="{'group_by': 'date_published:month'}"/>
<!-- Options: day, week, month, quarter, year -->

Setting a Default Filter

You can set a default filter in the window action:

<record id="library_book_action" model="ir.actions.act_window">
    <field name="name">Books</field>
    <field name="res_model">library.book</field>
    <field name="view_mode">list,form</field>
    <field name="context">{'search_default_filter_available': 1}</field>
    <!-- 'search_default_' + filter name = activate by default -->
</record>

Window Actions — Linking Models to Views

A window action is the bridge between menus and views. It tells the web client: “Open this model with these view types.”

Basic Window Action

<record id="library_book_action" 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>

What each field means:

Field Value Purpose
name Books Title shown in the breadcrumb and window title
res_model library.book The model to display
view_mode list,form View types to show (first one is the default)

Window Action with More Options

<record id="library_book_action" model="ir.actions.act_window">
    <field name="name">Books</field>
    <field name="res_model">library.book</field>
    <field name="view_mode">list,form,kanban</field>

    <!-- Pre-filter records -->
    <field name="domain">[('active', '=', True)]</field>

    <!-- Set default values for new records -->
    <field name="context">{
        'default_state': 'draft',
        'search_default_filter_available': 1,
    }</field>

    <!-- Help text shown when no records exist -->
    <field name="help" type="html">
        <p class="o_view_nocontent_smiling_face">
            Create your first book!
        </p>
        <p>
            Add books to your library catalog to track availability,
            authors, and borrowing.
        </p>
    </field>
</record>

The help field: Shows a friendly message with an icon when the list is empty. The o_view_nocontent_smiling_face class adds a smiley icon — a nice Odoo touch.

Linking a Specific View to an Action

By default, Odoo picks the view with the lowest priority for each view type. If you want to force a specific view:

<record id="library_book_action" model="ir.actions.act_window">
    <field name="name">Books</field>
    <field name="res_model">library.book</field>
    <field name="view_mode">list,form</field>
    <field name="view_ids" eval="[
        (5, 0, 0),
        (0, 0, {'view_mode': 'list', 'view_id': ref('library_book_list')}),
        (0, 0, {'view_mode': 'form', 'view_id': ref('library_book_form')}),
    ]"/>
</record>

Usually you don’t need this — the default auto-detection works fine for most cases.


Menu Items — Building Navigation

Menus create the navigation structure in Odoo’s top bar and sidebar.

Odoo menus are hierarchical — typically 3 levels deep:

Top Menu (App)           → "Library"
├── First-level menu     → "Catalog"
│   ├── Second-level     → "Books"
│   └── Second-level     → "Authors"
└── First-level menu     → "Configuration"
    └── Second-level     → "Tags"

Creating Menus with

<!-- Level 0: Top menu (the app menu in the top bar) -->
<menuitem
    id="library_root_menu"
    name="Library"
    sequence="100"/>
<!-- No action — this is just a container -->
<!-- sequence controls the order in the top bar -->

<!-- Level 1: First submenu (shows in the sidebar when the app is open) -->
<menuitem
    id="library_catalog_menu"
    name="Catalog"
    parent="library_root_menu"
    sequence="10"/>
<!-- parent links this to the root menu -->

<!-- Level 2: Actual menu items (link to actions) -->
<menuitem
    id="library_book_menu"
    name="Books"
    parent="library_catalog_menu"
    action="library_book_action"
    sequence="10"/>
<!-- action links to the window action that opens the book list/form -->

<menuitem
    id="library_author_menu"
    name="Authors"
    parent="library_catalog_menu"
    action="library_author_action"
    sequence="20"/>

<!-- Level 1: Configuration section -->
<menuitem
    id="library_config_menu"
    name="Configuration"
    parent="library_root_menu"
    sequence="90"/>

<menuitem
    id="library_tag_menu"
    name="Tags"
    parent="library_config_menu"
    action="library_tag_action"
    sequence="10"/>
Attribute Purpose Required?
id XML ID (unique identifier) Yes
name Display text Yes
parent ID of the parent menu No (root menus have no parent)
action ID of the window action to trigger No (container menus have no action)
sequence Order (lower = first) No (default: 10)
groups Security groups that can see this menu No (visible to all by default)

Security on Menus

<menuitem
    id="library_config_menu"
    name="Configuration"
    parent="library_root_menu"
    groups="base.group_system"
    sequence="90"/>
<!-- Only system administrators can see the Configuration menu -->

Hands-on: Create Complete Views and Menus for library.book

Let’s build the complete UI for our library module. We’ll create views for all three models (Book, Author, Tag) plus the menu structure.

Step 1: Create views/book_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <!-- ============================================ -->
    <!--                FORM VIEW                     -->
    <!-- ============================================ -->
    <record id="library_book_view_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>
                    <button name="action_mark_available"
                            type="object"
                            string="Mark Available"
                            class="btn-primary"
                            invisible="state != 'draft'"/>
                    <button name="action_mark_borrowed"
                            type="object"
                            string="Borrow"
                            invisible="state != 'available'"/>
                    <button name="action_mark_returned"
                            type="object"
                            string="Return"
                            class="btn-primary"
                            invisible="state != 'borrowed'"/>
                    <button name="action_mark_lost"
                            type="object"
                            string="Mark as Lost"
                            class="btn-danger"
                            invisible="state not in ('available', 'borrowed')"
                            confirm="Are you sure this book is lost?"/>
                    <field name="state" widget="statusbar"
                           statusbar_visible="draft,available,borrowed"/>
                </header>

                <sheet>
                    <!-- Cover image + Title area -->
                    <field name="cover_image" widget="image" class="oe_avatar"/>
                    <div class="oe_title">
                        <label for="name"/>
                        <h1>
                            <field name="name" placeholder="Enter the book title..."/>
                        </h1>
                        <h3>
                            <field name="author_id" placeholder="Select an author..."
                                   options="{'no_create': False}"/>
                        </h3>
                    </div>

                    <!-- Main fields in two columns -->
                    <group>
                        <group string="Book Details">
                            <field name="isbn"
                                   readonly="state != 'draft'"
                                   placeholder="e.g., 978-0547928227"/>
                            <field name="pages"/>
                            <field name="cover_type"/>
                            <field name="is_long_book"/>
                        </group>
                        <group string="Pricing &amp; Rating">
                            <field name="price"/>
                            <field name="price_per_page"/>
                            <field name="rating"/>
                        </group>
                    </group>

                    <group>
                        <group string="Publishing">
                            <field name="publisher_id"/>
                            <field name="date_published"/>
                            <field name="date_added"/>
                        </group>
                        <group string="Classification">
                            <field name="tag_ids" widget="many2many_tags"
                                   options="{'color_field': 'color'}"/>
                            <field name="tag_count"/>
                        </group>
                    </group>

                    <!-- Tabbed sections -->
                    <notebook>
                        <page string="Description" name="page_description">
                            <group>
                                <field name="synopsis" placeholder="Write a brief synopsis..."/>
                            </group>
                            <group>
                                <field name="description" placeholder="Full description with formatting..."/>
                            </group>
                        </page>
                        <page string="Internal Notes" name="page_notes">
                            <field name="notes" placeholder="Internal notes (not visible to members)..."/>
                        </page>
                    </notebook>
                </sheet>
            </form>
        </field>
    </record>

    <!-- ============================================ -->
    <!--                LIST VIEW                     -->
    <!-- ============================================ -->
    <record id="library_book_view_list" model="ir.ui.view">
        <field name="name">library.book.list</field>
        <field name="model">library.book</field>
        <field name="arch" type="xml">
            <list decoration-danger="state == 'lost'"
                  decoration-muted="active == False"
                  decoration-success="state == 'available'"
                  multi_edit="1">
                <field name="name"/>
                <field name="author_id"/>
                <field name="isbn" optional="show"/>
                <field name="pages" optional="hide"/>
                <field name="price" optional="show"/>
                <field name="cover_type" optional="hide"/>
                <field name="tag_ids" widget="many2many_tags"
                       options="{'color_field': 'color'}"/>
                <field name="state"
                       widget="badge"
                       decoration-success="state == 'available'"
                       decoration-info="state == 'borrowed'"
                       decoration-danger="state == 'lost'"
                       decoration-muted="state == 'draft'"/>
            </list>
        </field>
    </record>

    <!-- ============================================ -->
    <!--               SEARCH VIEW                    -->
    <!-- ============================================ -->
    <record id="library_book_view_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">
                <field name="name" string="Title"/>
                <field name="isbn"/>
                <field name="author_id"/>
                <field name="publisher_id"/>
                <field name="tag_ids"/>

                <separator/>

                <filter name="filter_available"
                        string="Available"
                        domain="[('state', '=', 'available')]"/>
                <filter name="filter_borrowed"
                        string="Borrowed"
                        domain="[('state', '=', 'borrowed')]"/>
                <filter name="filter_draft"
                        string="Draft"
                        domain="[('state', '=', 'draft')]"/>

                <separator/>

                <filter name="filter_long_books"
                        string="Long Books (500+ pages)"
                        domain="[('pages', '>=', 500)]"/>
                <filter name="filter_archived"
                        string="Archived"
                        domain="[('active', '=', False)]"/>

                <separator/>

                <group expand="0" string="Group By">
                    <filter name="group_author"
                            string="Author"
                            context="{'group_by': 'author_id'}"/>
                    <filter name="group_state"
                            string="Status"
                            context="{'group_by': 'state'}"/>
                    <filter name="group_publisher"
                            string="Publisher"
                            context="{'group_by': 'publisher_id'}"/>
                    <filter name="group_cover_type"
                            string="Cover Type"
                            context="{'group_by': 'cover_type'}"/>
                </group>
            </search>
        </field>
    </record>

    <!-- ============================================ -->
    <!--              WINDOW ACTION                   -->
    <!-- ============================================ -->
    <record id="library_book_action" model="ir.actions.act_window">
        <field name="name">Books</field>
        <field name="res_model">library.book</field>
        <field name="view_mode">list,form</field>
        <field name="context">{'search_default_filter_available': 1}</field>
        <field name="help" type="html">
            <p class="o_view_nocontent_smiling_face">
                Add your first book to the library!
            </p>
            <p>
                Start building your library catalog by adding books
                with their authors, tags, and details.
            </p>
        </field>
    </record>

</odoo>

Step 2: Create views/author_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <!-- Author Form View -->
    <record id="library_author_view_form" model="ir.ui.view">
        <field name="name">library.author.form</field>
        <field name="model">library.author</field>
        <field name="arch" type="xml">
            <form string="Author">
                <sheet>
                    <div class="oe_title">
                        <label for="name"/>
                        <h1>
                            <field name="name" placeholder="Author name..."/>
                        </h1>
                    </div>
                    <group>
                        <group string="Personal Info">
                            <field name="email" widget="email"/>
                            <field name="nationality"/>
                            <field name="date_of_birth"/>
                        </group>
                        <group string="Statistics">
                            <field name="book_count"/>
                            <field name="active"/>
                        </group>
                    </group>
                    <notebook>
                        <page string="Biography" name="page_bio">
                            <field name="biography"
                                   placeholder="Write the author's biography..."/>
                        </page>
                        <page string="Books" name="page_books">
                            <field name="book_ids">
                                <list>
                                    <field name="name"/>
                                    <field name="isbn"/>
                                    <field name="state" widget="badge"/>
                                </list>
                            </field>
                        </page>
                    </notebook>
                </sheet>
            </form>
        </field>
    </record>

    <!-- Author List View -->
    <record id="library_author_view_list" model="ir.ui.view">
        <field name="name">library.author.list</field>
        <field name="model">library.author</field>
        <field name="arch" type="xml">
            <list>
                <field name="name"/>
                <field name="email"/>
                <field name="nationality"/>
                <field name="book_count"/>
            </list>
        </field>
    </record>

    <!-- Author Search View -->
    <record id="library_author_view_search" model="ir.ui.view">
        <field name="name">library.author.search</field>
        <field name="model">library.author</field>
        <field name="arch" type="xml">
            <search>
                <field name="name"/>
                <field name="email"/>
                <field name="nationality"/>
                <filter name="filter_archived"
                        string="Archived"
                        domain="[('active', '=', False)]"/>
            </search>
        </field>
    </record>

    <!-- Author Action -->
    <record id="library_author_action" model="ir.actions.act_window">
        <field name="name">Authors</field>
        <field name="res_model">library.author</field>
        <field name="view_mode">list,form</field>
        <field name="help" type="html">
            <p class="o_view_nocontent_smiling_face">
                Add your first author!
            </p>
        </field>
    </record>

</odoo>

Step 3: Create views/tag_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <!-- Tag Form View -->
    <record id="library_tag_view_form" model="ir.ui.view">
        <field name="name">library.tag.form</field>
        <field name="model">library.tag</field>
        <field name="arch" type="xml">
            <form string="Tag">
                <sheet>
                    <group>
                        <group>
                            <field name="name"/>
                            <field name="color" widget="color_picker"/>
                        </group>
                        <group>
                            <field name="active"/>
                        </group>
                    </group>
                    <group>
                        <field name="description"
                               placeholder="Describe this tag..."/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <!-- Tag List View -->
    <record id="library_tag_view_list" model="ir.ui.view">
        <field name="name">library.tag.list</field>
        <field name="model">library.tag</field>
        <field name="arch" type="xml">
            <list editable="bottom">
                <field name="name"/>
                <field name="color" widget="color_picker"/>
                <field name="active"/>
            </list>
        </field>
    </record>

    <!-- Tag Action -->
    <record id="library_tag_action" model="ir.actions.act_window">
        <field name="name">Tags</field>
        <field name="res_model">library.tag</field>
        <field name="view_mode">list,form</field>
    </record>

</odoo>

Step 4: Create views/menu_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <!-- =========================== -->
    <!--     TOP MENU (App Menu)     -->
    <!-- =========================== -->
    <menuitem
        id="library_root_menu"
        name="Library"
        sequence="100"/>

    <!-- =========================== -->
    <!--     CATALOG SECTION         -->
    <!-- =========================== -->
    <menuitem
        id="library_catalog_menu"
        name="Catalog"
        parent="library_root_menu"
        sequence="10"/>

    <menuitem
        id="library_book_menu"
        name="Books"
        parent="library_catalog_menu"
        action="library_book_action"
        sequence="10"/>

    <menuitem
        id="library_author_menu"
        name="Authors"
        parent="library_catalog_menu"
        action="library_author_action"
        sequence="20"/>

    <!-- =========================== -->
    <!--   CONFIGURATION SECTION     -->
    <!-- =========================== -->
    <menuitem
        id="library_config_menu"
        name="Configuration"
        parent="library_root_menu"
        sequence="90"/>

    <menuitem
        id="library_tag_menu"
        name="Tags"
        parent="library_config_menu"
        action="library_tag_action"
        sequence="10"/>

</odoo>

Step 5: Update __manifest__.py

Uncomment and add the view files to the data list:

'data': [
    # 'security/ir.model.access.csv',   # We'll add this in Lesson 8
    'views/book_views.xml',
    'views/author_views.xml',
    'views/tag_views.xml',
    'views/menu_views.xml',             # Must be LAST (references actions from above)
],

Step 6: Upgrade and Test

# Docker:
docker compose exec odoo odoo -d odoo18dev -u library_app --stop-after-init
docker compose restart odoo

# Source:
python odoo/odoo-bin -c odoo.conf -d odoo18dev -u library_app

Now open http://localhost:8069 in your browser.

You should see a “Library” app in the top navigation bar. Click it, and you’ll see:

  1. Catalog → Books — A list view of books with the “Available” filter active by default
  2. Click any book → The form view with the status bar, buttons, and tabbed layout
  3. Catalog → Authors — Authors with their book count and embedded book list
  4. Configuration → Tags — An editable inline list for managing tags

Troubleshooting

Problem Solution
“Library” menu not visible Check __manifest__.py — are the XML files listed in data?
“Access Denied” or “You are not allowed to access this model” We haven’t set up security yet (Lesson 8). Temporary fix: use the admin user
Button click does nothing Check the button’s name matches the Python method name exactly
View error on upgrade Check XML syntax — missing closing tags, wrong attribute names
Fields not showing Make sure the field exists in the Python model and the module is upgraded

About the “Access Denied” error: If you’re logged in as admin, you might not see this. But if you log in as a regular user, you’ll get errors because we haven’t created security rules yet. We’ll fix this in Lesson 8. For now, use the admin account for testing.


Summary & What’s Next

Key Takeaways

  1. Odoo UI is defined in XML, not HTML. Views, actions, and menus are records in the database loaded from XML files.
  2. Form views use , , , and for layout. The
    holds buttons and the status bar.
  3. List views show records as tables. Use decoration-* for row colors, optional for hideable columns, editable for inline editing.
  4. Search views define search fields, predefined filters (domain), and group-by options (context={'group_by': 'field'}).
  5. Window actions connect models to views. Set view_mode, domain, context, and help.
  6. Menu items create 3-level navigation: Root → Section → Item. Only leaf menus have actions.
  7. File loading order in __manifest__.py matters: security → views → menus.
  8. Odoo 18 syntax for dynamic visibility: invisible="state != 'draft'" directly on the element (no attrs).

Files Created in This Lesson

File Contains
views/book_views.xml Form, List, Search views + Window Action for books
views/author_views.xml Form, List, Search views + Window Action for authors
views/tag_views.xml Form, List views + Window Action for tags
views/menu_views.xml Complete menu hierarchy

What’s Next?

In Lesson 7: Views — Part 2: Advanced Views & Widgets, we’ll add:

  • Kanban view — Card-based layout (like Trello)
  • Calendar, Pivot, and Graph views — Data visualization
  • Activity and Chatter — Communication tracking
  • View inheritance — Extending existing views with XPath

Our library module is now usable! In the next lesson, we’ll make it beautiful.


Exercises: 1. Create a simple form and list view for library.publisher. Add it to the Configuration menu. 2. Try adding the multi_edit="1" attribute to the book list view. This lets users edit multiple records at once by selecting rows and changing a field. 3. Experiment with the widget attribute on different fields: – — Makes the email clickable – — Makes the URL clickable – — Shows with currency symbol – — Shows as stars 4. Add a second window action library_book_borrowed_action that only shows borrowed books (pre-filter with domain). Create a new menu item “Borrowed Books” under Catalog that uses this action. 5. In the book form view, try making the “Publication Details” group invisible when the state is “draft”. Use the Odoo 18 syntax: .


Previous lesson: Lesson 5 — CRUD, Recordsets & Business Logic Next lesson: Lesson 7 — Advanced Views & Widgets

Leave a Reply

Your email address will not be published. Required fields are marked *