This approach separates business logic from the QWeb template by using models.AbstractModel.
Use this method when:
- The report requires complex calculations
- Multiple models must be combined
- Aggregation or restructuring of data is required
- You want a clean and scalable architecture
Module used in this example: custom_sgeede.
1. Report Action
File: report/sale_custom_report.xml
<odoo>
<record id="action_report_custom_sale_order" model="ir.actions.report">
<field name="name">Custom Sales Order PDF (Abstract)</field>
<field name="model">sale.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">custom_sgeede.custom_sale_order_report</field>
<field name="report_file">custom_sgeede.custom_sale_order_report</field>
<field name="print_report_name">
(object.name or 'SO') + ' - Custom Abstract.pdf'
</field>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_type">report</field>
</record>
</odoo>
The structure remains the same as the basic implementation. The key difference is in the Python report class.
2. Create the Abstract Report Model
File: report/sale_custom_report.py
from odoo import models, api
class ReportSimpleSale(models.AbstractModel):
_name = 'report.custom_sgeede.simple_sale_report'
_description = 'Sale Order Report'
@api.model
def _get_report_values(self, docids, data=None):
orders = self.env['sale.order'].browse(docids)
docs = []
for order in orders:
docs.append({
'name': order.name,
'customer': order.partner_id.name,
'total': order.amount_total,
})
return {
'doc_ids': docids,
'doc_model': 'sale.order',
'docs': docs,
}
Critical structure rules:
- _name must follow this format:
report.<module_name>.<report_name>
- _get_report_values() is the entry point for QWeb.
- docs is no longer a recordset. It is a list of dictionaries.
- All data processing is handled in Python, not inside QWeb.
3. QWeb Template Using AbstractModel Data
File: report/sale_custom_report.xml
<odoo>
<template id="simple_sale_report">
<t t-call="web.external_layout">
<div class="page">
<t t-foreach="docs" t-as="o">
<h2>Sales Order</h2>
<p>
<strong>Order:</strong>
<t t-esc="o['name']"/>
</p>
<p>
<strong>Customer:</strong>
<t t-esc="o['customer']"/>
</p>
<p>
<strong>Total:</strong>
<t t-esc="o['total']"/>
</p>
<div style="page-break-after: always;"/>
</t>
</div>
</t>
</template>
</odoo>
Core difference from the non-AbstractModel approach:
Without AbstractModel:
- QWeb directly accesses sale.order recordsets.
- Business logic is mixed inside the template.
With AbstractModel:
- QWeb receives structured, ready-to-render data.
- No calculations inside the template.
- Clean separation of concerns.
- Easier refactoring.
- More testable Python logic.
5. When You Should Use AbstractModel
Use AbstractModel when:
- The report combines 2 or more different models
- Advanced grouping is required
- Manual currency conversions are needed
- Multi-company logic must be handled
- The report structure differs from the database structure
- Data originates from a wizard
In large Odoo projects, AbstractModel is the correct architectural pattern.
