Skip to content

Master Odoo 18 OWL Favorites: Unlock Powerful UX Enhancements

odoo 18 owl favorites

 

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, and onWillStart 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.

  1. 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 custom Odoo 18 OWL Favorites functionality.
  2. 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 to res.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 retrieving Odoo 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.

  1. Create Folder Structure:
    Organize your OWL components. Inside my_favorites/static/src/, create a components folder, and within it, subfolders for chatter, menu, and menu_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)
  2. Patch the Chatter Component (chatter.js):
    We’ll use OWL’s patch 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 original setup method of the Chatter component, ensuring its core functionality remains intact.
    • useService("orm"): Gives us access to Odoo’s ORM, allowing us to perform searchRead, create, and unlink operations on our my_favorites.model.
    • useState({ favorite: false }): Declares a reactive state variable favorite. When this.state.favorite changes, OWL automatically re-renders the part of the UI that depends on it.
    • this.props.thread.id and this.props.thread.model: These props 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. The UI 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.
  3. 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 the mail.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 our onToggleFavorite method defined in chatter.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 for Odoo 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.

  1. 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, while HistoryMenuItem is our custom child component.
    • useBus(this.ui.bus, 'refresh-favorites-menu', this.getFavorites.bind(this));: This hook is the counterpart to the trigger call in the chatter. It tells HistoryMenu to execute getFavorites whenever the refresh-favorites-menu event is fired on the UI Bus. This ensures real-time updates for your Odoo 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 the action service to simulate an Odoo action, in this case, opening a record’s form view. The target parameter (current or new) dictates whether it opens in the same window or a new one.
    • this.dialogService.add(ConfirmationDialog, { ... });: Leverages Odoo’s built-in ConfirmationDialog for user prompts before deleting a favorite, improving UX and preventing accidental removals.
  2. 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 the favorites array in our component’s state, rendering a HistoryMenuItem for each.
    • HistoryMenuItem favorite="favorite" ...: Passes the individual favorite object and the onOpenFavorite and onDeleteFavorite functions as props to the child HistoryMenuItem component. This demonstrates how parent components communicate with their children.
  3. 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 the props this component expects to receive from its parent. This is good practice for clarity and validation.
  4. 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 #).

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.

  1. Create the Registration File (menu_service.js):
    This file tells Odoo where to place your custom Odoo 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(...): The user_menu_items category is where components for the top-right user menu dropdown (usually next to the user’s name) are registered. The sequence 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.

  1. Update assets_backend in your module’s XML:
    In my_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.
  2. 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 our Odoo 18 OWL Favorites solution upgrade-safe.
  • Reactive State Management (useState): The favorite status in the chatter and the favorites list in the menu are managed by useState. 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): The UI Bus played a crucial role. The chatter triggers an event, and the menu listens for it, allowing them to communicate and keep the Odoo 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 event bus.
  • 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): The Dropdown 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. Your my_favorites/static/src/components/chatter/chatter.scss can be

Discover more from teguhteja.id

Subscribe to get the latest posts sent to your email.

Tags:

Leave a Reply

WP Twitter Auto Publish Powered By : XYZScripts.com