Odoo QWeb Templates
Overview
QWeb is Odoo's primary server-side templating engine, used for PDF reports, website pages, and email templates. This skill generates correct, well-structured QWeb XML with proper directives, translation support, and report action bindings. It covers server-side QWeb only (not the JavaScript QWeb engine used in Kanban/Form widgets).
When to Use
- Creating a custom PDF report (invoice, delivery slip, certificate, patient card).
- Building a QWeb email template triggered by workflow actions.
- Designing Odoo website pages with dynamic content.
- Debugging QWeb rendering errors (
t-if,t-foreach,t-fieldissues). - Binding an
ir.actions.reportrecord to a QWeb template. - Migrating
t-esctot-outfor Odoo 15+ upgrades.
Prerequisites
- Odoo module development environment (Odoo 14+ recommended; directives differ between versions).
- Basic familiarity with XML data files and Odoo module structure (
__manifest__.py,reports/directory). - For PDF rendering:
wkhtmltopdfinstalled and configured in Odoo system parameters. - For testing: access to an Odoo instance with the target module installed or upgradeable.
Directive syntax, layout wrappers (web.html_container, web.external_layout, web.internal_layout), and email vs report variable scope (object vs docs) are in this file. Validate XML with the well-formedness check in Verification before upgrading the module.
Procedure
1. Identify the Report or Template Requirements
Determine the following before generating any code:
- Target model (e.g.,
hospital.patient,sale.order). - Report type:
qweb-pdf(most common),qweb-html, or email template. - Odoo version: affects directive choices (
t-outfor 15+,t-escfor 14 and below). - Fields to display and any conditional logic needed.
- Layout:
web.external_layoutfor customer-facing PDFs (includes company header/footer/logo),web.internal_layoutfor internal documents.
2. Create the Report Action Record
Place this in your module's data file (typically reports/report_patient_card.xml or similar):
<record id="action_report_patient_card" model="ir.actions.report">
<field name="name">Patient Card</field>
<field name="model">hospital.patient</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">hospital_management.report_patient_card</field>
<field name="binding_model_id" ref="model_hospital_patient"/>
</record>
Key fields:
report_namemust match the templateidprefixed with the module name:module_name.template_id.binding_model_idmakes the report appear in the Print menu of the target model's views.report_typeisqweb-pdffor PDF output orqweb-htmlfor HTML.
3. Create the QWeb Template
Place the template in the same data file or a separate template file:
<template id="report_patient_card">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page">
<h2>Patient Card</h2>
<table class="table table-bordered">
<tr>
<td><strong>Name:</strong></td>
<td><t t-field="doc.name"/></td>
</tr>
<tr>
<td><strong>Doctor:</strong></td>
<td><t t-field="doc.doctor_id.name"/></td>
</tr>
<tr>
<td><strong>Status:</strong></td>
<td><t t-field="doc.state"/></td>
</tr>
</table>
</div>
</t>
</t>
</t>
</template>
4. Register the Data File in __manifest__.py
{
'name': 'Hospital Management',
'version': '17.0.1.0.0',
'depends': ['web'],
'data': [
'reports/report_patient_card.xml',
],
}
5. Upgrade the Module
On Windows (PowerShell), upgrade the module from the Odoo shell or command line:
# From the Odoo installation directory
python odoo-bin -c odoo.conf -d your_database -u hospital_management --stop-after-init
Or from the Odoo web UI: Apps → Update Apps List → upgrade the module.
6. Test the Report
- Navigate to the target model's list or form view.
- Select one or more records.
- Click Print → Patient Card (the report name from the action record).
- Verify the PDF renders correctly with company header/footer.
7. Debugging Broken Templates
If a template fails to render:
- Check Odoo logs for QWeb compilation errors (usually syntax or undefined variable).
- Validate XML — ensure the file is well-formed:
# If python is available python -c "import xml.dom.minidom; xml.dom.minidom.parse('reports/report_patient_card.xml'); print('XML is well-formed')" - Common fixes:
- Missing
t-asont-foreach— addt-as="doc"or appropriate alias. - Using
t-escwhere HTML output is intended — switch tot-out(Odoo 15+) or restructure. - Referencing
docsin an email template — email templates useobject, notdocs. - Field not displaying — ensure the field exists on the model and the record has a value.
- Missing
Examples
Example 1: Conditional Rendering with t-if
<!-- Show a warning block only if the patient is not confirmed -->
<t t-if="doc.state == 'draft'">
<div class="alert alert-warning">
<strong>Warning:</strong> This patient has not been confirmed yet.
</div>
</t>
Example 2: Iterating Over a One2many Field
<!-- Display all appointments for a patient -->
<t t-foreach="doc.appointment_ids" t-as="apt">
<tr>
<td><t t-field="apt.date"/></td>
<td><t t-field="apt.doctor_id.name"/></td>
<td><t t-field="apt.state"/></td>
</tr>
</t>
Example 3: Safe HTML Output (Odoo 15+)
<!-- t-out renders HTML without escaping; use for trusted content only -->
<t t-out="doc.description_html"/>
For Odoo 14 and below, t-esc HTML-escapes output (prints tags as raw text). Use t-raw for unescaped output on Odoo 14, but be aware of XSS risks.
Example 4: Report with _get_report_values Helper
Python model method to pre-compute values:
from odoo import models
class PatientCardReport(models.AbstractModel):
_name = 'report.hospital_management.report_patient_card'
_description = 'Patient Card Report'
def _get_report_values(self, docids):
docs = self.env['hospital.patient'].browse(docids)
return {
'docs': docs,
'is_confirmed': lambda doc: doc.state != 'draft',
}
Then in the template:
<t t-if="is_confirmed(doc)">
<span class="badge badge-success">Confirmed</span>
</t>
Pitfalls
- Missing
t-asont-foreach: Withoutt-as="doc", you cannot access the current record inside the loop body. This is the most common QWeb error. - Using
t-escfor HTML content:t-escHTML-escapes output, printing tags as raw text. Uset-out(Odoo 15+) for safe HTML rendering of trusted content. On Odoo 14, uset-rawwith caution. - Email template variable scope: Email QWeb templates use
object(the single record), notdocs(the recordset). Mixing these causes silent rendering failures. - Raw Python in QWeb: Do not embed complex Python expressions in QWeb attributes. Compute values in the model or in a
_get_report_values()helper method. QWeb is for presentation, not business logic. - Forgetting
web.html_container: Withoutt-call="web.html_container", the PDF will lack proper HTML document structure and may render as a blank or broken page. - Template ID mismatch: The
report_namefield in the action record must exactly matchmodule_name.template_id. A mismatch results in "template not found" errors. - Not upgrading the module: After adding or modifying report XML, the module must be upgraded for changes to take effect. Simply restarting Odoo is not sufficient.
t-fieldon non-field values:t-fieldonly works on actual model field records. For computed or derived strings, uset-outort-esc.- Translation strings inline: Use
_lt()(lazy translation) for translatable string literals inside Python report helpers, not inlinet-escwith hardcoded strings.
Verification
Verify XML Well-Formedness
python -c "import xml.dom.minidom; xml.dom.minidom.parse('reports/report_patient_card.xml'); print('XML is well-formed')"
Expected output:
XML is well-formed
Verify Module Upgrade Succeeds
python odoo-bin -c odoo.conf -d your_database -u hospital_management --stop-after-init
Check Odoo logs for errors. A successful upgrade shows no traceback and the module loads cleanly.
Verify Report Action Exists
In the Odoo web UI or shell:
# Odoo shell
env['ir.actions.report'].search([('report_name', '=', 'hospital_management.report_patient_card')]).name
Expected output:
'Patient Card'
Verify Template Renders
- Open a record of the target model in Odoo.
- Click Print → Patient Card.
- Confirm the PDF downloads and displays:
- Company header and footer appear (from
web.external_layout). - All
t-fieldvalues render with proper formatting. - Conditional blocks (
t-if) show or hide correctly based on record state. - Loop blocks (
t-foreach) iterate over all related records.
- Company header and footer appear (from
Verify No QWeb Compilation Errors in Logs
After triggering the report, check the Odoo log file for any qweb or ValueError entries:
# On Windows, tail the log file (adjust path to your Odoo log location)
Get-Content -Path "C:\odoo\odoo.log" -Tail 50 | Select-String "qweb|ValueError|template"
No matches (or only INFO-level entries) indicates clean compilation.
Limitations
- Does not cover website controller routing for dynamic QWeb pages — that requires Python
http.routeknowledge and controller classes. - Email template QWeb has different variable scope than report QWeb (
objectvsdocs) — this skill primarily focuses on PDF reports; keep email templates onobject, notdocs. - QWeb JavaScript (used in Kanban/Form widgets) is a different engine; this skill covers server-side QWeb only.
- Does not cover wkhtmltopdf configuration for PDF rendering issues (page size, margins, header/footer overlap, missing CSS).
- Does not cover multi-company report layouts or company-specific template overrides via
ir.actions.reportinheritance.
Related Skills
odoo-module-structure— for module scaffolding,__manifest__.py, and data file registration.odoo-models-orm— for model methods,_get_report_values(), and field definitions.odoo-views-xml— for form/list/kanban views that include Print menu bindings.