Hello, Odoo enthusiasts and developers! Welcome back to Odoistic. Today, we’re diving into an incredibly exciting and powerful feature in Odoo 18 development: creating a custom Odoo 18 Systray Widget. Whether you’re a seasoned developer looking to enhance user experience or simply curious about customizing the Odoo backend, you’ve landed in the perfect spot.
In this comprehensive tutorial, we will walk you through building a dynamic OWL 2 widget that displays recent project tasks directly in the systray, just like native Odoo notifications. Imagine having critical task updates at your fingertips, elegantly integrated into your Odoo interface. This is not just about aesthetics; it’s about boosting productivity and ensuring vital information is always visible. Mastering the Odoo 18 Systray Widget will significantly upgrade your Odoo experience.
This guide is inspired by a detailed walkthrough available on Odoistic’s YouTube channel. You can follow along with the original video tutorial here: https://www.youtube.com/watch?v=1Gmxra8OqAI.
By the end of this guide, you will master:
- How to construct an advanced systray widget using the OWL 2 framework.
- Techniques for fetching and displaying real-time project tasks from the Odoo backend.
- Leveraging RPC controllers to establish seamless communication between your frontend and backend.
- Refining the user experience with Bootstrap for polished dropdown styling.
All these capabilities will be encapsulated within a lightweight, reusable Odoo module. So, let’s stop talking and start building this essential OWL task notification widget, a truly effective Odoo 18 Systray Widget!
Why a Custom Odoo 18 Systray Widget is a Game Changer for Your Business
In the fast-paced world of business, access to real-time, relevant information can significantly impact efficiency and decision-making. A custom Odoo 18 Systray Widget isn’t just a minor UI tweak; it’s a strategic enhancement that can revolutionize how users interact with their Odoo instance. This particular Odoo 18 Systray Widget will put crucial project tasks right where they need to be.
Imagine these scenarios:
- Project Managers: Get instant alerts for new tasks, overdue tasks, or tasks awaiting their review without navigating through various menus. Your Odoo 18 Systray Widget becomes a project nerve center.
- Sales Teams: See pending quotes or leads that require immediate action, ensuring no opportunity is missed. A custom Odoo 18 Systray Widget could highlight top priorities.
- Support Agents: Receive notifications for new support tickets or urgent customer inquiries, leading to quicker response times and improved customer satisfaction. This highlights the power of a well-implemented Odoo 18 Systray Widget.
Key benefits of integrating an Odoo Systray Widget:
- Enhanced User Experience (UX): By placing critical information in an easily accessible location (the top bar), you reduce clicks and navigation time. Users get a personalized dashboard of urgent items, leading to a smoother, more intuitive workflow. This makes the Odoo 18 Systray Widget a user-centric improvement.
- Increased Productivity: Immediate visibility of relevant tasks or notifications means users can prioritize their work effectively. No more digging through lists; the most important items are presented upfront. This direct access to vital data minimizes context switching, a notorious productivity killer. The Odoo 18 Systray Widget acts as a constant reminder of immediate priorities.
- Real-Time Insights: The ability to fetch and display data in real-time ensures that users are always working with the most current information, which is crucial for dynamic environments like project management or customer service. Our Odoo 18 Systray Widget will automatically refresh tasks, keeping data fresh and actionable.
- Customization and Flexibility: Odoo’s modular architecture, combined with OWL 2, offers unparalleled flexibility. You’re not just limited to project tasks; you can adapt this approach to display any critical data from any Odoo model. This makes the Odoo 18 Systray Widget a versatile tool for bespoke business needs.
- Modern UI Integration: OWL 2 components blend seamlessly with Odoo’s modern backend UI. Your custom widget will look and feel like an integral part of Odoo, maintaining a consistent and professional aesthetic. The Odoo 18 Systray Widget perfectly complements Odoo’s native design.
This tutorial focuses on displaying project tasks, but the underlying principles can be applied to myriad business requirements. By the end, you’ll have the foundational knowledge to extend this functionality across your Odoo deployments and build even more sophisticated Odoo 18 Systray Widget implementations.
Understanding the Architecture: Deconstructing the Odoo 18 Systray Widget
Building a sophisticated feature like a custom systray notification requires a clear understanding of its underlying architecture. The Odoo 18 Systray Widget we’re constructing relies on a robust interplay between frontend and backend components, primarily utilizing OWL 2 for the UI and Python for data handling. This architectural clarity is key to developing a powerful Odoo 18 Systray Widget.
Here’s a breakdown of the key architectural components involved in creating your Odoo 18 Systray Widget:
- OWL 2 Frontend Component (JavaScript): This is the core of our interactive widget. Written in JavaScript using the OWL 2 framework, it defines the widget’s behavior, state management, and interaction logic. It’s responsible for:
- Fetching data from the Odoo backend using Remote Procedure Calls (RPC).
- Managing its internal state (e.g., the list of tasks to display).
- Handling user interactions, such as clicking on a task to open it.
- Registering itself in the Odoo systray category, making it visible as an Odoo 18 Systray Widget.
- For more on OWL 2, refer to the Odoo OWL documentation.
- QWeb Template (XML): This XML file defines the visual structure and appearance of our OWL 2 component. It uses Odoo’s QWeb templating engine to render the HTML markup. It’s responsible for:
- Defining the button that appears in the systray.
- Structuring the dropdown menu.
- Iterating over the tasks provided by the OWL component and displaying them.
- Applying Bootstrap classes for responsive and elegant styling. This ensures your Odoo 18 Systray Widget looks polished. You can find more about Bootstrap on their official website.
- Backend Controller (Python): This is the server-side logic that acts as an API endpoint for our frontend. Written in Python, it exposes a custom route that the OWL component can call to request data. It’s responsible for:
- Querying the Odoo database for specific information (e.g., the latest project tasks).
- Processing the data and returning it in a structured format (JSON) that the frontend can easily consume.
- Ensuring proper authentication and authorization for data access. This Python component is crucial for the data behind the Odoo 18 Systray Widget.
- Odoo Module Structure: All these components are neatly organized within a standard Odoo module. This structure ensures that our widget is easily installable, manageable, and compatible with Odoo’s ecosystem. The
__manifest__.pyfile plays a crucial role in declaring dependencies and registering the frontend assets for the Odoo 18 Systray Widget.
This separation of concerns—frontend logic and presentation, backend data retrieval, and modular packaging—is fundamental to building scalable and maintainable custom features in Odoo. Now, let’s get our hands dirty and start implementing each part of this powerful Odoo 18 Systray Widget.
Step-by-Step Tutorial: Building Your Odoo 18 Systray Widget
Let’s begin by structuring our custom Odoo module. This systematic approach ensures maintainability and adherence to Odoo’s best practices for creating a robust Odoo 18 Systray Widget.
1. Setting Up the Module Structure
First, navigate to your custom Odoo addons path (e.g., ~/odoo/custom_addons/ or ~/odoo-dev/custom_modules/). Inside this directory, create your new module folder.
- Create the Module Folder:
mkdir owl_notification_widget
cd owl_notification_widget
- Create Base Files:
touch __init__.py
touch __manifest__.py
__init__.py: This file is essential for Python to recognize the folder as a package. For now, it can remain empty, but we’ll add content later.__manifest__.py: This file contains metadata about our module and defines its dependencies and assets, essential for your Odoo 18 Systray Widget.
- Create Subfolders:
mkdir controllers
mkdir static
mkdir static/description
mkdir static/src
mkdir static/src/js
mkdir static/src/xml
controllers/: Will house our Python backend controller.static/: Contains static assets like JavaScript, XML templates, and module icons.static/description/: Typically used for the module’sicon.pngoricon.svgfile, displayed in the Odoo Apps list.static/src/: Contains source files for frontend assets.static/src/js/: Where our OWL 2 JavaScript component will reside.static/src/xml/: Where our QWeb XML template will reside.
Your module structure, a blueprint for your Odoo 18 Systray Widget, should now look like this:
owl_notification_widget/
├── __init__.py
├── __manifest__.py
├── controllers/
│ └── __init__.py (will be created in step 5)
│ └── main.py (will be created in step 4)
└── static/
├── description/
│ └── icon.png (optional, will be added in step 7)
└── src/
├── js/
│ └── task_notification_widget.js (will be created in step 2)
└── xml/
└── task_notification_widget.xml (will be created in step 3)
2. Configuring the Manifest File (__manifest__.py)
Open __manifest__.py and paste the following code. This file is crucial as it tells Odoo about your module, its dependencies, and where to find its assets for the Odoo 18 Systray Widget.
{
'name': 'OWL Task Notification Widget',
'version': '1.0',
'author': 'Odoistic', # You can replace with your name
'summary': 'Displays recent project tasks in the Odoo systray using OWL 2.',
'category': 'Tools',
'depends': ['base', 'web', 'project'],
'assets': {
'web.assets_backend': [
'owl_notification_widget/static/src/js/task_notification_widget.js',
'owl_notification_widget/static/src/xml/task_notification_widget.xml',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
}
- Explanation:
name,version,author,summary,category: Standard metadata describing your module. A goodsummaryhelps users understand its purpose.depends: Specifies that this module requiresbase(core Odoo functionalities),web(for web client components and OWL support), andproject(because we’re dealing with project tasks). These modules must be installed for your custom Odoo 18 Systray Widget to function.assets: This is vital! It defines which JavaScript and XML files should be loaded into the backend asset bundle.web.assets_backendis the bundle used for the Odoo backend interface. Our JS and XML files are declared here, ensuring Odoo loads them correctly when a user accesses the backend. This is how the Odoo 18 Systray Widget components are registered.installable:Truemarks the module as installable.application:Falseindicates that this module adds functionality to existing applications rather than being a standalone application in the Apps menu.license: Specifies the module’s license.
3. Creating the JavaScript File (static/src/js/task_notification_widget.js)
This file contains the OWL 2 component logic for our Odoo 18 Systray Widget. Create task_notification_widget.js inside static/src/js/ and paste the following code:
/** @odoo-module */
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { _t } from "@web/core/l10n/translation"; // Import translation function
export class TaskNotificationWidget extends Component {
setup() {
this.state = useState({ tasks: [] }); // Reactive state to hold tasks
this.rpc = useService("rpc"); // Odoo RPC service for backend calls
this.notification = useService("notification"); // Odoo notification service
this.action = useService("action"); // Odoo action service to open records
// Bind 'this' to methods that will be used as event handlers
this.openTask = this.openTask.bind(this);
// Register lifecycle hooks
onMounted(this.onMounted);
onWillUnmount(this.onWillUnmount);
}
async onMounted() {
await this.fetchTasks(); // Fetch tasks immediately on component mount
this.timer = setInterval(this.fetchTasks.bind(this), 60000); // Refresh every minute (60000 ms)
}
onWillUnmount() {
clearInterval(this.timer); // Clean up timer to prevent memory leaks
}
async fetchTasks() {
try {
// Call the custom backend route to get tasks
this.state.tasks = await this.rpc(
"/task_notification/tasks",
{} // No parameters needed for this RPC call
);
} catch (error) {
// Display an error notification if tasks cannot be loaded
this.notification.add(_t("Could not load tasks."), {
type: "danger",
sticky: false, // Notification will disappear after a short time
});
console.error("Failed to fetch tasks for Odoo 18 Systray Widget:", error); // Log error for debugging
}
}
openTask(taskId) {
// Use the action service to open the task in a form view
this.action.doAction({
type: 'ir.actions.act_window', // Action type for opening a window
res_model: 'project.task', // Model name
res_id: taskId, // ID of the record to open
views: [[false, 'form']], // Display only the form view
target: 'current' // Open in the current window/tab
});
}
}
// Associate the OWL component with its QWeb template
TaskNotificationWidget.template = "owl_notification_widget.TaskNotificationWidget";
// Register the component in the systray category
registry.category("systray").add("owl_notification_widget.task_notification_widget", {
Component: TaskNotificationWidget,
sequence: 1, // Determines the order in the systray (lower number means further left)
});
- Explanation:
@odoo-module: A directive telling Odoo this is an OWL JavaScript module.import: Brings in necessary functionalities:Component,useState,onMounted,onWillUnmountfrom@odoo/owlfor core OWL component features.useServicefrom@web/core/utils/hooksto access Odoo services likerpc(for backend communication),notification(for user feedback), andaction(to trigger Odoo actions like opening records).registryfrom@web/core/registryto register our widget._tfor internationalization and translation.
setup(): The constructor-like method where component setup occurs.this.state = useState({ tasks: [] }): Initializes a reactive state variabletasksas an empty array. Any changes tothis.state.taskswill automatically trigger a re-render of the component.useService(...): Injects Odoo services for RPC calls, notifications, and actions, crucial for the functionality of our Odoo 18 Systray Widget.this.openTask = this.openTask.bind(this): Binds theopenTaskmethod to the component instance, ensuringthisrefers to the component when the method is used as an event handler (e.g., in a click).onMountedandonWillUnmount: Lifecycle hooks.onMountedcallsfetchTasksimmediately and sets up asetIntervalto refresh tasks every minute.onWillUnmountclears this timer to prevent memory leaks when the widget is removed.
fetchTasks(): An asynchronous method that usesthis.rpcto call our custom backend route/task_notification/tasks. It updatesthis.state.taskswith the fetched data. Includes error handling using thenotificationservice for the Odoo 18 Systray Widget.openTask(taskId): Triggered when a task in the dropdown is clicked. It usesthis.action.doActionto open theproject.taskrecord in a form view within the current Odoo window.TaskNotificationWidget.template: Links this OWL component to its visual template, defined in the XML file (next step).registry.category("systray").add(...): This is where the magic happens for the Odoo 18 Systray Widget. It registers ourTaskNotificationWidgetcomponent in thesystraycategory, making it appear in the top Odoo bar.sequence: 1determines its position (lower numbers appear further left).
4. Crafting the XML Template (static/src/xml/task_notification_widget.xml)
This XML file defines the visual representation of our Odoo 18 Systray Widget. Create task_notification_widget.xml inside static/src/xml/ and add the following code:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- Template definition for the Task Notification Widget -->
<t t-name="owl_notification_widget.TaskNotificationWidget" owl="1">
<div class="dropdown">
<!-- Button that triggers the dropdown for our Odoo 18 Systray Widget -->
<button class="btn btn-sm btn-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
title="View Latest Tasks">
<i class="fa fa-tasks"/> <!-- Font Awesome icon for tasks -->
Tasks <t t-esc="state.tasks.length"/> <!-- Displays the number of tasks -->
</button>
<!-- The actual dropdown menu content for the Odoo 18 Systray Widget -->
<div class="dropdown-menu dropdown-menu-end p-2">
<!-- Conditionally render tasks if they exist -->
<t t-if="state.tasks.length">
<!-- Loop through each task and display it as a clickable link -->
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<a class="dropdown-item" t-on-click.prevent="() => openTask(task.id)">
<t t-esc="task.name"/> <!-- Display task name -->
</a>
</t>
</t>
<!-- Fallback message if no tasks are available -->
<t t-else="">
<div class="dropdown-item text-muted">No tasks found.</div>
</t>
</div>
</div>
</t>
</templates>
- Explanation:
<templates>: The root element for QWeb templates.<t t-name="owl_notification_widget.TaskNotificationWidget" owl="1">: Defines our template. Thet-nameattribute must match theTaskNotificationWidget.templatevalue in our JavaScript file.owl="1"indicates it’s an OWL-compatible template for our Odoo 18 Systray Widget.<div class="dropdown">: A standard Bootstrap class to create a dropdown container.<button ...>: This is the interactive element in the systray.class="btn btn-sm btn-secondary dropdown-toggle": Bootstrap classes for styling a small, secondary (gray) button with dropdown toggle functionality.data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false": Essential Bootstrap attributes to enable dropdown behavior.<i class="fa fa-tasks"/>: Displays a Font Awesome task icon for better visual recognition.Tasks <t t-esc="state.tasks.length"/>: Displays the word “Tasks” followed by the current number of tasks, dynamically fetched fromthis.state.tasks.lengthin our JS component. This is the visual feedback of your Odoo 18 Systray Widget.
<div class="dropdown-menu dropdown-menu-end p-2">: The container for the dropdown’s content.dropdown-menu-endaligns it to the right,p-2adds padding.<t t-if="state.tasks.length">: A QWeb directive that conditionally renders the content inside ifstate.tasks(from our JS component) has elements.<t t-foreach="state.tasks" t-as="task" t-key="task.id">: Loops through eachtaskin thestate.tasksarray.t-keyis important for efficient rendering in OWL.<a class="dropdown-item" t-on-click.prevent="() => openTask(task.id)">: Each task is rendered as a clickable link.t-on-click.prevent: An OWL event binding. When clicked, it calls ouropenTaskJavaScript function, passing thetask.id..preventstops the default link behavior (navigating to a new page). This interaction is key for the Odoo 18 Systray Widget.<t t-esc="task.name"/>: Displays the name of the task.
<t t-else="">: Ifstate.tasks.lengthis 0, this block is rendered, showing a “No tasks found” message.
5. Creating the Backend Controller (controllers/main.py)
Now, let’s build the Python backend logic that our frontend component will communicate with to retrieve task data for the Odoo 18 Systray Widget. Create main.py inside controllers/ and paste the following:
from odoo import http
from odoo.http import request
import logging
_logger = logging.getLogger(__name__)
class TaskNotificationController(http.Controller):
"""
Odoo HTTP controller to provide recent project task data via JSON API.
This controller serves data for the OWL 2 Odoo 18 Systray Widget.
"""
@http.route('/task_notification/tasks', type='json', auth='user', methods=['GET'])
def get_tasks(self):
"""
Fetches the last 5 project tasks assigned to the current user.
Returns them as a list of dictionaries (JSON response).
This provides the data for the Odoo 18 Systray Widget.
"""
try:
# Fetch the current user's ID
current_user_id = request.session.uid
# Search for project tasks assigned to the current user
# Ordered by creation date (DESC) and limited to 5
tasks = request.env['project.task'].sudo().search(
[('user_id', '=', current_user_id)],
order='create_date DESC',
limit=5
)
# Convert project.task records into a list of dictionaries
# containing only the 'id' and 'name' for the frontend.
task_data = [{'id': task.id, 'name': task.name} for task in tasks]
_logger.info(f"Fetched {len(task_data)} tasks for user {current_user_id} for Odoo 18 Systray Widget.")
return task_data
except Exception as e:
_logger.error(f"Error fetching tasks for user {request.session.uid} for Odoo 18 Systray Widget: {e}")
# In case of an error, return an empty list or an error message
return []
- Explanation:
from odoo import httpandfrom odoo.http import request: Imports necessary modules for HTTP controllers and accessing the Odoo environment._logger = logging.getLogger(__name__): Sets up logging for debugging and error tracking.class TaskNotificationController(http.Controller):: Declares our controller class. You can define multiple routes within a single controller class.@http.route(...): This decorator defines the API endpoint for our Odoo 18 Systray Widget./task_notification/tasks: The URL path that our frontend will call.type='json': Crucially, this specifies that the route expects and returns JSON data, perfect for RPC calls from JavaScript.auth='user': Ensures that only logged-in Odoo users can access this route, providing a layer of security.methods=['GET']: Specifies that this route should be accessed using GET requests (though RPC often abstracts this).
get_tasks(self): The method that executes when the route is accessed.current_user_id = request.session.uid: Retrieves the ID of the currently logged-in user.request.env['project.task'].sudo().search(...): This is where we query the Odoo database.request.env['project.task']: Accesses theproject.taskmodel..sudo(): Temporarily elevates permissions to ensure the query can be performed even if the current user lacks specific access rights, though it’s generally good practice to design your security appropriately. For an Odoo 18 Systray Widget,sudo()might be acceptable for read operations if tasks are generally visible.search([('user_id', '=', current_user_id)], order='create_date DESC', limit=5): Searches for tasks where theuser_id(assigned user) matches thecurrent_user_id. It orders them bycreate_datein descending order (most recent first) andlimits the results to the top 5 tasks.
return [{'id': task.id, 'name': task.name} for task in tasks]: A list comprehension that converts the Odooproject.taskrecords into a list of Python dictionaries, each containing only theidandnameof the task. This simplified format is then automatically converted to JSON and sent back to the frontend, powering our Odoo 18 Systray Widget.try-exceptblock: Basic error handling to catch exceptions during task fetching and log them.
6. Updating Init Files (__init__.py)
To ensure Odoo recognizes our new Python controller, we need to update the __init__.py files.
- Create
controllers/__init__.py:
from . import main
This line tells Python to import main.py when the controllers package is loaded.
- Update top-level
__init__.py(inowl_notification_widget/):
from . import controllers
This line tells Odoo to load the controllers package (and thus main.py) when our owl_notification_widget module is loaded, completing the backend integration for your Odoo 18 Systray Widget.
7. Adding a Module Icon (Optional but Recommended)
For a professional look, add an icon to your module. This icon will appear in the Odoo Apps list.
- Place your icon: Save an image file (e.g.,
icon.pngoricon.svg, preferably 128×128 pixels) asicon.pngin thestatic/description/folder.
owl_notification_widget/
└── static/
└── description/
└── icon.png
This icon enhances the visibility and branding of your custom Odoo 18 Systray Widget within the Odoo ecosystem.
8. Installing Your Odoo 18 Systray Widget Module
With all files in place, it’s time to install and see your new widget in action!
- Restart Odoo Service: After making changes to Python files (
.py) or manifest files, you must restart your Odoo service (e.g.,sudo systemctl restart odoo.serviceor restart from your IDE). This allows Odoo to recognize the new module and its updated backend logic for the Odoo 18 Systray Widget. - Update Apps List:
- Log in to your Odoo instance as an administrator.
- Go to the “Apps” menu.
- Click on “Update Apps List” (sometimes found under the “Developer Tools” menu if not visible directly). This step ensures Odoo scans for new or updated modules.
- Search and Install:
- In the Apps search bar, type “OWL Task Notification Widget” (or whatever name you put in your
__manifest__.py). - You should see your module appear. Click on “Activate” (or “Install”).
- In the Apps search bar, type “OWL Task Notification Widget” (or whatever name you put in your
Once installed, refresh your Odoo page. You should now observe a new “Tasks” button with an icon and a number (representing the count of tasks) prominently displayed in your Odoo systray (the top bar). Clicking it will reveal a dropdown with your last five project tasks. Clicking on any task will open its form view! This custom Odoo 18 Systray Widget is now fully operational and ready to boost your productivity!
Expanding Your Odoo 18 Systray Widget
Discover more from teguhteja.id
Subscribe to get the latest posts sent to your email.

