Odoo 17 introduces a modern approach to managing assets (JavaScript, CSS/SCSS, and templates) in the backend. Understanding how these assets are organized, loaded, and extended is crucial for developing custom modules. In this article, we’ll explore how backend assets are structured in Odoo 17, the role of the web.assets_backend bundle, and how modules contribute to it. We’ll walk through a step-by-step example of adding a custom JavaScript (with an OWL component), an SCSS style, and a QWeb template to the backend. We’ll also demonstrate how to define and inherit assets in both the manifest and XML, show how OWL components integrate into the asset system, and discuss asset caching with tips for development. The goal is to provide a clear, structured guide for intermediate Odoo developers to navigate Odoo 17’s asset system with confidence.
Odoo 17 Asset Loading: Types and Bundles
Asset Types: Odoo’s frontend architecture deals with three main asset types – code, style, and templates:- JavaScript Code: Odoo supports multiple kinds of JS files (including modern ES6 modules). All JS files in bundles are processed (e.g. ES modules are transformed into Odoo modules), then concatenated and minified (unless in debug mode) into a single file. The resulting bundle is stored as an attachment and loaded via a <script> tag in the page header.
- CSS/SCSS Styles: Stylesheets can be written in plain CSS or SCSS. SCSS files are automatically compiled to CSS, then minified (in normal mode) and concatenate. The merged CSS bundle is also saved as an attachment and included via a <link> tag in the page header.
- QWeb Templates (XML): Templates are handled slightly differently. Static XML files are not bundled into an attachment; instead, they are read from the filesystem on demand and concatenated in memory. When the web client starts, it fetches all template files through the /web/webclient/qweb/ endpoint, so that the client-side QWeb renderer (or OWL) can use them.
Asset Bundles: Rather than loading each file individually, Odoo groups assets into bundles. A bundle is essentially a list of asset file paths (JS, CSS/SCSS, or XML) that are loaded together. For example, Odoo’s base system defines bundles like web.assets_common, web.assets_backend, and web.assets_frontend in the module manifests. Each bundle serves a different purpose:
- web.assets_common: Common assets used by both the backend and frontend (includes lower-level libraries like jQuery, Bootstrap, and Odoo framework core files.
- web.assets_backend: Assets specific to the backend web client (the Odoo internal UI). This includes the web client code (action manager, view logic) and associated template.
- web.assets_frontend: Assets for the website/portal frontend (e-commerce, portal pages, etc.) – not loaded into the backend interface.
When the Odoo interface loads, the base web client view will call the appropriate bundles (e.g. <t t-call-assets="web.assets_common"/> and <t t-call-assets="web.assets_backend"/> for the backend UI). The Odoo framework then gathers all files listed in those bundle definitions across all installed modules, processes them, and serves the aggregated files. On first load, the browser will fetch these assets; thereafter, Odoo uses caching by appending a unique checksum to asset URLs so the browser can cache them long-term. This means assets only reload when their content changes (ensuring efficient performance).
Create the Module Structure
Begin by scaffolding the module’s directories and files. Odoo expects static assets to reside in a static folder of your module. A common organization is to place source files under static/src, grouped by type (e.g. js, scss, xml subfolders). For our module (let’s call it my_module), the structure will look like this:
my_module/
├── __init__.py
├── __manifest__.py
├── static/
│ └── src/
│ ├── js/
│ │ └── hello_widget.js
│ ├── scss/
│ │ └── hello_widget.scss
│ └── xml/
│ └── hello_widget.xml
└── views/
└── hello_widget_views.xml
- static/src/js/hello_widget.js: Our custom JavaScript, which will define an OWL component (the “Hello Widget”).
- static/src/scss/hello_widget.scss: Some custom styles for the widget.
- static/src/xml/hello_widget.xml: A QWeb template that the OWL component will use for its UI.
- views/hello_widget_views.xml: An Odoo view file for integrating the widget into the interface (we’ll use it to add a client action and menu item for demonstration).
Define Assets in the Module Manifest
Create __manifest__.py and add an assets section if it doesn’t exist. We will list our asset files under the web.assets_backend bundle key:
{
'name': 'My Module',
'version': '17.0.1.0.0',
'depends': ['base', 'web'],
'assets': {
'web.assets_backend': [
'my_module/static/src/js/hello_widget.js',
'my_module/static/src/scss/hello_widget.scss',
'my_module/static/src/xml/hello_widget.xml',
],
},
'data': [
'views/hello_widget_views.xml', # include the view that adds menu/action
],
}
- We included 'web' in the depends list. This is critical for Odoo 17, because our JS uses the OWL framework provided by the web module (as mentioned, OWL components are defined in web).
- Under assets['web.assets_backend'], we listed the paths to our JS, SCSS, and XML files. This tells Odoo to append these files into the main backend bundle. In Odoo 17, you should include QWeb templates here (the old separate qweb manifest key is no longer needed or used).
- We also added an entry in the manifest’s data for our view file. This ensures Odoo installs our hello_widget_views.xml (which contains menu and action definitions) along with the module.
Add a Custom JavaScript (OWL Component)
Now we’ll implement the JavaScript logic for our widget in static/src/js/hello_widget.js. Odoo 17 has fully embraced the OWL framework and modern JavaScript syntax, so we will write an OWL component class and register it. Here’s a simple example:
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
export class HelloWidget extends Component {
static template = "my_module.HelloWidgetTemplate";
}
// Register the component in Odoo's action registry
// so it can be used as a client action
registry.category("actions").add("my_module.HelloWidgetAction", HelloWidget);
- The file begins with /** @odoo-module **/. This comment is essential in Odoo 17. It tells Odoo’s bundler to treat the file as an ES module. If you omit it, you’ll encounter errors like “Cannot use import statement outside a module". Always include this at the top of your JS files in Odoo 17.
- We use modern ES6 import statements to bring in the OWL Component class and Odoo’s registry. In Odoo 17, you no longer use odoo.define() with AMD-style requires; instead, you write standard JS modules and imports. (In fact, “JS is completely changed to OWL in Odoo 17,” so older style code must be updated to this new syntax.)
- We define a class HelloWidget extending OWL’s Component. This is our UI component. We assign a static template property referencing the QWeb template we will create (my_module.HelloWidgetTemplate).
- Finally, we register our component in the action registry with a unique name "my_module.HelloWidgetAction". By adding it to registry.category("actions"), we make it possible to use this component as a client action in Odoo (essentially a custom interface screen we can trigger).
Add a Custom SCSS Style
Next, let’s create static/src/scss/hello_widget.scss to style our widget. This file can contain any SCSS/CSS rules. For demonstration, we’ll add a style for a CSS class that our widget will use:
.hello-widget {
color: blue;
font-weight: bold;
}
Create a QWeb Template
Finally, we define the UI structure in an XML template. Create static/src/xml/hello_widget.xml with the following content:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.HelloWidgetTemplate">
<div class="hello-widget">
Hello from OWL Component!
</div>
</t>
</templates>
This template uses Odoo’s QWeb syntax. We give it a unique name "my_module.HelloWidgetTemplate", which matches the static template property we set in our JS class. The content is a simple <div> with some text. We also assign it the class hello-widget to leverage the styling from our SCSS.
Using the Component (Client Action and Menu)
To see our Hello Widget in action, we need to instruct Odoo to open it. We’ll do this by creating a client action and a menu item for it in views/hello_widget_views.xml:
<odoo>
<!-- Define a client action that uses our OWL component -->
<record id="action_hello_widget" model="ir.actions.client">
<field name="name">Hello Widget</field>
<field name="tag">my_module.HelloWidgetAction</field>
<field name="target">main</field>
</record>
<!-- Add a menu item to trigger the above action -->
<menuitem id="menu_hello_widget"
name="Hello Widget"
parent="base.menu_custom"
action="action_hello_widget"
sequence="100"/>
</odoo>
- We create an ir.actions.client record with tag="my_module.HelloWidgetAction", the same identifier we used when registering our component in JavaScript. This tells Odoo that when this action is activated.
- We add a menuitem (under an existing menu, here base.menu_custom which is a generic "Custom" menu in base) that links to the action. This gives us a clickable entry in the UI to launch our component.
Now, if you update the module and click the Hello Widget menu in the Odoo backend, the client action will load our OWL component. You should see a new screen (a blank canvas in the web client) showing the message “Hello from OWL Component!” in bold blue text. At this point, our assets are successfully integrated: the JS ran (registering the component), the template was rendered, and the CSS styling was applied.
