Odoo's reporting engine is powered by QWeb, the same XML-based template system that drives its web views. And just like views, reports can be inherited: you can extend or modify them from a custom module without ever editing core code. This is essential for keeping your customizations upgrade-safe.
In this post, we'll walk through exactly how report inheritance works in Odoo 19, from the basics to practical real-world patterns.
Every printable report in Odoo is made up of two parts:
- An ir.actions.report record – defines the report name, model, paper format, and which template to render.
- A QWeb template (an ir.ui.view of type qweb) – the actual HTML/XML layout rendered into PDF.
To inherit a report, you target the QWeb template, not the action record. You do this exactly the way you'd inherit a form or list view using inherit_id.
The Basic Inheritance Pattern
Step 1 – Find the template to inherit
First, identify the external ID of the report template you want to extend. For example, Odoo's standard invoice report template is: account.report_invoice_document
You can find this by enabling developer mode and inspecting the report under Settings > Technical > Reports, or by searching the source XML in the relevant Odoo module.
Step 2 – Create the inherited view in XML
<odoo>
<template id="custom_invoice_report"
inherit_id="account.report_invoice_document">
<!-- XPath to target a specific node -->
<xpath expr="//div[@class='page']" position="inside">
<div class="row">
<div class="col-12">
<p>Custom note: <t t-esc="o.custom_field_id.name"/></p>
</div>
</div>
</xpath>
</template>
</odoo>
Use your module's name as a prefix for your template id to avoid conflicts – e.g., my_module.custom_invoice_report.
XPath Position Attributes
The position attribute on your <xpath> node controls how your content is inserted relative to the matched node:
- inside – appends your content as the last child inside the matched element.
- before – inserts your content immediately before the matched element.
- after – inserts your content immediately after the matched element.
- replace – completely replaces the matched element with your content.
- attributes – modifies attribute values on the matched element (useful for adding CSS classes).
Example – adding a class attribute
<xpath expr="//table[@class='table table-sm']"
position="attributes">
<attribute name="class">table table-sm table-bordered</attribute>
</xpath>
Adding a Custom Field to the Sale Order Report
Say you've added a field delivery_notes to sale.order and want it to appear on the printed order. Here's the full inheritance:
<odoo>
<template id="sale_order_custom_notes"
inherit_id="sale.report_saleorder_document">
<!-- Insert after the order information table -->
<xpath expr="//p[@name='payment_term']" position="after">
<t t-if="doc.delivery_notes">
<div class="row mt-3">
<div class="col-12">
<strong>Delivery Notes:</strong>
<p><t t-esc="doc.delivery_notes"/></p>
</div>
</div>
</t>
</xpath>
</template>
</odoo>
Notice the t-if guard – it's always good practice to hide sections when the field is empty, keeping your printed output clean.
Inheriting Sub-Templates (t-call)
Many Odoo reports are composed of multiple sub-templates called with <t t-call="..."/>. For example, the invoice report calls a shared address block, a lines table, and a tax summary all separate named templates.
You can inherit any of those sub-templates directly by using their external ID as your inherit_id. This is far more surgical than inheriting the top-level document template.
<!-- Inheriting the invoice tax totals sub-template -->
<template id="custom_invoice_tax_totals"
inherit_id="account.document_tax_totals">
<xpath expr="//t[@t-foreach]" position="after">
<tr>
<td colspan="2">
<em>All prices include applicable duties.</em>
</td>
</tr>
</xpath>
</template>
Odoo 19 continues to support the report.layout mechanism for global styling – inherit from web.report_layout only if you're making layout-wide changes like header/footer branding across all reports.
Don't forget to declare your XML file in __manifest__.py:
'data': [
'views/report_templates.xml',
],
Reports go under data, not assets. If you need custom CSS for a report, that goes under assets separately but the template XML itself is always in data.
Report inheritance in Odoo 19 follows the same clean pattern as view inheritance find the template ID, write an XPath, and declare your position. The payoff is significant: your customizations stay isolated in your own module, making upgrades and maintenance far easier than copy-pasting core templates.
Whether you're adding a custom field, tweaking the layout, or inserting conditional blocks, XPath-based inheritance gives you precise control without the risks of direct modification.
To read more about Overview of Different Types of Inheritance in Odoo 19, refer to our blog Overview of Different Types of Inheritance in Odoo 19.