How Odoo Reports Work
Odoo generates PDF reports using QWeb (its template engine) + wkhtmltopdf (HTML-to-PDF converter). You write an HTML template with QWeb directives, Odoo renders it with data, and wkhtmltopdf converts the HTML to PDF.
Creating a Custom Report
Step 1: Report Action
<!-- views/report_equipment.xml -->
<record id="action_report_equipment" model="ir.actions.report">
<field name="name">Equipment Report</field>
<field name="model">equipment.item</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">equipment_tracking.report_equipment</field>
<field name="report_file">equipment_tracking.report_equipment</field>
<field name="binding_model_id" ref="model_equipment_item"/>
<field name="binding_type">report</field>
</record>Step 2: QWeb Template
<template id="report_equipment">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page">
<h2>Equipment Report</h2>
<table class="table table-sm">
<thead>
<tr>
<th>Name</th>
<th>Serial Number</th>
<th>Department</th>
<th>Status</th>
<th>Purchase Cost</th>
</tr>
</thead>
<tbody>
<tr>
<td><t t-esc="doc.name"/></td>
<td><t t-esc="doc.serial_number"/></td>
<td><t t-esc="doc.department_id.name"/></td>
<td><t t-esc="doc.status"/></td>
<td><t t-esc="'%.2f' % doc.purchase_cost"/></td>
</tr>
</tbody>
</table>
<!-- Conditional content -->
<t t-if="doc.notes">
<h3>Notes</h3>
<p><t t-esc="doc.notes"/></p>
</t>
<!-- Warranty info -->
<div t-if="doc.warranty_expiry" class="mt-3">
<strong>Warranty expires:</strong>
<t t-esc="doc.warranty_expiry" t-options='{"widget": "date"}'/>
</div>
</div>
</t>
</t>
</t>
</template>Key QWeb Directives
| Directive | Purpose | Example |
|---|---|---|
t-esc | Output escaped text | <t t-esc="doc.name"/> |
t-raw | Output unescaped HTML | <t t-raw="doc.description"/> |
t-if | Conditional rendering | <div t-if="doc.active">...</div> |
t-foreach | Loop | <t t-foreach="doc.line_ids" t-as="line"> |
t-set | Set variable | <t t-set="total" t-value="0"/> |
t-call | Include sub-template | <t t-call="web.external_layout"> |
t-att-* | Dynamic attribute | <div t-att-class="'text-danger' if x else ''"/> |
t-options | Formatting options | t-options='{"widget": "monetary"}' |
Formatting Values
<!-- Date formatting -->
<t t-esc="doc.date" t-options='{"widget": "date"}'/>
<!-- Monetary formatting -->
<t t-esc="doc.amount" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
<!-- Float formatting -->
<t t-esc="'%.2f' % doc.quantity"/>Page Breaks
<!-- Force page break between records -->
<div class="page" style="page-break-after: always;">
<!-- Record content -->
</div>Paper Format
<record id="paperformat_equipment" model="report.paperformat">
<field name="name">Equipment Report Format</field>
<field name="format">A4</field>
<field name="margin_top">40</field>
<field name="margin_bottom">20</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_spacing">35</field>
<field name="orientation">Portrait</field>
<field name="dpi">90</field>
</record>Common Issues
- Blank PDF → wkhtmltopdf not installed or wrong version
- Missing CSS → wkhtmltopdf cannot reach Odoo static files (set
report_url) - Report timeout → Large data set, increase
limit_time_real - Wrong paper size → Specify paper format in report action
AI-Powered Report Creation
Claude Code generates QWeb report templates from natural language descriptions: "Create a PDF report for equipment showing all items grouped by department with subtotals." Deploy and test on DeployMonkey.