Skip to content
•17 min read

Odoo 16 JavaScript Tutorial: OWL Field Widgets reference and full example.

Philippe L'ATTENTION
Odoo 16 JavaScript Tutorial - OWL Field Widgets Reference and full Example.

With version 16, Odoo continued its migration to its Component Oriented frontend framework OWL (version 2). Thanks to a solid legacy Framework adapter you could still write your fields widget the old way, but now is a good time to migrate your code. Now even the field widgets are written in OWL and this is what we will discuss here.

Let's go step by step and create a basic field widget.

A field Widget is now a pure OWL Component

We will make the most basic widget. A presentation widget with no interaction for now.

This widget will simply display the content of a field (preferably Text) inside a <pre> tag, to make it look like it's a Code block:

Preview of our "CodeField" widget
Preview of our "CodeField" widget

The JavaScript Field Component

This widget can be written with a very small amount of JavaScript, let's create a file called "static/src/js/code_field.js" with this:

/** @odoo-module **/
const {xml, Component} = owl;
import { standardFieldProps } from "@web/views/fields/standard_field_props";

export class CodeField extends Component {
    setup() {
        // This setup is useless here because we don't do anything
        // But this is where you will use Hooks
        super.setup();
    }
}

CodeField.template = xml`<pre t-esc="props.value" class="bg-primary text-white p-3 rounded"/>`;
CodeField.props = standardFieldProps;

This looks like a standard OWL Component, except that it has standardFieldProps as props. We will look more into it later in that course.

Add the field to the "fields" registry

What is shown above is just a Component, now we need to add that Component to the "fields" registry. For that we have to import the registry and add our field to it:

/** @odoo-module **/
const {xml, Component} = owl;
import { standardFieldProps } from "@web/views/fields/standard_field_props";
// Import the registry
import {registry} from "@web/core/registry";


export class CodeField extends Component {
    setup() {
        super.setup();
    }
}

CodeField.template = xml`<pre t-esc="props.value" class="bg-primary text-white p-3 rounded"/>`;
CodeField.props = standardFieldProps;

// Add the field to the correct category
registry.category("fields").add("code", CodeField);

The name you choose here "code" is very important because this is what you will use on the field : <field ... widget="code"> .

Adding the JS to the manifest.

Now in our __manifest__.py we add that JavaScript file:

{
    # ...
    "depends": ["base", "web"],
    "data": [],
    "qweb": [],
    "assets": {
        "web.assets_backend": [
            "/my_module/static/src/js/code_field.js",
        ]
    },
	# ...
}

And this is enough! You can use it directly on your field inside a view like so:

<field name="description" widget="code" />

How did we get the value?

You may have noticed on the inlined template that we use props.value:

<pre t-esc="props.value" class="bg-primary text-white p-3 rounded"/>

With t-esc we display props.value which is the content of the Field, in the database/from python, for that record, that we are visualizing.

That means that the parent Component of our Field widget gave us data, or props, at one point inside the Component Tree. The parent in question differs from contexts, but is usually the View Component, think FormView, TreeView, etc.

Schema representing data flow coming from the View, into the Field Component.

Our widget will then use these props and do whatever is needed to render the data adequately, allow editing of that value, save that value, etc. So what are these props we are dealing with?

A field widget gets the standardFieldProps "props".

Value is not the only prop given to your custom Field widget by default, there is an interface of props that are always passed to any field widget, they are called standardFieldProps.

These standards props can be seen in the file odoo/addons/web/static/src/views/fields/standard_field_props.js

export const standardFieldProps = {
    id: { type: String, optional: true },
    name: { type: String, optional: true },
    readonly: { type: Boolean, optional: true },
    record: { type: Object, optional: true },
    type: { type: String, optional: true },
    update: { type: Function, optional: true },
    value: true,
    decorations: { type: Object, optional: true },
    setDirty: { type: Function, optional: true },
};

Understanding all these props and what they do will make it easier for you to create new awesome fields, so let's list them.

id, name

The props id, and name will often be the same and corresponds to the name of the Field on your Odoo model.

readonly

This very useful prop is a boolean coming in true or false with the result of all the combinations of ACL (access control layer, or security), actual readonly attribute, or other server-side computation that makes a field read-only or editable. This prop will be used almost every time you create a new interactive widget because it will help you switch between edit and read-only mode.

Odoo 16 comes with an "always on" edit mode, but this prop should still be used to conditionally render different templates. A basic example of a template using these props:

<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">

    <t t-name="my_module.CodeField" owl="1">
        <t t-if="props.readonly">
            <pre t-esc="props.value" class="bg-primary text-white p-3 rounded"/>
        </t>
        <t t-else="">
            <textarea class="o_codeview" t-att-value="props.value"/>
        </t>
    </t>
</templates>

Note: If you have a hard time testing the readonly mode of your Component, remember that you could just add readonly="1" on the <field> in your Form/Tree View.

record

The props.record is a Record object defined in odoo/addons/web/static/src/views/basic_relational_model.js it contains a lot of info related to the current record displayed in the view. The notable properties of that object are:

  • data contains all the actual data of the given record.
  • mode of the record view, "edit" or "read-only".
  • fields a key, value store of all the other fields.
  • activeFields the fields used in the view.
  • resModel for example "product.template".
  • model the actual JavaScript class RelationalModel with all its methods.
  • context contains the Odoo usual "context" object with the user id, timezone, etc.

type

The type of field derived from the Field definition in python, "text", "binary", etc.

value

Self-explanatory, the value of that field coming from the server, and corresponding to that current record. Probably the most important of the props, given that a widget should display the value!

decorations

This prop contains a {key: value} store of "decoration" and their respective boolean evaluated value. Let's take our CodeField example, which is put on the product.template Form view, we can add these attributes to our widget:

<field 
    name="description" 
    widget="code" 
    decoration-warning="type=='product'"
    decoration-success="type=='service'"
/>

If our product.template is of type product and we check the content of this.props.decorations this is what we will get:

// inside a method of our class CodeField
// myMethod() {
//   console.log(this.props.decorations)
// }

// output
{
    "warning": true,
    "success": false,
}

You can use these props.decorations in your templates and conditionally add CSS classes to your Component and have different styles.

This is a great way to give customization possibilities from the XML View declaration to the user of your widget.

setDirty

This optional prop is a function accessible from your Field Component (or your Widget, as you prefer to call it) that takes a boolean as a parameter. Its job is to signal that the value of a field has been touched or has changed. It is not always used in every View (meaning that it can be undefined), and if you create your own view you will have to implement it yourself if you need to.

But when it is present, it becomes an important function that will help the parent View know that it should save the value contained in that field for that record!

// Inside your Widget Component, in a method that should update the value

async updateValue() {
    const value = this.getMyDerivedValue();
    const lastValue = (this.props.value || "").toString();
    if (value !== null && !(!lastValue && value === "") && value !== lastValue) {
        if (this.props.setDirty) {
            this.props.setDirty(true);
        }
        // continue updating
    }
}

update

This function is the most important one if your Widget should edit values.

Remember that the actual value of your field is a prop and as we know, props should never be modified directly, they are given by the parent Component in the tree and are used as is.

The Fields usually don't hold their own state so the way to notify that the value change, is to use the asynchronous props.update function that is given to our Field, for example

// Example async function that lives inside your Component
async updateValue() {
    const value = this.currentValue;
    const lastValue = (this.props.value || "").toString();
    if (value !== null && !(!lastValue && value === "") && value !== lastValue) {
        // calling the update function with await
        await this.props.update(value);
    }
}

This function will do the heavy lifting of really updating the data, then the value will come back inside props on the next willUpdateProps.

Soon we will see a complete example of where we will use this props.update function.

Field custom props and attributes parsing.

On top of the Field standard props, you can add your custom props like any other OWL Components.

Let's say we want to give the possibility to change the background color of our CodeField widget, for that we will change the props of our Component to add a backgroundColor prop:

export class CodeField extends Component {}

// Update the template to have t-attf-class compute color
CodeField.template = xml`<pre t-esc="props.value" t-attf-class="bg-#{props.backgroundColor} text-white p-3 rounded"/>`;

// defaultProps in case the user doesn't set a backgroundColor prop
CodeField.defaultProps = {
    backgroundColor: "primary",
};
// We spread standardFieldProps and add our own props
CodeField.props = {
    ...standardFieldProps,
    backgroundColor: {type: String, optional: true},
};
registry.category("fields").add("code", CodeField);

Extracting props from attributes

At that point, our widget works precisely the same way as before, but we would like the user to be able to give background_color attribute to the field like that:

<field 
	name="description"
    widget="code"
    background_color="black"
/>

How to get background_color ? For that Odoo gives us the extractProps static function that you define on the Component:

export class CodeField extends Component {}
CodeField.template = xml`<pre t-esc="props.value" t-attf-class="bg-#{props.backgroundColor} text-white p-3 rounded"/>`;
CodeField.defaultProps = {
    backgroundColor: "primary",
};
CodeField.props = {
    ...standardFieldProps,
    backgroundColor: {type: String, optional: true},
};
// Extract backgroundColor from the attributes
CodeField.extractProps = ({attrs, field}) => {
    return {
        // We are looking for attr "background_color", snake case
        backgroundColor: attrs.background_color,
    };
};
registry.category("fields").add("code", CodeField);

With that done the user of our Field can now customize the backgroundColor:

The widget now shows the black background

Okay, that's it for the basic overview of the Field widget, let's now create a more realistic example. With edit mode this time!


A full example, widget Markdown Editor Field

💡

The following example will make you use:

  • Lifecycle hooks onWillUpdateProps, onMounted, onWillStart
  • Bus event for emergency save with useBus
  • Async external libs loading.
  • Committing changes.
  • Extracting props from XML attrs.

In this example, we will migrate the Markdown Field widget we made in another series. If you want the starting code in Odoo 15 this is the repository: https://github.com/Coding-Dodo/web_widget_markdown/tree/15.0

And the series about creating that widget:

Introduction and goals

Let's take a look at the final result.

Preview of the Markdown Editor in an Odoo Form View

We will not go over the explanation of the library used "SimpleMDE" (Simple Markdown Editor) again, please check the articles linked just before. But to give a quick overview of the objectives:

  • This widget has 2 modes, in read-only, it will show the HTML version of the markdown. In Edit mode it will show the markdown editor.
  • The SimpleMDE is an Editor which will be embedded inside the Odoo Form view which itself is also an Editor!
  • Changes of value inside the markdown Editor should be pushed to the actual OWL Component on "blur" (out of focus) and different events.
  • Should play correctly with the auto-save feature of Odoo 16 and prevent loss of data.

Creating the MarkdownField template

Let's make the basis of our widget, first, let's create the template for our widget with a file named "/static/src/js/markdown_field.xml"

<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">

    <t t-name="web_widget_markdown.MarkdownField" owl="1">
        <t t-if="props.readonly">
            <div class="o_readonly" t-out="markupValue" />
        </t>
        <t t-else="">
            <textarea t-ref="textarea" class="o_markdownview" t-att-value="props.value" rows="15"/>
        </t>
    </t>

</templates>

Our template is very basic, switching between read-only and edit mode via the props.readonly value.

Read-only Mode

In "read-only" we render a simple <div> where we display the value with the t-out directive.

The t-out directive is new in OWL 2 and replaces the old t-raw , it lets us output raw HTML when combined with the markup JavaScript utilities from owl. OWL will not render HTML by default with t-esc and in our case, it is necessary to output the conversion from Markdown to HTML.

Edit Mode

For the "edit mode" we use a simple <textarea> for now. This "input" is bound with the props.value with the t-att-value directive.

Notice that we use a t-ref on the <textarea> to have a reference to that DOM Element that we will be able to manipulate later when we are going to inject the Markdown Editor, and need to attach it to a DOM Element.

The first version of the MarkdownField Component

Let's now tackle the JavaScript part, create a file named "/static/src/js/markdown_field.js".

/** @odoo-module **/
const {useRef, markup, Component} = owl;
import {registry} from "@web/core/registry";
import {standardFieldProps} from "@web/views/fields/standard_field_props";

export class MarkdownField extends Component {
    setup() {
        super.setup();
        this.textareaRef = useRef("textarea");
    }

    get markupValue() {
        // do the transformation into html here, il later part
        return this.props.value;
    }
}

MarkdownField.template = "web_widget_markdown.MarkdownField";
MarkdownField.props = standardFieldProps;

registry.category("fields").add("markdown", MarkdownField);

With this basic field done, apply it to a field on a Model to visualize it, in my case I created a fields.Text on "product.template" and added it to the form view like so:

<field name="description" widget="markdown"/>

With this, you can already go to the form view and see your widget.

For now, the widget is working and showing the editable content when not in "read-only" mode but nothing is saved! That's what we will tackle now

Saving the content of the <textarea> on Blur (out of focus).

We decide that we will save the content, the user typed in the <textarea> when it goes out of focus. So when the user clicks away, we will get the content and use the props.update function passed to our component.

Getting value from the <textarea> and checking if it changed

We will add 2 functions:

  • _isDirty will check if we are in "edit mode" and if the content of the <textarea> is different from the current props.value
  • getEditorValue will get the value from the <textarea>. The name of the function implies that we have to get the value from the editor, but for now it will be the value directly from the textarea.

Add these 2 functions to your Component.

/**
 * Returns the value from the editor, for now the editor is just a textarea
 * @returns {string}
 */
getEditorValue() {
    return this.textareaRef.el.value;
}

/**
 * Checks if the current value is different from the last saved value.
 * If the field is dirty it needs to be saved.
 * @returns {boolean}
 */
_isDirty() {
    return !this.props.readonly && this.props.value !== this.getEditorValue();
}

With that, we are ready to update the value in the database.

Committing the changes on blur Event.

To "commit" the changes we will use the async function given in the props as props.update.

We will also use the props.setDirty to mark that field as "dirty", so if the parent defines that function, we will set it to true to give information to the view that these modifications on that field should be saved.

Our main function will be called commitChanges, it will use our previous _isDirty() function to check if changes should be committed, and if yes, then call another function we will create called updateValue.

async commitChanges({urgent = false} = {}) {
    if (this._isDirty() || urgent) {
        await this.updateValue();
    }
}

async updateValue() {
    const value = this.getEditorValue();
    const lastValue = (this.props.value || "").toString();
    if (value !== null && !(!lastValue && value === "") && value !== lastValue) {
        if (this.props.setDirty) {
            this.props.setDirty(true);
        }
        await this.props.update(value);
    }
}

Adding the t-on-blur event listener

Now let's wire the commitChanges function to the "blur" event on the textarea, in the XML Template, add this t-on-blur:

<textarea t-ref="textarea" class="o_markdownview" t-att-value="props.value" rows="15" t-on-blur="commitChanges"/>

Try the widget on the form view.

You should now be able to make edits in your <textarea> and when you focus out of the field, the changes will be saved. Refresh your page or change the record or view, and you should see that the changes were saved in the database.

We will now add the SimpleMDE Markdown Editor instead of our basic textarea.

💡

If you stop here and are not interested in the SimpleMDE Editor integration, I would still advise you to continue to read since there is techniques that could be used on other widgets. Like delayed JS libs loading, or Bus events.

Loading and adding the SimpleMDE Editor

In the Odoo 15 version of the markdown widget, we loaded the SimpleMDE library when the widget was starting via the jsLibs property on the class:

var markdownField = basicFields.DebouncedField.extend(basicFields.TranslatableFieldMixin, {
    supportedFieldTypes: ['text'],
    template: 'FieldMarkdown',
    jsLibs: ['/web_widget_markdown/static/lib/simplemde.min.js'],
    //...
    

Load the SimpleMDE library asynchronously on start.

To have the same effect of loading on "start" we will the loadJS function and the onWillStart hook.

Import these 2 functions from their respective packages and add the onWillStart hook in the setup like that:

/** @odoo-module **/
const {useRef, onWillStart, markup, Component} = owl;
import {registry} from "@web/core/registry";
import {standardFieldProps} from "@web/views/fields/standard_field_props";
import {loadJS} from "@web/core/assets";

export class MarkdownField extends Component {
    setup() {
        super.setup();
        this.textareaRef = useRef("textarea");
        onWillStart(async () => {
            await loadJS("/web_widget_markdown/static/lib/simplemde.min.js");
        });

onWillStart is an asynchronous hook that will be called before the first rendering. It's the ideal lifecycle point for us to load the library.

Creating a reusable function to start the SimpleMDE Editor

We will create a function that takes care of calling new SimpleMDE(), it will be a private function inside our MarkdownField Component

/**
 * @param {string} [initValue]
 */
_startSimpleMDE(initValue) {
    var simplemdeConfig = {
        element: this.textareaRef.el,
        initialValue: initValue ?? this.props.value,
        uniqueId: `markdown-${this.props.id}`,
    };
    if (this.props.editorOptions) {
		simplemdeConfig = {...simplemdeConfig, ...this.props.editorOptions};
    }
    this.simplemde = new SimpleMDE(simplemdeConfig);
}

Fex things to notice here:

  • The element given to the SimpleMDE config object is the this.textareaRef.el that we got from the useRef hook in our setup.
  • We accept an optional initialValue, else we use the props.value as initial value.
  • We use a props.editorOptions that we will create and parse in a later section of that tutorial. This is used to have the same functionalities as the widget version in Odoo 15. Users could add the SimpleMDE options directly from the definition in the XML and we want to keep that feature.

Now that we have our "start editor" function, where to call that?

When to start the SimpleMDE Editor?

First, we will start the SimpleMDE Editor when the component is mounted. At that point the DOM is ready and the textareaRef is defined. To do that let's import the onMounted hook and use it in our setup to start the editor, if in "edit mode":

/** @odoo-module **/
// Import the onMounted hook
const {useRef, xml, onMounted, onWillStart, markup, Component} = owl;
// ... rest of the imports

export class MarkdownField extends Component {
    setup() {
        super.setup();
        this.textareaRef = useRef("textarea");
        onWillStart(async () => {
            await loadJS("/web_widget_markdown/static/lib/simplemde.min.js");
        });
        // when mounted we start the Editor
        onMounted(() => {
            if (!this.props.readonly) {
                this._startSimpleMDE();
            }
        });
        // ... rest of the class

Second, we will need to start the Editor if the user switched from "read-only" to "edit mode". To do that, we will check if new props were given to our component, with the onWillUpdateProps hook:

/** @odoo-module **/
// add the onWillUpdateProps import
const {useRef, xml, onMounted, onWillStart, markup, Component, onWillUpdateProps} = owl;
// ... rest of the imports

export class MarkdownField extends Component {
    setup() {
        super.setup();
        this.textareaRef = useRef("textarea");
        onWillStart(async () => {
            await loadJS("/web_widget_markdown/static/lib/simplemde.min.js");
        });
        onMounted(() => {
            if (!this.props.readonly) {
                this._startSimpleMDE();
            }
        });
		
        // If the Component switched to "edit mode" we start the Editor
        onWillUpdateProps((newProps) => {
            if (newProps.readonly != this.props.readonly && !newProps.readonly) {
                this._startSimpleMDE(newProps.value);
            }
        });
    }

With that, refresh your browser and you should see the SimpleMDE editor!

Finally we can have our SimpleMDE Editor on the FormView

Saving data on Blur

Now onto saving data! First, we will need to tweak our getEditorValue function to get the actual content of SimpleMDE Editor, and we will also change the get markupValue to use the underlying markdown function of SimpleMDE to convert markdown to HTML.

get markupValue() {
    // use the underlying function of SimpleMDE Prototype 
    return markup(SimpleMDE.prototype.markdown(this.props.value));
}

getEditorValue() {
    // If simplemde is present get its value with built-in function.
    if (this.simplemde) {
        return this.simplemde.value();
    } else {
        return null;
    }
}

Now that we have that, we can add an Event Listener to SimpleMDE and bind that to commitChanges.

Change the _startSimpleMDE function to add that event

/**
 * @param {string} [initValue]
 */
_startSimpleMDE(initValue) {
    var simplemdeConfig = {
        element: this.textareaRef.el,
        initialValue: initValue ?? this.props.value,
        uniqueId: `markdown-${this.props.id}`,
    };
    if (this.props.editorOptions) {
        simplemdeConfig = {...simplemdeConfig, ...this.props.editorOptions};
    }
    this.simplemde = new SimpleMDE(simplemdeConfig);
    // commitChanges on Blur event
    this.simplemde.codemirror.on("blur", this.commitChanges.bind(this));
}

You can now remove the t-on-blur inside the XML Template

<!-- t-on-blur removed -->
<textarea t-ref="textarea" class="o_codeview" t-att-value="props.value"/>

And that's it, test again your widget. Data should be saved when you focus out of the Field!

Urgent save on record changed, view changed, and other events.

Now, what happens if we change the record, or change the tab (in the form view) while we are still inside the Editor? The simple answer is; the user will lose his modifications.

To counter that, Odoo gives us 2 Bus Events that our Component can listen to and act accordingly to save the data.

To take advantage of the bus, we will use the conveniently named useBus hook with the events RELATIONAL_MODEL:WILL_SAVE_URGENTLY and RELATIONAL_MODEL:NEED_LOCAL_CHANGES.

/** @odoo-module **/
// ... other imports
// importing the useBus hook
import {useBus} from "@web/core/utils/hooks";

export class MarkdownField extends Component {
    setup() {
        super.setup();
		// ...
        useBus(this.env.bus, "RELATIONAL_MODEL:WILL_SAVE_URGENTLY", () =>
            this.commitChanges({urgent: true})
        );
        useBus(this.env.bus, "RELATIONAL_MODEL:NEED_LOCAL_CHANGES", ({detail}) =>
            detail.proms.push(this.commitChanges())
        );
        // ...
    }
    // ... rest of the class

That's all we need to make changing tabs, or records, safer as the changes will be saved to the database.

Optional save data "on change"

Note: I think it is unnecessary to add to our current widget but I will show you a way to tackle that for other use cases.

If we would like to save the content of the Editor on "change", we would need to debounce the event so it will not spam the database.

To help us with that, we need to import the debounce function:

import {debounce} from "@web/core/utils/timing";

The debounce function takes 2 arguments:

  • The first argument is a function, the callback
  • The second argument is the time delay in ms.

And then, when we start the SimpleMDE Editor we bind the "change" event on the "codemirror".

this.simplemde.codemirror.on("change", debounce(this.commitChanges.bind(this), 1000)

Debouncing the function like that let the user type and then commit data every 1-second break between changes.

Handling SimpleMDE options, "placeholder" with extractProps.

In this section, we will add some customization options for the Developer/User that will be read directly from attributes on the XML Field.

To do that we have the extractProps, let's update the static props properties of our Component accordingly, at the end of the file:

MarkdownField.template = "web_widget_markdown.MarkdownField";
MarkdownField.props = {
    ...standardFieldProps,
    isTranslatable: {type: Boolean, optional: true},
    placeholder: {type: String, optional: true},
    fieldName: {type: String, optional: true},
    editorOptions: {type: Object, optional: true},
};
MarkdownField.extractProps = ({attrs, field}) => {
    return {
        isTranslatable: field.translate,
        fieldName: field.name,
        placeholder: attrs.placeholder,

        editorOptions: {
            placeholder: attrs.placeholder,
        },
    };
};

Here we also extract data from field, notably the translate property that we convert into a isTranslatable props.

Handling the translation PopUp

Since we have access to props.isTranslatable we can display a translation Button.

We begin by importing the Translate button Component and adding it to our widget.

// ... other import
import {TranslationButton} from "@web/views/fields/translation_button";
// ...
// ... end of file
MarkdownField.components = {
    TranslationButton,
};
registry.category("fields").add("markdown", MarkdownField);

Finally, add that button inside the MarkdownField XML Template

<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">

    <t t-name="web_widget_markdown.MarkdownField" owl="1">
        <t t-if="props.readonly || props.notEditable">
            <div t-ref="readonlyElement" class="o_readonly" t-out="markupValue" />
        </t>
        <t t-else="">
            <textarea t-ref="textarea" class="o_codeview" t-att-value="props.value"/>
            <t t-if="props.isTranslatable">
                <span style="font-size: 15px; position: absolute; left: 22px; bottom: 5px;">
                    <TranslationButton
                        fieldName="props.name"
                        record="props.record"
                        updateField="props.update"
                    />
                </span>
            </t>
        </t>
    </t>

</templates>

Conclusion

This is the end of that tutorial about creating Fields Widget in Odoo 16, OWL Edition! I personally think this way is much simpler and clearer, and I am glad Odoo is switching all its front end to OWL 2.

The finished web_widget_markdown module in Odoo 16.0 is available here https://github.com/Coding-Dodo/web/tree/16.0/web_widget_markdown.

Thank you for supporting that blog it means a lot to me! And keep an eye out for more up-and-coming articles about OWL 2 and Odoo 16.