In Odoo 18, dynamic reports are powerful and flexible tools that give detailed look into different areas of business. Unlike static reports that show fixed data, dynamic reports allow users to interact with and customize the data in real time by adjusting filters, choosing specific metrics, and modifying visualizations. This makes it easier to explore the data from various perspectives and uncover insights that support smarter, faster decision-making .
Let's walk through the process of creating a dynamic report in Odoo 18.
To get started, create a new menu by defining it in an XML file located in the views directory, typically named views.xml.
Module structure
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Purchase report action and menu -->
<record id="dynamic_purchase_report_action" model="ir.actions.client">
<field name="name">Purchase Report</field>
<field name="tag">purchase_report</field>
</record>
<menuitem id="dynamic_purchase_report_menu"
name=" Dynamic Purchase Report"
parent="purchase.purchase_report"
action="dynamic_purchase_report_action"/>
</odoo>
In this configuration, the "Dynamic Purchase Report" menu is added under the Reporting section of the Purchase module.

In this step, a record named "Purchase Report" is created under ir.actions.client. The tag field is set to "purchase_report", which matches the widget's tag used to trigger the action when the corresponding menu item is clicked.Next, define the JavaScript file within the static/src/js directory. This file will handle the client-side logic and behavior for the dynamic report interface.
/** @odoo-module */
const { Component } = owl;
import { registry } from "@web/core/registry";
import { download } from "@web/core/network/download";
import { useService } from "@web/core/utils/hooks";
import { useRef, useState } from "@odoo/owl";
import { BlockUI } from "@web/core/ui/block_ui";
const actionRegistry = registry.category("actions");
import { uiService } from "@web/core/ui/ui_service";
// Extending components for adding purchase report class
class PurchaseReport extends Component {
async setup() {
super.setup(...arguments);
this.uiService = useService('ui');
this.initial_render = true;
this.orm = useService('orm');
this.action = useService('action');
this.start_date = useRef('date_from');
this.end_date = useRef('date_to');
this.order_by = useRef('order_by');
this.state = useState({
order_line: [],
data: null,
order_by : 'report_by_order',
wizard_id : []
});
this.load_data();
}
async load_data(wizard_id = null) {
/**
* Loads the data for the purchase report.
*/
let move_lines = ''
try {
if(wizard_id == null){
this.state.wizard_id = await this.orm.create("dynamic.purchase.report",[{}]);
}
this.state.data = await this.orm.call("dynamic.purchase.report", "purchase_report", [this.state.wizard_id]);
if (Array.isArray(this.state.data)) {
this.state.order_line = this.state.data.map(item => {
return item;
});
} else if (typeof this.state.data === 'object' && this.state.data !== null) {
this.state.order_line = this.state.data.report_lines;
}
}
catch (el) {
window.location.href;
}
}
async applyFilter(ev) {
let filter_data = {}
this.state.order_by = this.order_by.el.value
filter_data.date_from = this.start_date.el.value
filter_data.date_to = this.end_date.el.value
filter_data.report_type = this.order_by.el.value
let data = await this.orm.write("dynamic.purchase.report",this.state.wizard_id, filter_data);
this.load_data(this.state.wizard_id)
}
viewPurchaseOrder(ev) {
return this.action.doAction({
type: "ir.actions.act_window",
res_model: 'purchase.order',
res_id: parseInt(ev.target.id),
views: [[false, "form"]],
target: "current",
});
}
async print_xlsx() {
/**
* Generates and downloads an XLSX report for the purchase orders.
*/
var data = this.state.data
var action = {
'data': {
'model': 'dynamic.purchase.report',
'options': JSON.stringify(data['orders']),
'output_format': 'xlsx',
'report_data': JSON.stringify(data['report_lines']),
'report_name': 'Purchase Report',
'dfr_data': JSON.stringify(data),
},
};
this.uiService.block();
await download({
url: '/purchase_dynamic_xlsx_reports',
data: action.data,
complete: this.uiService.unblock(),
error: (error) => this.call('crash_manager', 'rpc_error', error),
});
}
async printPdf(ev) {
/**
* Generates and displays a PDF report for the purchase orders.
*
* @param {Event} ev - The event object triggered by the action.
* @returns {Promise} - A promise that resolves to the result of the action.
*/
ev.preventDefault();
var self = this;
var action_title = self.props.action.display_name;
return self.action.doAction({
'type': 'ir.actions.report',
'report_type': 'qweb-pdf',
'report_name': 'purchase_report_generator.purchase_order_report',
'report_file': 'purchase_report_generator.purchase_order_report',
'data': {
'report_data': this.state.data
},
'context': {
'active_model': 'purchase.report',
'landscape': 1,
'purchase_order_report': true
},
'display_name': 'Purchase Order',
});
}
}
PurchaseReport.template = 'PurchaseReport';
actionRegistry.add("purchase_report", PurchaseReport);
In this section, we extend the AbstractAction class to display the PurchaseReport model. The load_data function is called within the start method and uses an RPC call to retrieve data from a corresponding Python function. The data returned is then passed to the PurchaseReport model and rendered through the table_view_pr class, which is part of the main PurchaseReport component.
To support the RPC call from the JavaScript file, a corresponding QWeb model is defined in a Python file located in the models directory, such as models/filename.py.
import json
from odoo import http
from odoo.http import content_disposition, request
from odoo.tools import html_escape
class TBXLSXReportController(http.Controller):
""" This endpoint generates and provides an XLSX report in response to an
HTTP POST request. The function uses the provided data to create the
report and returns it as an XLSX file. """
@http.route('/purchase_dynamic_xlsx_reports', type='http', auth='user',
methods=['POST'], csrf=False)
def get_report_xlsx(self, model, options, output_format, report_data,
report_name, dfr_data, **kw):
""" Endpoint for getting xlsx report """
uid = request.session.uid
report_obj = request.env[model].with_user(uid)
dfr_data = dfr_data
options = options
token = 'dummy-because-api-expects-one'
try:
if output_format == 'xlsx':
response = request.make_response(
None,
headers=[
('Content-Type', 'application/vnd.ms-excel'),
('Content-Disposition',
content_disposition(report_name + '.xlsx'))
]
)
report_obj.get_purchase_xlsx_report(options, response,
report_data, dfr_data)
response.set_cookie('fileToken', token)
return response
except Exception as e:
se = 0
error = {
'code': 200,
'message': 'Odoo Server Error',
'data': se
}
return request.make_response(html_escape(json.dumps(error)))
After defining the main Python function, additional helper functions can be added to execute simple queries. The results from these queries are collected row by row into a list, which is then returned as the final output.
Custom model
def _get_report_sub_lines(self, data):
"""Function for get report value using sql query"""
report_sub_lines = []
if data.get('report_type') == 'report_by_order':
query = """ select l.name,l.date_order, l.partner_id,l.amount_total,
l.notes,l.user_id,res_partner.name as partner,
res_users.partner_id as user_partner,sum(purchase_order_line.product_qty),
l.id as id,(SELECT res_partner.name as salesman FROM res_partner
WHERE res_partner.id = res_users.partner_id) from purchase_order as l
left join res_partner on l.partner_id = res_partner.id
left join res_users on l.user_id = res_users.id
left join purchase_order_line on l.id = purchase_order_line.order_id
where 1=1 """
if data.get('date_from'):
query += """and l.date_order >= '%s' """ % data.get('date_from')
if data.get('date_to'):
query += """ and l.date_order <= '%s' """ % data.get('date_to')
query += """group by l.user_id,res_users.partner_id,res_partner.name,
l.partner_id,l.date_order,l.name,l.amount_total,l.notes,l.id"""
self._cr.execute(query)
report_by_order = self._cr.dictfetchall()
report_sub_lines.append(report_by_order)
elif data.get('report_type') == 'report_by_order_detail':
query = """ select l.name,l.date_order,l.partner_id,l.amount_total,
l.notes, l.user_id,res_partner.name as partner,res_users.partner_id
as user_partner,sum(purchase_order_line.product_qty),
purchase_order_line.name as product, purchase_order_line.price_unit,
purchase_order_line.price_subtotal,l.amount_total,
purchase_order_line.product_id,product_product.default_code,
(SELECT res_partner.name as salesman FROM res_partner WHERE
res_partner.id = res_users.partner_id)from purchase_order as l
left join res_partner on l.partner_id = res_partner.id
left join res_users on l.user_id = res_users.id
left join purchase_order_line on l.id = purchase_order_line.order_id
left join product_product on purchase_order_line.product_id = product_product.id
where 1=1 """
if data.get('date_from'):
query += """ and l.date_order >= '%s' """ % data.get('date_from')
if data.get('date_to'):
query += """ and l.date_order <= '%s' """ % data.get('date_to')
query += """group by l.user_id,res_users.partner_id,res_partner.name,
l.partner_id,l.date_order,l.name,l.amount_total,l.notes,
purchase_order_line.name,purchase_order_line.price_unit,
purchase_order_line.price_subtotal,l.amount_total,
purchase_order_line.product_id,product_product.default_code"""
self._cr.execute(query)
report_by_order_details = self._cr.dictfetchall()
report_sub_lines.append(report_by_order_details)
elif data.get('report_type') == 'report_by_product':
query = """ select l.amount_total,sum(purchase_order_line.product_qty)
as qty, purchase_order_line.name as product, purchase_order_line.price_unit,
product_product.default_code,product_category.name from purchase_order
as l left join purchase_order_line on l.id = purchase_order_line.order_id
left join product_product on purchase_order_line.product_id = product_product.id
left join product_template on purchase_order_line.product_id = product_template.id
left join product_category on product_category.id = product_template.categ_id
where 1=1 """
if data.get('date_from'):
query += """and l.date_order >= '%s' """ % data.get('date_from')
if data.get('date_to'):
query += """ and l.date_order <= '%s' """ % data.get('date_to')
query += """group by l.amount_total,purchase_order_line.name,
purchase_order_line.price_unit,purchase_order_line.product_id,
product_product.default_code,product_template.categ_id,
product_category.name"""
self._cr.execute(query)
report_by_product = self._cr.dictfetchall()
report_sub_lines.append(report_by_product)
elif data.get('report_type') == 'report_by_categories':
query = """ select product_category.name,sum(l.product_qty) as qty,
sum(l.price_subtotal) as amount_total from purchase_order_line as l
left join product_template on l.product_id = product_template.id
left join product_category on product_category.id = product_template.categ_id
left join purchase_order on l.order_id = purchase_order.id
where 1=1 """
if data.get('date_from'):
query += """and pos_order.date_order >= '%s' """ % data.get(
'date_from')
if data.get('date_to'):
query += """ and pos_order.date_order <= '%s' """ % data.get(
'date_to')
query += "group by product_category.name"
self._cr.execute(query)
report_by_categories = self._cr.dictfetchall()
report_sub_lines.append(report_by_categories)
elif data.get('report_type') == 'report_by_purchase_representative':
query = """ select res_partner.name,sum(purchase_order_line.product_qty)
as qty,sum(purchase_order_line.price_subtotal) as amount,count(l.id)
as order from purchase_order as l left join res_users on
l.user_id = res_users.id left join res_partner on
res_users.partner_id = res_partner.id left join purchase_order_line
on l.id = purchase_order_line.order_id where 1=1 """
if data.get('date_from'):
query += """ and l.date_order >= '%s' """ % data.get('date_from')
if data.get('date_to'):
query += """ and l.date_order <= '%s' """ % data.get('date_to')
query += "group by res_partner.name"
self._cr.execute(query)
report_by_purchase_representative = self._cr.dictfetchall()
report_sub_lines.append(report_by_purchase_representative)
elif data.get('report_type') == 'report_by_state':
query = """ select l.state,sum(purchase_order_line.product_qty) as
qty,sum(purchase_order_line.price_subtotal) as amount,
count(l.id) as order from purchase_order as l
left join res_users on l.user_id = res_users.id left join
res_partner on res_users.partner_id = res_partner.id
left join purchase_order_line on l.id = purchase_order_line.order_id
where 1=1 """
if data.get('date_from'):
query += """and so.date_order >= '%s' """ % data.get('date_from')
if data.get('date_to'):
query += """ and so.date_order <= '%s' """ % data.get('date_to')
query += "group by l.state"
self._cr.execute(query)
report_by_state = self._cr.dictfetchall()
report_sub_lines.append(report_by_state)
return report_sub_lines
def _get_report_values(self, data):
"""Get report values based on the provided data."""
docs = data['model']
if data.get('report_type'):
report_res = \
self._get_report_sub_lines(data)[0]
else:
report_res = self._get_report_sub_lines(data)
return {
'doc_ids': self.ids,
'docs': docs,
'PURCHASE': report_res,
}
As a result, the RPC call will wrap this list and send the data to the QWeb model. Next, define the templates in the static/src/xml directory to handle the structure and display of the report in the UI.
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="PurchaseReport" owl="1">
<!-- Section contains a structure for the purchase report, including a filter
view and a table view. It has div elements for the filter view and table view,
with respective classes for styling. -->
<div class="">
<div>
<center>
<h1 style="margin: 20px;">Purchase Report</h1>
</center>
</div>
</div>
<div class="print-btns">
<div class="sub_container_left"
style="width: 285px; margin-left: 36px;">
<div class="report_print">
<button type="button" class="btn btn-primary" id="pdf"
style="float: left; margin-right: 9px;"
t-on-click="printPdf">
Print (PDF)
</button>
<button type="button" class="btn btn-primary" id="xlsx"
t-on-click="print_xlsx">
Export (XLSX)
</button>
</div>
</div>
<br/>
<div class="sub_container_right">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle time_range_pr"
type="button" id="date_chose"
data-bs-toggle="dropdown" aria-expanded="false">
<span class="fa fa-calendar" title="Dates" role="img"
aria-label="Dates"/>
Date Range
</button>
<div class="dropdown-menu my_custom_dropdown" role="menu"
aria-labelledby="date_chose">
<div class="form-group">
<label class="" for="date_from">Start Date :</label>
<div class="input-group date" id="date_from"
data-target-input="nearest">
<input type="date" name="date_from"
t-ref="date_from"
class="form-control datetimepicker-input"
data-target="#date_from"
t-att-name="prefix"/>
<div class="input-group-append"
data-target="#date_from"
data-toggle="datetimepicker"
style="pointer-events: none;">
</div>
</div>
<label class="" for="date_to">End Date :</label>
<div class="input-group date" id="date_to"
data-target-input="nearest">
<input type="date" name="date_to"
t-ref="date_to"
class="form-control datetimepicker-input"
data-target="#date_to"
t-att-name="prefix"/>
<div class="input-group-append"
data-target="#date_to"
data-toggle="datetimepicker"
style="pointer-events: none;">
</div>
</div>
</div>
</div>
</div>
<div class="search-Result-Selection">
<div class="dropdown">
<a class="btn btn-secondary dropdown-togglereport-type"
href="#" role="button" id="dropdownMenuLink"
data-bs-toggle="dropdown" aria-expanded="false">
<span class="fa fa-book"/>
<span class="low_case dropdown-toggle">Report Type
:
</span>
</a>
<select id="selection" class="dropdown-menu report_type"
aria-labelledby="dropdownMenuLink"
t-ref="order_by"
name="states[]">
<div role="separator" class="dropdown-divider"/>
<option value="report_by_order" selected="">Report
By Order
</option>
<option value="report_by_order_detail">Report By
Order Detail
</option>
<option value="report_by_product">Report By
Product
</option>
<option value="report_by_categories">Report By
Categories
</option>
<option value="report_by_purchase_representative">
Report By Purchase Representative
</option>
<option value="report_by_state">Report By
State
</option>
</select>
<span id="report_res" t-out="state.order_by"/>
</div>
</div>
<div class="apply_filter">
<button type="button" id="apply_filter"
class="btn btn-primary" t-on-click="applyFilter">
Apply
</button>
</div>
</div>
</div>
<div class="overflow-auto" style="height: 70vh; padding:10px">
<div t-if="state.order_by == 'report_by_order'">
<div class="table_main_view">
<table cellspacing="0" width="100%">
<thead>
<tr class="table_pr_head">
<th>Order</th>
<th class="mon_fld">Date Order</th>
<th class="mon_fld">Customer</th>
<th class="mon_fld">Purchase Representative</th>
<th class="mon_fld">Total Qty</th>
<th class="mon_fld">Amount Total</th>
<th class="mon_fld">Note</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.order_line"
t-as="dynamic_purchase_report"
t-key="dynamic_purchase_report_index">
<tr style="border: 1.5px solid black;"
class="pr-line"
t-att-data-account-id="dynamic_purchase_report['id']"
t-attf-data-target=".a{{dynamic_purchase_report['id']}}">
<td>
<t t-if="dynamic_purchase_report['id']">
<div class="dropdown dropdown-toggle">
<a data-toggle="dropdown"
href="#"
id="table_toggle_btn"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="caret"/>
<span>
<t t-esc="dynamic_purchase_report['name']"/>
</span>
</a>
<ul class="dropdown-menu"
role="menu"
aria-labelledby="table_toggle_btn">
<li>
<a class="view_purchase_order"
tabindex="-1"
href="#"
t-att-id="dynamic_purchase_report['id']"
t-on-click="viewPurchaseOrder">
View Purchase Order
</a>
</li>
</ul>
</div>
</t>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['date_order']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['partner']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['salesman']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['sum']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['amount_total']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['notes']"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<!--Report for order detail-->
<div t-if="state.order_by == 'report_by_order_detail'">
<div class="table_main_view">
<table cellspacing="0" width="100%">
<thead>
<tr class="table_pr_head">
<th>Order</th>
<th class="mon_fld">Date Order</th>
<th class="mon_fld">Customer</th>
<th class="mon_fld">Purchase Representative</th>
<th class="mon_fld">Product Code</th>
<th class="mon_fld">Product Name</th>
<th class="mon_fld">Price unit</th>
<th class="mon_fld">Qty</th>
<th class="mon_fld">Price Subtotal</th>
</tr>
</thead>
<tbody>
<t t-log="state.order_line"/>
<t t-foreach="state.order_line"
t-as="dynamic_purchase_report"
t-key="dynamic_purchase_report_index">
<tr style="border: 1.5px solid black;"
class="pr-line"
t-att-data-account-id="dynamic_purchase_report['id']"
t-attf-data-target=".a{{dynamic_purchase_report['id']}}">
<td>
<div class="dropdown dropdown-toggle">
<a data-toggle="dropdown" href="#"
id="table_toggle_btn"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="caret"/>
<span>
<t t-esc="dynamic_purchase_report['name']"/>
</span>
</a>
<ul class="dropdown-menu"
role="menu"
aria-labelledby="table_toggle_btn">
<li>
<a class="view_purchase_order"
tabindex="-1" href="#"
t-att-id="dynamic_purchase_report['id']">
View Purchase Order
</a>
</li>
</ul>
</div>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['date_order']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['partner']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['salesman']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['default_code']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['product']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['price_unit']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['sum']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['price_subtotal']"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<!-- Report for product -->
<div t-if="state.order_by == 'report_by_product'">
<div class="table_main_view">
<table cellspacing="0" width="100%">
<thead>
<tr class="table_pr_head">
<th>Category</th>
<th class="mon_fld">Product Code</th>
<th class="mon_fld">Product Name</th>
<th class="mon_fld">Qty</th>
<th class="mon_fld">Amount Total</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.order_line"
t-as="dynamic_purchase_report"
t-key="dynamic_purchase_report_index">
<tr style="border: 1.5px solid black;"
class="pr-line"
t-att-data-account-id="dynamic_purchase_report['id']"
t-attf-data-target=".a{{dynamic_purchase_report['id']}}">
<td>
<span>
<t t-esc="dynamic_purchase_report['name']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['default_code']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['product']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['qty']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['amount_total']"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<!-- Report for Categories -->
<div t-if="state.order_by == 'report_by_categories'">
<div class="table_main_view">
<table cellspacing="0" width="100%">
<thead>
<tr class="table_pr_head">
<th>Category</th>
<th class="mon_fld">Qty</th>
<th class="mon_fld">Amount Total</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.order_line"
t-as="dynamic_purchase_report"
t-key="dynamic_purchase_report_index">
<tr style="border: 1.5px solid black;"
class="pr-line"
t-att-data-account-id="dynamic_purchase_report['id']"
t-attf-data-target=".a{{dynamic_purchase_report['id']}}">
<td style="border: 0px solid black;">
<t t-esc="dynamic_purchase_report['name']"/>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['qty']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['amount_total']"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<!-- Report for purchase_representative -->
<div t-if="state.order_by == 'report_by_purchase_representative'">
<div class="table_main_view">
<table cellspacing="0" width="100%">
<thead>
<tr class="table_pr_head">
<th>Purchase Representative</th>
<th class="mon_fld">Total Order</th>
<th class="mon_fld">Total Qty</th>
<th class="mon_fld">Total Amount</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.order_line"
t-as="dynamic_purchase_report"
t-key="dynamic_purchase_report_index">
<tr style="border: 1.5px solid black;"
class="pr-line"
data-toggle="collapse"
t-att-data-account-id="dynamic_purchase_report['id']"
t-attf-data-target=".a{{dynamic_purchase_report['id']}}">
<td style="border: 0px solid black;">
<span>
<t t-esc="dynamic_purchase_report['name']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['order']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['qty']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['amount']"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<!-- Report for State -->
<div t-if="state.order_by == 'report_by_state'">
<div class="table_main_view">
<table cellspacing="0" width="100%">
<thead>
<tr class="table_pr_head">
<th>State</th>
<th class="mon_fld">Total Count</th>
<th class="mon_fld">Quantity</th>
<th class="mon_fld">Amount</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.order_line"
t-as="dynamic_purchase_report"
t-key="dynamic_purchase_report_index">
<tr style="border: 1.5px solid black;"
class="pr-line"
data-toggle="collapse"
t-att-data-account-id="dynamic_purchase_report['id']"
t-attf-data-target=".a{{dynamic_purchase_report['id']}}">
<td style="border: 0px solid black;">
<span>
<t t-esc="dynamic_purchase_report['state']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['order']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['qty']"/>
</span>
</td>
<td style="text-align:center;">
<span>
<t t-esc="dynamic_purchase_report['amount']"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</div>
</t>
</templates>
This overview of the dynamic purchase report includes multiple filter options for refining the displayed data.

This view displays the products along with their filtered details, and it also provides an option to filter the data by date range.
Dynamic reporting in Odoo 18 enables businesses to harness the full power of their data through flexible, customizable, and interactive reporting capabilities.
To read more about How to Create a Dynamic Report in Odoo 17, refer to our blog How to Create a Dynamic Report in Odoo 17.