Â
Source Video: OWL básico en Odoo 18 | Paso a paso
Master Odoo 18 OWL Favorites: Unlock Powerful UX Enhancements
Are you tired of navigating through countless records to find the documents you frequently use in Odoo? Imagine a world where your most important sales orders, products, or purchase records are just a click away, readily accessible from a custom “favorites” menu. With the latest advancements in Odoo 18, leveraging the Odoo Web Library (OWL) framework, this isn’t just a dream – it’s a powerful reality you can implement today. This tutorial will empower you to create a robust Odoo 18 OWL Favorites
feature, significantly enhancing user experience and productivity.
The goal is simple yet transformative: to allow users to mark any document as a favorite directly from its chatter, and then access these favorited items from a dedicated, dynamic menu in the Odoo top bar. This elegant solution, built entirely with OWL, demonstrates the flexibility and power of Odoo’s modern frontend development.
Why Odoo 18 OWL Favorites
Will Revolutionize Your Workflow
In a fast-paced business environment, every second counts. Traditional Odoo navigation, while powerful, can sometimes involve multiple clicks or searches to locate frequently accessed records. This often leads to fragmented workflows and wasted time. Implementing Odoo 18 OWL Favorites
addresses this head-on by:
- Boosting Productivity: Quickly jump to critical documents (e.g., high-priority sales orders, frequently reordered products, ongoing projects) without repetitive searches.
- Enhancing User Experience: Provides a personalized and intuitive interface, making Odoo feel more tailored to individual user needs. Users will appreciate the convenience of having their essential documents at their fingertips.
- Streamlining Operations: For roles like sales managers, purchasing agents, or project leads, having instant access to specific records allows for better follow-up and faster decision-making.
- Leveraging Modern Odoo Capabilities: This feature is a prime example of how custom OWL development can extend Odoo’s core functionality, offering deeper integration and a seamless user experience that is difficult to achieve with older methods.
- Promoting Engagement: A more user-friendly system naturally leads to greater user adoption and satisfaction, making your Odoo instance a more valuable tool.
The Odoo 18 OWL Favorites
feature we’re about to build is more than just a bookmarking system; it’s a testament to the interactive and reactive possibilities of Odoo 18’s frontend.
Understanding OWL: The Heart of Odoo 18’s Frontend
Before diving into the implementation, it’s crucial to understand OWL (Odoo Web Library). OWL is Odoo’s modern JavaScript framework, designed for building interactive and performant web interfaces. It adopts a component-based architecture, making UI development modular, reusable, and easier to maintain.
Key OWL concepts you’ll encounter in this tutorial include:
- Components: Self-contained, reusable pieces of UI with their own logic and template.
- Hooks: Special functions that let you “hook into” React features in your function components. Examples include
useState
for reactive data,useService
for accessing Odoo’s backend services,useBus
for inter-component communication, andonWillStart
for component initialization. - Props: Short for “properties,” these are how data is passed from a parent component to a child component, ensuring a clear flow of information.
- Patching (Inheritance): A powerful mechanism in Odoo to modify existing OWL components without directly altering their core code. This ensures cleaner upgrades and less risk of conflicts.
- Services: Provide an interface to Odoo’s core functionalities, such as interacting with the database (ORM), managing UI actions (Actions), displaying messages (Notifications), and handling user interface events (UI).
By mastering these concepts through the Odoo 18 OWL Favorites
implementation, you’ll gain valuable skills for any future Odoo 18 frontend development.
Step-by-Step Tutorial: Building Your Odoo 18 OWL Favorites
Feature
Let’s break down the process of creating this essential feature. We will cover everything from setting up your module to the intricacies of component interaction.
Step 1: Laying the Foundation – Module and Data Model
Every Odoo customization begins with a module. This module will house all our OWL components and the backend logic for storing favorite documents.
- Create a New Odoo Module:
Start by creating a standard Odoo module (e.g.,my_favorites
). Ensure it has the necessary__manifest__.py
file and is correctly installed in your Odoo instance. This module will serve as the container for our customOdoo 18 OWL Favorites
functionality. - Define a Data Model for Favorites:
We need a simple backend model to store which documents a user has marked as favorite. This model will typically have three key fields:user_id
: A many-to-one field linking tores.users
to identify who favorited the document.res_id
: An integer field storing the ID of the favorited record (e.g.,sale.order
ID,product.template
ID).res_model
: A character field storing the technical name of the model of the favorited record (e.g.,'sale.order'
,'product.template'
).
Here’s an example for
my_favorites/models/my_favorites_model.py
:from odoo import models, fields, api class MyFavorite(models.Model): _name = 'my_favorites.model' _description = 'My Favorite Documents' user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self.env.user) res_id = fields.Integer(string='Resource ID', required=True) res_model = fields.Char(string='Resource Model', required=True) _sql_constraints = [ ('unique_favorite', 'unique(user_id, res_id, res_model)', 'You can only favorite this item once!') ]
This model (
my_favorites.model
) will serve as the backbone for storing and retrievingOdoo 18 OWL Favorites
data.
Step 2: Enhancing the Chatter – The Favorite Star
The first visible change will be a star icon in the chatter section of any record. This star will allow users to toggle the favorite status of a document.
- Create Folder Structure:
Organize your OWL components. Insidemy_favorites/static/src/
, create acomponents
folder, and within it, subfolders forchatter
,menu
, andmenu_item
.my_favorites/static/src/components/chatter/chatter.js
my_favorites/static/src/components/chatter/chatter.xml
my_favorites/static/src/components/chatter/chatter.scss
(for styling, if needed)
- Patch the Chatter Component (
chatter.js
):
We’ll use OWL’spatch
mechanism to extend the existing Odoo chatter component. This allows us to add new methods and manage the favorite state./** @odoo-module */ import { Chatter } from '@mail/components/chatter/chatter'; import { patch } from "@web/core/utils/patch"; import { useState, useService } from "@odoo/owl"; patch(Chatter.prototype, 'my_favorites.Chatter', { setup() { this._super(); // Call the original setup method this.orm = useService("orm"); // Service for database operations this.notification = useService("notification"); // Service for user notifications this.ui = useService("ui"); // UI service for bus events this.state = useState({ favorite: false, favoriteId: null }); // Reactive state for favorite status // Initialize favorite status when the component sets up this.isFavorite(); }, /** * Checks if the current document is favorited by the current user. */ async isFavorite() { const records = await this.orm.searchRead( "my_favorites.model", [ ['user_id', '=', this.env.session.uid], ['res_id', '=', this.props.thread.id], ['res_model', '=', this.props.thread.model] ], ['id'] // Only fetch the ID ); if (records.length > 0) { this.state.favorite = true; this.state.favoriteId = records[0].id; // Store the favorite record ID } else { this.state.favorite = false; this.state.favoriteId = null; } }, /** * Toggles the favorite status of the current document. */ async onToggleFavorite() { if (this.state.favorite) { // Remove from favorites await this.orm.unlink("my_favorites.model", [this.state.favoriteId]); this.state.favorite = false; this.state.favoriteId = null; this.notification.add(this.env._t("Removed from favorites"), { type: 'danger', sticky: false }); } else { // Add to favorites const create_vals = { user_id: this.env.session.uid, res_id: this.props.thread.id, res_model: this.props.thread.model }; const result = await this.orm.create("my_favorites.model", [create_vals]); this.state.favoriteId = result; // Store the new favorite record ID this.notification.add(this.env._t("Added to favorites"), { type: 'success', sticky: true }); this.state.favorite = true; } // CRUCIAL: Notify the top menu to refresh its list of favorites this.ui.bus.trigger('refresh-favorites-menu'); }, });
_super()
: Calls the originalsetup
method of theChatter
component, ensuring its core functionality remains intact.useService("orm")
: Gives us access to Odoo’s ORM, allowing us to performsearchRead
,create
, andunlink
operations on ourmy_favorites.model
.useState({ favorite: false })
: Declares a reactive state variablefavorite
. Whenthis.state.favorite
changes, OWL automatically re-renders the part of the UI that depends on it.this.props.thread.id
andthis.props.thread.model
: Theseprops
are passed from the parent component (the form view) to the chatter, providing the context of the current document. This is how the chatter knows which document it’s currently associated with.this.ui.bus.trigger('refresh-favorites-menu');
: This is a powerful feature for inter-component communication. TheUI Bus
acts as a global event dispatcher. When the favorite status changes in the chatter, it sends a signal (refresh-favorites-menu
) that other components can listen for. This ensures our top menu updates in real-time.
- Add the Button to the Chatter Template (
chatter.xml
):
Now, we need to modify the chatter’s XML template to display our star icon.<templates> <t t-name="my_favorites.Chatter"> <t t-inherit="mail.Chatter" t-inherit-mode="extension"> <!-- Locate the chatter tools and insert our button --> <xpath expr="//div[hasclass('o_Chatter_tools')]" position="inside"> <a href="#" t-on-click="onToggleFavorite" title="Toggle Favorite" aria-label="Toggle Favorite" class="o_MyFavorites_star"> <t t-if="state.favorite"> <i class="fa fa-star fa-lg" style="color:yellow;"/> </t> <t t-else=""> <i class="fa fa-star-o fa-lg" /> </t> </a> </xpath> </t> </t> </templates>
t-inherit="mail.Chatter" t-inherit-mode="extension"
: Specifies that we are extending themail.Chatter
template.xpath expr="//div[hasclass('o_Chatter_tools')]" position="inside"
: This XPath expression precisely targets the location within the chatter’s HTML where we want to insert our button – typically next to other action buttons.t-on-click="onToggleFavorite"
: Binds the click event of the<a>
tag to ouronToggleFavorite
method defined inchatter.js
.t-if="state.favorite"
: Conditionally renders a filled yellow star (fa-star
) if the document is a favorite, or an outlined star (fa-star-o
) if it’s not. This provides immediate visual feedback forOdoo 18 OWL Favorites
.
Step 3: Crafting the Dynamic Top Menu
The top menu will display a list of all favorited documents and allow users to open or remove them. This requires a new OWL component.
- Create the Javascript File (
menu.js
):
This will be our main component for the top favorites menu./** @odoo-module */ import { Dropdown } from "@web/core/dropdown/dropdown"; import { useService, useBus, useState, onWillStart } from "@odoo/owl"; import { Component } from "@odoo/owl"; import { HistoryMenuItem } from "@my_favorites/static/src/components/menu_item/menu_item.js"; // Import child component import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; // For confirmation dialogs export class HistoryMenu extends Component { static template = "my_favorites.HistoryMenu"; static components = { Dropdown, HistoryMenuItem }; // Register child component setup() { this.orm = useService("orm"); this.action = useService("action"); // Service for opening records/windows this.dialogService = useService("dialog"); // Service for confirmation dialogs this.ui = useService("ui"); this.state = useState({ favorites: [] }); // Reactive state for the list of favorites // Listen for the 'refresh-favorites-menu' event from the UI Bus useBus(this.ui.bus, 'refresh-favorites-menu', this.getFavorites.bind(this)); // Load favorites when the component is first created/rendered onWillStart(async () => { await this.getFavorites(); }); } /** * Fetches the current user's favorite documents from the database. */ async getFavorites() { const favorite_items = await this.orm.searchRead( "my_favorites.model", [['user_id', '=', this.env.session.uid]], ['res_id', 'res_model'] // Fields to read ); this.state.favorites = favorite_items; // Update reactive state } /** * Opens a favorited document. * @param {Object} favorite - The favorite item object. * @param {string} action_type - 'current' to open in current window, 'new' for new window. */ onOpenFavorite(favorite, action_type) { this.action.doAction({ name: this.env._t("Open Favorite"), type: 'ir.actions.act_window', res_model: favorite.res_model, res_id: favorite.res_id, views: [[false, 'form']], // Open in form view target: action_type // 'current' or 'new' }); } /** * Deletes a favorited document after a confirmation. * @param {Object} favorite - The favorite item object to delete. */ onDeleteFavorite(favorite) { this.dialogService.add(ConfirmationDialog, { body: this.env._t("Are you sure you want to remove this item from your favorites?"), confirm: async () => { await this.orm.unlink("my_favorites.model", [favorite.id]); this.ui.bus.trigger('refresh-favorites-menu'); // Notify bus to refresh await this.getFavorites(); // Refresh local list }, cancel: () => { // console.log('Deletion cancelled'); }, }); } } HistoryMenu.props = {}; // Define props if any are expected from parent components
static components = { Dropdown, HistoryMenuItem };
: Registers other OWL components that this component will use in its template.Dropdown
is an Odoo core component, whileHistoryMenuItem
is our custom child component.useBus(this.ui.bus, 'refresh-favorites-menu', this.getFavorites.bind(this));
: This hook is the counterpart to thetrigger
call in the chatter. It tellsHistoryMenu
to executegetFavorites
whenever therefresh-favorites-menu
event is fired on theUI Bus
. This ensures real-time updates for yourOdoo 18 OWL Favorites
list.onWillStart(async () => { ... });
: This hook runs exactly once, right before the component is rendered for the first time. It’s perfect for initial data fetching, ensuring the favorites list is populated when the Odoo app loads.this.action.doAction(...)
: Uses theaction
service to simulate an Odoo action, in this case, opening a record’s form view. Thetarget
parameter (current
ornew
) dictates whether it opens in the same window or a new one.this.dialogService.add(ConfirmationDialog, { ... });
: Leverages Odoo’s built-inConfirmationDialog
for user prompts before deleting a favorite, improving UX and preventing accidental removals.
- Create the Menu Template (
menu.xml
):
This template will define the visual structure of our dropdown menu.<templates> <t t-name="my_favorites.HistoryMenu"> <Dropdown class="'dropdown-menu-favorites'"> <t t-set-slot="toggler"> <i class="fa fa-heart fa-lg" title="My Favorites" aria-label="My Favorites"/> </t> <t t-if="state.favorites.length > 0"> <ul class="o_favorites_list"> <t t-foreach="state.favorites" t-as="favorite" t-key="favorite.id"> <!-- Render each favorite item using a child component --> <HistoryMenuItem favorite="favorite" onOpenFavorite="onOpenFavorite" onDeleteFavorite="onDeleteFavorite"/> </t> </ul> </t> <t t-else=""> <ul class="o_favorites_list"> <li> <a href="#" class="dropdown-item"> <t t-esc="env._t('No favorites added yet')"/> </a> </li> </ul> </t> </Dropdown> </t> </templates>
<Dropdown>
: This is a core Odoo OWL component that provides dropdown functionality out of the box.t-set-slot="toggler"
: Defines the content that acts as the trigger for the dropdown. Here, it’s a heart icon. Font Awesome icons are great for this (fa-heart
).t-foreach="state.favorites"
: Iterates over thefavorites
array in our component’s state, rendering aHistoryMenuItem
for each.HistoryMenuItem favorite="favorite" ...
: Passes the individualfavorite
object and theonOpenFavorite
andonDeleteFavorite
functions asprops
to the childHistoryMenuItem
component. This demonstrates how parent components communicate with their children.
- Create the Single Menu Item Component (
menu_item.js
):
This small component handles the display and actions for each individual favorited item in the dropdown./** @odoo-module */ import { Component } from "@odoo/owl"; export class HistoryMenuItem extends Component { static template = "my_favorites.HistoryMenuItem"; setup() { // No specific setup needed for this simple child component } /** * Passes the open favorite action up to the parent component. * @param {Object} favorite - The favorite item object. * @param {string} action_type - 'current' or 'new'. */ onOpenFavoriteAction(favorite, action_type) { this.props.onOpenFavorite(favorite, action_type); } /** * Passes the delete favorite action up to the parent component. * @param {Object} favorite - The favorite item object. */ onDeleteFavoriteAction(favorite) { this.props.onDeleteFavorite(favorite); } } HistoryMenuItem.props = { favorite: { type: Object, optional: false }, onOpenFavorite: { type: Function, optional: false }, onDeleteFavorite: { type: Function, optional: false }, };
HistoryMenuItem.props = { ... }
: Explicitly defines theprops
this component expects to receive from its parent. This is good practice for clarity and validation.
- Create the Single Menu Item Template (
menu_item.xml
):
This template defines how each favorited item looks within the dropdown.<templates> <t t-name="my_favorites.HistoryMenuItem"> <li> <div class="o_favorites_item d-flex align-items-center justify-content-between"> <a href="#" class="dropdown-item flex-grow-1" t-on-click.prevent="() => this.onOpenFavoriteAction(props.favorite, 'current')"> <t t-esc="props.favorite.res_model"/>: <t t-esc="props.favorite.res_id"/> </a> <div class="o_favorites_item_actions d-flex align-items-center"> <a href="#" class="dropdown-item" t-on-click.prevent="() => this.onOpenFavoriteAction(props.favorite, 'new')" title="Open in New Window"> <i class="fa fa-arrow-right"/> </a> <a href="#" class="dropdown-item text-danger" t-on-click.prevent="() => this.onDeleteFavoriteAction(props.favorite)" title="Remove from Favorites"> <i class="fa fa-trash"/> </a> </div> </div> </li> </t> </templates>
- This template provides three interactive elements for each favorite:
- A link to open the document in the current window.
- An arrow icon to open it in a new window.
- A trash icon to delete it from
Odoo 18 OWL Favorites
.
t-on-click.prevent
: Prevents the default browser behavior for the<a>
tag (which would typically navigate to#
).
- This template provides three interactive elements for each favorite:
Step 4: Registering Your Innovation (C Stripe)
For our HistoryMenu
component to appear in the Odoo top bar, it needs to be registered with Odoo’s component registry.
- Create the Registration File (
menu_service.js
):
This file tells Odoo where to place your customOdoo 18 OWL Favorites
menu./** @odoo-module */ import { registry } from "@web/core/registry"; import { HistoryMenu } from "@my_favorites/static/src/components/menu/menu.js"; // Register the HistoryMenu component in the "user_menu_items" category registry.category("user_menu_items").add("my_favorites.HistoryMenu", HistoryMenu, { sequence: 10 });
registry.category("user_menu_items").add(...)
: Theuser_menu_items
category is where components for the top-right user menu dropdown (usually next to the user’s name) are registered. Thesequence
parameter controls the order of appearance.
Step 5: Bringing It All Together – Assets and Testing
Finally, we need to ensure Odoo loads all our new JavaScript, XML, and CSS files.
- Update
assets_backend
in your module’s XML:
Inmy_favorites/views/assets.xml
(or similar), declare your assets.<?xml version="1.0" encoding="utf-8"?> <odoo> <data> <template id="assets_backend" name="my_favorites assets" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> <!-- SCSS for styling the chatter star (optional) --> <link rel="stylesheet" href="/my_favorites/static/src/components/chatter/chatter.scss"/> <!-- JS files for OWL components --> <script type="text/javascript" src="/my_favorites/static/src/components/chatter/chatter.js"/> <script type="text/javascript" src="/my_favorites/static/src/components/menu/menu.js"/> <script type="text/javascript" src="/my_favorites/static/src/components/menu_item/menu_item.js"/> <script type="text/javascript" src="/my_favorites/static/src/js/menu_service.js"/> <!-- XML templates for OWL components --> <t t-call="web.qweb_add_template"> <t t-set="templates"> <t t-call="my_favorites.Chatter"/> <t t-call="my_favorites.HistoryMenu"/> <t t-call="my_favorites.HistoryMenuItem"/> </t> </t> </xpath> </template> </data> </odoo>
inherit_id="web.assets_backend"
: This ensures your assets are loaded along with Odoo’s core backend assets.<script type="text/javascript" src="..."/>
: Includes your JavaScript files. Ensure the paths are correct.<t t-call="web.qweb_add_template">
: This is how OWL components’ XML templates are registered and made available to the framework.
- Restart and Test:
- Restart your Odoo service.
- Upgrade your
my_favorites
module through the Odoo Apps menu. - Navigate to any record with a chatter (e.g., a Sales Order, Product, Contact). You should now see the star icon.
- Click the star to add/remove a favorite. Observe the notification messages.
- Check the top menu (usually near your username). You should see the heart icon, and clicking it should display your
Odoo 18 OWL Favorites
. - Test opening items, opening in new windows, and deleting from the menu.
Key OWL Concepts Revisited for Odoo 18 OWL Favorites
This project beautifully illustrates several core OWL concepts:
- Component-Based Architecture: We’ve built three distinct components (
Chatter
extension,HistoryMenu
,HistoryMenuItem
), each with a specific responsibility, promoting reusability and maintainability. - Patching Existing Components: By patching the
Chatter
component, we seamlessly added new functionality without altering Odoo’s core code, making ourOdoo 18 OWL Favorites
solution upgrade-safe. - Reactive State Management (
useState
): Thefavorite
status in the chatter and thefavorites
list in the menu are managed byuseState
. Any change to these state variables automatically triggers a re-render of the relevant UI parts, providing a dynamic user experience. - Inter-Component Communication (
useBus
): TheUI Bus
played a crucial role. The chatter triggers an event, and the menu listens for it, allowing them to communicate and keep theOdoo 18 OWL Favorites
list synchronized in real-time without direct coupling. - Service-Oriented Design (
useService
): We extensively used Odoo’s services:orm
: For all database interactions (creating, reading, deleting favorites).notification
: For providing user feedback (e.g., “Added to favorites”).action
: For opening records from the favorites menu.dialog
: For confirmation prompts before deletion.ui
: To access the global eventbus
.
- Life Cycle Hooks (
onWillStart
):onWillStart
ensured that our favorites menu loaded the initial list of items as soon as the component was ready, guaranteeing a smooth startup. - Slots (
Dropdown
toggler slot): TheDropdown
component demonstrated the use of slots, allowing us to “fill” predefined areas within a parent component with custom content (our heart icon).
This complete implementation of Odoo 18 OWL Favorites
is a testament to the power and elegance of Odoo’s modern frontend development.
Practical Tips and Best Practices
As you implement Odoo 18 OWL Favorites
and other OWL-based features, consider these tips:
- Modularize: Always break down complex features into smaller, manageable components. Each component should ideally have a single responsibility.
- Use Services Wisely: Leverage Odoo’s extensive services. They provide consistent APIs for common Odoo functionalities and simplify development. Refer to the official Odoo documentation on OWL services for a comprehensive list.
- Error Handling: In a production environment, add robust error handling to your
async
functions (e.g.,try-catch
blocks) to manage potential issues with ORM calls or other services. - Styling: For advanced styling, consider using SCSS with
_variables.scss
to maintain Odoo’s visual consistency. Yourmy_favorites/static/src/components/chatter/chatter.scss
can be
Discover more from teguhteja.id
Subscribe to get the latest posts sent to your email.