Custom Reports With Qweb

QWeb is Odoo's primary templating engine which uses an XML-based syntax, making it easy to design layouts with placeholders for data. Within Odoo, QWeb seamlessly integrates with your data models to generate professional-looking PDFs, web pages, and other printable reports.

As a follow-up from the last article - Odoo Development: A Comprehensive Guide, where we've built a Cat Cattery app to manage predigreed kittens for adoption, we'll now create a printable PDF reports for kittens who have been adopted.


Prerequisites:

  1. Completion of the previous tutorial is not required to follow along, simply grab the starter files here. Otherwise, feel free to adapt the concepts to your own project.
  2. Odoo reports are just HTML pages that are then converted into PDF files. For this conversion, the wkhtmltopdf, short for Webkit HTML to PDF command-line tool is used. The most up-to-date Odoo information about wkhtmltopdf can be found at the following github repo.
  3. (Optional) In case you haven't changed the default company's information in Settings | Companies | Update Info,  it is up to you to try adding a company's logo, addresses, etc. as these detail will be displayed in our reports. This is what mine looks like:


Great! Now we're good to go:


Step 1: Add Business Reports

One simple yet useful feature for a cat cattery app is to print out a report which shows the cats that have been adopted and how many remains in facility.

To implement this, we need to have report files which are stored in a /report subdirectory. First, create a cat_cattery_kitten_report.xml file inside the subdirectory, and let's not forget to declare it in the __mainifest__.py file.

Now we'll work on modifying this file.

1. Add the Report Action

The report action triggers the execution of a report, similarly to how window actions trigger web client view presentations. A report action is a record in the ir.actions.report XML model, and it can be inspected by using the Settings | Technical | Actions | Reports menu option.


To add the report action and trigger the execution of the report, edit the report/cat_cattery_kitten_report.xml file, as follows: 


XML
  
<odoo>
    <record id="action_breeder_kitten_report" model="ir.actions.report">                                        
      <field name="name">Kitten Report</field>                                        
      <field name="model">cat_breeder.kitten</field>                                        
      <field name="report_type">qweb-pdf</field>                                        
      <field name="report_name">cat_breeder.kitten_report</field>                                        
      <field name="binding_model_id" ref="model_cat_breeder_kitten"/>                                      
      <field name="binding_type">report</field>                                    
    </record>                                
</odoo>
  • name​ is the report action's title.
  • model​ is the technical name of the report's base model.
  • report_type​ is the type of document to generate. The options are qweb-pdf​, qweb-html​, or qweb-text​. 
  • report_name​ is the XML ID of the QWeb template to be used to generate the report's content. Unlike other identifier references, it must be a complete reference that includes the module name; that is, <module_name>.<identifier_name>​.
  • binding_model_id​ is a many-to-one field for identifying the model where the report print option should be available.
  • binding_type​ should be set to report​.


This report action makes the report available at the top of the Kittens view, on the Print button, next to the Action button:


[Print Action in box highlight]


2. Set a Paper Layout

Odoo proposes a few page formats out of the box, including European A4 and US Letter. Additional page formats can be added, including those for specific page orientations. The existing formats can be inspected using the Settings | Technical | Reporting | Paper Format menu option.

Let's choose an A4 Lanscape format for the adopted kitten report, by adding the following data record at the beginning of report/cat_cattery_kitten_report.xml file:

XML
  
<record id="paperformat_euro_landscape" model="report.paperformat">
        <field name="name">A4 Landscape</field>
        <field name="format">A4</field>
        <field name="orientation">Landscape</field>
        <field name="margin_top">40</field>
        <field name="margin_bottom">32</field>
        <field name="margin_left">7</field>
        <field name="margin_right">7</field>
        <field name="header_line" eval="False" />
        <field name="header_spacing">35</field>
        <field name="dpi">90</field>
    </record>

This is a copy of the European A4 format, defined by the base module, in the data/report_paperformat_data.xml file, with the orientation changed from portrait to landscape.


To use this paper format, stay in the report/cat_cattery_kitten_report.xml  file, and add the paperfomat_id​ field to the report action action_breeder_kitten_report​:

XML
  
<record id="action_breeder_kitten_report" model="ir.actions.report">

      <!-- rest of the code  -->

      <field name="paperformat_id" ref="paperformat_euro_landscape" />                                 
</record>  



Step 2: Design Report Content

Odoo reports are generated using QWeb templates. QWeb generates HTML that can then be converted into a PDF document. 


1. Add a Template

To ensure proper page formatting, let's first create a minimum viable template for the Qweb report with a few essential QWeb directives and flow controls.

In the /report subdirectory, add a new file - cat_cattery_kitten_report_templates.xml with the following:

XML
  
<odoo>
    <template id="kitten_report">
        <t t-call="web.html_container">
            <t t-call="web.external_layout">
                <div class="page">

                    <!-- Report header content -->
                    <div class="container">
                       
                    <!-- Report content -->
                        <t t-foreach="docs" t-as="o">
                            
                        <!-- Report footer content -->
                        <div>
                        </div>

                    </div> <!-- container -->
                </div> <!-- page -->
            </t>
        </t>
    </template>
</odoo>
  • the t-call​ directives are used to call the standard report structures - web.html_container​ and web.external_layout 
  • The web.html_container​ template does the basic setup to support an HTML document.
  • The web.external_layout​ template handles the report header and footer using the corresponding company setup.
  • The docs​ variable represents the base record set to generate the report, and uses a t-foreach​ QWeb directive to iterate through each record.


With the basic skeleton for the report in place, it is time to start designing the report content.


2. Add Header Content

This content layout uses the Bootstrap 4 grid system, which was added with the <div class="container"> element.​

XML
  
<!-- Report header content -->
<div class="container">
    <div class="row bg-primary py-2" style="color: magenta">
        <div class="col-4">Portrait</div>
        <div class="col-2">Name</div>
        <div class="col-2">Breed</div>
        <div class="col-2">Age</div>
        <div class="col-2">Color</div>
</div>


The previous code adds a header row with column titles. After this, there is a t-foreach loop to iterate through each record and render a row for each in the report content.


3. Add Report Content

Unlike Kanban views, the report QWeb templates are rendered on the server side and use the Python QWeb implementation. So, there are some differences to be aware of, compared to the JavaScript QWeb implementation.


Rows are added using a <div class="row">​ element. A row contains cells, and each cell can span several columns so that the row takes up 12 columns. Each cell is added using a <div class="col-N">​ element, where N is the number of columns it spans.

XML
  
<!-- Report content -->
    <t t-foreach="docs" t-as="o">
        <t t-if="o.state == 'adopted'">
            <div class="row mt-2">
                <div class="col-4">
                    <span t-field="o.image" 
                    t-options="{'widget': 'image',
                    'style': 'max-width: 150px'}"/>
                </div>
                <div class="col-2">
                    <span t-field="o.name" />
                </div>
                <div class="col-2">
                    <span t-field="o.breed_id" />
                </div>
                <div class="col-2">
                    <span t-field="o.age" /> Weeks
                </div>
                <div class="col-2">
                    <span t-field="o.color" />
                </div>
            </div>
        </t>
    </t>
  • the t-field​ attributes are being used to render field data.
  • The dot notation can be used to access fields from related data records. For example, o.name​ gets the value of the name field from the o record.
  • t-options​ is set with a dictionary-like data structure. The widget key can be used to represent the field data. i.e. "widget": 'image'​ to display the image​ field.


4. Add footer Content

It is common to have a final row which display certain summary statistics in business reports. For demonstration, let's have our report displaying the numbers of Total, Available, and Adopted kittens. 

XML
  
<!-- Report footer content -->
<div class="row py-5">
    <div class="col-4">
        Total Kittens: <t t-out="len(docs)" />
    </div>
    <div class="col-4">
        Available Kittens: <t t-out="len([o for o in docs if o.state == 'available'])" />
    </div>
    <div class="col-4">
        Adopted Kittens: <t t-out="len([o for o in docs if o.state == 'adopted'])" /> 
    </div>
</div>
  • ​The t-out can render the result of a (Python) code expression as an HTML-escaped value.
  • The len()​ Python function is used to count the number of elements in a collection.


Well done! Now, to see the report, in Cat Cattery | Kittens, select all kitten records, next to the Action button, you will see a Print button appears. Click on the button and you'll have a simple report that looks like this:



Step 3: Create Custom Reports

A report can not only be rendered using data available in the selected records, but also arbitrary data that's been prepared by specific business logic, this is all possible as custom reports.

A custom report example for our Cat Cattery app could be an Adopter report, where for each user, the report shows a list of kittens who have been adopted by this user.

Prior to creating the custom report, a few changes need to be made in the cat_cattery.kitten​ model. In the models/kitten.py file:
First, add a new Relational Field:

python
  
class Kitten(models.Model):
    # rest of the code
    
    # --------------------------------------- Relational Fields ----------------------------------
    adopter_id = fields.Many2one("res.partner", string="Adopter")
    
  • adopter_id​ is a many-to-one relational field linked to the res.parter​ model


Second, to ensure only adopted​ kittens can have their adopters, we need to create a Constraint logic that will raises an error message when there is an invalid entry to the field adopter_id​. Again, in the models/kitten.py file: 

python
  
from odoo.exceptions import UserError, ValidationError

class Kitten(models.Model):

    # rest of the code
    
    # --------------------------------------- Constraints ----------------------------------
    @api.constrains("state", "adopter_id")
    def _check_adopter_id(self):
        for record in self:
            if record.state != "adopted" and record.adopter_id:
                raise ValidationError("Only adopted kittens can have an adopter.")
   
  • The ValidationError object is used to report validation issues to the user.
  • The decorator constains() is invoked on records where the specification of the adopter_id field's constraint (which depends on the value of state) is not met and raises error.

Don't forget to make this new field available in the Form view, modify the form view in views/cat_cattery_kitten_views.xml file by adding this line:

XML
  
<record model="ir.ui.view" id="breeder_kitten_view_form">
  <field name="name">breeder.kitten.form</field>
  <field name="model">cat_cattery.kitten</field>
  <field name="arch" type="xml">
    <form string="Kitten">
      ...
      <sheet>

        ...

        <group>

          ...

          <group>
            <field name="adopter_id"/>
          </group>
        </group>
      </sheet>
    </form>
  </field>
</record>


And lastly, we need to register the adopters of our adopted kittens. For this example, our 3 fluffy kittens: Floki, Mango, and Saline will be fostered by an user in the res.partner​ model named Addison Olson:



Now our data models are ready, let's move on the custom report!


1. Prepare Custom Report Data

The custom report will be available in the Contacts app where one or more partners can be selected, and the report will present the kittens adopted by each. Go to Apps and activate the module.



A custom report can add whatever data that's needed to the report rendering context. This is done using the AbstractModel​ object which has no database representation and holds no data, followed by a naming convention of report.<module>.<report-name>​ where a method - _get_report_values()​ that returns a dictionary with the variables to add to the rendering context is implemented. 

To do so, create a report/cat_cattery_adopter_report.py file and add the following:

python
  
from odoo import api, models

class AdopterReport(models.AbstractModel):
    _name = "report.cat_breeder.adopter_report"
    
    @api.model
    def _get_report_values(self, docids, data=None):
        domain = [("adopter_id", "in", docids)]
        kittens = self.env["cat_breeder.kitten"].search(domain)
        adopters = kittens.mapped("adopter_id")
        adopter_kittens = [
                    (adopter,
                    kittens.filtered(lambda kitten: kitten.adopter_id == adopter))
                    for adopter in adopters
                    ]
        docargs = {
        "adopter_kittens": adopter_kittens,
        }
        return docargs
   
  • The abstract model's name following the convention is report.cat_breeder.adopter_report​ (thus the report identifier name will be adopter_report)
  • The _get_report_values() method is decorated with the api.model
  • The docids​ argument is a list of the numeric IDs selected to print the report. The base model to run the report is res.partner​, so these will be partner IDs.
  • The business logic finds the adopted kittens by the adopters and group them by their new owners. The result is a list of tuples - adopter_kittens
  • The method returns a dictionary as the data structure which can be iterated in a loop for report template rendering.


For this file to be loaded by the module, it is also necessary to do the following:

  • Add to the report/__init__.py file with a from . import breeder_adopter_report​ line.
  • Add a from . import report​ line to the __init__.py top file.

 

2. Add the Report Template

Similar to regular reports, the next step is to create the QWeb template that's used to render the report.

We'll create an XML file that defines a report action and a report template. The template will have context variables available as whatever key/values are returned by the _get_report_values​ method.

In the report subdirectory, make a breeder_adopter_report.py file and add to it:

XML
  
<odoo>
    <record id="action_publisher_report" model="ir.actions.report">
        <field name="name">Kittens by Adopter</field>
        <field name="model">res.partner</field>
        <field name="report_type">qweb-pdf</field>
        <field name="report_name">cat_breeder.adopter_report</field>
        <field name="binding_model_id" ref="base.model_res_partner" />
        <field name="binding_type">report</field>
    </record>

    <template id="adopter_report">
        <t t-call="web.html_container">
            <t t-call="web.external_layout">
                <div class="page">
                    <div class="container">
                        <t t-foreach="adopter_kittens" t-as="group">
                            <div class="py-3" style="text-align: center;">
                                <h2 t-field="group[0].name" />
                                <span t-field="group[0].image_1920" 
                                    t-options="{'widget': 'image',
                                    'style': 'max-width: 300px'}"/>
                            </div>
                            <div class="py-3">
                                <h5>Adopted Kittens:</h5>
                                <ul>
                                    <t t-foreach="group[1]" t-as="kitten">
                                        <li class="mt-2">
                                            <span t-field="kitten.image" 
                                                t-options="{'widget': 'image',
                                                'style': 'max-width: 50px'}"/>
                                            <b><span t-field="kitten.name" /></b>,
                                            <span t-field="kitten.breed_id" />
                                        </li>
                                    </t>
                                </ul>
                            </div>
                        </t>
                    </div>
                </div>
            </t>
        </t>
    </template>
</odoo>

The above XML includes two records – one for adding the Kittens by Adopter​ report action and another for adding the adopter_report template.

When running this report, the report engine will try finding a report.cat_breeder.adopter_report model. If it exists, as is the case here, the _get_report_values()​ method is used to add variables to the rendering context.

The QWeb template can then use the adopter_kittens variable to access the added data. It is a list containing a tuple for each publisher. The first tuple element, group[0]​, is the adopter's Contact record that's used on the group header, while the second tuple element, group[1], is the record set containing the adopted kittens, presented using a second for loop.


To print the report, on Menu, go to Contacts, select the record of Addison Olson, then click on Print, select the report "Kittens by Adopter" that we've created, a PDF file will be downloaded.

​​


And this is what the custom report looks like:



Hooray!

This tutorial provided you with the tools and techniques to implement printable reports which often can be important parts of a business application. Now, you can ensure that your business application doesn't fall short of your user's needs.


The source code of this tutorial is here.

Sign in to leave a comment
Odoo Development: A Comprehensive Guide
Ready to embark on your Odoo adventure? Let's build your very first Odoo application – with a purr-fect twist!