Enable Dark Mode!
how-to-create-a-new-view-type-in-odoo-19.jpg
By: Renu M

How to Create A New View Type in Odoo 19

Technical Odoo 19 Odoo Enterprises Odoo Community

List, form, kanban, calendar, and other view kinds are all pre-installed in Odoo. However, what if your business logic calls for something entirely different? Using OWL (Odoo Web Library) components on the front end and a Python model extension on the back end, you may register a completely new view type in Odoo 19.

When the basic list, kanban, or form views in Odoo are insufficient to show data in a business-friendly manner, custom views can be helpful. Developers have complete control over how records are retrieved, organised, and displayed in the user interface by creating a custom view type. Because of this versatility, businesses may design highly engaging, aesthetically pleasing experiences that are customized for their workflows.

We create a module entitled beautiful_view in this guide, which registers a new view type called "beautiful." The end product is a responsive, card-based record display that is compatible with any Odoo models.

Module File Structure

The complete beautiful_view module has the following layout:

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

Every Odoo view in OWL follows a strict separation of concerns across four JS classes plus the arch parser:

  • ir.actions.act_window> triggers the view
  • beautiful_view.js> view descriptor registered in the OWL views registry
  • BeautifulArchParser> reads the XML arch definition
  • BeautifulController> top-level OWL component, owns the model
  • BeautifulModel> data fetching via orm.searchRead
  • BeautifulRenderer> pure display component, renders cards

When the basic list, kanban, or form views in Odoo are insufficient to show data in a business-friendly manner, custom views can be helpful. Developers have complete control over how records are retrieved, organized, and displayed in the user interface by creating a custom view type. Because of this versatility, businesses may design highly engaging, aesthetically pleasing experiences that are customized for their workflows.

Step 1: __manifest__.py

The manifest declares the module metadata, its dependency on web and sale, and registers all frontend assets under web.assets_backend.

{
    '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 — Registering the View Type

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.

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: Arch Parser

The arch parser reads the view's XML definition (the <beautiful> tag and its child <field> nodes) and returns a structured archInfo object that the rest of the view stack consumes.

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: Model

The model handles all data fetching. It uses KeepLast, a concurrency utility that automatically cancels in-flight requests when a newer one is triggered and calls orm.searchRead to load records.

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']; // fallback
} this.records = await this.keepLast.add(
this.orm.searchRead(this.resModel, this.domain, fieldsToFetch)
);
}
}

Step 5: Renderer

The renderer is a pure OWL component responsible for displaying the records. It has no data-fetching logic; it simply receives records, archInfo, and resModel as props and renders each as a card. Clicking a card navigates to its form view.

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: Controller

The controller is the top-level OWL component for the view. It wraps everything inside Odoo's Layout component (which provides the control panel, breadcrumbs, etc.) and owns the model instance. It uses onWillStart to load data before the first render.

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();
        });
    }
}

The controller serves as the central coordinator of the custom view. It initializes services, prepares the model, manages lifecycle hooks, and connects the renderer with Odoo’s standard layout system.

Step 7: View Registration (beautiful_view.js)

This is the entry point that ties everything together. It defines the 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.

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,   // ? Must match the exported class name
    Model: BeautifulModel,
    Renderer: BeautifulRenderer,
    props(genericProps, view) {
        const { ArchParser } = view;
        const { arch } = genericProps;
        const archInfo = new ArchParser().parse(arch);  // ? Now ArchParser is a constructor
        return {
            ...genericProps,
            Model: view.Model,
            Renderer: view.Renderer,
            archInfo,
        };
    },
};
registry.category("views").add("beautiful", beautifulView);

Step 8: OWL Templates (XML)

The OWL templates define the HTML structure rendered by the controller and renderer. There are two templates: one for the controller (which wraps everything in Layout) and one for the renderer (which outputs the card grid).

<?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" aria-hidden="true"></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" aria-hidden="true"></i> Click to open
                    </div>
                </div>
            </t>
        </div>
    </t>
</templates> 

Step 9: CSS Styling

Styling plays a major role in improving the usability of custom views. A clean and responsive design helps users quickly identify important information while maintaining consistency with Odoo’s modern interface. The stylesheet turns the plain HTML into a clean, card-based grid with hover effects and a branded header per card.

.beautiful-grid {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
    padding: 20px;
    background-color: #f0f2f5;
}
.beautiful-card {
    width: 100%;
    max-width: 600px;          /* comfortable reading width */
    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;
    &:hover {
        transform: translateY(-2px);
        box-shadow: 0 8px 20px rgba(0,0,0,0.15);
    }
    .card-header {
        background: #E70846FF;
        color: white;
        padding: 12px 16px;
        font-size: 16px;
        font-weight: 600;
        display: flex;
        align-items: center;
        gap: 10px;
        i {
            font-size: 20px;
        }
    }
    .card-body {
        padding: 16px;
        flex: 1;
        .field-row {
            margin-bottom: 8px;
            font-size: 14px;
            display: flex;
            align-items: baseline;
            gap: 8px;
            .field-label {
                font-weight: 600;
                color: #555;
                min-width: 80px;
                font-size: 12px;
                text-transform: capitalize;
            }
            .field-value {
                color: #1f2d3d;
                word-break: break-word;
            }
        }
    }
    .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 is the data XML that creates the actual view record, the window action, and a menu item to open it. Here we apply the view to res.partner under the Sales menu.

<?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 the 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 Contacts (base.menu_contacts exists) -->
    <menuitem id="menu_beautiful_partner"
              name="Beautiful Partners"
              parent="sale.sale_menu_root"
              action="action_beautiful_partner"
              sequence="10"/>
</odoo>

Output

Once the module is installed and upgraded, the new custom view becomes available just like any native Odoo view. Users can access it directly through menu items or window actions configured with the custom view mode.

How to Create A New View Type in Odoo 19-cybrosys

The adaptability of Odoo's OWL framework and modular architecture is demonstrated by creating a custom view type in Odoo 19. Developers can create entirely new user experiences that are suited to business needs by fusing OWL frontend components with Python backend extensions.

This method opens the door for sophisticated interactions, dashboards, analytical layouts, and highly customized workflows in addition to improving visual display. Once the Controller, Model, Renderer, and ArchParser structures are known, developers can leverage the same architecture to create a variety of different creative Odoo view kinds.

To read more about How to Create a New View Type in Odoo 18, refer to our blog How to Create a New View Type in Odoo 18.


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. You can design it exactly as you need.

How many JavaScript classes are needed to build a custom view?

Four main classes: ArchParser, Model, Renderer, and Controller. Plus a registration file to tie them together.

What does each JavaScript class do in simple terms?

In a custom Odoo view, every JavaScript class has a distinct function. In order to help Odoo understand how the view should be organized, the ArchParser analyzes and interprets the view's XML definition. By retrieving and organizing records from the database, the model manages data-related tasks. The renderer is in charge of defining the view's visual look and putting the data on the screen. As the coordinator, the Controller oversees user actions, manages interactions between various components, and integrates the view with Odoo's overall framework and layout.

Do I need to change anything in Python to register a new view?

Yes. You must extend two models: ir.ui.view and ir.actions.act_window.view to add your view type name (like 'beautiful') to Odoo's selection fields.

What is the role of the arch parser?

It reads your view's XML code (like ) and extracts which fields to display and any special settings.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message