Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 13 Part 1: Odoo's Built-in Services

  • All Blogs
  • OWL Book
  • Chapter 13 Part 1: Odoo's Built-in Services
  • August 25, 2026 by
    Chapter 13 Part 1: Odoo's Built-in Services
    Grover Menacho

    In Chapter 12, we mastered server communication with the orm and rpc services. But Odoo's service ecosystem extends far beyond data operations. The platform provides a rich collection of built-in services that handle everything from user notifications to navigation, dialog management, and system integration.

    These services are the secret to creating components that feel like native parts of the Odoo experience. Instead of building custom solutions for common UI patterns, you can leverage Odoo's battle-tested services to provide consistent, polished user interactions.

    This chapter is split into two parts. Part 1 (this one) covers the services you'll reach for on almost every project: notification, action, dialog, and a handful of smaller essentials. Part 2 covers advanced composition patterns, performance optimization, and error recovery for more complex scenarios — skip ahead once you're comfortable with the basics here.


    From useService to the server: the service already exists, you only get a reference.{width=100%}

    The notification Service: User Feedback Done Right

    User feedback is crucial for good UX. Users need to know when operations succeed, when errors occur, and when important events happen. The notification service provides Odoo's signature toast notifications that appear in the top-right corner of the screen.

    Basic Notification Usage

    Let's build a comprehensive example that demonstrates all notification types and advanced features. We'll build the NotificationDemo class one group of methods at a time — each code block below continues directly inside the same class body.

    JavaScript (notification_demo.js):

    First, the imports, the class declaration, and setup(), where we grab the two services we'll need:

    import { Component, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class NotificationDemo extends Component {
      static template = "my_module.NotificationDemo";
    
      setup() {
        this.state = useState({
          messageText: "Operation completed successfully!",
          notificationCount: 0
        });
    
        this.notification = useService("notification");
        this.orm = useService("orm");
      }
    

    Next, the four basic notification types. Notice the pattern: call this.notification.add(message, options) and pass a type that matches the situation (success, info, warning, or danger):

      // Basic notification types
      showSuccessNotification() {
        this.notification.add(this.state.messageText, {
          title: "Success",
          type: "success",
          sticky: false // Auto-dismiss after a few seconds
        });
    
        this.incrementNotificationCount();
        console.log("NotificationDemo: Success notification shown");
      }
    
      showInfoNotification() {
        this.notification.add("Here's some useful information for you.", {
          title: "Information",
          type: "info"
        });
    
        this.incrementNotificationCount();
      }
    
      showWarningNotification() {
        this.notification.add("Please review this carefully before proceeding.", {
          title: "Warning",
          type: "warning",
          sticky: true // Stays until manually dismissed
        });
    
        this.incrementNotificationCount();
      }
    
      showErrorNotification() {
        this.notification.add("Something went wrong. Please try again.", {
          title: "Error",
          type: "danger",
          sticky: true
        });
    
        this.incrementNotificationCount();
      }
    

    Now two methods that show notifications doing real work: one adds a custom CSS class for styling, the other wraps an async operation that can fail, showing feedback before, on success, and on error:

      // Advanced notification with custom content
      showRichNotification() {
        this.notification.add("Your document has been processed.", {
          title: "Document Processing Complete",
          type: "success",
          sticky: false,
          className: "custom-notification-style" // Custom CSS class
        });
    
        this.incrementNotificationCount();
      }
    
      // Real-world example: Save operation with feedback
      async saveDataWithFeedback() {
        try {
          // Show immediate feedback
          this.notification.add("Saving your changes...", {
            title: "Saving",
            type: "info"
          });
    
          // Simulate save operation
          await this.simulateAsyncOperation();
    
          // Show success feedback
          this.notification.add("Your changes have been saved successfully.", {
            title: "Saved",
            type: "success"
          });
    
          console.log("NotificationDemo: Data saved successfully");
    
        } catch (error) {
          // Show error feedback
          this.notification.add(`Failed to save: ${error.message}`, {
            title: "Save Failed",
            type: "danger",
            sticky: true // Keep error visible until user dismisses
          });
    
          console.error("NotificationDemo: Save failed:", error);
        }
    
        this.incrementNotificationCount();
      }
    

    The last two methods handle a multi-step bulk operation with progress updates, and a form validation flow that gives feedback either way:

      // Bulk operation with progress feedback
      async performBulkOperation() {
        const items = ['Item A', 'Item B', 'Item C', 'Item D'];
        let processed = 0;
    
        try {
          // Initial notification
          this.notification.add(`Starting bulk operation on ${items.length} items...`, {
            title: "Bulk Operation",
            type: "info"
          });
    
          for (const item of items) {
            await this.simulateAsyncOperation(500); // Simulate processing time
            processed++;
    
            // Progress notification
            this.notification.add(`Processed ${processed}/${items.length} items`, {
              title: "Progress Update",
              type: "info"
            });
          }
    
          // Completion notification
          this.notification.add(`Successfully processed all ${items.length} items!`, {
            title: "Bulk Operation Complete",
            type: "success",
            sticky: false
          });
    
        } catch (error) {
          this.notification.add(`Bulk operation failed after processing ${processed} items.`, {
            title: "Operation Failed",
            type: "danger",
            sticky: true
          });
        }
      }
    
      // Validation feedback example
      validateAndNotify() {
        if (!this.state.messageText.trim()) {
          this.notification.add("Message text cannot be empty.", {
            title: "Validation Error",
            type: "warning"
          });
          return false;
        }
    
        if (this.state.messageText.length > 100) {
          this.notification.add("Message text is too long (maximum 100 characters).", {
            title: "Validation Error",
            type: "warning"
          });
          return false;
        }
    
        this.notification.add("Message validation passed!", {
          title: "Valid Input",
          type: "success"
        });
    
        return true;
      }
    

    Finally, the small helper methods that keep the demo tidy, and the closing brace for the class:

      // Helper methods
      incrementNotificationCount() {
        this.state.notificationCount++;
      }
    
      async simulateAsyncOperation(delay = 1000) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            // Simulate occasional failures
            if (Math.random() < 0.1) {
              reject(new Error("Simulated network error"));
            } else {
              resolve();
            }
          }, delay);
        });
      }
    
      onMessageChange(event) {
        this.state.messageText = event.target.value;
      }
    
      resetDemo() {
        this.state.messageText = "Operation completed successfully!";
        this.state.notificationCount = 0;
      }
    }
    

    Template (notification_demo.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
        <t t-name="my_module.NotificationDemo" owl="1">
            <div class="notification-demo card">
                <div class="card-header">
                    <h4>Notification Service Demo</h4>
                    <small class="text-muted">
                        Notifications shown: <span class="badge bg-secondary" t-esc="state.notificationCount"/>
                    </small>
                </div>
    
                <div class="card-body">
                    <!-- Custom Message Input -->
                    <div class="mb-4">
                        <label class="form-label">Custom Message</label>
                        <div class="input-group">
                            <input 
                                type="text" 
                                class="form-control"
                                t-model="state.messageText"
                                placeholder="Enter notification message"
                                maxlength="100"
                            />
                            <button 
                                class="btn btn-outline-secondary"
                                t-on-click="validateAndNotify">
                                Validate
                            </button>
                        </div>
                        <div class="form-text">
                            Message length: <t t-esc="state.messageText.length"/>/100 characters
                        </div>
                    </div>
    
                    <!-- Basic Notification Types -->
                    <div class="mb-4">
                        <h6>Basic Notification Types</h6>
                        <div class="btn-group me-2 mb-2" role="group">
                            <button 
                                class="btn btn-success btn-sm"
                                t-on-click="showSuccessNotification">
                                <i class="fa fa-check"></i> Success
                            </button>
                            <button 
                                class="btn btn-info btn-sm"
                                t-on-click="showInfoNotification">
                                <i class="fa fa-info-circle"></i> Info
                            </button>
                            <button 
                                class="btn btn-warning btn-sm"
                                t-on-click="showWarningNotification">
                                <i class="fa fa-exclamation-triangle"></i> Warning
                            </button>
                            <button 
                                class="btn btn-danger btn-sm"
                                t-on-click="showErrorNotification">
                                <i class="fa fa-times"></i> Error
                            </button>
                        </div>
                    </div>
    
                    <!-- Advanced Examples -->
                    <div class="mb-4">
                        <h6>Real-World Examples</h6>
                        <div class="d-flex gap-2 flex-wrap">
                            <button 
                                class="btn btn-primary btn-sm"
                                t-on-click="showRichNotification">
                                <i class="fa fa-star"></i> Rich Notification
                            </button>
    
                            <button 
                                class="btn btn-outline-primary btn-sm"
                                t-on-click="saveDataWithFeedback">
                                <i class="fa fa-save"></i> Save with Feedback
                            </button>
    
                            <button 
                                class="btn btn-outline-secondary btn-sm"
                                t-on-click="performBulkOperation">
                                <i class="fa fa-tasks"></i> Bulk Operation
                            </button>
                        </div>
                    </div>
    
                    <!-- Demo Controls -->
                    <div class="border-top pt-3">
                        <button 
                            class="btn btn-outline-secondary btn-sm"
                            t-on-click="resetDemo">
                            <i class="fa fa-refresh"></i> Reset Demo
                        </button>
                    </div>
                </div>
            </div>
        </t>
    </templates>
    

    Notification Best Practices

    Do's: - Use appropriate notification types (success for completed actions, warning for important info, danger for errors) - Make error notifications sticky so users can read them fully - Provide clear, actionable messages - Use notifications for feedback on user actions

    Don'ts: - Don't spam users with too many notifications - Don't use notifications for critical system errors (use dialogs instead) - Avoid generic messages like "Error occurred" - Don't use notifications for information that needs user response


    The action Service: Navigation and Integration

    The action service is your gateway to Odoo's action system. It can trigger any action in the system—open forms, show lists, run reports, execute server actions, and much more.

    Understanding Action Types

    Odoo supports several action types: - Window Actions: Open views (form, list, kanban, etc.) - Server Actions: Execute Python code on the server - Report Actions: Generate and display reports - URL Actions: Navigate to external URLs - Client Actions: Open custom client-side components

    Let's build a comprehensive example, again method group by method group.

    JavaScript (action_navigator.js):

    First, the setup and the data loader that fills the two select boxes we'll use to pick a record:

    import { Component, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class ActionNavigator extends Component {
      static template = "my_module.ActionNavigator";
    
      setup() {
        this.state = useState({
          selectedPartnerId: null,
          selectedProductId: null,
          partners: [],
          products: []
        });
    
        this.action = useService("action");
        this.orm = useService("orm");
        this.notification = useService("notification");
    
        // Load sample data
        this.loadSampleData();
      }
    
      async loadSampleData() {
        try {
          const [partners, products] = await Promise.all([
            this.orm.searchRead("res.partner", [], ["name"], { limit: 10 }),
            this.orm.searchRead("product.product", [], ["name"], { limit: 10 })
          ]);
    
          this.state.partners = partners;
          this.state.products = products;
        } catch (error) {
          console.error("ActionNavigator: Failed to load sample data:", error);
        }
      }
    

    With sample data loaded, let's add the basic window-action methods — opening a form, a list, a kanban, plus the variations for opening in a new window, in a modal, or waiting on the result with a callback:

      // Basic window actions
      openCustomerForm(partnerId = null) {
        console.log(`ActionNavigator: Opening customer form for ID: ${partnerId || 'new'}`);
    
        this.action.doAction({
          type: "ir.actions.act_window",
          name: partnerId ? "Edit Customer" : "New Customer",
          res_model: "res.partner",
          res_id: partnerId || false,
          views: [[false, "form"]],
          target: "current", // Opens in current view
          context: {
            default_is_company: true,
            default_customer_rank: 1
          }
        });
      }
    
      openCustomerList() {
        console.log("ActionNavigator: Opening customer list view");
    
        this.action.doAction({
          type: "ir.actions.act_window",
          name: "Customers",
          res_model: "res.partner",
          views: [
            [false, "list"],
            [false, "form"]
          ],
          domain: [["is_company", "=", true]],
          target: "current"
        });
      }
    
      openCustomerKanban() {
        this.action.doAction({
          type: "ir.actions.act_window",
          name: "Customer Pipeline",
          res_model: "res.partner",
          views: [
            [false, "kanban"],
            [false, "form"]
          ],
          domain: [["is_company", "=", true]],
          target: "current"
        });
      }
    
      // Open in new tab/window
      openInNewWindow(partnerId) {
        console.log(`ActionNavigator: Opening customer ${partnerId} in new window`);
    
        this.action.doAction({
          type: "ir.actions.act_window",
          res_model: "res.partner",
          res_id: partnerId,
          views: [[false, "form"]],
          target: "new" // Opens in new window/tab
        });
      }
    
      // Modal dialogs
      openInModal(partnerId) {
        console.log(`ActionNavigator: Opening customer ${partnerId} in modal`);
    
        this.action.doAction({
          type: "ir.actions.act_window",
          res_model: "res.partner",
          res_id: partnerId,
          views: [[false, "form"]],
          target: "new",
          flags: {
            mode: "readonly" // Optional: open in read-only mode
          }
        });
      }
    
      // Action with callback
      async openWithCallback(partnerId) {
        console.log(`ActionNavigator: Opening customer ${partnerId} with callback`);
    
        try {
          const result = await this.action.doAction({
            type: "ir.actions.act_window",
            res_model: "res.partner",
            res_id: partnerId,
            views: [[false, "form"]],
            target: "new"
          });
    
          console.log("ActionNavigator: Action completed with result:", result);
    
          this.notification.add("Customer form was opened successfully!", {
            type: "success"
          });
    
        } catch (error) {
          console.error("ActionNavigator: Action failed:", error);
    
          this.notification.add("Failed to open customer form.", {
            type: "danger"
          });
        }
      }
    

    Beyond opening forms, the action service can also open related records, run server actions and reports, navigate to external URLs, and open your own custom client actions:

      // Open related records
      openCustomerSales(partnerId) {
        console.log(`ActionNavigator: Opening sales for customer ${partnerId}`);
    
        this.action.doAction({
          type: "ir.actions.act_window",
          name: "Customer Sales",
          res_model: "sale.order",
          views: [
            [false, "list"],
            [false, "form"]
          ],
          domain: [["partner_id", "=", partnerId]],
          context: {
            default_partner_id: partnerId,
            search_default_partner_id: partnerId
          },
          target: "current"
        });
      }
    
      // Server actions
      async executeServerAction(actionXmlId) {
        console.log(`ActionNavigator: Executing server action: ${actionXmlId}`);
    
        try {
          const result = await this.action.doAction(actionXmlId, {
            // Additional context for the server action
            active_ids: [this.state.selectedPartnerId],
            active_id: this.state.selectedPartnerId,
            active_model: "res.partner"
          });
    
          this.notification.add("Server action executed successfully!", {
            type: "success"
          });
    
          console.log("ActionNavigator: Server action result:", result);
    
        } catch (error) {
          console.error("ActionNavigator: Server action failed:", error);
    
          this.notification.add(`Server action failed: ${error.message}`, {
            type: "danger"
          });
        }
      }
    
      // Report actions
      openCustomerReport(partnerId) {
        console.log(`ActionNavigator: Opening report for customer ${partnerId}`);
    
        this.action.doAction({
          type: "ir.actions.report",
          report_name: "base.report_partner_ledger", // Example report
          context: {
            active_ids: [partnerId],
            active_model: "res.partner"
          }
        });
      }
    
      // URL actions
      openExternalUrl(url) {
        console.log(`ActionNavigator: Opening external URL: ${url}`);
    
        this.action.doAction({
          type: "ir.actions.act_url",
          url: url,
          target: "new"
        });
      }
    
      // Custom client actions
      openCustomClientAction() {
        console.log("ActionNavigator: Opening custom client action");
    
        this.action.doAction({
          type: "ir.actions.client",
          tag: "my_custom_action", // Your custom action tag
          name: "Custom Dashboard",
          params: {
            // Custom parameters for your action
            customer_id: this.state.selectedPartnerId
          }
        });
      }
    
      // Navigation methods
      goToAppsMenu() {
        this.action.doAction("base.open_module_tree");
      }
    
      goToSettings() {
        this.action.doAction("base.action_res_config_settings");
      }
    

    Finally, a few small helpers the template needs to read and react to the current selection:

      // Helper methods for template
      onPartnerSelect(event) {
        this.state.selectedPartnerId = parseInt(event.target.value) || null;
      }
    
      onProductSelect(event) {
        this.state.selectedProductId = parseInt(event.target.value) || null;
      }
    
      get selectedPartner() {
        return this.state.partners.find(p => p.id === this.state.selectedPartnerId);
      }
    
      get hasSelectedPartner() {
        return this.state.selectedPartnerId !== null;
      }
    }
    

    Template (action_navigator.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
        <t t-name="my_module.ActionNavigator" owl="1">
            <div class="action-navigator">
                <div class="row">
                    <!-- Selection Panel -->
                    <div class="col-md-4">
                        <div class="card">
                            <div class="card-header">
                                <h5>Select Records</h5>
                            </div>
                            <div class="card-body">
                                <!-- Partner Selection -->
                                <div class="mb-3">
                                    <label class="form-label">Customer</label>
                                    <select 
                                        class="form-select"
                                        t-on-change="onPartnerSelect">
                                        <option value="">Select a customer...</option>
                                        <t t-foreach="state.partners" t-as="partner" t-key="partner.id">
                                            <option 
                                                t-att-value="partner.id"
                                                t-att-selected="state.selectedPartnerId === partner.id"
                                                t-esc="partner.name"/>
                                        </t>
                                    </select>
                                </div>
    
                                <!-- Product Selection -->
                                <div class="mb-3">
                                    <label class="form-label">Product</label>
                                    <select 
                                        class="form-select"
                                        t-on-change="onProductSelect">
                                        <option value="">Select a product...</option>
                                        <t t-foreach="state.products" t-as="product" t-key="product.id">
                                            <option 
                                                t-att-value="product.id"
                                                t-att-selected="state.selectedProductId === product.id"
                                                t-esc="product.name"/>
                                        </t>
                                    </select>
                                </div>
    
                                <!-- Selection Info -->
                                <t t-if="hasSelectedPartner">
                                    <div class="alert alert-info">
                                        <small>
                                            Selected: <strong t-esc="selectedPartner.name"/>
                                        </small>
                                    </div>
                                </t>
                            </div>
                        </div>
                    </div>
    
                    <!-- Action Panel -->
                    <div class="col-md-8">
                        <div class="card">
                            <div class="card-header">
                                <h5>Action Examples</h5>
                            </div>
                            <div class="card-body">
                                <!-- Window Actions -->
                                <div class="mb-4">
                                    <h6>Window Actions</h6>
                                    <div class="btn-group mb-2 me-2" role="group">
                                        <button 
                                            class="btn btn-primary btn-sm"
                                            t-on-click="() => this.openCustomerForm()">
                                            <i class="fa fa-plus"></i> New Customer
                                        </button>
                                        <button 
                                            class="btn btn-outline-primary btn-sm"
                                            t-on-click="() => this.openCustomerForm(state.selectedPartnerId)"
                                            t-att-disabled="!hasSelectedPartner">
                                            <i class="fa fa-edit"></i> Edit Selected
                                        </button>
                                    </div>
    
                                    <div class="btn-group mb-2" role="group">
                                        <button 
                                            class="btn btn-info btn-sm"
                                            t-on-click="openCustomerList">
                                            <i class="fa fa-list"></i> Customer List
                                        </button>
                                        <button 
                                            class="btn btn-info btn-sm"
                                            t-on-click="openCustomerKanban">
                                            <i class="fa fa-columns"></i> Customer Kanban
                                        </button>
                                    </div>
                                </div>
    
                                <!-- Target Options -->
                                <div class="mb-4">
                                    <h6>Open Targets</h6>
                                    <div class="btn-group mb-2" role="group">
                                        <button 
                                            class="btn btn-outline-secondary btn-sm"
                                            t-on-click="() => this.openInNewWindow(state.selectedPartnerId)"
                                            t-att-disabled="!hasSelectedPartner">
                                            <i class="fa fa-external-link"></i> New Window
                                        </button>
                                        <button 
                                            class="btn btn-outline-secondary btn-sm"
                                            t-on-click="() => this.openInModal(state.selectedPartnerId)"
                                            t-att-disabled="!hasSelectedPartner">
                                            <i class="fa fa-window-restore"></i> Modal
                                        </button>
                                        <button 
                                            class="btn btn-outline-secondary btn-sm"
                                            t-on-click="() => this.openWithCallback(state.selectedPartnerId)"
                                            t-att-disabled="!hasSelectedPartner">
                                            <i class="fa fa-reply"></i> With Callback
                                        </button>
                                    </div>
                                </div>
    
                                <!-- Related Records -->
                                <div class="mb-4">
                                    <h6>Related Records</h6>
                                    <button 
                                        class="btn btn-success btn-sm"
                                        t-on-click="() => this.openCustomerSales(state.selectedPartnerId)"
                                        t-att-disabled="!hasSelectedPartner">
                                        <i class="fa fa-shopping-cart"></i> Customer Sales
                                    </button>
                                </div>
    
                                <!-- Reports and External -->
                                <div class="mb-4">
                                    <h6>Reports &amp; External</h6>
                                    <div class="btn-group mb-2 me-2" role="group">
                                        <button 
                                            class="btn btn-warning btn-sm"
                                            t-on-click="() => this.openCustomerReport(state.selectedPartnerId)"
                                            t-att-disabled="!hasSelectedPartner">
                                            <i class="fa fa-file-pdf-o"></i> Customer Report
                                        </button>
                                    </div>
    
                                    <div class="btn-group mb-2" role="group">
                                        <button 
                                            class="btn btn-outline-info btn-sm"
                                            t-on-click="() => this.openExternalUrl('https://www.odoo.com')">
                                            <i class="fa fa-globe"></i> Open Odoo.com
                                        </button>
                                    </div>
                                </div>
    
                                <!-- System Navigation -->
                                <div class="mb-4">
                                    <h6>System Navigation</h6>
                                    <div class="btn-group" role="group">
                                        <button 
                                            class="btn btn-secondary btn-sm"
                                            t-on-click="goToAppsMenu">
                                            <i class="fa fa-th"></i> Apps Menu
                                        </button>
                                        <button 
                                            class="btn btn-secondary btn-sm"
                                            t-on-click="goToSettings">
                                            <i class="fa fa-cog"></i> Settings
                                        </button>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </t>
    </templates>
    

    The dialog Service: User Interactions

    The dialog service provides sophisticated modal dialogs for user interactions:

    JavaScript Example:

    import { Component } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
    
    export class DialogDemo extends Component {
      static template = "my_module.DialogDemo";
    
      setup() {
        this.dialog = useService("dialog");
        this.notification = useService("notification");
      }
    
      // Confirmation dialog
      showConfirmationDialog() {
        this.dialog.add(ConfirmationDialog, {
          title: "Confirm Delete",
          body: "Are you sure you want to delete this record? This action cannot be undone.",
          confirm: () => {
            this.notification.add("Record deleted successfully!", { type: "success" });
            console.log("User confirmed deletion");
          },
          cancel: () => {
            this.notification.add("Deletion cancelled.", { type: "info" });
            console.log("User cancelled deletion");
          }
        });
      }
    
      // Custom dialog with form
      showCustomDialog() {
        this.dialog.add(CustomFormDialog, {
          title: "Create New Item",
          onSave: (data) => {
            console.log("Dialog saved with data:", data);
            this.notification.add("Item created successfully!", { type: "success" });
          }
        });
      }
    }
    

    Additional Essential Services

    The user Service

    Access current user information:

    setup() {
      this.user = useService("user");
    
      console.log("Current user:", this.user.name);
      console.log("Is admin:", this.user.isAdmin);
      console.log("User context:", this.user.context);
    
      // Group membership checks are asynchronous
      onWillStart(async () => {
        this.isSaleManager = await this.user.hasGroup("sales_team.group_sale_manager");
      });
    }
    

    The router Service

    Handle URL routing:

    setup() {
      this.router = useService("router");
    
      // Navigate to a specific route
      this.navigateToCustomers = () => {
        this.router.pushState({ action: "customers" });
      };
    }
    

    The company Service

    Access current company information:

    setup() {
      this.company = useService("company");
    
      console.log("Current company:", this.company.currentCompany.name);
      console.log("Allowed companies:", this.company.allowedCompanies);
    }
    

    Service Integration Patterns

    Pattern 1: Coordinated Service Usage

    async createCustomerWithFeedback(customerData) {
      try {
        // 1. Show progress notification
        this.notification.add("Creating customer...", { type: "info" });
    
        // 2. Create the customer (create returns an array of new ids)
        const [customerId] = await this.orm.create("res.partner", [customerData]);
    
        // 3. Show success notification
        this.notification.add("Customer created successfully!", { type: "success" });
    
        // 4. Navigate to the new customer
        this.action.doAction({
          type: "ir.actions.act_window",
          res_model: "res.partner",
          res_id: customerId,
          views: [[false, "form"]],
          target: "current"
        });
    
      } catch (error) {
        this.notification.add(`Failed to create customer: ${error.message}`, {
          type: "danger",
          sticky: true
        });
      }
    }
    

    Pattern 2: Conditional Actions Based on User Permissions

    async deleteRecord(recordId) {
      // Check if user has delete permissions (hasGroup is async)
      const canDelete = this.user.isAdmin || (await this.user.hasGroup("base.group_system"));
      if (!canDelete) {
        this.notification.add("You don't have permission to delete records.", {
          type: "warning"
        });
        return;
      }
    
      // Show confirmation dialog
      this.dialog.add(ConfirmationDialog, {
        title: "Confirm Deletion",
        body: "This will permanently delete the record.",
        confirm: async () => {
          try {
            await this.orm.unlink("model.name", [recordId]);
            this.notification.add("Record deleted successfully!", { type: "success" });
          } catch (error) {
            this.notification.add("Failed to delete record.", { type: "danger" });
          }
        }
      });
    }
    

    Testing Service Integration

    Services can be mocked for testing:

    describe("ServiceIntegration", () => {
      test("should show notification on success", async () => {
        const mockNotification = {
          add: jest.fn()
        };
    
        const mockAction = {
          doAction: jest.fn().mockResolvedValue(true)
        };
    
        const component = await makeTestComponent(MyComponent, {
          services: {
            notification: mockNotification,
            action: mockAction,
            orm: mockOrm
          }
        });
    
        await component.performAction();
    
        expect(mockNotification.add).toHaveBeenCalledWith(
          "Operation completed successfully!",
          { type: "success" }
        );
    
        expect(mockAction.doAction).toHaveBeenCalledWith({
          type: "ir.actions.act_window",
          res_model: "test.model",
          views: [[false, "form"]],
          target: "current"
        });
      });
    
      test("should handle service errors gracefully", async () => {
        const mockNotification = {
          add: jest.fn()
        };
    
        const mockOrm = {
          create: jest.fn().mockRejectedValue(new Error("Database error"))
        };
    
        const component = await makeTestComponent(MyComponent, {
          services: {
            notification: mockNotification,
            orm: mockOrm
          }
        });
    
        await component.createRecord();
    
        expect(mockNotification.add).toHaveBeenCalledWith(
          "Failed to create record: Database error",
          { type: "danger", sticky: true }
        );
      });
    });
    

    Service Best Practices

    Do's

    1. Use appropriate services for each task
    2. Handle errors gracefully with user-friendly messages
    3. Provide feedback for all user actions
    4. Cache expensive operations when possible
    5. Test service interactions with proper mocks
    6. Use confirmation dialogs for destructive actions
    7. Coordinate services for complex workflows

    Don'ts

    1. Don't ignore service errors - always handle exceptions
    2. Don't spam notifications - be selective about what requires user attention
    3. Don't block UI - use loading states for long operations
    4. Don't hardcode action IDs - use XML IDs when possible
    5. Don't forget to cleanup - clear timers and subscriptions in service callbacks

    Common Pitfalls

    Pitfall 1: Treating hasGroup() as Synchronous

    // Wrong - hasGroup() returns a Promise; the `if` always takes this branch,
    // because a Promise object is truthy regardless of what it resolves to
    setup() {
      this.user = useService("user");
      if (this.user.hasGroup("base.group_system")) {
        // Runs unconditionally, before the group check has even resolved!
      }
    }
    
    // Correct - await it, typically inside onWillStart or another async method
    onWillStart(async () => {
      this.isSystemAdmin = await this.user.hasGroup("base.group_system");
    });
    

    Pitfall 2: Requesting a Service Outside setup()

    // Wrong - useService only works while a component is being set up;
    // calling it later (e.g. from an event handler) throws an error
    someButtonHandler() {
      const notification = useService("notification"); // Error!
      notification.add("Done!");
    }
    
    // Correct - request the service once in setup(), then reuse the reference
    setup() {
      this.notification = useService("notification");
    }
    
    someButtonHandler() {
      this.notification.add("Done!");
    }
    

    Pitfall 3: Swallowing ORM Errors Silently

    // Wrong - if orm.create() rejects, the promise is unhandled and the user
    // sees nothing happen at all
    async saveRecord(data) {
      await this.orm.create("res.partner", [data]);
    }
    
    // Correct - catch the error and tell the user what happened
    async saveRecord(data) {
      try {
        await this.orm.create("res.partner", [data]);
        this.notification.add("Saved!", { type: "success" });
      } catch (error) {
        this.notification.add(`Save failed: ${error.message}`, {
          type: "danger",
          sticky: true
        });
      }
    }
    

    Pitfall 4: Reaching for a Notification When You Need a Dialog

    // Wrong - a toast is easy to miss, disappears on its own, and can't
    // block the next line of code from running
    this.notification.add("This will permanently delete the record. Continue?", {
      type: "warning"
    });
    this.orm.unlink("res.partner", [id]); // Runs immediately, no matter what!
    
    // Correct - a destructive action needs a confirmation dialog, which pauses
    // the flow until the user actually decides
    this.dialog.add(ConfirmationDialog, {
      title: "Confirm Deletion",
      body: "This will permanently delete the record. Continue?",
      confirm: () => this.orm.unlink("res.partner", [id]),
    });
    

    Exercises

    1. Notification variety: Add a button that shows a warning notification with sticky: true, and another that shows an info notification that auto-dismisses after a few seconds. Confirm you can see the behavioral difference.
    2. Guarded delete: Write a deleteRecord(id) method that first checks await this.user.hasGroup("base.group_system"). If the user doesn't belong to that group, show a warning notification instead of deleting.
    3. Confirm, then act: Use the dialog service to show a ConfirmationDialog before calling this.orm.unlink(...). On cancel, show an info notification saying the deletion was cancelled instead.

    TL;DR: Reach for notification to give feedback, dialog for confirmations and custom dialogs, action to navigate, and remember user.hasGroup() is async — always inject services through useService, never build your own.

    Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch13_1_ex1. Installation instructions are in the repository README.


    What's Next?

    In Part 2, we'll build on these fundamentals with service composition patterns for multi-step workflows, performance techniques like debouncing and caching, and strategies for recovering gracefully from failures — the patterns you'll reach for once your components start doing more than a single API call.

    in OWL Book
    Chapter 13 Part 2: Advanced Service Patterns and Optimization
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website