- Introduction
- How can I show context in the view?
- Another way With jinja2
- Refactor out a mixin
- What about other directives?
- Possibilities
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!