← Back to list

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.

Osama Alhalabi · 2026-03-26 08:22 · 53 claps · 5.1 min read
#odoo #odoo-erp #odoo-17 #odoo18 #odoo-19
Open on Medium ↗
Wiki topics: 🎬 · Film & Television

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.

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 a SELECT COUNT(*) query. Fast and efficient for KPIs.
  • read_group() — generates a SELECT ... GROUP BY query. 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.

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 text
  • t-ref — bind a DOM element to a useRef() 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.

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() and read_group() and returns JSON.
  • An OWL component that calls the controller, stores data in useState(), and renders Chart.js on <canvas> elements via useRef().
  • An XML template that defines the layout using t-if, t-esc, and t-ref.
  • A menu XML that registers an ir.actions.client pointing 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