How to Create a New View Type in Odoo 19: Complete Guide
Odoo ships with several built-in view types such as list, form, kanban, calendar, and pivot, but what if your business logic calls for something entirely different? Using OWL (Odoo Web Library) components on the frontend and a Python model extension on the backend, you can register a completely new view type in Odoo 19. Custom views give developers complete control over how records are retrieved, organized, and displayed in the user interface, enabling highly engaging, aesthetically pleasing experiences tailored to specific workflows. This tutorial walks through creating a module called beautiful_view that registers a new view type named "beautiful" with a responsive, card-based record display compatible with any Odoo model.
What You'll Learn:
- How to structure a custom view module with models, static assets, and views
- How to register a new view type at the Python level in ir.ui.view
- How to implement the four JavaScript classes: ArchParser, Model, Renderer, Controller
- How to create OWL templates and CSS for the custom view
- How to configure window actions and menu items to use the new view
Module File Structure
The complete beautiful_view module follows a standard Odoo module layout with models, static assets (JavaScript, CSS, XML templates), and view definitions. Every Odoo view in OWL follows a strict separation of concerns across four JavaScript classes plus the arch parser: ir.actions.act_window triggers the view, beautiful_view.js registers it in the OWL views registry, BeautifulArchParser reads the XML arch definition, BeautifulController acts as the top-level OWL component, BeautifulModel handles data fetching, and BeautifulRenderer renders the card display.
beautiful_view/
+-- models/
+-- __init__.py
+-- ir_ui_view.py
+-- static/src/
+-- css/
+ +-- beautiful_view.css
+-- js/
+ +-- beautiful_arch_parser.js
+ +-- beautiful_controller.js
+ +-- beautiful_model.js
+ +-- beautiful_renderer.js
+ +-- beautiful_view.js
+-- xml/
+-- beautiful_templates.xml
+-- views/
+-- beautiful_views.xml
+-- __init__.py
+-- __manifest__.py
Architecture Overview
ArchParser
Reads the view's XML arch definition using visitXML and returns a structured archInfo object consumed by the Model and Renderer.
Model
Handles all data fetching via orm.searchRead with KeepLast concurrency utility that cancels in-flight requests when a newer one is triggered.
Renderer
Pure display OWL component with no data-fetching logic. Receives records as props and renders each as a card, handling click navigation to form view.
Controller
Top-level OWL component that initializes services, owns the model instance, manages lifecycle hooks, and connects the renderer with Odoo's Layout system.
Step 1: __manifest__.py
The manifest declares module metadata, its dependency on web and sale, and registers all frontend assets under web.assets_backend. This includes all JavaScript files, the OWL XML templates, and the CSS stylesheet.
{
'name': 'Beautiful View',
'version': '19.0.1.0.0',
'category': 'Technical',
'summary': 'Custom "beautiful" view type - works with any model',
'depends': ['web', 'sale'],
'assets': {
'web.assets_backend': [
'beautiful_view/static/src/js/beautiful_controller.js',
'beautiful_view/static/src/js/beautiful_renderer.js',
'beautiful_view/static/src/js/beautiful_model.js',
'beautiful_view/static/src/js/beautiful_arch_parser.js',
'beautiful_view/static/src/js/beautiful_view.js',
'beautiful_view/static/src/xml/beautiful_templates.xml',
'beautiful_view/static/src/css/beautiful_view.css',
],
},
'data': [
'views/beautiful_views.xml',
],
'installable': True,
}
Step 2: Python Model Registration
Before Odoo will accept view_mode="beautiful" in an action, you must register the type at the Python level by extending ir.ui.view and ir.actions.act_window.view. The IrUiView class adds 'beautiful' to the type selection and provides view info. The ActWindowView class adds 'beautiful' to the view_mode selection.
from odoo import fields, models
class IrUiView(models.Model):
_inherit = 'ir.ui.view'
type = fields.Selection(
selection_add=[('beautiful', 'Beautiful')]
)
def _get_view_info(self):
info = super()._get_view_info()
info['beautiful'] = {'icon': 'fa fa-picture-o'}
return info
class ActWindowView(models.Model):
_inherit = 'ir.actions.act_window.view'
view_mode = fields.Selection(
selection_add=[('beautiful', 'Beautiful')],
ondelete={'beautiful': 'cascade'}
)
Step 3: JavaScript Arch Parser
The arch parser reads the view's XML arch definition (the beautiful tag and its child field nodes) and returns a structured archInfo object consumed by the rest of the view stack. It uses visitXML from @web/core/utils/xml to traverse the XML nodes.
import { visitXML } from "@web/core/utils/xml";
export class BeautifulArchParser {
parse(arch) {
const archInfo = {
defaultField: null,
fields: [],
};
visitXML(arch, (node) => {
if (node.tagName === "beautiful") {
if (node.hasAttribute("fieldFromTheArch")) {
archInfo.defaultField =
node.getAttribute("fieldFromTheArch");
}
} else if (node.tagName === "field") {
const fieldName = node.getAttribute("name");
if (fieldName) {
archInfo.fields.push(fieldName);
}
}
});
return archInfo;
}
}
Step 4: JavaScript Model
The model handles all data fetching using orm.searchRead. It uses KeepLast, a concurrency utility that automatically cancels in-flight requests when a newer one is triggered, preventing race conditions when users rapidly change filters. It determines which fields to fetch based on the archInfo from the parser.
import { KeepLast } from "@web/core/utils/concurrency";
export class BeautifulModel {
constructor(orm, resModel, fields, archInfo, domain) {
this.orm = orm;
this.resModel = resModel;
this.archInfo = archInfo;
this.domain = domain;
this.keepLast = new KeepLast();
this.records = [];
}
async load() {
let fieldsToFetch = this.archInfo.fields;
if (!fieldsToFetch.length && this.archInfo.defaultField) {
fieldsToFetch = [this.archInfo.defaultField];
}
if (fieldsToFetch.length === 0) {
fieldsToFetch = ['id'];
}
this.records = await this.keepLast.add(
this.orm.searchRead(
this.resModel, this.domain, fieldsToFetch
)
);
}
}
Step 5: JavaScript Renderer
The renderer is a pure OWL display component with no data-fetching logic. It receives records, archInfo, and resModel as props and renders each record as a card. Clicking a card navigates to its form view using the action service.
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class BeautifulRenderer extends Component {
static template = "beautiful_view.BeautifulRenderer";
setup() {
this.action = useService("action");
}
openRecord(props, record) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: props.resModel,
res_id: record.id,
view_mode: 'form',
target: "current",
views: [[false, "form"]],
});
}
}
Step 6: JavaScript Controller
The controller is the top-level OWL component that wraps everything inside Odoo's Layout component (providing the control panel, breadcrumbs, etc.) and owns the model instance. It uses onWillStart to load data before the first render and acts as the central coordinator.
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, useState } from "@odoo/owl";
export class BeautifulController extends Component {
static template = "beautiful_view.BeautifulController";
static components = { Layout };
setup() {
this.orm = useService("orm");
this.model = useState(
new this.props.Model(
this.orm,
this.props.resModel,
this.props.fields,
this.props.archInfo,
this.props.domain
)
);
onWillStart(async () => {
await this.model.load();
});
}
}
Step 7: View Registration
The view registration file is the entry point that ties everything together. It defines a view descriptor object and adds it to Odoo's view registry under the key "beautiful". The props() function is called by the framework to transform generic view props into view-specific ones, instantiating the arch parser and parsing the arch XML.
import { registry } from "@web/core/registry";
import { BeautifulController } from "./beautiful_controller";
import { BeautifulArchParser } from "./beautiful_arch_parser";
import { BeautifulModel } from "./beautiful_model";
import { BeautifulRenderer } from "./beautiful_renderer";
export const beautifulView = {
type: "beautiful",
display_name: "Beautiful",
icon: "fa fa-picture-o",
multiRecord: true,
Controller: BeautifulController,
ArchParser: BeautifulArchParser,
Model: BeautifulModel,
Renderer: BeautifulRenderer,
props(genericProps, view) {
const { ArchParser } = view;
const { arch } = genericProps;
const archInfo = new ArchParser().parse(arch);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer,
archInfo,
};
},
};
registry.category("views").add("beautiful", beautifulView);
Step 8: OWL Templates
Two OWL templates define the HTML structure. The controller template wraps the renderer in Odoo's Layout component. The renderer template outputs a card grid where each record displays a header with an icon and title, a body with field rows, and a clickable footer that navigates to the form view.
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="beautiful_view.BeautifulController">
<Layout display="props.display"
className="'h-100 overflow-auto'">
<t t-component="props.Renderer"
records="model.records"
archInfo="props.archInfo"
resModel="props.resModel"
fields="props.fields"/>
</Layout>
</t>
<t t-name="beautiful_view.BeautifulRenderer">
<div class="beautiful-grid">
<t t-foreach="props.records"
t-as="record" t-key="record.id">
<div class="beautiful-card">
<div class="card-header">
<i class="fa fa-user-circle-o"></i>
<strong class="record-title">
<t t-esc="record[props.archInfo.defaultField
|| 'display_name']
|| 'Record #' + record.id"/>
</strong>
</div>
<div class="card-body">
<t t-foreach="props.archInfo.fields"
t-as="fname" t-key="fname">
<div class="field-row"
t-if="record[fname]">
<span class="field-label">
<t t-esc="fname.replace('_', ' ')
.toUpperCase()"/>:
</span>
<span class="field-value">
<t t-esc="record[fname]"/>
</span>
</div>
</t>
</div>
<div class="card-footer"
t-on-click="() => this.openRecord(
props, record)">
<i class="fa fa-external-link"></i>
Click to open
</div>
</div>
</t>
</div>
</t>
</templates>
Step 9: CSS Styling
The stylesheet turns the plain HTML into a clean, card-based grid with hover effects and a branded header. Cards are centered with a max-width of 600px, use a red header bar matching Odoo's design language, and feature smooth hover animations with shadow elevation.
.beautiful-grid {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
background-color: #f0f2f5;
}
.beautiful-card {
width: 100%;
max-width: 600px;
background: #f8f4f4;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.2s ease;
cursor: pointer;
overflow: hidden;
display: flex;
flex-direction: column;
}
.beautiful-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.beautiful-card .card-header {
background: #E70846;
color: white;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.beautiful-card .card-body {
padding: 16px;
flex: 1;
}
.beautiful-card .card-body .field-row {
margin-bottom: 8px;
font-size: 14px;
display: flex;
align-items: baseline;
gap: 8px;
}
.beautiful-card .card-body .field-label {
font-weight: 600;
color: #555;
min-width: 80px;
font-size: 12px;
text-transform: capitalize;
}
.beautiful-card .card-body .field-value {
color: #1f2d3d;
word-break: break-word;
}
.beautiful-card .card-footer {
padding: 10px 16px;
border-top: 1px solid #e9ecef;
font-size: 12px;
color: #7c69a9;
text-align: right;
background: #fafafa;
}
Step 10: Data Views XML
The final piece creates the actual view record, the window action, and a menu item to open it. This example applies the "beautiful" view to res.partner under the Sales menu, displaying the display_name, email, and phone fields in card format.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Beautiful view definition -->
<record id="beautiful_partner_view"
model="ir.ui.view">
<field name="name">
res.partner.beautiful.view
</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<beautiful>
<field name="display_name"/>
<field name="email"/>
<field name="phone"/>
</beautiful>
</field>
</record>
<!-- Action to open beautiful view -->
<record id="action_beautiful_partner"
model="ir.actions.act_window">
<field name="name">
Beautiful Partners
</field>
<field name="res_model">res.partner</field>
<field name="view_mode">beautiful</field>
<field name="view_id"
ref="beautiful_partner_view"/>
</record>
<!-- Menu item under Sales -->
<menuitem id="menu_beautiful_partner"
name="Beautiful Partners"
parent="sale.sale_menu_root"
action="action_beautiful_partner"
sequence="10"/>
</odoo>
Once the module is installed and upgraded, the new custom view becomes available just like any native Odoo view. Users can access it through the menu item, which renders partners in a card-based layout with a red header, field body, and clickable footer that navigates to the form view.
Reusable Architecture for Any View Type
Once the Controller, Model, Renderer, and ArchParser structures are understood, developers can leverage the same architecture to create a variety of different creative Odoo view kinds. This method opens the door for sophisticated interactions, dashboards, analytical layouts, and highly customized workflows beyond simple card displays. The OWL framework's modular design makes view types fully self-contained and portable across modules.
Frequently Asked Questions
What is a custom view type in Odoo?
A custom view type is a completely new way to display records in Odoo, different from standard views like list, form, or kanban. Using OWL components on the frontend and Python model extensions on the backend, developers have full control over how records are retrieved, organized, and displayed in the user interface.
How many JavaScript classes are needed to build a custom view in Odoo 19?
Four main JavaScript classes: ArchParser (reads and interprets the XML arch definition), Model (handles data fetching via orm.searchRead with KeepLast concurrency), Renderer (pure display component for visual rendering), and Controller (top-level coordinator managing interactions). A registration file ties them together in the view registry.
Do I need to change anything in Python to register a new view in Odoo 19?
Yes. You must extend ir.ui.view to add your view type to the type selection field, and ir.actions.act_window.view to add your view mode to the view_mode selection field. This tells Odoo to accept your custom view type in window actions.
What is the role of the arch parser in a custom Odoo view?
The arch parser reads the view's XML arch definition using visitXML and returns a structured archInfo object. It extracts which fields to display and any special attributes from the XML, passing this information to the model and renderer classes.
What is KeepLast in Odoo 19 OWL views?
KeepLast is a concurrency utility from @web/core/utils/concurrency that automatically cancels in-flight requests when a newer one is triggered. In custom views, it is used in the Model class to ensure that only the most recent data fetch is processed, preventing race conditions when users rapidly change filters or search terms.
Need Help with Odoo Development?
Our Odoo development experts can help you build custom view types, create OWL-based UI components, implement complex workflows, and optimize your Odoo 19 customizations for performance and usability.
About the author
Founder & Odoo Practice Lead, Braincuber Technologies
Founder of Braincuber. Has scoped and shipped 500+ Odoo implementations for US mid-market and global brands. Takes every founder call personally — no SDR layer between buyers and the people building the system.
