Skip to content
Back to blog
DevelopmentQWebReports

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.

COConsultor Odoo31 May 20264 min read

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 (or t-out) prints the data raw. A float comes out as 250000.0. A date, in technical format.
  • t-field prints 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.

#Development#QWeb#Reports
Share article

Comments (0)

Be the first to comment.

Sign in to leave a comment.

Sign in

Comments are reviewed before publishing.

Ready to get the most out of Odoo?

Tell us your challenge. In a first 30-minute call we'll tell you how Odoo can help, no strings attached.