Ir al contenido
  • Inicio
  • Libro
  • Blog
  • Sobre nosotros
  • Ayuda
  • Contáctanos
  •  [email protected]
  • Inicia sesión
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contáctanos
Simplify It S.R.L.
      • Inicio
      • Libro
      • Blog
      • Sobre nosotros
      • Ayuda
      • Contáctanos
    •  [email protected]
    • English (US) Español (BO)
    • Inicia sesión
    • Contáctanos

    Capítulo 12: Comunicación con el Servidor usando useService

  • Todos los blogs
  • Libro OWL
  • Capítulo 12: Comunicación con el Servidor usando useService
  • 25 de agosto de 2026 por
    Capítulo 12: Comunicación con el Servidor usando useService
    Grover Menacho

    ¿Por qué este capítulo? Todo addon de Odoo no trivial eventualmente necesita leer o escribir registros reales — un componente que solo gestiona estado local es un juguete. Este capítulo es donde tus componentes empiezan a hablar con la base de datos real. ¿Qué trata de resolver Odoo con esto? Odoo necesita una forma consistente y segura de que el código frontend llegue al ORM y a controladores personalizados sin que cada desarrollador arme a mano sus propias llamadas AJAX, manejo de errores y validaciones de seguridad. Aplicación en la vida real: Cada widget de dashboard, acción de kanban personalizada y formulario de portal que he construido para un cliente eventualmente se reduce a llamadas orm.searchRead/create/write exactamente como las de este capítulo.

    En los capítulos anteriores, hemos construido componentes que pueden gestionar estado, responder a eventos y manejar escenarios complejos de ciclo de vida. Pero una aplicación web moderna no está completa sin la capacidad de comunicarse con el servidor. Tus hermosos componentes OWL necesitan obtener datos reales, guardar cambios de usuario e integrarse perfectamente con el backend de Odoo.

    Aquí es donde entra el sistema de servicios. La arquitectura de servicios de Odoo proporciona una forma limpia y consistente para que tus componentes frontend se comuniquen con el backend de Python, muestren notificaciones, activen acciones y mucho más.

    La puerta de entrada a este poderoso sistema es el hook useService—el puente de tus componentes hacia todo el ecosistema de Odoo.


    Entendiendo la Arquitectura de Servicios de Odoo

    Los servicios en Odoo son objetos singleton que proporcionan funcionalidad específica a través de toda tu aplicación. Están diseñados para ser:

    • Centralizados: Una instancia por tipo de servicio
    • Consistentes: La misma API en todas partes de la app
    • Integrados: Conectados perfectamente al backend de Odoo
    • Comprobables: Fáciles de simular y probar

    Piensa en los servicios como las "utilidades" de tu aplicación—herramientas especializadas que manejan responsabilidades específicas para que tus componentes puedan enfocarse en su trabajo principal: renderizar UI y manejar interacciones de usuario.

    El Hook useService

    El hook useService es notablemente simple:

    import { useService } from "@web/core/utils/hooks";
    
    // Dentro del setup() de tu componente
    const serviceName = useService("service_name");
    

    ¡Eso es todo! Obtienes una instancia de servicio completamente configurada lista para usar. Exploremos los servicios más importantes que usarás diariamente.


    El Servicio orm: Tu Puerta de Entrada a la Base de Datos

    El servicio orm es tu herramienta principal para operaciones de base de datos. Proporciona una API limpia basada en Promises que refleja los métodos ORM de Python de Odoo, haciendo que la comunicación con el servidor se sienta natural e intuitiva.

    Operaciones CRUD Básicas

    Construyamos un componente completo de gestión de productos que demuestre todas las operaciones básicas del ORM:

    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: ""
          }
        });
    
        // Obtener el servicio ORM
        this.orm = useService("orm");
    
        // Cargar datos iniciales cuando el componente se monte
        useEffect(
          () => {
            this.loadInitialData();
          },
          () => []
        );
      }
    
      async loadInitialData() {
        try {
          console.log("ProductManager: Cargando datos iniciales");
          this.state.loading = true;
          this.state.error = null;
    
          // Cargar productos y categorías en paralelo para mejor rendimiento
          const [products, categories] = await Promise.all([
            this.loadProducts(),
            this.loadCategories()
          ]);
    
          this.state.products = products;
          this.state.categories = categories;
    
          console.log(`ProductManager: Cargados ${products.length} productos y ${categories.length} categorías`);
    
        } catch (error) {
          console.error("ProductManager: Falló la carga de datos iniciales:", error);
          this.state.error = "Falló la carga de datos. Por favor, actualiza la página.";
        } finally {
          this.state.loading = false;
        }
      }
    
      async loadProducts() {
        // searchRead: Buscar registros y leer sus campos en una sola llamada
        return await this.orm.searchRead(
          "product.product",                    // Nombre del modelo
          [["sale_ok", "=", true]],            // Dominio (filtros de búsqueda)
          ["id", "name", "list_price", "categ_id", "qty_available"], // Campos a leer
          { 
            order: "name asc",                 // Orden de clasificación
            limit: 100                         // Máximo de registros
          }
        );
      }
    
      async loadCategories() {
        return await this.orm.searchRead(
          "product.category",
          [],                                  // Dominio vacío = todos los registros
          ["id", "name"],
          { order: "name asc" }
        );
      }
    
      async createProduct() {
        if (!this.state.formData.name.trim()) {
          this.state.error = "El nombre del producto es requerido";
          return;
        }
    
        try {
          console.log("ProductManager: Creando nuevo producto", this.state.formData);
    
          // create: Crear nuevos registros
          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: Producto creado con ID ${newProductIds[0]}`);
    
          // Recargar productos para mostrar el nuevo
          await this.refreshProducts();
    
          // Reiniciar formulario
          this.resetForm();
    
          this.state.error = null;
    
        } catch (error) {
          console.error("ProductManager: Falló la creación del producto:", error);
          this.state.error = `Falló la creación del producto: ${error.message}`;
        }
      }
    
      async updateProduct(productId, updates) {
        try {
          console.log(`ProductManager: Actualizando producto ${productId}`, updates);
    
          // write: Actualizar registros existentes
          await this.orm.write(
            "product.product",
            [productId],                       // Lista de IDs de registros a actualizar
            updates                            // Diccionario de actualizaciones de campos
          );
    
          console.log(`ProductManager: Producto ${productId} actualizado exitosamente`);
    
          // Actualizar la lista de productos
          await this.refreshProducts();
    
        } catch (error) {
          console.error(`ProductManager: Falló la actualización del producto ${productId}:`, error);
          this.state.error = `Falló la actualización del producto: ${error.message}`;
        }
      }
    
      async deleteProduct(productId) {
        try {
          console.log(`ProductManager: Eliminando producto ${productId}`);
    
          // unlink: Eliminar registros
          await this.orm.unlink("product.product", [productId]);
    
          console.log(`ProductManager: Producto ${productId} eliminado exitosamente`);
    
          // Remover del estado local inmediatamente para mejor UX
          this.state.products = this.state.products.filter(p => p.id !== productId);
    
          // Limpiar selección si el producto eliminado estaba seleccionado
          if (this.state.selectedProduct?.id === productId) {
            this.state.selectedProduct = null;
          }
    
        } catch (error) {
          console.error(`ProductManager: Falló la eliminación del producto ${productId}:`, error);
          this.state.error = `Falló la eliminación del producto: ${error.message}`;
        }
      }
    
      async refreshProducts() {
        try {
          const products = await this.loadProducts();
          this.state.products = products;
          console.log("ProductManager: Productos actualizados exitosamente");
        } catch (error) {
          console.error("ProductManager: Falló la actualización de productos:", error);
        }
      }
    
      // Métodos auxiliares para manejo de formularios
      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;
      }
    
      // Acciones rápidas para gestión de productos
      async quickUpdatePrice(productId, newPrice) {
        await this.updateProduct(productId, { list_price: newPrice });
      }
    
      async toggleProductAvailability(productId, currentStatus) {
        await this.updateProduct(productId, { sale_ok: !currentStatus });
      }
    
      // Propiedades computadas para el template
      get formattedProducts() {
        return this.state.products.map(product => ({
          ...product,
          formattedPrice: `$${product.list_price.toFixed(2)}`,
          categoryName: product.categ_id ? product.categ_id[1] : "Sin Categoría",
          stockStatus: product.qty_available > 0 ? "En Stock" : "Agotado"
        }));
      }
    
      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">
                <!-- Encabezado -->
                <div class="d-flex justify-content-between align-items-center mb-4">
                    <h2>Gestor de Productos</h2>
                    <button 
                        class="btn btn-outline-secondary btn-sm"
                        t-on-click="refreshProducts"
                        t-att-disabled="state.loading">
                        <i class="fa fa-refresh"></i> Actualizar
                    </button>
                </div>
    
                <!-- Mostrar Errores -->
                <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>
    
                <!-- Estado de Carga -->
                <t t-if="state.loading">
                    <div class="text-center py-5">
                        <div class="spinner-border text-primary" role="status">
                            <span class="visually-hidden">Cargando productos...</span>
                        </div>
                        <p class="mt-2 text-muted">Cargando productos y categorías...</p>
                    </div>
                </t>
    
                <!-- Contenido Principal -->
                <t t-else="">
                    <div class="row">
                        <!-- Formulario de Producto -->
                        <div class="col-md-4">
                            <div class="card">
                                <div class="card-header">
                                    <h5 class="mb-0">
                                        <t t-if="state.selectedProduct">Editar Producto</t>
                                        <t t-else="">Nuevo Producto</t>
                                    </h5>
                                </div>
                                <div class="card-body">
                                    <form t-on-submit.prevent="createProduct">
                                        <!-- Nombre del Producto -->
                                        <div class="mb-3">
                                            <label class="form-label">Nombre del Producto *</label>
                                            <input 
                                                type="text" 
                                                class="form-control"
                                                t-model="state.formData.name"
                                                placeholder="Ingresa el nombre del producto"
                                                required
                                            />
                                        </div>
    
                                        <!-- Precio -->
                                        <div class="mb-3">
                                            <label class="form-label">Precio</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>
    
                                        <!-- Categoría -->
                                        <div class="mb-3">
                                            <label class="form-label">Categoría</label>
                                            <select 
                                                class="form-select"
                                                t-model="state.formData.categ_id">
                                                <option value="">Seleccionar Categoría</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>
    
                                        <!-- Botones de Acción -->
                                        <div class="d-flex gap-2">
                                            <button 
                                                type="submit"
                                                class="btn btn-primary flex-fill"
                                                t-att-disabled="!isFormValid">
                                                <t t-if="state.selectedProduct">Actualizar Producto</t>
                                                <t t-else="">Crear Producto</t>
                                            </button>
    
                                            <button 
                                                type="button"
                                                class="btn btn-outline-secondary"
                                                t-on-click="resetForm">
                                                Limpiar
                                            </button>
                                        </div>
                                    </form>
                                </div>
                            </div>
                        </div>
    
                        <!-- Lista de Productos -->
                        <div class="col-md-8">
                            <div class="card">
                                <div class="card-header">
                                    <h5 class="mb-0">
                                        Productos (<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 se encontraron productos. ¡Crea tu primer producto!</p>
                                        </div>
                                    </t>
                                    <t t-else="">
                                        <div class="table-responsive">
                                            <table class="table table-hover mb-0">
                                                <thead class="table-light">
                                                    <tr>
                                                        <th>Nombre</th>
                                                        <th>Categoría</th>
                                                        <th>Precio</th>
                                                        <th>Stock</th>
                                                        <th class="text-end">Acciones</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="Editar">
                                                                        <i class="fa fa-edit"></i>
                                                                    </button>
                                                                    <button 
                                                                        class="btn btn-outline-danger"
                                                                        t-on-click="() => this.deleteProduct(product.id)"
                                                                        title="Eliminar">
                                                                        <i class="fa fa-trash"></i>
                                                                    </button>
                                                                </div>
                                                            </td>
                                                        </tr>
                                                    </t>
                                                </tbody>
                                            </table>
                                        </div>
                                    </t>
                                </div>
                            </div>
                        </div>
                    </div>
                </t>
            </div>
        </t>
    </templates>
    

    Este ejemplo demuestra todas las operaciones clave del ORM:

    • searchRead: Buscar y leer registros en una operación
    • create: Crear nuevos registros
    • write: Actualizar registros existentes
    • unlink: Eliminar registros

    Operaciones Avanzadas del ORM

    El servicio orm proporciona métodos más avanzados para escenarios complejos:

    Usando read para Registros Específicos:

    // Leer registros específicos por ID
    const products = await this.orm.read(
      "product.product",
      [1, 2, 3],                           // IDs de registros específicos
      ["name", "list_price", "description"] // Campos a leer
    );
    

    Usando search y searchCount:

    // Obtener solo IDs de registros que coincidan con criterios
    const productIds = await this.orm.search(
      "product.product",
      [["sale_ok", "=", true]],
      { limit: 5, offset: 10 }
    );
    
    // Contar registros sin obtenerlos
    const totalProducts = await this.orm.searchCount(
      "product.product",
      [["sale_ok", "=", true]]
    );
    

    Llamando Métodos Personalizados del Modelo:

    // Llamar cualquier método en tu modelo Python
    const result = await this.orm.call(
      "product.product",      // Nombre del modelo
      "my_custom_method",     // Nombre del método
      [recordId],            // Args (generalmente IDs de registros)
      {                      // Kwargs
        param1: "value1",
        param2: "value2"
      }
    );
    

    El Servicio rpc: Comunicación Directa con Controladores

    Mientras que el servicio orm es perfecto para operaciones de modelo, a veces necesitas llamar rutas de controlador directamente. El servicio rpc (abreviatura de Remote Procedure Call, "llamada a procedimiento remoto": invocar una función que en realidad se ejecuta en el servidor, como si fuera local) proporciona esta capacidad:

    Ejemplo JavaScript:

    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: Generando reporte personalizado");
    
          // Llamar tu ruta de controlador personalizada
          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: Reporte generado exitosamente");
    
        } catch (error) {
          console.error("CustomReportGenerator: Falló la generación del reporte:", error);
          // Manejar error apropiadamente
        } finally {
          this.state.loading = false;
        }
      }
    
      async downloadReportPDF() {
        try {
          console.log("CustomReportGenerator: Descargando reporte PDF");
    
          // Esto podría retornar un blob o URL para descarga
          const pdfData = await this.rpc("/my_module/generate_report", {
            filters: this.state.filters,
            format: "pdf"
          });
    
          // Manejar lógica de descarga PDF aquí
    
        } catch (error) {
          console.error("CustomReportGenerator: Falló la descarga PDF:", error);
        }
      }
    }
    

    Manejo de Errores y Retroalimentación de Usuario

    Las aplicaciones profesionales necesitan un manejo robusto de errores. Aquí tienes cómo manejar escenarios comunes:

    Patrón de Manejo Completo de Errores:

    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("Falló la operación de base de datos:", error);
    
        // Diferentes tipos de error necesitan diferente manejo
        if (error.code === 403) {
          this.state.error = "No tienes permisos para acceder a estos datos.";
        } else if (error.code === 404) {
          this.state.error = "Los datos solicitados no fueron encontrados.";
        } else if (error.message.includes("network")) {
          this.state.error = "Error de red. Por favor verifica tu conexión.";
        } else {
          this.state.error = `Falló la operación: ${error.message}`;
        }
    
      } finally {
        this.state.loading = false;
      }
    }
    

    Patrón de Actualizaciones Optimistas:

    async quickUpdate(recordId, field, value) {
      // Almacenar valor original para rollback
      const originalData = [...this.state.records];
    
      try {
        // Actualizar UI inmediatamente (optimista)
        const record = this.state.records.find(r => r.id === recordId);
        if (record) {
          record[field] = value;
        }
    
        // Luego actualizar servidor
        await this.orm.write("model.name", [recordId], { [field]: value });
    
        console.log("Actualización rápida exitosa");
    
      } catch (error) {
        // Rollback UI en error
        this.state.records = originalData;
        console.error("Falló la actualización rápida, rollback realizado:", error);
    
        // Mostrar mensaje de error amigable
        this.showError(`Falló la actualización de ${field}. Por favor intenta de nuevo.`);
      }
    }
    

    Patrones de Integración de Servicios

    Patrón 1: Composición de Servicios

    setup() {
      // Múltiples servicios trabajando juntos
      this.orm = useService("orm");
      this.rpc = useService("rpc");
      this.notification = useService("notification");
      this.action = useService("action");
    
      // Operación compuesta usando múltiples servicios
      this.performComplexOperation = async () => {
        try {
          // 1. Guardar datos vía ORM
          const [recordId] = await this.orm.create("model.name", [this.state.formData]);
    
          // 2. Activar procesamiento del lado del servidor vía RPC
          await this.rpc("/custom/process_record", { record_id: recordId });
    
          // 3. Mostrar notificación de éxito
          this.notification.add("¡Registro creado y procesado exitosamente!", {
            type: "success"
          });
    
          // 4. Navegar al nuevo registro
          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("Falló la operación. Por favor intenta de nuevo.", {
            type: "danger"
          });
        }
      };
    }
    

    Patrón 2: Abstracción de Servicios

    setup() {
      this.orm = useService("orm");
    
      // Crear capa de abstracción para tu dominio específico
      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);
        }
      };
    }
    

    Consideraciones de Rendimiento

    Operaciones en Lote

    // MALO: Múltiples llamadas individuales
    for (const productId of productIds) {
      await this.orm.write("product.product", [productId], { active: false });
    }
    
    // BUENO: Una sola llamada en lote
    await this.orm.write("product.product", productIds, { active: false });
    

    Selección Eficiente de Campos

    // MALO: Cargar datos innecesarios
    const products = await this.orm.searchRead("product.product", [], []);
    
    // BUENO: Solo cargar lo que necesitas
    const products = await this.orm.searchRead(
      "product.product", 
      [], 
      ["id", "name", "list_price"] // Solo los campos que realmente usas
    );
    

    Patrón de Cache Inteligente

    setup() {
      this.orm = useService("orm");
      this.cache = new Map();
    
      this.getCachedData = async (cacheKey, fetcher) => {
        if (this.cache.has(cacheKey)) {
          console.log(`Usando datos en cache para ${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"])
        );
      };
    }
    

    Errores Comunes y Soluciones

    Error 1: Asumir que searchRead Devuelve un Objeto Envoltorio

    // Malo - algunos ORMs envuelven los resultados en un objeto; el de Odoo no
    const { records } = await this.orm.searchRead("res.partner", [], ["name"]);
    
    // Correcto - searchRead resuelve directamente a un array de registros
    const records = await this.orm.searchRead("res.partner", [], ["name"]);
    

    Error 2: Olvidar await

    // Malo - todo método de orm devuelve una Promise; sin await guardas la Promise misma
    loadProducts() {
      this.state.products = this.orm.searchRead("product.product", [], ["name"]);
    }
    
    // Correcto
    async loadProducts() {
      this.state.products = await this.orm.searchRead("product.product", [], ["name"]);
    }
    

    Error 3: No Manejar Errores del Servidor

    // Malo - un rechazo no manejado puede romper toda la acción
    async saveRecord() {
      await this.orm.create("res.partner", [this.state.formData]);
    }
    
    // Correcto - envuelve en try/catch y da retroalimentación al usuario
    async saveRecord() {
      try {
        await this.orm.create("res.partner", [this.state.formData]);
      } catch (error) {
        this.state.error = "No se pudo guardar el registro. Intenta de nuevo.";
      }
    }
    

    Error 4: Olvidar que create Devuelve un Array de IDs

    // Malo - asumir que create resuelve a un solo id
    const id = await this.orm.create("res.partner", [{ name: "Test" }]);
    
    // Correcto - create siempre resuelve a un array, incluso para un solo registro
    const [id] = await this.orm.create("res.partner", [{ name: "Test" }]);
    

    Probando la Integración de Servicios

    Como los componentes reciben los servicios a través de useService, las pruebas pueden sustituirlos por mocks. Los helpers exactos dependen de tu versión de Odoo (cubriremos el framework de pruebas real de Odoo en el Capítulo 16 Parte 1), pero la idea siempre se ve así:

    // Pseudo-código: la forma de una prueba con servicios simulados
    
    describe("ProductManager", () => {
      test("debería cargar productos al montar", async () => {
        const mockOrm = {
          searchRead: jest.fn().mockResolvedValue([
            { id: 1, name: "Producto de Prueba", list_price: 10.0 }
          ])
        };
    
        const component = await makeTestComponent(ProductManager, {
          services: {
            orm: mockOrm
          }
        });
    
        await nextTick(); // Esperar a que los efectos se completen
    
        expect(mockOrm.searchRead).toHaveBeenCalledWith(
          "product.product",
          [["sale_ok", "=", true]],
          expect.any(Array),
          expect.any(Object)
        );
    
        expect(component.state.products).toHaveLength(1);
      });
    });
    

    Resumen

    El hook useService y el sistema de servicios de Odoo proporcionan:

    • Servicio orm: Operaciones CRUD completas para modelos de Odoo
    • Servicio rpc: Comunicación directa con controladores personalizados
    • API consistente: Los mismos patrones a través de toda tu aplicación
    • Manejo de errores: Gestión y recuperación de errores incorporada
    • Rendimiento: Comunicación optimizada con el backend

    Principios clave para el uso efectivo de servicios:

    1. Usar el servicio correcto para cada tarea (ORM para modelos, RPC para controladores)
    2. Manejar errores graciosamente con mensajes amigables para el usuario
    3. Operaciones en lote cuando sea posible para mejor rendimiento
    4. Cache estratégico para reducir solicitudes al servidor
    5. Probar con simulaciones para asegurar confiabilidad

    En el próximo capítulo, exploraremos más servicios incorporados de Odoo como notificaciones y acciones, que te ayudarán a crear experiencias de usuario verdaderamente integradas.

    TL;DR: Usa useService("orm") para CRUD contra modelos de Odoo (searchRead, create, write, unlink) y useService("rpc") para controladores personalizados — siempre con await, siempre envolviendo las llamadas en try/catch, y recordando que create devuelve un array de ids, no un id suelto.

    Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch12_ex1. Las instrucciones de instalación están en el README del repositorio.


    Ejercicios

    1. CRUD básico: Construye un componente pequeño que liste registros de res.partner (searchRead con solo name y email), y agrega un botón que cree un nuevo contacto llamado "Contacto de Prueba" usando orm.create. Recuerda que create devuelve un array de ids.
    2. Maneja el error: Envuelve una llamada a orm.write en un try/catch y muestra una alerta de Bootstrap con un mensaje amigable si falla. Pruébalo pasando un nombre de modelo inválido y observa qué pasa sin el try/catch primero.
    3. Cuenta antes de traer: Usa orm.searchCount para mostrar "X contactos encontrados" antes de cargar la lista completa con searchRead, para que el usuario vea un número de inmediato mientras los datos se cargan.

    ¿Qué Sigue?

    Con datos fluyendo entre tus componentes y el servidor, el Capítulo 13 pasa al resto de los servicios integrados de Odoo — notificaciones, diálogos, acciones y permisos — las piezas que hacen que un componente se sienta parte nativa de Odoo en vez de un widget pegado con cinta adhesiva.

    en Libro OWL
    Capítulo 13 Parte 1: Servicios Integrados de Odoo
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Enlaces útiles
    • Inicio
    • Sobre nosotros
    • Blog
    • El libro de OWL 2.0
    • Ayuda
    • Contáctanos
    Sobre nosotros

    Simplify It S.R.L. es una firma de implementación y desarrollo Odoo con base en La Paz, Bolivia, que trabaja con empresas de Latinoamérica y Norteamérica. Desarrollamos sobre Odoo desde la versión 6.1: módulos a medida, migraciones de versión y localización boliviana.

    También somos autores del libro de OWL 2.0, que publicamos gratis capítulo a capítulo en nuestro blog.

    Conecta con nosotros
    • Contáctanos
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Síguenos
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Con la tecnología de Odoo - Cree un sitio web gratuito