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:
- Finds the
ir.actions.reportrecord linked to the button - Calls the report's
ir.qwebtemplate with the selected records - Renders the template to HTML
- Passes the HTML to wkhtmltopdf for PDF conversion
- 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#
<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_containerwraps the full document with proper HTML boilerplateweb.external_layoutadds the company header and footerdocsis the recordset passed by the report enginet-foreach/t-asiterate over records;ois the loop variable
Report Action#
<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):
<span t-esc="o.name"/>Use t-field for Odoo-formatted values (respects field type, locale, and formatting options):
<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:
<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:
<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:
<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 block inside the template or inline on elements.
<template id="report_my_document">
<t t-call="web.html_container">
<style>
.page { font-size: 12px; }
.total-row { font-weight: bold; border-top: 2px solid #000; }
</style>
...
</t>
</template>Keep styles inside the template; external CSS files are not reliably loaded by wkhtmltopdf.
Testing a Report#
- Install your module and open a record of your model.
- Click Print → your report name in the action menu.
- If the report is not in the menu, check
binding_model_idandbinding_typein the action record. - For faster iteration: access the report URL directly:
/report/pdf/my_module.report_my_document/1 (replace 1 with the record ID).
- Use
/report/html/...instead of/report/pdf/...to see the HTML before PDF conversion - this shows template rendering errors more clearly.
Five Gotchas#
1. t-esc on a Many2one field shows an integer#
t-esc="o.partner_id" renders the database ID, not the name. Use t-esc="o.partner_id.name" or t-field="o.partner_id".
2. Blank PDF but no error#
wkhtmltopdf silently fails if the HTML has JavaScript errors or missing resources. Switch to the HTML preview URL to see the actual rendered output and look for template errors in the Odoo log.
3. Paper format not applied#
The paper format on the ir.actions.report record is only used when printing via Odoo. If you test via the URL, Odoo still applies the paper format - but check that paperformat_id is set on the record, not just on the template.
4. t-field on a computed field raises an error#
t-field requires a field stored on a browse record. Non-stored computed fields work only if the field is defined on the model. If you are computing a value ad-hoc, use t-set and t-esc instead.
5. report_name vs report_file confusion#
report_name is the fully qualified template XML ID used to find the QWeb template. report_file is the same value but used by some older Odoo versions as the template path. Set both to the same value: my_module.report_template_id.
ERPeek can answer "which QWeb templates are defined for this model?" and "what data does this report's Python method prepare?" across any Odoo version and custom addon set. See the contact page for a demo.

