Enable Dark Mode!
how-to-create-a-new-view-type-in-odoo-17.jpg
By: Mohammed Irfan T

How to Create a New View Type in Odoo 17

Technical Odoo 17

In Odoo, a view is a structured representation of data that defines how records should be displayed and interacted with in the user interface, and it is defined using XML specifications within the Odoo framework.

There are multiple types of views, each representing different visualizations, such as forms, lists, kanbans, and more. This blog will discuss the process of incorporating a new view type in Odoo 17.

Let's begin by examining the procedure for creating a new view type. Here, we can check how to create a “Grid” View.

First, set up a foundational add-on framework. Then, create a new view selection in the ir.ui.view model, establishing the server-side declaration for the "Grid" view type.

class View(models.Model):
    """
        Extends the base 'ir.ui.view' model to include a new type of view
        called 'grid'.
    """
    _inherit = 'ir.ui.view'
    type = fields.Selection(selection_add=[('grid', "Grid")])

Similarly, by adding this view type to the ir.actions.act_window.view model, we can access and open this specific view.

class IrActionsActWindowView(models.Model):
    """
       Extends the base 'ir.actions.act_window.view' model to include
       a new view mode called 'grid'.
   """
    _inherit = 'ir.actions.act_window.view'
    view_mode = fields.Selection(selection_add=[('grid', "Grid")],
                                 ondelete={'grid': 'cascade'})

Besides generating a new view type, it is essential to declare additional JavaScript files.

1. Create a controller file.

The main responsibility of a controller is to coordinate interactions among different elements within a view, including the Renderer, Model, Layout components etc…

/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
import { Layout } from "@web/search/layout";
import { useModelWithSampleData } from "@web/model/model";
import { CogMenu } from "@web/search/cog_menu/cog_menu";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { ViewButton } from "@web/views/view_button/view_button";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { extractFieldsFromArchInfo } from "@web/model/relational_model/utils";
import { session } from "@web/session";
import { useBus, useService } from "@web/core/utils/hooks";
import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler";
import { useSetupView } from "@web/views/view_hook";
export class GridController extends Component {
    static components = {
        Layout,
        Dropdown,
        DropdownItem,
        ViewButton,
        CogMenu,
        SearchBar,
    };
    async setup() {
        this.viewService = useService("view");
        this.dataSearch = []
        this.ui = useService("ui");
        useBus(this.ui.bus, "resize", this.render);
        this.archInfo = this.props.archInfo;
        const fields = this.props.fields;
        this.model = useState(useModelWithSampleData(this.props.Model, this.modelParams));
        this.searchBarToggler = useSearchBarToggler();
        useSetupView({
            rootRef: this.rootRef,
            beforeLeave: async () => {
                return this.model.root.leaveEditMode();
            },
            beforeUnload: async (ev) => {
                const editedRecord = this.model.root.editedRecord;
                if (editedRecord) {
                    const isValid = await editedRecord.urgentSave();
                    if (!isValid) {
                        ev.preventDefault();
                        ev.returnValue = "Unsaved changes";
                    }
                }
            },
            getOrderBy: () => {
                return this.model.root.orderBy;
            },
        });
    }
    get modelParams() {
        const { defaultGroupBy, rawExpand } = this.archInfo;
        const { activeFields, fields } = extractFieldsFromArchInfo(
            this.archInfo,
            this.props.fields
        );
        const groupByInfo = {};
        for (const fieldName in this.archInfo.groupBy.fields) {
            const fieldNodes = this.archInfo.groupBy.fields[fieldName].fieldNodes;
            const fields = this.archInfo.groupBy.fields[fieldName].fields;
            groupByInfo[fieldName] = extractFieldsFromArchInfo({ fieldNodes }, fields);
        }
        const modelConfig = this.props.state?.modelState?.config || {
            resModel: this.props.resModel,
            fields,
            activeFields,
            openGroupsByDefault: true,
        };
        return {
            config: modelConfig,
            state: this.props.state?.modelState,
            groupByInfo,
            limit: null,
            countLimit: this.archInfo.countLimit,
            defaultOrderBy: this.archInfo.defaultOrder,
            defaultGroupBy: this.props.searchMenuTypes.includes("groupBy") ? defaultGroupBy : false,
            groupsLimit: this.archInfo.groupsLimit,
            multiEdit: this.archInfo.multiEdit,
            activeIdsLimit: session.active_ids_limit,
        };
    }
}
GridController.template = "grid_view.GridView";

Add the template for the controller.

<?xml version="1.0" encoding="UTF-8" ?>
<template xml:space="preserve">
    <t t-name="grid_view.GridView" owl="1">
        <!-- The root element of the view component -->
        <div t-ref="root" t-att-class="props.className">
            <!-- Layout component for organizing the view -->
            <Layout className="model.useSampleModel ? 'o_view_sample_data' : ''"
                    display="props.display">
                <!-- Slot for additional actions in the control panel -->
                <t t-set-slot="control-panel-additional-actions">
                    <CogMenu/>
                </t>
                <!-- Slot for layout buttons -->
                <t t-set-slot="layout-buttons"/>
                <!-- Slot for layout actions -->
                <t t-set-slot="layout-actions">
                    <!-- SearchBar component rendered if showSearchBar is true -->
                    <SearchBar t-if="searchBarToggler.state.showSearchBar"/>
                </t>
                <!-- Slot for additional navigation actions in the control panel -->
                <t t-set-slot="control-panel-navigation-additional">
                    <!-- Render the component specified by searchBarToggler.component -->
                    <t t-component="searchBarToggler.component"
                       t-props="searchBarToggler.props"/>
                </t>
                <!-- Render the main content using the Renderer component -->
                <t t-component="props.Renderer" t-props="renderProps" list="model.root"/>
            </Layout>
        </div>
    </t>
</template>

2. Create a Renderer file 

The primary role of a renderer is to produce a visual representation of data by rendering the view, which encompasses records.

/** @odoo-module **/
import { Component } from "@odoo/owl";
import { View } from "@web/views/view";
import { Field } from "@web/views/fields/field";
import { Record } from "@web/model/record";
import { ViewScaleSelector } from "@web/views/view_components/view_scale_selector";
export class GridRenderer extends Component {
    async setup() {
    }
}
GridRenderer.template = "grid_view.GridRenderer";
GridRenderer.components = {
    View,
    Field,
    Record,
    ViewScaleSelector,
};

Add the template for the renderer.

<templates xml:space="preserve">
    <t t-name="grid_view.GridRenderer" owl="1">
        <div>
            <h1>New Grid View</h1>
        </div>
    </t>
</templates>

3. Create an Arch Parser.

The arch parser's responsibility is to parse the arch view, enabling the view to access the provided information.

/** @odoo-module **/
import { visitXML } from "@web/core/utils/xml";
import { _lt } from "@web/core/l10n/translation";
import { Field } from "@web/views/fields/field";
import { addFieldDependencies,archParseBoolean, processButton } from "@web/views/utils"
import { Widget } from "@web/views/widgets/widget";
export class GroupListArchParser {
    parse(arch, models, modelName, jsClass) {
        const fieldNodes = {};
        const fieldNextIds = {};
        const buttons = [];
        let buttonId = 0;
        visitXML(arch, (node) => {
            if (node.tagName === "button") {
                buttons.push({
                    ...processButton(node),
                    id: buttonId++,
                });
                return false;
            } else if (node.tagName === "field") {
                const fieldInfo = Field.parseFieldNode(node, models, modelName, "list", jsClass);
                if (!(fieldInfo.name in fieldNextIds)) {
                    fieldNextIds[fieldInfo.name] = 0;
                }
                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
                fieldNodes[fieldId] = fieldInfo;
                node.setAttribute("field_id", fieldId);
                return false;
            }
        });
        return { fieldNodes, buttons };
    }
}
export class GridArchParser{
    /**
     * Check if a column is visible based on the column invisible modifier.
     * @param {boolean} columnInvisibleModifier - The column invisible modifier.
     * @returns {boolean} - True if the column is visible, false otherwise.
     */
    isColumnVisible(columnInvisibleModifier) {
        return columnInvisibleModifier !== true;
    }
    /**
     * Parse a field node and return the parsed field information.
     * @param {Node} node - The field node to parse.
     * @param {Object} models - The models information.
     * @param {string} modelName - The name of the model.
     * @returns {Object} - The parsed field information.
     */
    parseFieldNode(node, models, modelName) {
        return Field.parseFieldNode(node, models, modelName, "grid");
    }
    parseWidgetNode(node, models, modelName) {
        return Widget.parseWidgetNode(node);
    }
    processButton(node) {
        return processButton(node);
    }
    /**
     * Parse the grid view architecture.
     * @param {string} arch - The XML architecture to parse.
     * @param {Object} models - The models information.
     * @param {string} modelName - The name of the model.
     * @returns {Object} - The parsed grid view architecture.
     */
    parse(xmlDoc, models, modelName) {
        const fieldNodes = {};
        const widgetNodes = {};
        let widgetNextId = 0;
        const columns = [];
        const fields = models[modelName];
        let buttonId = 0;
        const groupBy = {
            buttons: {},
            fields: {},
        };
        let headerButtons = [];
        const creates = [];
        const groupListArchParser = new GroupListArchParser();
        let buttonGroup;
        let handleField = null;
        const treeAttr = {
            limit: 200,
        };
        let nextId = 0;
        const activeFields = {};
        const fieldNextIds = {};
        visitXML(xmlDoc, (node) => {
            if (node.tagName !== "button") {
                buttonGroup = undefined;
            }
            if (node.tagName === "button") {
                const button = {
                    ...this.processButton(node),
                    defaultRank: "btn-link",
                    type: "button",
                    id: buttonId++,
                };
                if (buttonGroup) {
                    buttonGroup.buttons.push(button);
                    buttonGroup.column_invisible = combineModifiers(buttonGroup.column_invisible, node.getAttribute('column_invisible'), "AND");
                } else {
                    buttonGroup = {
                        id: `column_${nextId++}`,
                        type: "button_group",
                        buttons: [button],
                        hasLabel: false,
                        column_invisible: node.getAttribute('column_invisible'),
                    };
                    columns.push(buttonGroup);
                }
            } else if (node.tagName === "field") {
                const fieldInfo = this.parseFieldNode(node, models, modelName);
                if (!(fieldInfo.name in fieldNextIds)) {
                    fieldNextIds[fieldInfo.name] = 0;
                }
                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
                fieldNodes[fieldId] = fieldInfo;
                node.setAttribute("field_id", fieldId);
                if (fieldInfo.isHandle) {
                    handleField = fieldInfo.name;
                }
                const label = fieldInfo.field.label;
                columns.push({
                    ...fieldInfo,
                    id: `column_${nextId++}`,
                    className: node.getAttribute("class"), // for oe_edit_only and oe_read_only
                    optional: node.getAttribute("optional") || false,
                    type: "field",
                    hasLabel: !(
                        archParseBoolean(fieldInfo.attrs.nolabel) || fieldInfo.field.noLabel
                    ),
                    label: (fieldInfo.widget && label && label.toString()) || fieldInfo.string,
                });
                return false;
            } else if (node.tagName === "widget") {
                const widgetInfo = this.parseWidgetNode(node);
                const widgetId = `widget_${++widgetNextId}`;
                widgetNodes[widgetId] = widgetInfo;
                node.setAttribute("widget_id", widgetId);
                addFieldDependencies(widgetInfo, activeFields, models[modelName]);
                const widgetProps = {
                    name: widgetInfo.name,
                    // FIXME: this is dumb, we encode it into a weird object so that the widget
                    // can decode it later...
                    node: encodeObjectForTemplate({ attrs: widgetInfo.attrs }).slice(1, -1),
                    className: node.getAttribute("class") || "",
                };
                columns.push({
                    ...widgetInfo,
                    props: widgetProps,
                    id: `column_${nextId++}`,
                    type: "widget",
                });
            } else if (node.tagName === "groupby" && node.getAttribute("name")) {
                const fieldName = node.getAttribute("name");
                const xmlSerializer = new XMLSerializer();
                const groupByArch = xmlSerializer.serializeToString(node);
                const coModelName = fields[fieldName].relation;
                const groupByArchInfo = groupListArchParser.parse(groupByArch, models, coModelName);
                groupBy.buttons[fieldName] = groupByArchInfo.buttons;
                groupBy.fields[fieldName] = {
                    activeFields: groupByArchInfo.activeFields,
                    fieldNodes: groupByArchInfo.fieldNodes,
                    fields: models[coModelName],
                };
                return false;
            } else if (node.tagName === "header") {
                // AAB: not sure we need to handle invisible="1" button as the usecase seems way
                // less relevant than for fields (so for buttons, relying on the modifiers logic
                // that applies later on could be enough, even if the value is always true)
                headerButtons = [...node.children]
                    .map((node) => ({
                        ...processButton(node),
                        type: "button",
                        id: buttonId++,
                    }))
                    .filter((button) => button.modifiers.invisible !== true);
                return false;
            } else if (node.tagName === "control") {
                for (const childNode of node.children) {
                    if (childNode.tagName === "button") {
                        creates.push({
                            type: "button",
                            ...processButton(childNode),
                        });
                    } else if (childNode.tagName === "create") {
                        creates.push({
                            type: "create",
                            context: childNode.getAttribute("context"),
                            string: childNode.getAttribute("string"),
                        });
                    }
                }
                return false;
            }
        });
        return {
            creates,
            handleField,
            headerButtons,
            fieldNodes,
            widgetNodes,
            activeFields,
            columns,
            groupBy,
            xmlDoc,
            ...treeAttr,
        };
    }
}

4. Develop the view

Develop the view by consolidating all components, then proceed to officially register it within the views registry.

/** @odoo-module **/
import { _lt } from "@web/core/l10n/translation";
import { RelationalModel } from "@web/model/relational_model/relational_model";
import { registry } from "@web/core/registry";
import {GridRenderer} from "./grid_renderer";
import {GridController} from "./grid_controller";
import {GridArchParser} from "./grid_arch_parser";
//import {GridRelationalModel} from "./grid_relational_model";
export const gridView = {
    type: "grid",
    display_name: _lt("Grid"),
    icon: "fa fa-th",
    multiRecord: true,
    Controller: GridController,
    Renderer: GridRenderer,
    ArchParser: GridArchParser,
    Model: RelationalModel,
    /**
     * Function that returns the props for the grid view.
     * @param {object} genericProps - Generic properties of the view.
     * @param {object} view - The view object.
     * @returns {object} Props for the grid view.
     */
    props: (genericProps, view) => {
        const {
            ArchParser,
            Model,
            Renderer
        } = view;
        const {
            arch,
            relatedModels,
            resModel
        } = genericProps;
        const archInfo = new ArchParser().parse(arch, relatedModels, resModel);
        return {
            ...genericProps,
            archInfo,
            Model: view.Model,
            Renderer,
        };
    }
};
// Register the grid view configuration
registry.category("views").add("grid", gridView);

Add all these js files and the template XML files inside an asset bundle in the manifest.

'assets': {
        'web.assets_backend': [
            'grid_view/static/src/js/*.js',
            'grid_view/static/src/views/*.xml',
        ],
    },

Finally create an XML file to add the new view type in the required model.

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="view_my_grid" model="ir.ui.view">
        <field name="name">My Grid View</field>
        <field name="model">your.model.name</field>
        <field name="arch" type="xml">
            <grid string="My Grid View">
                <!-- Add your columns here -->
            </grid>
        </field>
    </record>
    <record id="module_name.record_id" model="ir.actions.act_window">
        <field name="view_mode">grid,form,kanban,tree</field>
    </record>
    
    <record id="record_id_view_grid" model="ir.actions.act_window.view">
            <field name="sequence" eval="1"/>
            <field name="view_mode">grid</field>
            <field name="view_id" ref="custom_grid_module_name.view_my_grid"/>
            <field name="act_window_id" ref="module_name.record_id"/>
    </record>
</odoo>

After completing these steps, proceed to open your new view type. On installing your custom module. You can see the new view type and the content you provided inside the renderer file in the new view in your target model.

How to Create a New View Type in Odoo 17-cybrosys

In summary, the blog simplifies the steps for adding a new view type in Odoo 17, empowering developers to customize and improve the platform's interface with ease.


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