Odoo 15 is out, and big internal rework has been done to the WebClient. You can still write Views in the old MVC Architecture but now you also can write pure OWL Component Views.

The architecture is a bit different from the old MVC but with some helpers, and the good rewrite of the WebClient, it is actually not that hard, as you will see in this follow-along tutorial.

As an example, we will completely migrate the module we created in our JavaScript 101 Series, Creating an OWL View that was a Hierarchical Tree View, let's go!


This article is meant to be followed along, so please consider pulling the Odoo 14 version of our Module and coding along with each change. To pull directly the main branch:

git clone https://github.com/Coding-Dodo/owl_tutorial_views.git

Then cd into the folder and create a new branch that will be your basis of work:

cd owl_tutorial_views
git checkout -b 15.0-mig-owl_tutorial_views

Finally, try to run the module again often so you can see from your own eyes what breaks and how we fix it.

Updating our assets bundling to the manifest

The first action is to use the new assets key in the __manifest__ file. Let's take everything that was inside our assets.xml (and remove the call to that file) and put it inside the manifest file inside the web.assets_backend bundle.

--- a/__manifest__.py
+++ b/__manifest__.py
@@ -4,14 +4,24 @@
     "author": "Coding Dodo",
     "website": "https://codingdodo.com",
     "category": "Tools",
-    "version": "14.0.1",
+    "version": "15.0.1",
     "depends": ["base", "web", "mail", "product"],
     "qweb": [
     "data": [
-        "views/assets.xml",
+    "assets": {
+        "web.assets_backend": [
+            "/owl_tutorial_views/static/src/components/tree_item/tree_item.scss",
+            "/owl_tutorial_views/static/src/owl_tree_view/owl_tree_view.scss",
+            "/owl_tutorial_views/static/src/components/tree_item/TreeItem.js",
+            "/owl_tutorial_views/static/src/owl_tree_view/owl_tree_view.js",
+            "/owl_tutorial_views/static/src/owl_tree_view/owl_tree_model.js",
+            "/owl_tutorial_views/static/src/owl_tree_view/owl_tree_controller.js",
+            "/owl_tutorial_views/static/src/owl_tree_view/owl_tree_renderer.js",
+        ],
+    },

Now for the Qweb XML files

The qweb key in the manifest should also be emptied, instead, the QWeb templates will go inside the web.assets_qweb bundle:

--- a/__manifest__.py
+++ b/__manifest__.py
@@ -6,14 +6,14 @@
     "category": "Tools",
     "version": "15.0.1",
     "depends": ["base", "web", "mail", "product"],
-    "qweb": [
-        "static/src/components/tree_item/TreeItem.xml",
-        "static/src/xml/owl_tree_view.xml",
-    ],
     "data": [
     "assets": {
+        "web.assets_qweb": [
+            "/owl_tutorial_views/static/src/components/tree_item/TreeItem.xml",
+            "/owl_tutorial_views/static/src/xml/owl_tree_view.xml",
+        ],
         "web.assets_backend": [

We would be tempted to try and run the module right now but we would get an error:

Missing dependencies:     ["web.patchMixin"]

PatchMixin disappeared!

The patchMixin doesn't exist anymore in Odoo v15, it was a temporary solution in v14. So we have to remove import statements and usage reference from our OWLTreeRenderer and our TreeItem Components:

--- a/static/src/components/tree_item/TreeItem.js
+++ b/static/src/components/tree_item/TreeItem.js
@@ -3,7 +3,6 @@ odoo.define(
   function (require) {
     "use strict";
     const { Component } = owl;
-    const patchMixin = require("web.patchMixin");
     const { useState } = owl.hooks;

     class TreeItem extends Component {
@@ -67,6 +66,6 @@ odoo.define(
       template: "owl_tutorial_views.TreeItem",

-    return patchMixin(TreeItem);
+    return TreeItem;

Same for the OWLTreeRenderer:

--- a/static/src/owl_tree_view/owl_tree_renderer.js
+++ b/static/src/owl_tree_view/owl_tree_renderer.js
@@ -2,7 +2,6 @@ odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {
   "use strict";

   const AbstractRendererOwl = require("web.AbstractRendererOwl");
-  const patchMixin = require("web.patchMixin");
   const QWeb = require("web.QWeb");
   const session = require("web.session");

@@ -52,5 +51,5 @@ odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {
     template: "owl_tutorial_views.OWLTreeRenderer",

-  return patchMixin(OWLTreeRenderer);
+  return OWLTreeRenderer;

Migrating to the Native JavaScript Module system

Odoo 15 introduced a new way of defining our JavaScript module instead of the usual odoo.define, we can now use a syntax similar to ES6 modules and import.

According to the official documentation:

Most new Odoo javascript code should use the native javascript module system. This is simpler, and brings the benefits of a better developer experience with a better integration with IDE.

So let's migrate the syntax.

Basic conversion for the TreeItem Component

So, let's change our TreeItem Component accordingly.

--- a/static/src/components/tree_item/TreeItem.js
+++ b/static/src/components/tree_item/TreeItem.js
@@ -1,11 +1,8 @@
-  "owl_tutorial_views/static/src/components/tree_item/TreeItem.js",
-  function (require) {
-    "use strict";
+/** @odoo-module **/
 const { Component } = owl;
 const { useState } = owl.hooks;

-    class TreeItem extends Component {
+export class TreeItem extends Component {
    * @override
@@ -65,7 +62,3 @@ odoo.define(
   template: "owl_tutorial_views.TreeItem",
-    return TreeItem;
-  }

Now that our TreeItem is converted to odoo-module how do we import it? Let's update OwlTreeRenderer:

--- a/static/src/owl_tree_view/owl_tree_renderer.js
+++ b/static/src/owl_tree_view/owl_tree_renderer.js
@@ -4,6 +4,9 @@ odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {
   const AbstractRendererOwl = require("web.AbstractRendererOwl");
   const QWeb = require("web.QWeb");
   const session = require("web.session");
+  const {
+    TreeItem,
+  } = require("@owl_tutorial_views/components/tree_item/TreeItem");

   const { useState } = owl.hooks;

@@ -23,9 +26,7 @@ odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {

-  const components = {
-    TreeItem: require("owl_tutorial_views/static/src/components/tree_item/TreeItem.js"),
-  };
+  const components = { TreeItem };
   Object.assign(OWLTreeRenderer, {
     defaultProps: {

Notice the conversion to @owl_tutorial_views/components/tree_item/TreeItem

This is the rule:

  • @name_of_your_module
  • + relative path beginning inside static/src.
  • don't put the .js extension

So for a file inside owl_tutorial_views/static/src/components/tree_item/TreeItem.js, we get @owl_tutorial_views/components/tree_item/TreeItem

Aliased JavaScript Module conversion for the OWLTreeRender Component.

To reduce some of the friction caused by redefining the import/require that are already here in the code, as described before, you can alias your odoo-module so it keeps the same name as before.

Let's update our OWLTreeRenderer to the new odoo-module syntax but, this time, we use an alias corresponding to the old name of the module.

First, for the owl_tree_renderer.js file:

--- a/static/src/owl_tree_view/owl_tree_renderer.js
+++ b/static/src/owl_tree_view/owl_tree_renderer.js
@@ -1,16 +1,11 @@
-odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {
-  "use strict";
-  const AbstractRendererOwl = require("web.AbstractRendererOwl");
-  const QWeb = require("web.QWeb");
-  const session = require("web.session");
-  const {
-    TreeItem,
-  } = require("@owl_tutorial_views/components/tree_item/TreeItem");
+/** @odoo-module alias=owl_tutorial_views.OWLTreeRenderer default=0 **/
 const { useState } = owl.hooks;
+import AbstractRendererOwl from "web.AbstractRendererOwl";
+import QWeb from "web.QWeb";
+import session from "web.session";
+import { TreeItem } from "@owl_tutorial_views/components/tree_item/TreeItem";

-  class OWLTreeRenderer extends AbstractRendererOwl {
+export default class OWLTreeRenderer extends AbstractRendererOwl {
   constructor(parent, props) {
     this.qweb = new QWeb(this.env.isDebug(), { _s: session.origin });
@@ -51,6 +46,3 @@ odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {
   template: "owl_tutorial_views.OWLTreeRenderer",
-  return OWLTreeRenderer;
Notice that compared to our last example, here we use export default. The default will allow us to directly require the object and not have to destructure it like { ComponentToImport } = require(...). There is less friction like that.

As you can see we also transformed every require into import.

Now inside the main View file owl_tree_view.js we don't have to change anything, our old require will still work.

const OWLTreeRenderer = require("owl_tutorial_views.OWLTreeRenderer");

Refresh your page and your module should now work correctly.

That's it for the minimal migration path that we could take on our JavaScript Module. The module is functional and no features were lost in the process but it still uses the old MVC architecture...

Transitioning from old MVC View to full OWL

This is where things are going to change a lot in our code. Let's have an overview of what we will do, then we will go through each step:

  • Migrate our Model to the @web/views/helpers/model and use the useModel hook.
  • Use the Layout Component, so our View has access to the SearchPanel and ControlPanel.
  • Getting rid of the Controller, the View will now react to events and call Model functions.

Doing this will bring a lot of changes mainly in the Model and View files but it is important to note that all the other files should be very lightly impacted by this transition.

What I want to highlight here is that, if you wrote your OWL Component from Odoo 14 version, you should be able to migrate your code to v15 with minimal friction.

Migrating to the new Model class

Switch to odoo-module and new Model Class.

Let's migrate our OWLTreeModel Class to the new Model from @web/views/helpers/model and transform the file into an odoo-module file.

Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Coding Dodo - Odoo, Python & JavaScript Tutorials.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.