Build a Real-Time Dashboard in Odoo with OWL and Chart.js
A step-by-step tutorial: Python model, controller, OWL component, and XML template — everything you need to add interactive charts to Odoo.

Build a Real-Time Dashboard in Odoo with OWL and Chart.js
A step-by-step tutorial: Python model, controller, OWL component, and XML template — everything you need to add interactive charts to Odoo.
Odoo gives you powerful models, views, and reports out of the box. But when a manager asks “show me production KPIs and inventory charts on one screen,” you need to build a custom dashboard.
The good news: Odoo (versions 17, 18, and 19) ships with OWL 2.x and a bundled copy of Chart.js. You don’t need npm, webpack, or any external dependency. In this tutorial, we’ll walk through building a dashboard from scratch — covering the controller, OWL component, and XML template.
By the end of this article, you’ll know how to create any OWL-based dashboard in Odoo — whether it’s for sales, HR, inventory, or manufacturing. The same approach works across Odoo 17, 18, and 19.
Architecture at a Glance
A custom dashboard in Odoo is surprisingly simple. It has four layers, each doing one job:

The data flow is straightforward: the OWL component calls the controller via fetch(), the controller queries Odoo models with read_group(), and returns JSON. Chart.js renders the data on <canvas> elements.

How the OWL component, controller, and ORM connect to serve live dashboard data.
Step 1: The Controller — Serve Chart Data
The controller is where your dashboard logic lives. It queries Odoo models and returns structured data that the frontend can render directly. Here’s a simple example:
controllers/main.py
from odoo import http
from odoo.http import request
class DashboardController(http.Controller):
@http.route(
'/my_dashboard/statistics',
type='jsonrpc', auth='user', methods=['POST'],
)
def get_statistics(self, **kwargs):
# KPI: count of confirmed sale orders
confirmed = request.env['sale.order'].search_count([
('state', '=', 'sale'),
])
# Chart: revenue grouped by month
revenue_data = request.env['sale.order'].read_group(
[('state', '=', 'sale')],
['amount_total'],
['date_order:month'],
)
return {
'kpis': {'confirmed_orders': confirmed},
'charts': {
'revenue_by_month': {
'labels': [r['date_order:month'] for r in revenue_data],
'data': [r['amount_total'] for r in revenue_data],
},
},
}
Two key Odoo ORM methods to know:
search_count()— generates aSELECT COUNT(*)query. Fast and efficient for KPIs.read_group()— generates aSELECT ... GROUP BYquery. Perfect for chart data because you get aggregated results in a single SQL call instead of looping over records in Python.
Notice the type='jsonrpc' routing — this is Odoo's preferred way to expose JSON endpoints (available in Odoo 17+). The controller automatically handles request parsing and response formatting.
Step 2: The OWL Component — Fetch and Render
The OWL component is a single JavaScript class that fetches data from the controller and renders Chart.js charts. Here is the core structure:
static/src/js/dashboard.js
/** @odoo-module **/
import { Component, useState, onWillStart,
onMounted, onWillUnmount, useRef } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { loadJS } from "@web/core/assets";
export class MyDashboard extends Component {
static template = "my_module.Dashboard";
static props = ["*"];
setup() {
this.state = useState({
loading: true,
kpis: {},
charts: {},
});
// Ref to the canvas element in the template
this.revenueChartRef = useRef("revenueChart");
this.chartInstance = null;
onWillStart(async () => {
// Load Chart.js from Odoo's bundled copy
await loadJS("/web/static/lib/Chart/Chart.js");
await this.loadData();
});
onMounted(() => this.renderChart());
onWillUnmount(() => {
if (this.chartInstance) this.chartInstance.destroy();
});
}
async loadData() {
const response = await fetch("/my_dashboard/statistics", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "call",
params: {},
}),
});
const json = await response.json();
const data = json.result || {};
this.state.kpis = data.kpis || {};
this.state.charts = data.charts || {};
this.state.loading = false;
}
renderChart() {
const canvas = this.revenueChartRef.el;
if (!canvas) return;
const data = this.state.charts.revenue_by_month;
this.chartInstance = new Chart(canvas.getContext("2d"), {
type: "bar",
data: {
labels: data.labels,
datasets: [{
label: "Revenue",
data: data.data,
backgroundColor: "#7C6CFF",
borderRadius: 6,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
},
});
}
}
// Register as Odoo client action
registry.category("actions").add("my_dashboard", MyDashboard);
Let’s break down the key patterns:
Loading Chart.js. Odoo ships Chart.js at /web/static/lib/Chart/Chart.js (available since Odoo 17). The loadJS() utility loads it once and caches it. No npm, no CDN, no asset duplication.
Canvas refs with useRef(). Instead of document.querySelector(), OWL gives you useRef(). In the template you write <canvas t-ref="revenueChart"/>, and in JS you access the DOM node via this.revenueChartRef.el. OWL manages the lifecycle for you.
Lifecycle hooks. onWillStart runs before the component renders (load data here). onMounted runs after the DOM is ready (render charts here). onWillUnmount handles cleanup (destroy Chart.js instances to prevent memory leaks).

From fetch() to canvas: how a single API call turns into KPI cards and interactive charts.
Step 3: The OWL Template — Structure the UI
The XML template defines your dashboard’s HTML structure using OWL’s template syntax:
static/src/xml/dashboard.xml
<templates>
<t t-name="my_module.Dashboard">
<div class="dashboard-container">
<h1>My Dashboard</h1>
<!-- Loading spinner -->
<div t-if="state.loading"
class="text-center p-5">
<div class="spinner-border"/>
</div>
<!-- Dashboard content -->
<div t-else="">
<!-- KPI Card -->
<div class="kpi-card">
<span class="kpi-label">Confirmed Orders</span>
<span class="kpi-value">
<t t-esc="state.kpis.confirmed_orders"/>
</span>
</div>
<!-- Chart Card -->
<div class="chart-card">
<h3>Revenue by Month</h3>
<canvas t-ref="revenueChart"/>
</div>
</div>
</div>
</t>
</templates>
The important OWL directives here:
t-if/t-else— conditional rendering (show spinner while loading)t-esc— output a value as escaped textt-ref— bind a DOM element to auseRef()in JS
If you have multiple KPIs, you can use t-foreach to loop over them dynamically instead of hard-coding each card.
Step 4: Register the Menu
The final piece is telling Odoo to show your dashboard as a menu item. You do this with a client action and a menu entry:
views/menu.xml
<record id="dashboard_action"
model="ir.actions.client">
<field name="name">My Dashboard</field>
<field name="tag">my_dashboard</field>
</record>
<menuitem id="menu_dashboard"
name="Dashboard"
parent="sale.sale_menu_root"
action="dashboard_action"
sequence="1"/>
The tag field must match the name you used in registry.category("actions").add("my_dashboard", ...). That's the glue between the Odoo menu system and your OWL component.
Step 5: Wire It Up in __manifest__.py
Register your static assets so Odoo bundles them:
manifest.py
{
'name': 'My Dashboard',
'version': '19.0.1.0.0',
'depends': ['base', 'web', 'sale'],
'data': [
'views/menu.xml',
],
'assets': {
'web.assets_backend': [
'my_module/static/src/xml/dashboard.xml',
'my_module/static/src/js/dashboard.js',
],
},
'installable': True,
'application': True,
}
Install the module, and your dashboard appears as a menu item under Sales. Click it, and you’ll see your KPI cards and Chart.js charts — all powered by live Odoo data.

The complete file structure — five files is all you need for a working dashboard.
Key Takeaways
Building a custom dashboard in Odoo comes down to four files:
- A controller that queries the ORM with
search_count()andread_group()and returns JSON. - An OWL component that calls the controller, stores data in
useState(), and renders Chart.js on<canvas>elements viauseRef(). - An XML template that defines the layout using
t-if,t-esc, andt-ref. - A menu XML that registers an
ir.actions.clientpointing to your component's registry name.
From here, you can extend this pattern with more charts (doughnut, line, radar — Chart.js supports them all), add data tables, or connect to any Odoo model. The architecture stays the same.
If this was useful, a clap or a follow helps more than you think. Got questions? Drop a comment — I read every one.
메타데이터
- post_id
- 1dc3d672e4c3
- slug
- build-a-real-time-dashboard-in-odoo-with-owl-and-chart-js-1dc3d672e4c3
- url
- https://medium.com/@eng.osama1998/build-a-real-time-dashboard-in-odoo-with-owl-and-chart-js-1dc3d672e4c3
- canonical_url
- https://medium.com/@eng.osama1998/build-a-real-time-dashboard-in-odoo-with-owl-and-chart-js-1dc3d672e4c3
- author_url
- https://medium.com/@eng.osama1998
- status
- ok
- fetched_at
- 2026-07-13 06:23:13