Articles > Odoo + Jinja Templating

Odoo + Jinja Templating

A simpler alternative for displaying static information

Written by
Holden Rehg
Posted on
March 16, 2021

Odoo has a built in templating engine called qweb which is used for report templates and the frontend javascript framework. But as many Odoo developers know, there are different view types built into Odoo that do not use the qweb engine. This means that the qweb directives are not available on form views, tree views, kanban views, etc. (The most common types of views.)

This can drive us crazy when we need to figure out how to display or loop through some basic information in the common views.

I've found a good option/workaround to show static information in the views by utilizing the jinja2 package (which is already a requirement of Odoo). I'm going to walk you through the current way and my new way to handle it.

How can I show context in the view?

One of the common use cases that I've seen pop up for Odoo developers is being able to display some simple information like the context variable in the view. So let's take a look at a sample view for a sale.order object where we might want to do that.

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="sale_order_form_inherit" model="ir.ui.view">
        <field name="name">sale.order.form.inherit</field>
        <field name="model">sale.order</field>
        <field name="inherit_id" ref="sale.view_order_form"/>
        <field name="arch" type="xml">
            <notebook position="inside">
                <page string="Simple Templating">
                    <group string="Current Context">
                        <!-- How can we show the current context dict here? -->
                    </group>
                </page>
            </notebook>
        </field>
    </record>
</odoo>
                

Views can't live without their models

Odoo views are some shallow, old, rich men who only date models.

They are related directly to a data model in the system. This generally makes things much simpler when developing because there's magic behind the scenes that automatically loads all of the data and renders the view. The major pitfall though is that when you aren't operating within the expected use cases of the system, then you have to start introducing workarounds.

For example, if you were working outside of Odoo and you wanted to render a view then you directly inject data into that view. Using pesudo-code, that might look something like:

<div>
    <h1>{{{ title }}}</h1>
</div>
                
template = load_html()
template.render(title="My Title")
                

It's a little bit of work, but very clear to us what's happening. But Odoo automatically injects the model and fields into the view giving you access to that information but nothing else.

Fields for everything

Essentially any piece of information that you want to display on a view, which is not a static string written directly into the XML, must be in a field on the model.

Looking back at our context example we obviously must create a new field.

from odoo import fields, models


class Order(models.Model):
    _inherit = "sale.order"

    context = fields.Text(compute="_compute_context")

    def _compute_context(self):
        self.context = self.env.context
                
<group string="Current Context">
    <field name="context"/>
</group>
                

And this does work. It's how we Odoo developers have operated, but it just feels wrong to be honest. When you start building out fairly complicated views then you continue to bloat your model more and more. Models in my mind are meant to represent some specific data structure that is stored in the system. It's easy to wrap my head around the fact that the system needs orders, so we have an Order model. The fields on that model should only track the must-have data for an order.

But as more and more fields are added for view rendering, then it gets complicated keeping things straight. What do we actually need for order data in the business logic and what do we need to show on the order view? Should the order model really be responsible for storing or computing context?

It feels like there needs to be more separation there.

Another way with jinja2

jinja2 is an open source templating engine that is actually already a requirement of the Odoo source. It's used within the core.

I wanted to take advantage of it as a developer to pass some simple data into a view for rendering, which does not have anything directly to do with our model data structure. We are still bound to using the model class, but we can at least escape from using fields.

Here's what that started to look like:

from odoo import fields, models
from jinja2 import Environment


class Order(models.Model):
    _inherit = "sale.order"

    def fields_view_get(self, *args, **kwargs):
        res = super().fields_view_get(*args, **kwargs)

        # Create our templating environment, using {{{ }}} as our templating tags to avoid conflict
        templater = Environment(
            variable_start_string="{{{",
            variable_end_string="}}}",
        )

        # Create a template object out of the current view (arch)
        template = templater.from_string(res["arch"])

        # Inject data into the view and replace our template tags with the data
        res["arch"] = template.render(
            message="Hello!"
        )

        return res
                

Now let's break down what's going on here.

1. Overriding fields_view_get

fields_view_get is the glue between the model and the view. It packages up all of the information that the view needs and returns a big dict. In that dictionary is a field called arch which is the xml from the view. That's what we need.

def fields_view_get(self, *args, **kwargs):
    res = super().fields_view_get(*args, **kwargs)

    ...

    return res
                

2. Generate a jinja2 environment

You can think of the jinja2.Environment class just like a configuration object. We just need to to set our open and end tags. I changed these to use triple brackets to avoid any conflict with core Odoo code. There are no occurences of triple brackets anywhere in the Odoo repository.

templater = Environment(
    variable_start_string="{{{",
    variable_end_string="}}}",
)
                

3. Load up our arch xml

Now we can create a template from the arch. jinja2 is a really simple to use package. You can take a string of html or xml and pass it into a template object.

template = templater.from_string(res["arch"])
                

4. Finally render is back out

Now we have a template object, created from the form xml. We just call the render function, inject whatever data we want, and put it back where it was before returning.

res["arch"] = template.render(
    message="Hello!"
)
                

Bringing it together with the view

At this point, we can use the message data that we injected anywhere in our form view.

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="sale_order_form_inherit" model="ir.ui.view">
        <field name="name">sale.order.form.inherit</field>
        <field name="model">sale.order</field>
        <field name="inherit_id" ref="sale.view_order_form"/>
        <field name="arch" type="xml">
            <notebook position="inside">
                <page string="Simple Templating">
                    <group string="Other Info">
                        <div colspan="2">
                            <strong>Message: </strong><span>{{{ message }}}</strong>
                        </div>
                    </group>
                </page>
            </notebook>
        </field>
    </record>
</odoo>
                

Pretty damn cool to see this work in a few lines of code. It lets us pass in any static data that we want.

Adding the context

Back to our original use case, let's add the context. We need to update the data that we are injecting in and update the view. I'm just going to use the entire env object to show the flexibility you have.

res["arch"] = template.render(
    env=self.env,
    message="Hello!",
)
                
<notebook position="inside">
    <page string="Simple Templating">
        <group string="Current Context">
            <div>{{{ env.context }}}</div>
        </group>
        <group string="Other Info">
            <div colspan="2">
                <strong>Message: </strong><span>{{{ message }}}</strong>
            </div>
        </group>
    </page>
</notebook>
                

Refactor out a mixin

This is a cool option for developers, but it's really not reusable in its current state. Am I going to override the fields_view_get on every model that needs this type of templating?

Of course not.

Let's make a mixin for it:

from odoo import models
from jinja2 import Environment


class JinjaMixin(models.AbstractModel):
    """Model mixin to enable jinja templating"""
    _name = "jinja.mixin"

    templater = Environment(
        variable_start_string="{{{",
        variable_end_string="}}}",
    )

    def view_data(self):
        return {}

    def fields_view_get(self, *args, **kwargs):
        res = super().fields_view_get(*args, **kwargs)
        res["arch"] = self.templater.from_string(res["arch"]).render(**self.view_data())
        return res
                

The mixin lets us update our model and have a single hook for injecting data. You can see it's following the same logic as shown above (condensed a bit for simplicity sake) and instead of directly passing a dict to our render method, we are using the view_data method which can be easily overwritten by models inheriting the mixin.

So this is what our order model can look like now:

class Order(models.Model):
    _name = "sale.order"
    _inherit = ["sale.order", "jinja.mixin"]

    def view_data(self):
        return {
            "env": self.env,
            "message": "Hello!",
        }
                

Pretty simple, right? You can easily use it across any model now.

What about other directives?

jinja2 has a lot of features built into it. Above we are doing the simplest possible thing of rendering a variable, but of course you have full access to the features of the package. For example it's possible to iterate over objects:

class Order(models.Model):
    _name = "sale.order"
    _inherit = ["sale.order", "jinja.mixin"]

    def view_data(self):
        return {
            "items": [1, 2, 3],
        }
                
<notebook position="inside">
    <page string="Simple Templating">
        <group string="Other Info">
            <ul colspan="2">
                {% for item in items %}
                <li>{{{ item  }}}</li>
                {% endfor  %}
            </ul>
        </group>
    </page>
</notebook>
                

Go through the documentation and see what the possibilities are.

Possibilities

This might not be the perfect solution, but I really think it's a viable one for developers who just need to display simple data. We do not have the option to use qweb directives in the most common types of views like form views and tree views. The work it would take to extend the core system to use qweb for those would not be worth it. It's a big rewrite project.

But this is a nice little workaround. A single mixin that's about 20 lines of code opens up a simple hook called view_data that injects whatever you want to render via jinja2.

I'm going to start playing around with this idea more and see about injecting other model data in, handling view updates, etc. and see where it takes me. Let me know if you try it out or have ideas about it. I'd love to hear them.

Best of luck coding on it.

Thanks For Reading

I appreciate you taking the time to read any of my articles. I hope it has helped you out in some way. If you're looking for more ramblings, take a look at theentire catalog of articles I've written. Give me a follow on Twitter or Github to see what else I've got going on. Feel free to reach out if you want to talk!

odoo
erp
open source
python
web development
Share:

Holden Rehg, Author

Posted March 16, 2021