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.
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.
| File | What it contains |
|---|---|
| __manifest__.py | Name, 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__.py | Imports the report folder, only if you add a Python report model |
| data/report_paperformat.xml | One report.paperformat record for the custom page size and margins |
| report/report_actions.xml | The ir.actions.report record that adds the Print menu entry |
| report/report_templates.xml | The QWeb template with the record id named in report_name |
| report/payment_summary.py | Optional 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?

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:
- name: "Payment Summary", used as the file name unless print_report_name is set.
- model: account.move, the records the report prints.
- report_type: qweb-pdf for a PDF, qweb-html for a browser page or qweb-text for plain text.
- report_name: custom_invoice_report.report_payment_summary, the template's full XML id.
- print_report_name: a Python expression such as 'Payment Summary - %s' % object.name.
- binding_model_id: a ref to account.model_account_move, so the entry appears under Print.
- 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.
| Item | Odoo 19 | Odoo 20 |
|---|---|---|
| report_file field | Exists; Odoo's own invoice action sets it | Removed from ir.actions.report; drop it |
| Groups field | group_ids (groups_id in Odoo 18) | group_ids |
| domain field | Limits the Print entry to matching records | Same |
| PDF engine | wkhtmltopdf | wkhtmltopdf by default, with a new engine hook |
| Layout templates | Standard, bold, boxed, striped, bubble, wave, folder | Standard, wave, bubble, folder, center, dual, lines, compact |
| Invoice action | account.account_invoices with report_file | account.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.
- Open a template tag whose id is report_payment_summary, matching the end of report_name.
- Inside it, t-call web.html_container.
- Add a t-foreach over docs with t-as set to o.
- Inside the loop, t-call web.external_layout.
- Add a div with class page and put your headings, tables and fields there.
- 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:
- doc_ids: the docids argument as received.
- doc_model: account.move.
- docs: the browsed account.move records.
- 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:
- Create a template with inherit_id set to account.report_invoice_document.
- Add an xpath with a precise expr, for example a div with a known name or class.
- Use position after, before, inside, replace or attributes.
- Keep each xpath short, so a change in Odoo's template breaks one block, not the whole invoice.
- 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.
| Need | Studio | Custom module |
|---|---|---|
| Add a field or move a block | Fast, no developer needed | Possible but slower |
| Brand-new report with its own action | Limited | Full control |
| Computed totals or data from other models | Hard | Python report model |
| Version control and testing | Changes live in the database | Code in Git, tested before release |
| Odoo Online | Supported | Custom modules are not supported |
| Upgrade effort | Studio changes need review | Code 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.
| Symptom | Likely cause | Fix |
|---|---|---|
| No header or footer in the PDF | wkhtmltopdf build without patched Qt | Install wkhtmltopdf 0.12.6, as Odoo's install docs advise |
| PDF prints without styles | wkhtmltopdf cannot load assets from the base URL | Set web.base.url, or the report.url system parameter, to an address the server can reach |
| Template renders but body is empty | Custom report model does not return docs | Return doc_ids, doc_model and docs in _get_report_values |
| Amounts without symbol or rounding | Float printed with plain t-out | Add the monetary widget with display_currency |
| Report very slow on large documents | wkhtmltopdf slows sharply on tables with many rows | Keep Odoo's table splitting, reduce rows or summarize |
| Install fails on Odoo 20 | report_file or old layout ids in XML | Remove 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.
| Industry | Pain or cost driver | Odoo approach | How iVentureTeam helps |
|---|---|---|---|
| Retail in the GCC | Bilingual tax receipts and invoices | t-lang, RTL text and inherited invoice templates | Build Arabic and English layouts |
| Manufacturing | Job cards, BOM sheets and labels | Custom reports on production orders | Design shop-floor documents |
| Wholesale and distribution | Packing lists and delivery slips per customer | Inherited stock reports with paper formats | Add customer-specific layouts |
| Professional services | Timesheet-backed invoices | Custom report model with grouped hours | Add computed summaries |
| Exporters | Proforma invoices in several currencies | Monetary widget with from_currency | Handle 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

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?

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.
Free Odoo tools
All tools- Odoo Health CheckVersion, modules, findings and upgrade cost
- Odoo App SelectorWhich apps you need, by industry
- Odoo Project EstimateIndicative cost and timeline in minutes
- Odoo Implementation Cost CalculatorImplementation cost and timeline, in minutes
- Odoo Migration Cost CalculatorVersion jump or legacy ERP, priced in minutes
Continue reading
Odoo Sales Order Approval: Thresholds, Studio Rules and Audit Trail
Odoo sales order approval for Odoo 19 and 20: Studio approval rules on Confirm, amount thresholds, purchase approval, custom modules and audit trail.
Odoo 19 vs Odoo 20: Which to Choose for a New or Running Project
Odoo 19 vs Odoo 20 for a new or running project: a decision framework by go-live date, modules and customization level, plus switch costs and support.
Odoo eCommerce Design: 15 Practices That Lift Conversion in Odoo 19/20
Odoo eCommerce design guide for Odoo 19 and 20: 15 shop, product page, variant, checkout, speed, SEO and mobile practices that lift conversion.
