TL;DR: What You Need to Know
of businesses customize invoices
average time per report template
key report types to customize
- XML-based templating: QWeb uses Odoo's XML report engine with XPath expressions
- Inherit, don't override: Always use report inheritance to preserve upgrade compatibility
- CSS + Bootstrap: Style reports with Bootstrap 5 classes and custom CSS variables
Odoo's QWeb reporting engine is the backbone of every PDF document your business generates. Invoices, delivery slips, purchase orders, quotes, and internal reports all flow through QWeb templates. Yet most businesses accept the default layouts without realizing how much customization is possible without touching core code.
Over 80% of Odoo implementations require at least minor report modifications to match company branding, add custom fields, or comply with local regulations. Understanding QWeb customization gives you full control over every document that leaves your system.
- Brand consistency: Match every report to your corporate identity
- Regulatory compliance: Add required fields for tax authorities
- Operational efficiency: Surface critical data that warehouse and accounting teams need
Understanding the QWeb Report Architecture
QWeb is Odoo's XML-based templating engine. Every PDF report follows a three-layer architecture: the report action, the template definition, and the rendering pipeline. Understanding this structure is essential before making any modifications.
1. Report Action Definition
The report action in ir.actions.report links a model to a QWeb template and specifies the output format:
<record id="action_report_invoice" model="ir.actions.report">
<field name="name">Invoices</field>
<field name="model">account.move</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">account.report_invoice_document</field>
<field name="report_file">account.report_invoice_document</field>
<field name="print_report_name">
'Invoice - %s' % (object.name or 'Draft')
</field>
</record>
The report_name points to the template that generates the actual content. The print_report_name field controls the generated filename. Changing the filename pattern helps users identify documents in download folders.
2. Template Structure
Each QWeb template is an XML record with a specific structure. The template receives the document object (e.g., an invoice record) and renders it as HTML, which is then converted to PDF by wkhtmltopdf:
<template id="report_invoice_document">
<t t-call="web.external_layout">
<t t-set="doc" t-value="doc"/>
<div class="page">
<!-- Report content here -->
</div>
</t>
</template>
The web.external_layout wrapper provides the standard header and footer. You can swap this for web.internal_layout for reports that should not include company branding.
3. The Rendering Pipeline
When a user clicks "Print," Odoo processes the template through these steps:
- Template resolution: Finds the correct template based on report action
- Data binding: Attaches the record set to the template context
- HTML generation: Processes QWeb directives (
t-if,t-foreach,t-field) - PDF conversion: wkhtmltopdf converts the HTML output to a PDF file
Understanding this pipeline helps you debug rendering issues. If your data appears correctly in the HTML preview but not in the PDF, the issue is likely in CSS compatibility with wkhtmltopdf.
- Use the preview: Debug templates in HTML mode before generating PDFs
- Check wkhtmltopdf version: Odoo 17+ requires 0.12.6 or newer
- Test with real data: Template behavior changes with empty vs populated fields
Inheriting Reports: The Safe Customization Pattern
Never modify core Odoo templates directly. Direct modifications get overwritten during module updates and break upgrade paths. Instead, use Odoo's inheritance mechanism to layer your changes on top of the standard templates.
4. XPath-Based Inheritance
XPath inheritance lets you target specific elements within a template and modify them. The most common operations are replacing, inserting before, inserting after, and setting attributes:
<template id="report_invoice_inherit"
inherit_id="account.report_invoice_document">
<!-- Replace the customer address block -->
<xpath expr="//div[@name='partner_header']" position="replace">
<div class="col-6" name="custom_partner_header">
<span t-field="o.partner_id"
t-options='{"widget": "contact",
"fields": ["name", "phone", "email"],
"no_marker": true}'/>
<p t-if="o.partner_id.vat">
<strong>VAT:</strong>
<span t-field="o.partner_id.vat"/>
</p>
</div>
</xpath>
</template>
The expr attribute is an XPath expression that locates the target element. The position attribute specifies the operation. Common positions include replace, before, after, inside, and attributes.
5. Attribute Modification
To change an element's attributes without replacing its content, use the attributes position:
<xpath expr="//table[@name='invoice_line_table']/thead/tr/th[1]"
position="attributes">
<attribute name="class">text-center fw-bold</attribute>
</xpath>
This approach is ideal for adding CSS classes, changing data attributes, or modifying visibility classes. It preserves the element's content and child structure while updating its properties.
6. Creating Entirely New Reports
When you need a completely new report (not a modification of an existing one), create a new template and register it with a new report action:
<template id="report_custom_packing_slip">
<t t-call="web.external_layout">
<div class="page">
<h2>Packing Slip:
<span t-field="o.name"/>
</h2>
<table class="table table-sm">
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Location</th>
</tr>
</thead>
<t t-foreach="o.move_line_ids" t-as="line">
<tr>
<td><span t-field="line.product_id.name"/></td>
<td><span t-field="line.qty_done"/></td>
<td><span t-field="line.location_dest_id.name"/></td>
</tr>
</t>
</table>
</div>
</t>
</template>
- Unique template IDs: Prefix with your module name to avoid collisions
- Use t-field: Always use
t-fieldinstead of raw field access for proper formatting - Test XPath expressions: Use browser dev tools to verify XPath before deploying
Customizing Invoice Templates: Practical Examples
Invoice customization is the most common QWeb modification. Businesses need to add payment terms, bank details, custom line columns, terms and conditions, and regulatory fields. Here are the most requested invoice customizations.
7. Adding Custom Columns to Invoice Lines
Standard Odoo invoices show product, description, quantity, unit price, and taxes. Many businesses need additional columns like project codes, batch numbers, or internal references:
<!-- Add a 'Project Code' column after Description -->
<xpath expr="//table[@name='invoice_line_table']/thead/tr"
position="inside">
<th class="text-start">Project Code</th>
</xpath>
<xpath expr="//table[@name='invoice_line_table']/tbody//tr"
position="inside">
<td>
<span t-field="line.analytic_distribution"
t-options='{"widget": "analytic_distribution"}'/>
</td>
</xpath>
8. Adding Payment Information and QR Codes
European businesses need SEPA QR codes on invoices. Here's how to add a payment information block:
<xpath expr="//div[@name='payment_term']" position="after">
<div class="row mt-4" name="payment_info">
<div class="col-6">
<h4>Payment Information</h4>
<p><strong>Bank:</strong>
<span t-field="o.journal_id.bank_id.name"/></p>
<p><strong>IBAN:</strong>
<span t-field="o.journal_id.bank_id.sanitized_acc_number"/></p>
<p><strong>BIC:</strong>
<span t-field="o.journal_id.bank_id.bank_bic"/></p>
</div>
</div>
</xpath>
9. Conditional Content Based on Document State
Show different content depending on the invoice state, customer type, or currency:
<t t-if="o.currency_id.name == 'USD'">
<p>All amounts are in US Dollars.</p>
</t>
<t t-if="o.partner_id.country_id.code == 'DE'">
<p>Steuernummer: <span t-field="o.company_id.vat"/></p>
</t>
<t t-if="o.payment_state == 'not_paid'">
<p class="text-danger fw-bold">PAYMENT DUE IMMEDIATELY</p>
</t>
- Condition before field: Wrap optional fields in
t-ifto hide empty values - Currency formatting: Use
t-field-optionswith{"widget": "monetary"} - Multi-language: Use
t-translation="off"for content that should not be translated
Advanced QWeb Techniques
Once you understand the basics, QWeb offers powerful features for complex report requirements. These techniques handle multi-page layouts, dynamic styling, sub-report embedding, and Python-assisted data processing.
10. Multi-Page Reports with Page Breaks
Control where pages break in PDF output using CSS page-break properties:
<div style="page-break-after: always;">
<h2>Section 1: Order Summary</h2>
<!-- Content -->
</div>
<div style="page-break-before: always;">
<h2>Section 2: Line Details</h2>
<!-- Content -->
</div>
You can also use page-break-inside: avoid on tables to prevent rows from splitting across pages. This is essential for financial reports where a single line item must stay on one page.
11. Dynamic Styling with Inline Expressions
Apply conditional styling based on data values. This is particularly useful for highlighting overdue invoices, low stock warnings, or budget overruns:
<tr t-attf-class="#{
'table-danger' if o.payment_state == 'overdue'
else 'table-success' if o.payment_state == 'paid'
else ''
}">
<td><span t-field="o.name"/></td>
<td><span t-field="o.amount_total"/></td>
</tr>
12. Calling Python Methods from Templates
You can call methods defined on the report model directly from QWeb templates:
<!-- In Python report model -->
class CustomReport(models.AbstractModel):
_name = 'report.custom.custom_report'
def _get_custom_data(self, doc):
return self.env['custom.model'].search([
('order_id', '=', doc.id)
])
<!-- In QWeb template -->
<t t-set="custom_data"
t-value="docs[0]._get_custom_data()"/>
<t t-foreach="custom_data" t-as="item">
<p><span t-field="item.name"/></p>
</t>
This pattern lets you perform complex data aggregation in Python and pass the results to the template for rendering. It keeps the template clean and moves business logic to Python code where it belongs.
- Page numbers: Use
<span class="page"/>and<span class="topage"/>for automatic page numbering - Custom fonts: Define
@font-facein the<style>block at the top of your template - Barcode generation: Use
<img t-att-src="'/report/barcode/?type=%s&value=%s' % ('Code128', o.name)"/>
Testing and Debugging QWeb Reports
Proper testing prevents broken reports from reaching production. Odoo provides several debugging tools and testing strategies for QWeb report development.
13. HTML Preview Mode
Before generating a PDF, preview your report as HTML to debug layout issues faster. Navigate to the record and select "Print" then choose "Preview in HTML" from the dropdown. The HTML preview renders instantly without wkhtmltopdf conversion, making it much faster for iterative development.
14. Debugging Template Errors
Common template errors and their solutions:
- "XPath element not found": The XPath expression didn't match any element. Check the template structure in Developer Mode and verify the element still exists in your Odoo version
- "t-field on non-record": You're trying to use
t-fieldon a value that isn't a record. Uset-escfor plain values instead - "wkhtmltopdf failed": Usually a CSS compatibility issue. Remove complex CSS selectors, gradients, or flexbox layouts that wkhtmltopdf doesn't support
- "QWebException: 'NoneType' has no attribute": A field is empty. Wrap access in
t-ifto check for existence before rendering
15. Version Control for Report Templates
Store report customizations in a dedicated module rather than editing through the Odoo interface. This enables version control, code review, and safe deployment across environments:
custom_reports/
├── __manifest__.py
├── views/
│ └── report_templates.xml
├── reports/
│ ├── __init__.py
│ └── custom_reports.py
└── static/
└── description/
└── icon.png
- Module-based approach: Keeps customizations deployable and versionable
- Upgrade testing: Test templates against new Odoo versions before upgrading production
- Backup templates: Export customized templates via Odoo's export feature before major changes
QWeb report customization in Odoo follows a clear inheritance pattern that protects upgrade compatibility. Always inherit rather than override core templates, use t-field for proper data formatting, test in HTML preview mode before PDF generation, and store customizations in dedicated modules for version control and safe deployment.
Frequently Asked Questions
Can I customize Odoo reports without coding?
Basic modifications like adding your company logo, changing colors, and adjusting header/footer text can be done through Odoo's Document Layout settings in General Settings. However, adding custom fields, changing table layouts, or creating entirely new reports requires QWeb XML customization in a custom module.
How do I find the XPath expression for an element in an Odoo report?
Activate Developer Mode, go to Settings > Technical > User Interface > Views, and search for the report template name. Open the template in XML view to see the full structure. You can also use the browser's Inspect Element on the HTML preview to identify element names and classes, then construct the XPath expression using those attributes.
Will my report customizations survive an Odoo version upgrade?
Inherited customizations in a separate module generally survive upgrades because they layer on top of the base templates. However, if Odoo changes the structure of the base template (renaming elements, removing classes), your XPath expressions may break. Test all custom reports against the new version before upgrading production.
How do I add a barcode or QR code to an Odoo report?
Use Odoo's built-in barcode report endpoint: <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', o.name, 200, 60)"/>. For QR codes, change the type parameter to 'QR'. The barcode endpoint supports Code128, EAN13, QR, and Code39 formats.
Why does my QWeb report look different in PDF vs HTML preview?
The PDF is generated by wkhtmltopdf, which uses an older WebKit rendering engine. It doesn't fully support modern CSS features like flexbox, CSS grid, or certain positioning properties. If your layout looks correct in HTML but broken in PDF, simplify the CSS by replacing flexbox with float-based layouts, avoid CSS grid, and use inline styles instead of external stylesheets.
Need Custom Reports for Your Odoo System?
Explore Odoo Skillz for pre-built report templates, customization modules, and expert guidance to get your documents looking exactly right.
References
- Odoo Documentation — Reports & QWeb Templates (2026)
- Odoo Community Association — Reporting Engine Modules (2026)
- wkhtmltopdf — HTML to PDF Converter Documentation (2026)
- Odoo Mates — Custom Report Development Guide (2025)
- Cybrosys — Odoo Report Customization Best Practices (2025)



