Skip to main content
iVentureTeam
Odoo 19

Odoo QWeb Report Guide: Build Custom PDF Reports in Odoo 19 and 20

Build an Odoo QWeb report in Odoo 19 and 20: report action, template, paperformat, currency formatting, invoice inheritance and fixes for broken PDFs.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
November 6, 202613 min read7 views
Odoo QWeb Report Guide: Build Custom PDF Reports in Odoo 19 and 20 cover

Most QWeb tutorials stop at "hello world" or still use Odoo 16 syntax. This guide builds a working Odoo custom PDF report for Odoo 19 and 20, and our Odoo customization services team uses the same pattern on client projects.

It is for developers, technical leads and IT managers who own printed documents in Odoo. If you are new to module structure, start with our guide to Odoo module development, then come back here.

What is an Odoo QWeb report?

An Odoo QWeb report is a QWeb template, bound to a report action, that Odoo renders as HTML and converts to a PDF. The same engine renders website pages, so the template uses normal QWeb control flow such as t-foreach, t-if and t-call.

The Odoo QWeb reports reference says the PDF rendering is done by wkhtmltopdf. That reference page is identical in the Odoo 19 and Odoo 20 documentation branches.

Every template gets docs (the records printed), doc_ids, doc_model, user, res_company and context_timestamp. You can open any report as a web page at /report/html/ plus the report name and a record id, the fastest way to debug.

Which files does a custom QWeb report module need?

A custom Odoo QWeb report module needs a manifest, a report action file and a template file; a paper format and a Python report model are optional. The table shows a small module named custom_invoice_report that adds a "Payment Summary" PDF to invoices.

FileWhat it contains
__manifest__.pyName, version 19.0.1.0.0 or 20.0.1.0.0, depends on account, license LGPL-3, and a data list in load order
__init__.pyImports the report folder, only if you add a Python report model
data/report_paperformat.xmlOne report.paperformat record for the custom page size and margins
report/report_actions.xmlThe ir.actions.report record that adds the Print menu entry
report/report_templates.xmlThe QWeb template with the record id named in report_name
report/payment_summary.pyOptional AbstractModel that adds computed values to the template

Load order matters: list the paper format file before the report action, because the action refers to it by XML id. Otherwise the install stops with an external id error.

How do you declare ir.actions.report in Odoo 19 and 20?

ir.actions.report: Odoo 19 vs Odoo 20 report changes

You declare an Odoo report with an ir.actions.report record that names the model, the template and the Print menu binding. The report action reference lists the fields, but it lags the source code.

In our example the record id is action_report_payment_summary. Set these fields in plain XML field tags:

  1. name: "Payment Summary", used as the file name unless print_report_name is set.
  2. model: account.move, the records the report prints.
  3. report_type: qweb-pdf for a PDF, qweb-html for a browser page or qweb-text for plain text.
  4. report_name: custom_invoice_report.report_payment_summary, the template's full XML id.
  5. print_report_name: a Python expression such as 'Payment Summary - %s' % object.name.
  6. binding_model_id: a ref to account.model_account_move, so the entry appears under Print.
  7. paperformat_id: a ref to your paper format record; without it Odoo uses the company paper format.

The table compares what the Odoo 19 and Odoo 20 source code expects.

ItemOdoo 19Odoo 20
report_file fieldExists; Odoo's own invoice action sets itRemoved from ir.actions.report; drop it
Groups fieldgroup_ids (groups_id in Odoo 18)group_ids
domain fieldLimits the Print entry to matching recordsSame
PDF enginewkhtmltopdfwkhtmltopdf by default, with a new engine hook
Layout templatesStandard, bold, boxed, striped, bubble, wave, folderStandard, wave, bubble, folder, center, dual, lines, compact
Invoice actionaccount.account_invoices with report_fileaccount.account_invoices without report_file

If your Odoo 19 module sets report_file, remove that field line before installing on Odoo 20. Odoo rejects XML that writes to a field the model no longer has.

How do you write the QWeb report template?

Write the template as a loop over docs inside web.html_container, with each record wrapped in web.external_layout and its content inside a div with class page. That structure gives you the company header, footer and page breaks.

  1. Open a template tag whose id is report_payment_summary, matching the end of report_name.
  2. Inside it, t-call web.html_container.
  3. Add a t-foreach over docs with t-as set to o.
  4. Inside the loop, t-call web.external_layout.
  5. Add a div with class page and put your headings, tables and fields there.
  6. Use t-if and t-else for optional blocks, such as a "No payments yet" line.

Use t-field for record fields, since it applies the field's formatting and translation. Use t-out for computed values; t-esc is the older name. Bootstrap classes such as row and table-sm work inside the page div.

For the customer's language, call a separate document template with t-lang set to o.partner_id.lang, as Odoo's sale order report does.

How do you set a custom paperformat for an Odoo report?

Create a report.paperformat record and link it from the report action's paperformat_id field. Without a link, Odoo uses the company's default paper format, which is A4 or US Letter in standard data.

A paper format record supports these fields:

  • format: A4, Letter, Legal and other preset sizes, or custom with page_height and page_width in mm.
  • orientation: Portrait or Landscape.
  • margin_top, margin_bottom, margin_left, margin_right: in mm.
  • header_spacing and header_line: space and an optional line under the header.
  • dpi: output resolution; Odoo's own A4 and Letter records use 90.

You cannot set both a preset format and a custom page size. Odoo's standard A4 record uses a 52 mm top margin and 52 mm header spacing; if the header overlaps the body, raise both together.

How do you format currency in an Odoo QWeb report?

Format money with t-options set to the monetary widget and a display_currency, such as o.currency_id. Odoo's own invoice template in Odoo 20 does exactly this for line subtotals and payment amounts.

The monetary converter in the Odoo 20 source rounds with the currency's rounding, places the symbol where the currency defines, and uses the reader's language for separators. A plain t-out on a float does none of that.

Three options are worth knowing:

  • display_currency: required when the value is not a monetary field, for example a computed total.
  • from_currency with date and company_id: converts the amount from one currency to another at the rate of that date.
  • decimal_places: overrides the currency's decimals, useful for unit prices.

On a monetary field, t-field already knows the currency. The widget matters most for computed values.

How do you add computed values with a custom report model?

Create an AbstractModel named report.custom_invoice_report.report_payment_summary and return the template values from _get_report_values. Odoo looks for a model named "report." plus the report_name before it builds the default values.

The catch is in the reference documentation: once you use a custom report model, Odoo no longer adds doc_ids, doc_model and docs for you. A common bug is a template that loops over docs and prints nothing, because the custom model forgot to return them.

A safe _get_report_values returns these keys:

  1. doc_ids: the docids argument as received.
  2. doc_model: account.move.
  3. docs: the browsed account.move records.
  4. Your extras: for example a dictionary of totals per invoice, built in Python.

Keep heavy logic in Python and the template for layout, and import the model file from both __init__.py files.

How do you customize the invoice report in Odoo without breaking upgrades?

Inherit account.report_invoice_document with a small template that uses xpath to add or replace only the part you need. Copying the whole invoice template into your module is the fastest way to create upgrade work.

In Odoo 20, the invoice print action points to account.report_invoice_with_payments, which calls account.report_invoice_document for the layout. To customize the invoice report in Odoo:

  1. Create a template with inherit_id set to account.report_invoice_document.
  2. Add an xpath with a precise expr, for example a div with a known name or class.
  3. Use position after, before, inside, replace or attributes.
  4. Keep each xpath short, so a change in Odoo's template breaks one block, not the whole invoice.
  5. Print the invoice in HTML mode and compare it with the standard one.

This kind of Odoo report inheritance also suits quotes and delivery slips. Our Odoo customization guide explains why small inherited changes survive upgrades better than copies.

Studio or code: which way should you customize Odoo PDF reports?

Use Studio for layout and field changes on existing reports; write a module for new reports, computed data or anything you must version. Studio is part of the Custom plan, listed on Odoo pricing at US$61.00 per user a month billed yearly in the US.

Odoo 20 improved the Studio report editor. Our Odoo 20 Studio release notes show new Editor, Preview and Sources tabs, Many2many fields in the picker and a warning on shared blocks.

NeedStudioCustom module
Add a field or move a blockFast, no developer neededPossible but slower
Brand-new report with its own actionLimitedFull control
Computed totals or data from other modelsHardPython report model
Version control and testingChanges live in the databaseCode in Git, tested before release
Odoo OnlineSupportedCustom modules are not supported
Upgrade effortStudio changes need reviewCode changes need review

Odoo Online does not support custom modules, so a coded report needs Odoo.sh or on-premise. See Odoo Studio vs custom development for the trade-off.

Why is my Odoo PDF report blank, unstyled or slow?

Most broken Odoo PDF reports come from four causes: the wrong wkhtmltopdf build, a server that cannot reach its own URL, a custom model that drops docs, or very long tables. Check them in this order.

SymptomLikely causeFix
No header or footer in the PDFwkhtmltopdf build without patched QtInstall wkhtmltopdf 0.12.6, as Odoo's install docs advise
PDF prints without styleswkhtmltopdf cannot load assets from the base URLSet web.base.url, or the report.url system parameter, to an address the server can reach
Template renders but body is emptyCustom report model does not return docsReturn doc_ids, doc_model and docs in _get_report_values
Amounts without symbol or roundingFloat printed with plain t-outAdd the monetary widget with display_currency
Report very slow on large documentswkhtmltopdf slows sharply on tables with many rowsKeep Odoo's table splitting, reduce rows or summarize
Install fails on Odoo 20report_file or old layout ids in XMLRemove report_file and retarget layout inherits

The Odoo source install guide says wkhtmltopdf must be installed manually in version 0.12.6 to support headers and footers.

For the styles issue, the Odoo 20 report code builds asset links from the report.url parameter when it is set, and from the base URL otherwise. Behind a proxy or Docker network, point report.url at an internal address such as the local Odoo port.

Which industries need custom Odoo PDF reports most?

Regulated and document-heavy industries need custom QWeb reports most, because their printed documents carry legal, tax or customer-facing data. The table shows common cases from projects like ours.

IndustryPain or cost driverOdoo approachHow iVentureTeam helps
Retail in the GCCBilingual tax receipts and invoicest-lang, RTL text and inherited invoice templatesBuild Arabic and English layouts
ManufacturingJob cards, BOM sheets and labelsCustom reports on production ordersDesign shop-floor documents
Wholesale and distributionPacking lists and delivery slips per customerInherited stock reports with paper formatsAdd customer-specific layouts
Professional servicesTimesheet-backed invoicesCustom report model with grouped hoursAdd computed summaries
ExportersProforma invoices in several currenciesMonetary widget with from_currencyHandle currency and rates

Odoo 20 also moved product, stock and manufacturing label reports onto new shared base templates. Our Odoo 20 general release notes warn that custom label templates inheriting the old QWeb will likely break.

How do you test and upgrade custom QWeb reports?

Test every custom report on real records in HTML mode first, then as a PDF, then on the next Odoo version before you upgrade. Reports fail quietly.

  • Print one record, then several records together from the list view.
  • Test empty values, long names, many lines, discounts and a second currency.
  • Print in each customer language you support.
  • Search your module for report_file, groups_id and layout ids removed in Odoo 20.
  • Re-run every inherited xpath against the new base template.

Our guide to upgrading custom Odoo modules shows how to scope report work. For a cost range on a wider project, try the Odoo project estimate tool.

How iVentureTeam helps with Odoo QWeb reports

Planning Odoo customization? Price it first

iVentureTeam designs, builds and upgrades custom Odoo QWeb reports that print correctly in every language and currency you use. We have delivered 150+ ERP projects across 35+ countries with 40+ Odoo specialists over 13+ years.

  • Report audit: we list every custom and Studio-edited report and flag what breaks in Odoo 20.
  • New reports: report actions, templates, paper formats and Python report models, in version control.
  • Invoice and document branding: inherited invoice, quote and delivery layouts that stay upgrade-friendly.
  • Upgrade fixes: report_file, group_ids and layout changes handled during your version upgrade.
  • Dedicated developers: hire an Odoo developer for ongoing report and module work.

Odoo QWeb report case study: bilingual Arabic POS receipts

A Middle East retail business needed every point of sale receipt printed in Arabic and English, without slowing the cashier. POS receipts are QWeb templates too, rendered in the browser rather than through wkhtmltopdf.

On Odoo 11, the standard POS receipt printed in a single language. Arabic-speaking customers could not read it, and audit needs favored English line items next to the regional language.

We rebuilt the POS receipt template so each element prints in both English and Arabic on one slip. Product name, taxable amount, non-taxable amount, subtotal and total were localized, with right-to-left Arabic rendered correctly on thermal printers.

Printing runs through a Raspberry Pi at each POS station, and delivery took 3 weeks. Future changes such as logos or VAT fields are template edits, not projects. Read the bilingual Arabic POS receipts case study.

The bottom line on Odoo QWeb reports

A clean Odoo QWeb report is a small module: one action, one template, an optional paper format and Python model, and inherited changes instead of copies. Build it that way and upgrades stay cheap.

Format money with the monetary widget, return docs from any custom report model, and check Odoo 20 changes such as the removed report_file field and the new layout set before you upgrade.

Ready to fix or build your Odoo reports?

Book a report review

Send us the report you need, or the one that broke after an upgrade. Talk to our Odoo report developers, call +91-90235-15208, or email business@iventureteam.com.

Frequently Asked Questions

What is a QWeb report in Odoo?

+

It is an XML template written in QWeb, bound to an ir.actions.report record. Odoo renders the template to HTML and converts it to PDF with wkhtmltopdf, so you can also open it as a web page for debugging.

How do I create a custom PDF report in Odoo 19?

+

Create a module with a report action, a QWeb template that calls web.external_layout and, if needed, a paper format. Set report_type to qweb-pdf and binding_model_id so the report appears under the Print menu.

What changed for QWeb reports in Odoo 20?

+

The report_file field was removed from ir.actions.report, and the layout set changed: bold, boxed and striped are gone, while center, dual, lines and compact are new. wkhtmltopdf is still the default engine.

How do I format currency in an Odoo QWeb report?

+

Use t-options with the monetary widget and display_currency set to the record's currency, such as o.currency_id. Odoo then rounds the amount, adds the symbol and uses the reader's number separators.

How do I customize the invoice report in Odoo?

+

Inherit account.report_invoice_document and use small xpath changes to add or replace blocks. Studio can make simple changes on the Custom plan. Avoid copying the full template, because it makes every upgrade harder.

Why does my Odoo PDF report print without styles?

+

Usually wkhtmltopdf cannot load the CSS from the server's base URL. Set web.base.url, or the report.url system parameter, to an address the Odoo server can reach, especially behind a proxy or inside Docker.

Which wkhtmltopdf version does Odoo need?

+

Odoo's install guide says to install wkhtmltopdf version 0.12.6 manually so headers and footers print. Odoo.sh and Odoo Online manage the PDF engine for you.

Can I add custom reports on Odoo Online?

+

Not as code. Odoo Online does not support custom modules, so a coded report needs Odoo.sh or on-premise. On Odoo Online you can edit reports with Studio, which is part of the Custom plan.

Free Odoo upgrade plan

Planning an Odoo upgrade?

Tell us your version, edition and custom modules. We check what breaks, what it costs to fix and the safest path, before any work begins.

Free, with no obligation. A senior consultant replies within 12 business hours.