Odoo QWeb Report Snippet Generator

The boilerplate and the gotchas — inheritance xpaths, null-safe fields, and PDF pagination — in one place.

Scaffold a new report

Model name
Module
Report title
Detail (optional)
Layout
Paper format
Output
ir.actions.report
<record id="action_sale_order_report" model="ir.actions.report">
    <field name="name">Quotation</field>
    <field name="model">sale.order</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">sale_custom.sale_order_report</field>
    <field name="report_file">sale_custom.sale_order_report</field>
    <field name="paperformat_id" ref="base.paperformat_euro"/>
</record>
QWeb template
<template id="sale_order_report">
    <t t-call="web.html_container">
        <t t-foreach="docs" t-as="doc">
            <t t-call="web.external_layout">
                <div class="page">
                    <h2>Quotation: <span t-field="doc.name"/></h2>
                    <!-- Add your report content here -->
                </div>
            </t>
        </t>
    </t>
</template>
Advertisement

The basics

What is QWeb, and why do reports break so often?

QWeb is Odoo's XML templating language — the same engine that renders website pages also renders every PDF/HTML report, built from directives like t-field, t-if, t-foreach and t-call. Reports fail in a small number of very recurring ways: a t-field reaching into an empty related record, an xpath inheritance that assumed the page would never change shape, and PDF pagination splitting content in the wrong place. This tool targets exactly those four situations instead of being a general-purpose QWeb editor.

Getting started

How to use this tool

New Report

Scaffold a brand-new report from a model name — get a matching ir.actions.report record and a QWeb template with the right layout and paper format wired up.

Extend Report

The most common real task: inherit an existing report (pick from common core reports or paste any external id), supply an xpath, and this tool warns you if that xpath looks fragile.

Field Snippet

Pick a field and its display widget (monetary, contact, image...) and get the correct t-field/ t-options pair, optionally guarded against the classic empty-value crash.

Page Breaks

Three ready-to-copy CSS patterns for the single most-reported QWeb problem: PDF pagination splitting rows, ignoring page breaks, or losing the table header on later pages.

Reference

Field widget reference

What t-field generates for each display widget the Field Snippet tab supports.

Text / plain
<span t-field="doc.name"/>
Any char/text field — no widget needed.
Monetary
<span t-field="doc.amount_total" t-options='{"widget": "monetary", ...}'/>
Needs a currency field via t-options — this tool fills it in for you.
Date / Datetime
<span t-field="doc.date_order"/>
Formatted per the user's language automatically.
Many2one
<span t-field="doc.partner_id"/>
Renders the related record's display name.
Contact
<div t-field="doc.partner_id" t-options='{"widget": "contact", ...}'/>
Formats a partner as a full postal/contact block.
Image
<img t-att-src="image_data_uri(doc.image)"/>
Always existence-checked — an empty binary renders nothing.
HTML
<span t-field="doc.description"/>
Renders stored HTML as-is — same tag as text, different field type.
Boolean
<span t-field="doc.active"/>
Renders a localized Yes/No, no special widget required.

Reference

Common core reports to extend

The external ids behind Odoo's most frequently customized reports — also available as quick-select buttons in the Extend Report tab.

Quotation / Sales Order sale.report_saleorder_document
Invoice account.report_invoice_document
Delivery Slip stock.report_deliveryslip
Transfer / Picking stock.report_picking
Purchase Order purchase.report_purchaseorder_document

Extending reports safely

Strong xpath vs. fragile xpath

An inheriting <xpath> is only as durable as the selector it's anchored to. A selector with no attribute predicate matches whatever happens to be in that position today — and silently breaks, or silently targets the wrong element, the moment core or another module reorders the page.

//div[@name='so_total_summary'] Strong

Strong — anchored to a name attribute the core module explicitly kept stable across versions.

//div[3]/table/tr[2] Fragile

Fragile — a positional path with no predicate. Any module that adds, removes, or reorders a div before this one silently breaks it.

//span[@t-field='doc.partner_id'] Strong

Strong — anchored to the field being rendered, which rarely changes even when surrounding markup does.

Reference

Page break & pagination patterns

PDF pagination (wkhtmltopdf/WeasyPrint) is the single most reported QWeb report problem — these three CSS patterns, live in the Page Breaks tab above, cover almost every case.

Force a new page
<p style="page-break-before: always;"></p>

Insert this between iterations of a t-foreach (guarded so it never fires before the first item) to start each one on its own page — e.g. one page per invoice when printing several at once.

Don't split this block across pages
<div style="page-break-inside: avoid;"> <!-- content that must stay together --> </div>

Wrap a table row, a heading with its paragraph, or any block that should never be cut in half at a page boundary.

Repeat a table header on every page
<thead style="display: table-header-group;"> <tr><th>...</th></tr> </thead>

table-header-group is the standard CSS value for <thead> — this just makes sure nothing in your report's CSS overrides it, so a long table's header repeats on every printed page.

FAQ

Frequently asked questions

Why do I get "'NoneType' object has no attribute" when rendering a report?

The most common QWeb report crash. It happens when t-field (or a t-esc expression) touches a related record that's empty — e.g. doc.partner_id.name when partner_id isn't set. Wrap the block in <t t-if="doc.partner_id">...</t>, or use this tool's Field Snippet tab with 'Guard against empty values' checked, which generates that guard for you automatically.

Why does t-field do nothing when I put it on a <t> tag?

t-field renders by replacing the CONTENT of a real HTML element (it needs a DOM node to attach formatting/widget behavior to). Put it on a <span>, <div>, or similar — never on a bare <t>, which has no element of its own in the rendered output.

Why did my xpath inheritance break after an Odoo upgrade or another module's update?

Positional xpaths (//div[3], //table/tr[2]) silently point at whatever happens to be third or second after any module reorders the page — no error, just the wrong spot or a crash. Anchor to a stable attribute instead: a name, an id, a field, or a distinctive class the core template is unlikely to rename. This tool's Extend Report tab flags any xpath with no attribute predicate.

How do I find the external ID of the report I want to extend?

Enable developer mode, open Settings > Technical > Reports (or print the document and check 'Bug' > download the source), and look up the ir.actions.report / template pair — or check the module's report/*.xml file directly on GitHub. The common reports listed in the Extend Report tab's quick-select cover the most frequently extended ones (sale, invoice, delivery, transfer, purchase).

What's the difference between t-field, t-esc/t-out, and t-raw?

t-field is model-aware: it applies the field's widget/formatting (currency, dates, translations). t-esc (and its modern alias t-out) prints any Python expression as escaped text with no formatting. t-raw prints unescaped HTML and is unsafe with user-controlled data — avoid it unless the content is trusted and already-sanitized HTML.

Why does my table row get split across two PDF pages?

wkhtmltopdf/WeasyPrint paginate strictly by page height with no per-row awareness by default. Wrap the row (or block) in page-break-inside: avoid; — see the 'Don't split this block' snippet on the Page Breaks tab.

How do I repeat my table header on every printed page?

Use display: table-header-group on the <thead> — the browser/PDF engine reprints it at the top of every new page automatically. See the "Repeat a table header" snippet on the Page Breaks tab.

How do I force a section to always start on a new page?

Insert an empty element with page-break-before: always right where the new page should start (a section title, an appendix, a second invoice copy). See the "Force a new page" snippet on the Page Breaks tab.

Why is my QWeb PDF report slow to generate or timing out?

Large t-foreach loops over many-to-many/one2many fields (e.g. hundreds of order lines) each re-evaluate their own field widgets and sub-queries — this is the most common report performance complaint. Prefetch related fields on the reporting model, keep loops flat rather than deeply nested, and avoid calling compute methods per-row inside the template.

My report shows the wrong company's data in a multi-company setup — why?

QWeb reports don't automatically filter by the current company unless the underlying recordset or a t-if="doc.company_id == user.company_id"-style guard does it. Multi-company records (bank details, fiscal position, letterhead) need an explicit company check in the template, not an implicit one.