All posts
Developer10 min read

Odoo QWeb report development: building PDF and HTML reports from scratch

QWeb is the XML template engine behind every Odoo PDF report. This guide covers the minimum structure for a working report action, iterating over records, formatting fields, applying paper sizes, and the five gotchas that produce blank or broken output.

What QWeb Is#

QWeb is Odoo's XML-based template engine. It is used to render PDF reports (via wkhtmltopdf), HTML reports, and email templates. A report is defined as an ir.actions.report record that points to a QWeb template.

When a user clicks "Print" on a record, Odoo:

  1. Finds the ir.actions.report record linked to the button
  2. Calls the report's ir.qweb template with the selected records
  3. Renders the template to HTML
  4. Passes the HTML to wkhtmltopdf for PDF conversion
  5. Returns the PDF to the browser

Minimum Report Structure#

A working custom report requires two things: a QWeb template and a report action.

QWeb Template#

xml
<odoo>
  <template id="report_my_document">
    <t t-call="web.html_container">
      <t t-foreach="docs" t-as="o">
        <t t-call="web.external_layout">
          <div class="page">
            <h2 t-esc="o.name"/>
            <p>Reference: <span t-esc="o.ref"/></p>
          </div>
        </t>
      </t>
    </t>
  </template>
</odoo>

Key elements:

  • web.html_container wraps the full document with proper HTML boilerplate
  • web.external_layout adds the company header and footer
  • docs is the recordset passed by the report engine
  • t-foreach / t-as iterate over records; o is the loop variable

Report Action#

xml
<odoo>
  <record id="action_report_my_document" model="ir.actions.report">
    <field name="name">My Document</field>
    <field name="model">my.model</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">my_module.report_my_document</field>
    <field name="report_file">my_module.report_my_document</field>
    <field name="binding_model_id" ref="model_my_model"/>
    <field name="binding_type">report</field>
  </record>
</odoo>

binding_model_id and binding_type wire the report into the Print menu of the model's list/form views automatically.


Rendering Field Values#

t-esc vs t-field#

Use t-esc for plain string output (HTML-escaped):

xml
<span t-esc="o.name"/>

Use t-field for Odoo-formatted values (respects field type, locale, and formatting options):

xml
<span t-field="o.amount_total"/>
<span t-field="o.date_order" t-options='{"widget": "date"}'/>
<span t-field="o.currency_id"/>

t-field requires an actual browse record field reference - it will not work on raw Python expressions.

Calling Methods#

You can call model methods from templates:

xml
<t t-set="lines" t-value="o._get_report_lines()"/>
<t t-foreach="lines" t-as="line">
  <tr>
    <td t-esc="line['description']"/>
    <td t-esc="line['qty']"/>
  </tr>
</t>

Define _get_report_lines() on the model to prepare structured data. This avoids complex logic in the template.


Paper Format#

Set the page size and margins on the report action:

xml
<record id="action_report_my_document" model="ir.actions.report">
  ...
  <field name="paperformat_id" ref="base.paperformat_euro"/>
</record>

Or define a custom paper format:

xml
<record id="paperformat_my_custom" model="report.paperformat">
  <field name="name">My Custom Format</field>
  <field name="default" eval="False"/>
  <field name="format">custom</field>
  <field name="page_height">297</field>
  <field name="page_width">210</field>
  <field name="orientation">Portrait</field>
  <field name="margin_top">20</field>
  <field name="margin_bottom">20</field>
  <field name="margin_left">15</field>
  <field name="margin_right">15</field>
  <field name="header_line" eval="False"/>
  <field name="header_spacing">3</field>
  <field name="dpi">90</field>
</record>

Styling#

QWeb reports use inline CSS and Bootstrap classes (Odoo includes Bootstrap in reports). Write styles in a