Views — Part 2: Advanced Views & Widgets

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–6 completed, library_app module with basic views and menus working


Kanban View: Cards Instead of Rows

The kanban view displays records as cards, optionally organized in columns. Think of Trello or Jira boards — that’s exactly what a kanban view looks like.

When to Use Kanban

  • Workflow boards — cards grouped by status (Draft → Available → Borrowed)
  • Visual browsing — when users prefer scanning cards over reading tables
  • Quick overview — show key info (image, title, tags) at a glance

Kanban View Structure

Unlike form and list views, kanban views use QWeb templates inside the XML. QWeb is Odoo’s templating engine (similar to Jinja2). Here’s the basic structure:

<record id="library_book_view_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">
            <!-- 1. Declare which fields to load from the database -->
            <field name="name"/>
            <field name="author_id"/>
            <field name="state"/>
            <field name="cover_image"/>
            <field name="tag_ids"/>
            <field name="pages"/>
            <field name="price"/>
            <field name="color"/>

            <!-- 2. Define the card template using QWeb -->
            <templates>
                <t t-name="card">
                    <!-- Card content goes here -->
                </t>
            </templates>
        </kanban>
    </field>
</record>

Key parts:

  • — The root element (replaces
    or )
  • default_group_by="state" — Group cards into columns by this field
  • outside — Preload these fields (required for QWeb to access them)
  • — The QWeb template for each card

QWeb Basics for Kanban Cards

QWeb directives (the t-* attributes) let you add logic to templates:

<!-- Output a field value as text -->
<span t-out="record.name.value"/>

<!-- Conditional display -->
<span t-if="record.pages.raw_value > 500">Long book!</span>

<!-- Loop (rare in kanban, but possible) -->
<t t-foreach="record.tag_ids.raw_value" t-as="tag_id">
    <span t-out="tag_id"/>
</t>

Accessing record data in QWeb:

Syntax Returns Use for
record.name.value Formatted display value ("The Hobbit") Showing to users
record.name.raw_value Raw Python value ("The Hobbit") Comparisons and logic
record.author_id.value Display name ("J.R.R. Tolkien") Showing related record
record.author_id.raw_value ID (3) Comparisons
record.state.value Display label ("Available") Showing to users
record.state.raw_value Database value ("available") Conditional logic

Complete Kanban Card Example

<templates>
    <t t-name="card">
        <!-- Card header with cover image -->
        <div class="o_kanban_image" t-if="record.cover_image.raw_value">
            <field name="cover_image" widget="image"
                   options="{'preview_image': 'cover_image'}"/>
        </div>

        <!-- Card body -->
        <div class="flex-grow-1">
            <!-- Title -->
            <strong class="o_kanban_record_title">
                <field name="name"/>
            </strong>

            <!-- Author -->
            <div t-if="record.author_id.value" class="text-muted">
                <field name="author_id"/>
            </div>

            <!-- Price and pages info -->
            <div class="mt-1">
                <span t-if="record.pages.raw_value">
                    <field name="pages"/> pages
                </span>
                <span t-if="record.price.raw_value" class="float-end fw-bold">
                    $<field name="price"/>
                </span>
            </div>

            <!-- Tags at the bottom -->
            <div class="mt-2">
                <field name="tag_ids" widget="many2many_tags"
                       options="{'color_field': 'color'}"/>
            </div>
        </div>
    </t>
</templates>

Kanban Column Grouping

When you set default_group_by, cards are organized into columns:

<!-- Group by state: creates columns Draft | Available | Borrowed | Lost -->
<kanban default_group_by="state">
┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
│  Draft   │  │ Available │  │ Borrowed │  │   Lost   │
├──────────┤  ├──────────┤  ├──────────┤  ├──────────┤
│ ┌──────┐ │  │ ┌──────┐ │  │ ┌──────┐ │  │          │
│ │Book A│ │  │ │Book C│ │  │ │Book E│ │  │          │
│ └──────┘ │  │ └──────┘ │  │ └──────┘ │  │          │
│ ┌──────┐ │  │ ┌──────┐ │  │          │  │          │
│ │Book B│ │  │ │Book D│ │  │          │  │          │
│ └──────┘ │  │ └──────┘ │  │          │  │          │
└──────────┘  └──────────┘  └──────────┘  └──────────┘

Users can drag and drop cards between columns to change the state — very intuitive for workflow management.

Kanban with

Show completion progress at the top of each column:

<kanban default_group_by="state">
    <progressbar field="state"
                 colors='{"draft": "muted", "available": "success",
                          "borrowed": "info", "lost": "danger"}'/>
    <!-- ... templates ... -->
</kanban>

This adds a colored progress bar at the top of each group, showing the distribution of states.


Calendar View

The calendar view displays records on a calendar based on date fields.

<record id="library_book_view_calendar" model="ir.ui.view">
    <field name="name">library.book.calendar</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
        <calendar string="Books"
                  date_start="date_published"
                  color="author_id"
                  mode="month"
                  event_open_popup="true"
                  quick_create="false">
            <field name="name"/>
            <field name="author_id"/>
            <field name="state"/>
        </calendar>
    </field>
</record>

Attributes:

Attribute Value Meaning
date_start date_published The date field to position events
date_stop (optional) End date for multi-day events
color author_id Color-code events by this field
mode month Default view: day, week, month, year
event_open_popup true Open events in a popup instead of navigating away
quick_create false Disable click-to-create on empty dates

For the calendar to work, the model needs at least one Date or Datetime field. In our case, date_published works perfectly.

To add the calendar to your module, include calendar in the action’s view_mode:

<field name="view_mode">list,form,kanban,calendar</field>

Pivot View and Graph View

These views are for data analysis — they aggregate and visualize your data.

Graph View

Displays data as charts (bar, line, pie):

<record id="library_book_view_graph" model="ir.ui.view">
    <field name="name">library.book.graph</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
        <graph string="Books Analysis" type="bar">
            <field name="author_id"/>           <!-- X axis: authors -->
            <field name="pages" type="measure"/>  <!-- Y axis: sum of pages -->
        </graph>
    </field>
</record>

Graph types: bar (default), line, pie

Field roles:

  • Fields without type="measure" → used for grouping (X axis / categories)
  • Fields with type="measure" → used for values (Y axis / size)

Pivot View

A spreadsheet-like table with rows, columns, and measures:

<record id="library_book_view_pivot" model="ir.ui.view">
    <field name="name">library.book.pivot</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
        <pivot string="Books Analysis">
            <field name="author_id" type="row"/>       <!-- Rows -->
            <field name="state" type="col"/>            <!-- Columns -->
            <field name="pages" type="measure"/>        <!-- Values: sum of pages -->
            <field name="price" type="measure"/>        <!-- Values: sum of price -->
        </pivot>
    </field>
</record>

Field types:

Type Role Example
type="row" Row grouping Group by author
type="col" Column grouping Group by state
type="measure" Aggregated values Sum of pages, sum of price

The pivot view is interactive — users can drag fields between rows, columns, and measures, expand/collapse groups, and download as Excel.

Add both to view_mode:

<field name="view_mode">list,form,kanban,calendar,graph,pivot</field>

Activity View and Chatter Integration

The chatter is Odoo’s built-in communication system — a comment thread attached to every record. It shows messages, activity schedule, and followers.

Adding Chatter to Your Model

Step 1: Add 'mail' to your module’s dependencies in __manifest__.py:

'depends': ['base', 'mail'],

Step 2: Inherit from mail.thread and mail.activity.mixin in your model:

class LibraryBook(models.Model):
    _name = 'library.book'
    _description = 'Library Book'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    # _inherit as a list adds mixin functionality without changing _name
    # This is called "mixin inheritance" — covered fully in Lesson 9

Step 3: Add the chatter to the form view, AFTER :

<form string="Book">
    <header>
        <!-- ... buttons and statusbar ... -->
    </header>
    <sheet>
        <!-- ... form content ... -->
    </sheet>

    <!-- Chatter section — MUST be after </sheet>, inside <form> -->
    <chatter/>
</form>

That’s it! The tag automatically renders:

  • Message thread — users can post comments and notes
  • Activity scheduling — plan and track activities (calls, meetings, to-dos)
  • Followers — subscribe users to receive notifications
  • Log notes — internal notes not sent to external contacts

Tracking Field Changes in Chatter

Remember the tracking=True attribute from Lesson 3? Now it comes to life:

state = fields.Selection([...], tracking=True)
name = fields.Char(tracking=True)
author_id = fields.Many2one('library.author', tracking=True)

With tracking=True and mail.thread inherited, every time these fields change, a message is automatically posted in the chatter:

Administrator, 2 minutes ago
    ● Status: Draft → Available
    ● Author: (empty) → J.R.R. Tolkien

The Activity View

Once mail.activity.mixin is inherited, you can add an activity view:

<record id="library_book_view_activity" model="ir.ui.view">
    <field name="name">library.book.activity</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
        <activity string="Books">
            <templates>
                <div t-name="activity-box">
                    <field name="name"/>
                    <field name="author_id" muted="1"/>
                </div>
            </templates>
        </activity>
    </field>
</record>

Add activity to view_mode:

<field name="view_mode">list,form,kanban,calendar,activity</field>

Widget System: Common Widgets Reference

Widgets control how a field is rendered in the UI. The same field can look completely different depending on the widget.

Text & Selection Widgets

Widget Used On Renders As
email Char Clickable email link
url Char Clickable URL link
phone Char Clickable phone link (opens dialer on mobile)
CopyClipboardChar Char Text with a copy-to-clipboard button
password Char Masked dots (like password inputs)
radio Selection Radio buttons instead of dropdown
badge Selection Colored badge/chip
statusbar Selection Horizontal pipeline
color_picker Integer Color palette selector
priority Selection Star rating (for [('0','Normal'), ('1','Important'), ('2','Urgent')])

Number Widgets

Widget Used On Renders As
monetary Float/Monetary Amount with currency symbol ($14.99)
percentage Float Percentage with % symbol
progressbar Float/Integer Horizontal progress bar
float_time Float Hours:Minutes (1.5 → 01:30)

Relational Widgets

Widget Used On Renders As
many2many_tags Many2many Colored tag chips
many2many_checkboxes Many2many Checkbox list
selection Many2one Dropdown (instead of search-enabled select)

Date & Binary Widgets

Widget Used On Renders As
daterange Date/Datetime Date range picker (needs options={'end_date_field': '...'})
remaining_days Date “In 5 days” or “3 days ago”
image Binary/Image Image display
binary Binary File upload/download button

Widget Usage Examples

<!-- Email field: rendered as clickable link -->
<field name="email" widget="email"/>

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

<!-- Rating as stars (requires Selection with '0', '1', '2' options) -->
<field name="priority" widget="priority"/>

<!-- Radio buttons instead of dropdown -->
<field name="cover_type" widget="radio"/>

<!-- Progress bar for a percentage field -->
<field name="completion" widget="progressbar"/>

<!-- Monetary with currency -->
<field name="price" widget="monetary" options="{'currency_field': 'currency_id'}"/>

<!-- Status as a colored badge -->
<field name="state" widget="badge"
       decoration-success="state == 'available'"
       decoration-danger="state == 'lost'"/>

Statusbar Widget and Selection Fields as Pipelines

We briefly saw the statusbar in Lesson 6, but let’s go deeper.

Full Statusbar Configuration

<field name="state" widget="statusbar"
       statusbar_visible="draft,available,borrowed"
       options="{'clickable': true}"/>
  • statusbar_visible — Which states appear as steps. States not listed are hidden but still reachable
  • options="{'clickable': true}" — Allow users to click on a step to jump to that state (default: true in Odoo 18)

Using Selection Fields as Kanban Pipelines

When a Selection field is used as default_group_by in a kanban view, users can drag cards between columns to change the value. This creates a workflow board:

<kanban default_group_by="state">
    <!-- Cards can be dragged from "Draft" to "Available" -->
</kanban>

Controlling column behavior:

<kanban default_group_by="state"
        group_create="false"
        group_delete="false"
        group_edit="false"
        records_draggable="true">
Attribute Default Meaning
group_create true Allow creating new columns
group_delete true Allow deleting empty columns
group_edit true Allow renaming columns
records_draggable true Allow drag-and-drop between columns

For Selection-based grouping, you typically set group_create, group_delete, and group_edit to false since the columns are defined by the Python code, not by the user.


Dynamic Visibility in Odoo 18 — The attrs Replacement

This is an important section because Odoo 18 changed how dynamic visibility works, and many tutorials and StackOverflow answers still use the old syntax.

The Old Way (Odoo 16 and Earlier)

<!-- ❌ OLD SYNTAX — don't use in Odoo 18 -->
<field name="isbn" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="price" attrs="{'invisible': [('state', '=', 'lost')]}"/>
<group attrs="{'invisible': [('state', '=', 'draft')]}">
    <field name="date_published"/>
</group>

The New Way (Odoo 18)

<!-- ✅ NEW SYNTAX — use this in Odoo 18 -->
<field name="isbn" readonly="state != 'draft'"/>
<field name="price" invisible="state == 'lost'"/>
<group invisible="state == 'draft'" string="Publishing">
    <field name="date_published"/>
</group>

What Changed and Why

Feature Old (attrs) New (Odoo 18)
Syntax Domain notation [('field', 'op', 'value')] Python-like expression field op value
Readability attrs="{'invisible': [('state', '=', 'draft')]}" invisible="state == 'draft'"
Complexity Hard to read for nested conditions Natural Python expressions
Attribute name Inside attrs dict Direct attribute on the element

Expression Syntax Guide

The new expressions support standard comparison and logical operators:

<!-- Simple comparison -->
invisible="state == 'draft'"
readonly="state != 'draft'"
required="state == 'available'"

<!-- Multiple conditions with and/or -->
invisible="state == 'draft' and not isbn"
readonly="state != 'draft' or is_locked"
invisible="state in ('lost', 'borrowed')"
invisible="state not in ('draft', 'available')"

<!-- Checking empty/set fields -->
invisible="not author_id"          <!-- Hidden when no author -->
invisible="author_id"              <!-- Hidden when author IS set -->

<!-- Numeric comparisons -->
invisible="pages &lt; 100"         <!-- Note: use &lt; for < in XML -->
invisible="price &gt; 0"           <!-- Note: use &gt; for > in XML -->

<!-- Combining conditions -->
readonly="state != 'draft' and not is_admin"
invisible="state == 'draft' or (state == 'available' and not isbn)"

XML escaping: Since these expressions are inside XML attributes, you need to escape < and >:

Character XML Escape Example
< < invisible="pages < 100"
> > invisible="pages > 500"
& & (rarely needed in expressions)

Applying to Any Element

The invisible, readonly, and required attributes work on any XML element, not just :

<!-- Hide an entire group -->
<group invisible="state == 'draft'" string="Publication Details">
    <field name="isbn"/>
    <field name="date_published"/>
</group>

<!-- Hide a button -->
<button name="action_mark_available"
        invisible="state != 'draft'"
        string="Mark Available"/>

<!-- Hide a notebook page -->
<page string="Advanced" invisible="state == 'draft'" name="page_advanced">
    <!-- ... -->
</page>

<!-- Hide a div -->
<div invisible="not author_id" class="text-muted">
    Author info available
</div>

View Inheritance: Extending Existing Views with XPath

View inheritance is one of Odoo's most powerful features. It lets you modify existing views without replacing them entirely. This is how you customize built-in Odoo modules or add features to views defined by other modules.

Why View Inheritance?

Imagine you want to add a field to the Contacts form (from the base module). You could:

  • ❌ Copy the entire form view and modify it (800+ lines of XML — maintenance nightmare)
  • ✅ Create an inherited view that adds your field to the existing form (3 lines of XML)

Basic Syntax

<record id="library_book_form_inherit_custom" model="ir.ui.view">
    <field name="name">library.book.form.inherit.custom</field>
    <field name="model">library.book</field>
    <field name="inherit_id" ref="library_book_view_form"/>
    <!-- inherit_id points to the view we want to extend -->
    <field name="arch" type="xml">

        <!-- XPath expressions to locate and modify elements -->

    </field>
</record>

Key difference from a normal view: The inherit_id field references the parent view we're extending.

XPath Selectors

XPath is an XML query language. In Odoo, you use it to locate specific elements in the parent view, then specify what to do (add before, add after, replace, add inside, etc.).

Common XPath patterns:

<!-- Find a field by name -->
<xpath expr="//field[@name='isbn']" position="after">
    <field name="new_field"/>
</xpath>

<!-- Find a group by string attribute -->
<xpath expr="//group[@string='Book Details']" position="inside">
    <field name="new_field"/>
</xpath>

<!-- Find a page by name attribute -->
<xpath expr="//page[@name='page_description']" position="after">
    <page string="New Tab" name="page_new">
        <field name="new_field"/>
    </page>
</xpath>

<!-- Find the header -->
<xpath expr="//header" position="inside">
    <button name="new_action" type="object" string="New Button"/>
</xpath>

<!-- Find a button by name -->
<xpath expr="//button[@name='action_mark_available']" position="before">
    <button name="new_action" type="object" string="Before Button"/>
</xpath>

<!-- Find the sheet element -->
<xpath expr="//sheet" position="before">
    <div class="alert alert-info" role="alert">
        This is a banner above the sheet!
    </div>
</xpath>

Position Values

Position What It Does
inside Add as a child (at the end) of the matched element
before Add as a sibling, before the matched element
after Add as a sibling, after the matched element
replace Replace the matched element entirely
attributes Modify attributes of the matched element

Shorthand (Without XPath Tag)

For the most common case — adding after a field — Odoo provides a shorthand:

<!-- Shorthand: directly target a field -->
<field name="isbn" position="after">
    <field name="new_field"/>
</field>

<!-- This is equivalent to: -->
<xpath expr="//field[@name='isbn']" position="after">
    <field name="new_field"/>
</xpath>

Modifying Attributes

<!-- Make a field readonly -->
<xpath expr="//field[@name='isbn']" position="attributes">
    <attribute name="readonly">True</attribute>
</xpath>

<!-- Add invisible condition -->
<xpath expr="//field[@name='price']" position="attributes">
    <attribute name="invisible">state == 'draft'</attribute>
</xpath>

<!-- Change a button's string -->
<xpath expr="//button[@name='action_mark_available']" position="attributes">
    <attribute name="string">Publish</attribute>
    <attribute name="class">btn-success</attribute>
</xpath>

Replacing an Element

<!-- Replace a field entirely -->
<xpath expr="//field[@name='old_field']" position="replace">
    <field name="new_field" widget="many2many_tags"/>
</xpath>

<!-- Remove a field (replace with nothing) -->
<xpath expr="//field[@name='unwanted_field']" position="replace"/>

Real-World Example: Extending res.partner

This is a very common pattern — adding fields to the Contact form:

<!-- In your module's views/res_partner_views.xml -->
<record id="res_partner_form_inherit_library" model="ir.ui.view">
    <field name="name">res.partner.form.inherit.library</field>
    <field name="model">res.partner</field>
    <field name="inherit_id" ref="base.view_partner_form"/>
    <field name="arch" type="xml">

        <!-- Add a "Library" tab to the Contact form -->
        <xpath expr="//notebook" position="inside">
            <page string="Library" name="page_library">
                <group>
                    <field name="library_member_id"/>
                    <field name="favorite_book_ids" widget="many2many_tags"/>
                </group>
            </page>
        </xpath>

    </field>
</record>

This adds a "Library" tab to every contact's form — without touching the original base module code.

Inheritance Priority

When multiple modules inherit the same view, Odoo applies them in order of priority (default: 16). Lower numbers are applied first:

<record id="my_inherit_view" model="ir.ui.view">
    <field name="name">my.inherit</field>
    <field name="model">library.book</field>
    <field name="inherit_id" ref="library_book_view_form"/>
    <field name="priority">20</field>  <!-- Applied after priority 16 views -->
    <field name="arch" type="xml">
        <!-- ... -->
    </field>
</record>

Hands-on: Add Kanban View, Chatter, and Advanced Widgets

Let's enhance our library module with everything we've learned.

Step 1: Update __manifest__.py Dependencies

'depends': ['base', 'mail'],    # Add 'mail' for chatter support

Step 2: Update models/book.py — Add Chatter Mixins

class LibraryBook(models.Model):
    _name = 'library.book'
    _description = 'Library Book'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    #           ↑ message thread    ↑ activity scheduling
    _order = 'name ASC'

    # Add tracking to key fields:
    name = fields.Char(
        string='Title',
        required=True,
        index=True,
        tracking=True,    # ← Add this
    )

    state = fields.Selection(
        selection=[
            ('draft', 'Draft'),
            ('available', 'Available'),
            ('borrowed', 'Borrowed'),
            ('lost', 'Lost'),
        ],
        string='Status',
        default='draft',
        required=True,
        copy=False,
        tracking=True,    # ← Add this
    )

    author_id = fields.Many2one(
        comodel_name='library.author',
        string='Author',
        ondelete='restrict',
        index=True,
        tracking=True,    # ← Add this
    )

    # ... keep all other fields ...

Step 3: Add Kanban View to views/book_views.xml

Add this BEFORE the section:

    <!-- ============================================ -->
    <!--               KANBAN VIEW                    -->
    <!-- ============================================ -->
    <record id="library_book_view_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"
                    group_create="false"
                    group_delete="false"
                    group_edit="false">
                <field name="name"/>
                <field name="author_id"/>
                <field name="state"/>
                <field name="cover_image"/>
                <field name="tag_ids"/>
                <field name="pages"/>
                <field name="price"/>
                <field name="rating"/>

                <progressbar field="state"
                             colors='{"draft": "muted", "available": "success",
                                      "borrowed": "info", "lost": "danger"}'/>

                <templates>
                    <t t-name="card">
                        <div class="o_kanban_image" t-if="record.cover_image.raw_value">
                            <field name="cover_image" widget="image"/>
                        </div>
                        <div class="flex-grow-1">
                            <strong class="o_kanban_record_title">
                                <field name="name"/>
                            </strong>
                            <div t-if="record.author_id.value" class="text-muted small">
                                <field name="author_id"/>
                            </div>
                            <div class="mt-1 small">
                                <span t-if="record.pages.raw_value">
                                    <field name="pages"/> pages
                                </span>
                                <span t-if="record.price.raw_value" class="float-end fw-bold text-primary">
                                    $<field name="price"/>
                                </span>
                            </div>
                            <div class="mt-2">
                                <field name="tag_ids" widget="many2many_tags"
                                       options="{'color_field': 'color'}"/>
                            </div>
                        </div>
                    </t>
                </templates>
            </kanban>
        </field>
    </record>

Step 4: Add Chatter to the Form View

In views/book_views.xml, find the closing tag in the form view, and add just before it:

                </notebook>
            </sheet>

            <!-- ADD THIS: Chatter (messages + activities + followers) -->
            <chatter/>

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

Step 5: Update the Window Action

Add kanban and activity to view_mode:

<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,activity</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>
    </field>
</record>

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

What to test:

  1. Kanban view — Click the kanban icon (grid icon) in the Books list. You should see cards grouped by status. Try dragging a card from "Draft" to "Available."
  2. Chatter — Open a book's form view. Scroll down below the sheet. You should see the chatter with "Send message", "Log note", and "Schedule activity" buttons.
  3. Tracking — Change a book's status or author. A message should appear in the chatter logging the change.
  4. Activity view — Click the clock icon in the view switcher. You should see the activity board.

Summary & What's Next

Key Takeaways

  1. Kanban views use QWeb templates inside XML. Use default_group_by for column grouping and for visual progress.
  2. Calendar, Graph, and Pivot views add data visualization with minimal XML. Just specify which fields to display and how.
  3. Chatter requires mail.thread mixin + in the form view. Add tracking=True to fields you want to log.
  4. Widgets control field rendering: many2many_tags, badge, statusbar, email, monetary, priority, etc.
  5. Odoo 18 uses direct expressions (invisible="state == 'draft'") instead of the old attrs syntax.
  6. View inheritance uses XPath to surgically modify existing views. Use inherit_id + position (inside, before, after, replace, attributes).

View Types Summary

View XML Tag Purpose view_mode
Form
Edit single record form
List Table of records list
Kanban Card board kanban
Search Search/filter/group (auto)
Calendar Date-based display calendar
Graph Charts (bar/line/pie) graph
Pivot Spreadsheet analysis pivot
Activity Activity board activity

What's Next?

In Lesson 8: Security — Access Control in Odoo, we'll finally fix the "Access Denied" errors. You'll learn:

  • User groups and module categories
  • Access Control Lists (CSV)
  • Record rules (row-level security)
  • Field-level access with groups
  • Multi-company security

Without security, your module only works for the admin user. After Lesson 8, it'll work for everyone — with proper permissions.


Exercises: 1. Add a Graph view to the library module that shows a bar chart of book count per author. Add graph to view_mode. 2. Add a Pivot view that shows pages and price by author (rows) and state (columns). Add pivot to view_mode. 3. Experiment with the kanban card design: add the rating field as stars, or show the cover type as a badge. Customize the card layout to your liking. 4. Create an inherited view that adds a new field to the author form. For example, add a website field to library.author, then use view inheritance to add it to the existing author form view. 5. Try adding to the author form view too. You'll need to add _inherit = ['mail.thread'] to the LibraryAuthor model. 6. Use the attributes position to change the book list view: make the name column bold by adding decoration-bf="1", or change the state widget from badge to radio.


Previous lesson: Lesson 6 — Views: XML-Based UI (Part 1) Next lesson: Lesson 8 — Security: Access Control in Odoo

Leave a Reply

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