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 12: Server Communication with useService

  • All Blogs
  • OWL Book
  • Chapter 12: Server Communication with useService
  • August 25, 2026 by
    Chapter 12: Server Communication with useService
    Grover Menacho

    Why this chapter? Every non-trivial Odoo addon eventually needs to read or write real records — a component that only manages local state is a toy. This chapter is where your components start talking to the actual database. What is Odoo trying to solve with this? Odoo needs a consistent, safe way for frontend code to reach the ORM and custom controllers without every developer hand-rolling their own AJAX calls, error handling, and security checks. Real-world application: Every dashboard widget, custom kanban action, and portal form I've built for a client eventually comes down to orm.searchRead/create/write calls exactly like the ones in this chapter.

    In the previous chapters, we've built components that can manage state, respond to events, and handle complex lifecycle scenarios. But a modern web application isn't complete without the ability to communicate with the server. Your beautiful OWL components need to fetch real data, save user changes, and integrate seamlessly with Odoo's backend.

    This is where the service system comes in. Odoo's service architecture provides a clean, consistent way for your frontend components to communicate with the Python backend, display notifications, trigger actions, and much more.

    The gateway to this powerful system is the useService hook—your components' bridge to the entire Odoo ecosystem.


    Understanding Odoo's Service Architecture

    Services in Odoo are singleton objects that provide specific functionality across your entire application. They're designed to be:

    • Centralized: One instance per service type
    • Consistent: Same API everywhere in the app
    • Integrated: Seamlessly connected to Odoo's backend
    • Testable: Easy to mock and test

    Think of services as your application's "utilities"—specialized tools that handle specific concerns so your components can focus on their primary job: rendering UI and handling user interactions.

    The useService Hook

    The useService hook is remarkably simple:

    import { useService } from "@web/core/utils/hooks";
    
    // Inside your component's setup()
    const serviceName = useService("service_name");
    

    That's it! You get a fully configured service instance ready to use. Let's explore the most important services you'll use daily.


    The orm Service: Your Database Gateway

    The orm service is your primary tool for database operations. It provides a clean, Promise-based API that mirrors Odoo's Python ORM methods, making server communication feel natural and intuitive.

    Basic CRUD Operations

    Let's build a comprehensive product management component that demonstrates all the core ORM operations:

    JavaScript (product_manager.js):

    import { Component, useEffect, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class ProductManager extends Component {
      static template = "my_module.ProductManager";
    
      setup() {
        this.state = useState({
          products: [],
          categories: [],
          loading: true,
          error: null,
          selectedProduct: null,
          formData: {
            name: "",
            list_price: 0,
            categ_id: null,
            description: ""
          }
        });
    
        // Get the ORM service
        this.orm = useService("orm");
    
        // Load initial data when component mounts
        useEffect(
          () => {
            this.loadInitialData();
          },
          () => []
        );
      }
    
      async loadInitialData() {
        try {
          console.log("ProductManager: Loading initial data");
          this.state.loading = true;
          this.state.error = null;
    
          // Load products and categories in parallel for better performance
          const [products, categories] = await Promise.all([
            this.loadProducts(),
            this.loadCategories()
          ]);
    
          this.state.products = products;
          this.state.categories = categories;
    
          console.log(`ProductManager: Loaded ${products.length} products and ${categories.length} categories`);
    
        } catch (error) {
          console.error("ProductManager: Failed to load initial data:", error);
          this.state.error = "Failed to load data. Please refresh the page.";
        } finally {
          this.state.loading = false;
        }
      }
    
      async loadProducts() {
        // searchRead: Search for records and read their fields in one call
        return await this.orm.searchRead(
          "product.product",                    // Model name
          [["sale_ok", "=", true]],            // Domain (search filters)
          ["id", "name", "list_price", "categ_id", "qty_available"], // Fields to read
          { 
            order: "name asc",                 // Sort order
            limit: 100                         // Maximum records
          }
        );
      }
    
      async loadCategories() {
        return await this.orm.searchRead(
          "product.category",
          [],                                  // Empty domain = all records
          ["id", "name"],
          { order: "name asc" }
        );
      }
    
      async createProduct() {
        if (!this.state.formData.name.trim()) {
          this.state.error = "Product name is required";
          return;
        }
    
        try {
          console.log("ProductManager: Creating new product", this.state.formData);
    
          // create: Create new records
          const newProductIds = await this.orm.create(
            "product.product",
            [{
              name: this.state.formData.name,
              list_price: this.state.formData.list_price,
              categ_id: this.state.formData.categ_id,
              sale_ok: true,
              purchase_ok: true
            }]
          );
    
          console.log(`ProductManager: Created product with ID ${newProductIds[0]}`);
    
          // Reload products to show the new one
          await this.refreshProducts();
    
          // Reset form
          this.resetForm();
    
          this.state.error = null;
    
        } catch (error) {
          console.error("ProductManager: Failed to create product:", error);
          this.state.error = `Failed to create product: ${error.message}`;
        }
      }
    
      async updateProduct(productId, updates) {
        try {
          console.log(`ProductManager: Updating product ${productId}`, updates);
    
          // write: Update existing records
          await this.orm.write(
            "product.product",
            [productId],                       // List of record IDs to update
            updates                            // Dictionary of field updates
          );
    
          console.log(`ProductManager: Successfully updated product ${productId}`);
    
          // Refresh the product list
          await this.refreshProducts();
    
        } catch (error) {
          console.error(`ProductManager: Failed to update product ${productId}:`, error);
          this.state.error = `Failed to update product: ${error.message}`;
        }
      }
    
      async deleteProduct(productId) {
        try {
          console.log(`ProductManager: Deleting product ${productId}`);
    
          // unlink: Delete records
          await this.orm.unlink("product.product", [productId]);
    
          console.log(`ProductManager: Successfully deleted product ${productId}`);
    
          // Remove from local state immediately for better UX
          this.state.products = this.state.products.filter(p => p.id !== productId);
    
          // Clear selection if deleted product was selected
          if (this.state.selectedProduct?.id === productId) {
            this.state.selectedProduct = null;
          }
    
        } catch (error) {
          console.error(`ProductManager: Failed to delete product ${productId}:`, error);
          this.state.error = `Failed to delete product: ${error.message}`;
        }
      }
    
      async refreshProducts() {
        try {
          const products = await this.loadProducts();
          this.state.products = products;
          console.log("ProductManager: Products refreshed successfully");
        } catch (error) {
          console.error("ProductManager: Failed to refresh products:", error);
        }
      }
    
      // Helper methods for form handling
      selectProduct(product) {
        this.state.selectedProduct = product;
        this.state.formData = {
          name: product.name,
          list_price: product.list_price,
          categ_id: product.categ_id[0],
          description: ""
        };
      }
    
      resetForm() {
        this.state.formData = {
          name: "",
          list_price: 0,
          categ_id: null,
          description: ""
        };
        this.state.selectedProduct = null;
      }
    
      onFormChange(field, value) {
        this.state.formData[field] = value;
      }
    
      // Quick actions for product management
      async quickUpdatePrice(productId, newPrice) {
        await this.updateProduct(productId, { list_price: newPrice });
      }
    
      async toggleProductAvailability(productId, currentStatus) {
        await this.updateProduct(productId, { sale_ok: !currentStatus });
      }
    
      // Computed properties for template
      get formattedProducts() {
        return this.state.products.map(product => ({
          ...product,
          formattedPrice: `$${product.list_price.toFixed(2)}`,
          categoryName: product.categ_id ? product.categ_id[1] : "No Category",
          stockStatus: product.qty_available > 0 ? "In Stock" : "Out of Stock"
        }));
      }
    
      get isFormValid() {
        return this.state.formData.name.trim().length > 0;
      }
    }
    

    Template (product_manager.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
        <t t-name="my_module.ProductManager" owl="1">
            <div class="product-manager">
                <!-- Header -->
                <div class="d-flex justify-content-between align-items-center mb-4">
                    <h2>Product Manager</h2>
                    <button 
                        class="btn btn-outline-secondary btn-sm"
                        t-on-click="refreshProducts"
                        t-att-disabled="state.loading">
                        <i class="fa fa-refresh"></i> Refresh
                    </button>
                </div>
    
                <!-- Error Display -->
                <t t-if="state.error">
                    <div class="alert alert-danger alert-dismissible">
                        <t t-esc="state.error"/>
                        <button 
                            type="button" 
                            class="btn-close" 
                            t-on-click="() => this.state.error = null">
                        </button>
                    </div>
                </t>
    
                <!-- Loading State -->
                <t t-if="state.loading">
                    <div class="text-center py-5">
                        <div class="spinner-border text-primary" role="status">
                            <span class="visually-hidden">Loading products...</span>
                        </div>
                        <p class="mt-2 text-muted">Loading products and categories...</p>
                    </div>
                </t>
    
                <!-- Main Content -->
                <t t-else="">
                    <div class="row">
                        <!-- Product Form -->
                        <div class="col-md-4">
                            <div class="card">
                                <div class="card-header">
                                    <h5 class="mb-0">
                                        <t t-if="state.selectedProduct">Edit Product</t>
                                        <t t-else="">New Product</t>
                                    </h5>
                                </div>
                                <div class="card-body">
                                    <form t-on-submit.prevent="createProduct">
                                        <!-- Product Name -->
                                        <div class="mb-3">
                                            <label class="form-label">Product Name *</label>
                                            <input 
                                                type="text" 
                                                class="form-control"
                                                t-model="state.formData.name"
                                                placeholder="Enter product name"
                                                required
                                            />
                                        </div>
    
                                        <!-- Price -->
                                        <div class="mb-3">
                                            <label class="form-label">Price</label>
                                            <div class="input-group">
                                                <span class="input-group-text">$</span>
                                                <input 
                                                    type="number" 
                                                    class="form-control"
                                                    t-model="state.formData.list_price"
                                                    min="0"
                                                    step="0.01"
                                                />
                                            </div>
                                        </div>
    
                                        <!-- Category -->
                                        <div class="mb-3">
                                            <label class="form-label">Category</label>
                                            <select 
                                                class="form-select"
                                                t-model="state.formData.categ_id">
                                                <option value="">Select Category</option>
                                                <t t-foreach="state.categories" t-as="category" t-key="category.id">
                                                    <option t-att-value="category.id" t-esc="category.name"/>
                                                </t>
                                            </select>
                                        </div>
    
                                        <!-- Action Buttons -->
                                        <div class="d-flex gap-2">
                                            <button 
                                                type="submit"
                                                class="btn btn-primary flex-fill"
                                                t-att-disabled="!isFormValid">
                                                <t t-if="state.selectedProduct">Update Product</t>
                                                <t t-else="">Create Product</t>
                                            </button>
    
                                            <button 
                                                type="button"
                                                class="btn btn-outline-secondary"
                                                t-on-click="resetForm">
                                                Clear
                                            </button>
                                        </div>
                                    </form>
                                </div>
                            </div>
                        </div>
    
                        <!-- Product List -->
                        <div class="col-md-8">
                            <div class="card">
                                <div class="card-header">
                                    <h5 class="mb-0">
                                        Products (<t t-esc="state.products.length"/>)
                                    </h5>
                                </div>
                                <div class="card-body p-0">
                                    <t t-if="state.products.length === 0">
                                        <div class="text-center py-4 text-muted">
                                            <i class="fa fa-box-open fa-2x mb-2"></i>
                                            <p>No products found. Create your first product!</p>
                                        </div>
                                    </t>
                                    <t t-else="">
                                        <div class="table-responsive">
                                            <table class="table table-hover mb-0">
                                                <thead class="table-light">
                                                    <tr>
                                                        <th>Name</th>
                                                        <th>Category</th>
                                                        <th>Price</th>
                                                        <th>Stock</th>
                                                        <th class="text-end">Actions</th>
                                                    </tr>
                                                </thead>
                                                <tbody>
                                                    <t t-foreach="formattedProducts" t-as="product" t-key="product.id">
                                                        <tr t-att-class="state.selectedProduct?.id === product.id ? 'table-active' : ''">
                                                            <td>
                                                                <strong t-esc="product.name"/>
                                                            </td>
                                                            <td>
                                                                <small class="text-muted" t-esc="product.categoryName"/>
                                                            </td>
                                                            <td>
                                                                <span t-esc="product.formattedPrice"/>
                                                            </td>
                                                            <td>
                                                                <span 
                                                                    class="badge"
                                                                    t-att-class="product.qty_available > 0 ? 'bg-success' : 'bg-warning'">
                                                                    <t t-esc="product.stockStatus"/>
                                                                </span>
                                                            </td>
                                                            <td class="text-end">
                                                                <div class="btn-group btn-group-sm">
                                                                    <button 
                                                                        class="btn btn-outline-primary"
                                                                        t-on-click="() => this.selectProduct(product)"
                                                                        title="Edit">
                                                                        <i class="fa fa-edit"></i>
                                                                    </button>
                                                                    <button 
                                                                        class="btn btn-outline-danger"
                                                                        t-on-click="() => this.deleteProduct(product.id)"
                                                                        title="Delete">
                                                                        <i class="fa fa-trash"></i>
                                                                    </button>
                                                                </div>
                                                            </td>
                                                        </tr>
                                                    </t>
                                                </tbody>
                                            </table>
                                        </div>
                                    </t>
                                </div>
                            </div>
                        </div>
                    </div>
                </t>
            </div>
        </t>
    </templates>
    

    This example demonstrates all the key ORM operations:

    • searchRead: Search and read records in one operation
    • create: Create new records
    • write: Update existing records
    • unlink: Delete records

    Advanced ORM Operations

    The orm service provides more advanced methods for complex scenarios:

    Using read for Specific Records:

    // Read specific records by ID
    const products = await this.orm.read(
      "product.product",
      [1, 2, 3],                           // Specific record IDs
      ["name", "list_price", "description"] // Fields to read
    );
    

    Using search and searchCount:

    // Get only record IDs matching criteria
    const productIds = await this.orm.search(
      "product.product",
      [["sale_ok", "=", true]],
      { limit: 5, offset: 10 }
    );
    
    // Count records without fetching them
    const totalProducts = await this.orm.searchCount(
      "product.product",
      [["sale_ok", "=", true]]
    );
    

    Calling Custom Model Methods:

    // Call any method on your Python model
    const result = await this.orm.call(
      "product.product",      // Model name
      "my_custom_method",     // Method name
      [recordId],            // Args (usually record IDs)
      {                      // Kwargs
        param1: "value1",
        param2: "value2"
      }
    );
    

    The rpc Service: Direct Controller Communication

    While the orm service is perfect for model operations, sometimes you need to call controller routes directly. The rpc service (short for Remote Procedure Call — invoking a function that actually runs on the server, as if it were local) provides this capability:

    JavaScript Example:

    import { Component, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class CustomReportGenerator extends Component {
      static template = "my_module.CustomReportGenerator";
    
      setup() {
        this.state = useState({
          reportData: null,
          loading: false,
          filters: {
            start_date: "",
            end_date: "",
            partner_ids: []
          }
        });
    
        this.rpc = useService("rpc");
      }
    
      async generateReport() {
        try {
          this.state.loading = true;
    
          console.log("CustomReportGenerator: Generating custom report");
    
          // Call your custom controller route
          const reportData = await this.rpc("/my_module/generate_report", {
            filters: this.state.filters,
            format: "json",
            include_details: true
          });
    
          this.state.reportData = reportData;
          console.log("CustomReportGenerator: Report generated successfully");
    
        } catch (error) {
          console.error("CustomReportGenerator: Report generation failed:", error);
          // Handle error appropriately
        } finally {
          this.state.loading = false;
        }
      }
    
      async downloadReportPDF() {
        try {
          console.log("CustomReportGenerator: Downloading PDF report");
    
          // This might return a blob or URL for download
          const pdfData = await this.rpc("/my_module/generate_report", {
            filters: this.state.filters,
            format: "pdf"
          });
    
          // Handle PDF download logic here
    
        } catch (error) {
          console.error("CustomReportGenerator: PDF download failed:", error);
        }
      }
    }
    

    Error Handling and User Feedback

    Professional applications need robust error handling. Here's how to handle common scenarios:

    Comprehensive Error Handling Pattern:

    async performDatabaseOperation() {
      try {
        this.state.loading = true;
        this.state.error = null;
    
        const result = await this.orm.searchRead(
          "some.model",
          this.buildDomain(),
          this.getRequiredFields()
        );
    
        this.state.data = result;
    
      } catch (error) {
        console.error("Database operation failed:", error);
    
        // Different error types need different handling
        if (error.code === 403) {
          this.state.error = "You don't have permission to access this data.";
        } else if (error.code === 404) {
          this.state.error = "The requested data was not found.";
        } else if (error.message.includes("network")) {
          this.state.error = "Network error. Please check your connection.";
        } else {
          this.state.error = `Operation failed: ${error.message}`;
        }
    
      } finally {
        this.state.loading = false;
      }
    }
    

    Optimistic Updates Pattern:

    async quickUpdate(recordId, field, value) {
      // Store original value for rollback
      const originalData = [...this.state.records];
    
      try {
        // Update UI immediately (optimistic)
        const record = this.state.records.find(r => r.id === recordId);
        if (record) {
          record[field] = value;
        }
    
        // Then update server
        await this.orm.write("model.name", [recordId], { [field]: value });
    
        console.log("Quick update successful");
    
      } catch (error) {
        // Rollback UI on error
        this.state.records = originalData;
        console.error("Quick update failed, rolled back:", error);
    
        // Show user-friendly error message
        this.showError(`Failed to update ${field}. Please try again.`);
      }
    }
    

    Service Integration Patterns

    Pattern 1: Service Composition

    setup() {
      // Multiple services working together
      this.orm = useService("orm");
      this.rpc = useService("rpc");
      this.notification = useService("notification");
      this.action = useService("action");
    
      // Composed operation using multiple services
      this.performComplexOperation = async () => {
        try {
          // 1. Save data via ORM
          const [recordId] = await this.orm.create("model.name", [this.state.formData]);
    
          // 2. Trigger server-side processing via RPC
          await this.rpc("/custom/process_record", { record_id: recordId });
    
          // 3. Show success notification
          this.notification.add("Record created and processed successfully!", {
            type: "success"
          });
    
          // 4. Navigate to the new record
          this.action.doAction({
            type: "ir.actions.act_window",
            res_model: "model.name",
            res_id: recordId,
            views: [[false, "form"]],
            target: "current"
          });
    
        } catch (error) {
          this.notification.add("Operation failed. Please try again.", {
            type: "danger"
          });
        }
      };
    }
    

    Pattern 2: Service Abstraction

    setup() {
      this.orm = useService("orm");
    
      // Create abstraction layer for your specific domain
      this.customerService = {
        async getCustomers(filters = {}) {
          return await this.orm.searchRead(
            "res.partner",
            [["is_company", "=", true], ...this.buildDomain(filters)],
            ["name", "email", "phone", "city"],
            { order: "name asc" }
          );
        },
    
        async createCustomer(customerData) {
          return await this.orm.create("res.partner", [customerData]);
        },
    
        async updateCustomer(customerId, updates) {
          return await this.orm.write("res.partner", [customerId], updates);
        }
      };
    }
    

    Performance Considerations

    Batch Operations

    // BAD: Multiple individual calls
    for (const productId of productIds) {
      await this.orm.write("product.product", [productId], { active: false });
    }
    
    // GOOD: Single batch call
    await this.orm.write("product.product", productIds, { active: false });
    

    Efficient Field Selection

    // BAD: Loading unnecessary data
    const products = await this.orm.searchRead("product.product", [], []);
    
    // GOOD: Only load what you need
    const products = await this.orm.searchRead(
      "product.product", 
      [], 
      ["id", "name", "list_price"] // Only the fields you actually use
    );
    

    Smart Caching Pattern

    setup() {
      this.orm = useService("orm");
      this.cache = new Map();
    
      this.getCachedData = async (cacheKey, fetcher) => {
        if (this.cache.has(cacheKey)) {
          console.log(`Using cached data for ${cacheKey}`);
          return this.cache.get(cacheKey);
        }
    
        const data = await fetcher();
        this.cache.set(cacheKey, data);
        return data;
      };
    
      this.getCategories = () => {
        return this.getCachedData('categories', () => 
          this.orm.searchRead("product.category", [], ["id", "name"])
        );
      };
    }
    

    Common Pitfalls and Solutions

    Pitfall 1: Assuming searchRead Returns a Wrapper Object

    // Wrong - some ORMs wrap results in an object; Odoo's doesn't
    const { records } = await this.orm.searchRead("res.partner", [], ["name"]);
    
    // Correct - searchRead resolves directly to an array of records
    const records = await this.orm.searchRead("res.partner", [], ["name"]);
    

    Pitfall 2: Forgetting await

    // Wrong - every orm method returns a Promise; without await you store the Promise itself
    loadProducts() {
      this.state.products = this.orm.searchRead("product.product", [], ["name"]);
    }
    
    // Correct
    async loadProducts() {
      this.state.products = await this.orm.searchRead("product.product", [], ["name"]);
    }
    

    Pitfall 3: Not Handling Server Errors

    // Wrong - an unhandled rejection can crash the whole action
    async saveRecord() {
      await this.orm.create("res.partner", [this.state.formData]);
    }
    
    // Correct - wrap in try/catch and give the user feedback
    async saveRecord() {
      try {
        await this.orm.create("res.partner", [this.state.formData]);
      } catch (error) {
        this.state.error = "Could not save the record. Please try again.";
      }
    }
    

    Pitfall 4: Forgetting that create Returns an Array of IDs

    // Wrong - assuming create resolves to a single id
    const id = await this.orm.create("res.partner", [{ name: "Test" }]);
    
    // Correct - create always resolves to an array, even for a single record
    const [id] = await this.orm.create("res.partner", [{ name: "Test" }]);
    

    Testing Service Integration

    Because components receive services through useService, tests can swap in mocks. The exact helpers depend on your Odoo version (we'll cover Odoo's real test framework in Chapter 16 Part 1), but the idea always looks like this:

    // Pseudo-code: the shape of a service-mocking test
    
    describe("ProductManager", () => {
      test("should load products on mount", async () => {
        const mockOrm = {
          searchRead: jest.fn().mockResolvedValue([
            { id: 1, name: "Test Product", list_price: 10.0 }
          ])
        };
    
        const component = await makeTestComponent(ProductManager, {
          services: {
            orm: mockOrm
          }
        });
    
        await nextTick(); // Wait for effects to complete
    
        expect(mockOrm.searchRead).toHaveBeenCalledWith(
          "product.product",
          [["sale_ok", "=", true]],
          expect.any(Array),
          expect.any(Object)
        );
    
        expect(component.state.products).toHaveLength(1);
      });
    });
    

    Summary

    The useService hook and Odoo's service system provide:

    • orm service: Complete CRUD operations for Odoo models
    • rpc service: Direct communication with custom controllers
    • Consistent API: Same patterns across your entire application
    • Error handling: Built-in error management and recovery
    • Performance: Optimized communication with the backend

    Key principles for effective service usage:

    1. Use the right service for each task (ORM for models, RPC for controllers)
    2. Handle errors gracefully with user-friendly messages
    3. Batch operations when possible for better performance
    4. Cache strategically to reduce server requests
    5. Test with mocks to ensure reliability

    In the next chapter, we'll explore more of Odoo's built-in services like notifications and actions, which will help you create truly integrated user experiences.

    TL;DR: Use useService("orm") for CRUD against Odoo models (searchRead, create, write, unlink) and useService("rpc") for custom controllers — always await them, always wrap calls in try/catch, and remember create returns an array of ids, not a bare id.

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


    Exercises

    1. Basic CRUD: Build a tiny component that lists res.partner records (searchRead with just name and email), and add a button that creates a new partner named "Test Contact" using orm.create. Remember create returns an array of ids.
    2. Handle the error: Wrap a orm.write call in a try/catch and show a Bootstrap alert with a friendly message if it fails. Test it by passing an invalid model name and see what happens without the try/catch first.
    3. Count before you fetch: Use orm.searchCount to show "X contacts found" before actually loading the full list with searchRead, so the user sees a number immediately while the data streams in.

    What's Next?

    With data flowing between your components and the server, Chapter 13 turns to the rest of Odoo's built-in services — notifications, dialogs, actions, and permissions — the pieces that make a component feel like a native part of Odoo instead of a bolted-on widget.

    in OWL Book
    Chapter 13 Part 1: Odoo's Built-in Services
    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