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_appmodule 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 (replacesor)default_group_by="state"— Group cards into columns by this fieldoutside— 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 reachableoptions="{'clickable': true}"— Allow users to click on a step to jump to that state (default:truein 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 < 100" <!-- Note: use < for < in XML -->
invisible="price > 0" <!-- Note: use > 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:
- 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."
- 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.
- Tracking — Change a book's status or author. A message should appear in the chatter logging the change.
- Activity view — Click the clock icon in the view switcher. You should see the activity board.
Summary & What's Next
Key Takeaways
- Kanban views use QWeb templates inside XML. Use
default_group_byfor column grouping andfor visual progress. - Calendar, Graph, and Pivot views add data visualization with minimal XML. Just specify which fields to display and how.
- Chatter requires
mail.threadmixin +in the form view. Addtracking=Trueto fields you want to log. - Widgets control field rendering:
many2many_tags,badge,statusbar,email,monetary,priority, etc. - Odoo 18 uses direct expressions (
invisible="state == 'draft'") instead of the oldattrssyntax. - 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
graphtoview_mode. 2. Add a Pivot view that shows pages and price by author (rows) and state (columns). Addpivottoview_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 awebsitefield tolibrary.author, then use view inheritance to add it to the existing author form view. 5. Try addingto the author form view too. You'll need to add_inherit = ['mail.thread']to theLibraryAuthormodel. 6. Use theattributesposition to change the book list view: make thenamecolumn bold by addingdecoration-bf="1", or change thestatewidget frombadgetoradio.
Previous lesson: Lesson 6 — Views: XML-Based UI (Part 1) Next lesson: Lesson 8 — Security: Access Control in Odoo
