PDF reports with QWeb in Odoo: from the form to the document that closes deals
QWeb is HTML that turns into PDF. We show you how to build a report, loop over records with t-foreach, and why t-field saves you a thousand headaches.
Part of our complete guide: Odoo module development.
There's a part of Odoo the end client sees more than any other and that, paradoxically, is usually the most neglected: the PDF reports. The invoice, the quote, the product sheet, the work order. They're the document that leaves your company with your logo and reaches the client. And yet, too often it's a misaligned PDF, with the date in the wrong format and a total that doesn't match the screen.
The good news: in Odoo, reports are QWeb, which deep down is HTML with superpowers. If you can lay out a table, you can build a report. Let's see how.
Two pieces: the action and the template
A report is always two things. First, a report action (ir.actions.report) that tells Odoo "for this model, offer this PDF and add it to the Print menu":
<record id="action_report_property" model="ir.actions.report">
<field name="name">Property sheet</field>
<field name="model">estate.property</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">estate.report_property</field>
<field name="binding_model_id" ref="model_estate_property"/>
<field name="binding_type">report</field>
</record>
That binding_model_id is the detail many people forget: it's what makes the "Print" button appear on the form. Without it, the report exists but there's no way to launch it from the interface, and the "I can't find it anywhere" complaints begin.
The template: HTML that loops over your data
The second piece is the QWeb template. This is where you see QWeb is HTML with t- directives. You lean on Odoo's standard layouts (web.html_container, web.external_layout) to inherit the header with the logo and the company footer for free:
<template id="report_property">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page">
<h2 t-field="doc.name"/>
<p>Expected price: <span t-field="doc.expected_price"/></p>
<table class="table table-sm">
<thead>
<tr><th>Client</th><th>Offer</th></tr>
</thead>
<tbody>
<tr t-foreach="doc.offer_ids" t-as="offer">
<td t-field="offer.partner_id"/>
<td t-field="offer.price"/>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
That docs is the collection of records you sent to print; the t-foreach walks them one by one (and it works here with the offers' One2many too, just like we saw in relations between models). As you can see, the table table-sm classes are Bootstrap: Odoo ships it out of the box, so you don't lay out from scratch.
t-field vs t-esc: the detail that separates a professional PDF from a shoddy one
Here's the nuance with the biggest impact on document quality, and almost nobody explains it well. You have two ways to print a value:
t-esc(ort-out) prints the data raw. A float comes out as250000.0. A date, in technical format.t-fieldprints the data formatted according to its type and the company settings: the price comes out as€250,000.00, the date in the locale's format, the Many2one as the record's name.
Simple rule: to show a record's fields, always use t-field. Reserve t-esc/t-out for on-the-fly computed values that aren't a field. The day you get this, your PDFs stop looking like a database dump and start looking like company documents.
Inherit standard reports, don't copy them
Need to add your field to Odoo's standard invoice? The temptation is to duplicate the entire invoice template and edit it. Wrong. Just like with views, you inherit with xpath —the same principle we explained in inheritance in Odoo—:
<template id="report_invoice_zone" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='informations']" position="inside">
<p>Delivery zone: <span t-field="o.delivery_zone"/></p>
</xpath>
</template>
That way your addition survives upgrades, instead of leaving you with a frozen invoice that loses every improvement Odoo ships.
The rendering-engine gotcha
A warning that saves hours: the PDF is generated by wkhtmltopdf, which is not a modern browser. There's CSS that looks perfect on screen and breaks in the PDF (finicky flexbox, ignored margins, unexpected page breaks). The cure is to lay out on the Bootstrap classes Odoo already uses and always test the real PDF, not just the HTML preview. What you see in the browser is not what the engine prints.
Why this isn't a minor detail
A well-laid-out quote conveys seriousness before the client reads a single figure; a misaligned one sows doubt. The report is silent marketing. That's why, when we build an Odoo, the documents that go out get the same care as the business logic.
Do your Odoo PDFs project an image that doesn't represent you, or do you need a custom report that doesn't exist today? In a diagnosis session we review them, or see it within our custom development. For the rest of the journey, you have the anatomy of a module and automated actions.
Comments (0)
Be the first to comment.
Sign in to leave a comment.
Sign inComments are reviewed before publishing.
Related articles
Automated actions in Odoo: let the ERP do the boring work
Server actions, automation rules and scheduled jobs: the three pieces that make Odoo react, notify and run on its own —without anyone having to remember.
Inheritance in Odoo: the feature that decides whether your code survives upgrades
Classical extension, delegation and view inheritance. The three ways to modify Odoo without touching the core —and why inheriting instead of copying saves you every migration.
Relations between models in Odoo: Many2one, One2many and Many2many without the confusion
The three relations that connect your data, what each one really stores in the database, and the commands to write them without breaking anything.