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 13 Parte 2: Patrones Avanzados de Servicios y Optimización

  • Todos los blogs
  • Libro OWL
  • Capítulo 13 Parte 2: Patrones Avanzados de Servicios y Optimización
  • 25 de agosto de 2026 por
    Capítulo 13 Parte 2: Patrones Avanzados de Servicios y Optimización
    Grover Menacho

    ¿Por qué este capítulo? Una sola llamada a notification.add() es fácil; coordinar tres servicios en un mismo flujo, evitar que una caja de búsqueda golpee al servidor en cada tecla, y recuperarse de una llamada de red inestable son las cosas que realmente consumen un día de trabajo una vez que un addon crece más allá de su primera versión. ¿Qué trata de resolver Odoo con esto? La interfaz de Odoo tiene que seguir respondiendo y siendo resiliente bajo uso real — conexiones inestables, endpoints lentos, usuarios que hacen doble clic — así que el framework se apoya en patrones como debounce, caché y reintentos estructurados en vez de dejar que cada desarrollador los reinvente. Aplicación en la vida real: Búsqueda con debounce, reintento con backoff en una llamada inestable a una API externa, y flujos de trabajo de varios pasos que abarcan varios servicios son patrones a los que recurro constantemente una vez que un addon de cliente pasa de su primera prueba de concepto.

    En la Parte 1 cubrimos los servicios que usarás todos los días — notification, action, dialog, y algunos esenciales — junto con los errores comunes que cometen los desarrolladores junior con ellos. Ahora abordaremos escenarios más exigentes: componer varios servicios en un solo flujo de trabajo, mantener rápidas las llamadas costosas a servicios, y recuperarnos con gracia cuando una llamada falla.


    Patrones Avanzados de Servicios

    Patrón 3: Composición de Servicios para Flujos de Trabajo Complejos

    async procesarOrdenCliente(datosOrden) {
      const pasos = [
        { nombre: "Validando datos de orden", accion: () => this.validarDatosOrden(datosOrden) },
        { nombre: "Creando cliente", accion: () => this.crearCliente(datosOrden.cliente) },
        { nombre: "Creando orden", accion: () => this.crearOrden(datosOrden) },
        { nombre: "Enviando confirmación", accion: () => this.enviarEmailConfirmacion(datosOrden) }
      ];
    
      let pasosCompletados = 0;
    
      try {
        // Notificación inicial de progreso
        this.notification.add(`Iniciando procesamiento de orden (${pasos.length} pasos)...`, {
          type: "info"
        });
    
        for (const paso of pasos) {
          console.log(`Procesando paso: ${paso.nombre}`);
    
          // Ejecutar paso
          await paso.accion();
          pasosCompletados++;
    
          // Notificación de progreso
          this.notification.add(
            `${paso.nombre} completado (${pasosCompletados}/${pasos.length})`, 
            { type: "info" }
          );
        }
    
        // Notificación de éxito y navegación
        this.notification.add("¡Orden procesada exitosamente!", {
          type: "success",
          sticky: false
        });
    
        // Navegar a la orden
        this.action.doAction({
          type: "ir.actions.act_window",
          res_model: "sale.order",
          res_id: datosOrden.idOrden,
          views: [[false, "form"]],
          target: "current"
        });
    
      } catch (error) {
        console.error(`Procesamiento de orden falló en paso ${pasosCompletados + 1}:`, error);
    
        this.notification.add(
          `Procesamiento de orden falló en paso: ${pasos[pasosCompletados]?.nombre || 'Desconocido'}. ${error.message}`,
          { type: "danger", sticky: true }
        );
    
        // Opcionalmente mostrar diálogo de rollback
        this.dialog.add(ConfirmationDialog, {
          title: "Procesamiento Falló",
          body: "¿Te gustaría deshacer los cambios hechos hasta ahora?",
          confirm: () => this.deshacerCambios(pasosCompletados),
          cancel: () => console.log("Usuario eligió no deshacer")
        });
      }
    }
    

    Patrón 4: Gestión de Estado Basada en Servicios

    Este patrón combina varios servicios dentro de un solo componente: useState para datos reactivos locales, y orm/action/notification/dialog/user coordinados en conjunto. Construiremos DashboardClientes en tres partes.

    Primero, el setup() inicial: estado, referencias a servicios, y un pequeño objeto cargadorDatos que agrupa las consultas de solo lectura que necesita el dashboard:

    export class DashboardClientes extends Component {
      static template = "mi_modulo.DashboardClientes";
    
      setup() {
        this.state = useState({
          clientes: [],
          clienteSeleccionado: null,
          cargando: false,
          filtros: {
            activo: true,
            pais: null,
            categoria: null
          }
        });
    
        // Inicializar servicios
        this.orm = useService("orm");
        this.action = useService("action");
        this.notification = useService("notification");
        this.dialog = useService("dialog");
        this.user = useService("user");
    
        // Cargador de datos basado en servicios
        this.cargadorDatos = {
          clientes: () => this.orm.searchRead(
            "res.partner",
            this.construirDominioClientes(),
            ["name", "email", "phone", "country_id", "category_id"],
            { order: "name asc" }
          ),
    
          paises: () => this.orm.searchRead(
            "res.country",
            [],
            ["name", "code"],
            { order: "name asc" }
          ),
    
          categorias: () => this.orm.searchRead(
            "res.partner.category",
            [],
            ["name", "color"],
            { order: "name asc" }
          )
        };
    
        // Inicializar dashboard
        useEffect(
          () => {
            this.inicializarDashboard();
          },
          () => []
        );
    
        // Actualizaciones reactivas de filtros
        useEffect(
          () => {
            this.refrescarClientes();
          },
          () => [this.state.filtros.activo, this.state.filtros.pais, this.state.filtros.categoria]
        );
      }
    

    Después, el método que se ejecuta una vez al montar, cargando los tres conjuntos de datos en paralelo e informando éxito o fallo:

      async inicializarDashboard() {
        try {
          this.state.cargando = true;
    
          // Cargar todos los datos requeridos en paralelo
          const [clientes, paises, categorias] = await Promise.all([
            this.cargadorDatos.clientes(),
            this.cargadorDatos.paises(),
            this.cargadorDatos.categorias()
          ]);
    
          this.state.clientes = clientes;
          this.state.paises = paises;
          this.state.categorias = categorias;
    
          this.notification.add("¡Dashboard cargado exitosamente!", {
            type: "success"
          });
    
        } catch (error) {
          console.error("Inicialización de dashboard falló:", error);
    
          this.notification.add("Error al cargar datos del dashboard.", {
            type: "danger",
            sticky: true
          });
        } finally {
          this.state.cargando = false;
        }
      }
    

    Finalmente, los métodos de acción masiva: una tabla de posibles acciones (archivar, eliminar, exportar), un paso de confirmación para las peligrosas, y el método que realmente ejecuta la acción elegida:

      // Acciones orquestadas por servicios
      async realizarAccionMasiva(accion, idsClientes) {
        const mapaAcciones = {
          archivar: {
            titulo: "Archivar Clientes",
            metodo: () => this.orm.write("res.partner", idsClientes, { active: false }),
            mensajeExito: `${idsClientes.length} clientes archivados exitosamente`
          },
    
          eliminar: {
            titulo: "Eliminar Clientes",
            metodo: () => this.orm.unlink("res.partner", idsClientes),
            mensajeExito: `${idsClientes.length} clientes eliminados exitosamente`,
            peligrosa: true
          },
    
          exportar: {
            titulo: "Exportar Clientes",
            metodo: () => this.action.doAction({
              type: "ir.actions.report",
              report_name: "base.report_partner_list",
              context: { active_ids: idsClientes }
            }),
            mensajeExito: "Exportación de clientes iniciada"
          }
        };
    
        const configAccion = mapaAcciones[accion];
        if (!configAccion) return;
    
        // Mostrar confirmación para acciones peligrosas
        if (configAccion.peligrosa) {
          this.dialog.add(ConfirmationDialog, {
            title: `Confirmar ${configAccion.titulo}`,
            body: `Esto ${accion} permanentemente ${idsClientes.length} clientes. Esta acción no se puede deshacer.`,
            confirm: () => this.ejecutarAccionMasiva(configAccion)
          });
        } else {
          await this.ejecutarAccionMasiva(configAccion);
        }
      }
    
      async ejecutarAccionMasiva(configAccion) {
        try {
          this.notification.add(`${configAccion.titulo} en progreso...`, {
            type: "info"
          });
    
          await configAccion.metodo();
    
          this.notification.add(configAccion.mensajeExito, {
            type: "success"
          });
    
          // Refrescar datos después de acción masiva
          await this.refrescarClientes();
    
        } catch (error) {
          console.error(`${configAccion.titulo} falló:`, error);
    
          this.notification.add(`${configAccion.titulo} falló: ${error.message}`, {
            type: "danger",
            sticky: true
          });
        }
      }
    }
    

    Optimización de Rendimiento de Servicios

    Llamadas de Servicio con Debounce

    Debounce significa retrasar una llamada a una función hasta que una ráfaga de eventos que la disparan se detenga durante un período determinado — por ejemplo, esperar a que el usuario deje de escribir antes de disparar una búsqueda, en lugar de enviar una solicitud por cada tecla presionada. Es una técnica común para evitar que llamadas costosas a servicios (como una búsqueda ORM) saturen el servidor.

    setup() {
      this.orm = useService("orm");
    
      // Debounce para búsquedas costosas
      this.busquedaConDebounce = this.debounce(async (terminoBusqueda) => {
        if (!terminoBusqueda.trim()) return;
    
        try {
          const resultados = await this.orm.searchRead(
            "res.partner",
            [["name", "ilike", terminoBusqueda]],
            ["name", "email"],
            { limit: 10 }
          );
    
          this.state.resultadosBusqueda = resultados;
        } catch (error) {
          console.error("Búsqueda falló:", error);
        }
      }, 300); // Esperar 300ms después de que el usuario pare de escribir
    }
    
    debounce(func, espera) {
      let timeout;
      return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), espera);
      };
    }
    
    alCambiarBusqueda(event) {
      const terminoBusqueda = event.target.value;
      this.state.terminoBusqueda = terminoBusqueda;
      this.busquedaConDebounce(terminoBusqueda);
    }
    

    Odoo ya incluye un helper de debounce listo para usar, así que en addons reales rara vez necesitas escribir el tuyo: import { debounce } from "@web/core/utils/timing";.

    Caché de Respuestas de Servicio

    setup() {
      this.orm = useService("orm");
      this.cache = new Map();
      this.expiracionCache = new Map();
    
      this.obtenerDatosEnCache = async (claveCache, obtenedor, ttl = 5 * 60 * 1000) => {
        // Verificar si tenemos datos válidos en caché
        if (this.cache.has(claveCache)) {
          const expiracion = this.expiracionCache.get(claveCache);
          if (Date.now() < expiracion) {
            console.log(`Usando datos en caché para: ${claveCache}`);
            return this.cache.get(claveCache);
          }
        }
    
        // Obtener datos frescos
        console.log(`Obteniendo datos frescos para: ${claveCache}`);
        const datos = await obtenedor();
    
        // Cachear el resultado
        this.cache.set(claveCache, datos);
        this.expiracionCache.set(claveCache, Date.now() + ttl);
    
        return datos;
      };
    }
    
    async cargarPaises() {
      return this.obtenerDatosEnCache(
        'paises',
        () => this.orm.searchRead("res.country", [], ["name", "code"]),
        10 * 60 * 1000 // Cachear por 10 minutos
      );
    }
    

    Estrategias de Recuperación de Errores de Servicio

    Reintento Automático con Backoff Exponencial

    Backoff exponencial significa esperar cada vez más tiempo entre cada intento de reintento (1s, luego 2s, luego 4s, y así sucesivamente) en lugar de reintentar de inmediato — esto le da espacio a un servidor con problemas para recuperarse, en vez de bombardearlo con reintentos instantáneos.

    async realizarOperacionResilientte(operacion, maxReintentos = 3) {
      let ultimoError;
    
      for (let intento = 1; intento <= maxReintentos; intento++) {
        try {
          const resultado = await operacion();
    
          if (intento > 1) {
            this.notification.add("Operación exitosa después de reintento.", {
              type: "success"
            });
          }
    
          return resultado;
    
        } catch (error) {
          ultimoError = error;
          console.warn(`Intento ${intento} falló:`, error);
    
          if (intento < maxReintentos) {
            const retraso = Math.pow(2, intento - 1) * 1000; // Backoff exponencial
    
            this.notification.add(`Operación falló, reintentando en ${retraso/1000}s... (${intento}/${maxReintentos})`, {
              type: "warning"
            });
    
            await new Promise(resolve => setTimeout(resolve, retraso));
          }
        }
      }
    
      // Todos los reintentos fallaron
      this.notification.add(`Operación falló después de ${maxReintentos} intentos: ${ultimoError.message}`, {
        type: "danger",
        sticky: true
      });
    
      throw ultimoError;
    }
    
    // Uso
    async guardarDatos() {
      await this.realizarOperacionResilientte(async () => {
        return await this.orm.create("nombre.modelo", [this.state.datosFormulario]);
      });
    }
    

    Ejemplo de Integración del Mundo Real

    Aquí hay un ejemplo completo mostrando cómo múltiples servicios trabajan juntos en un escenario de producción. Lo veremos en dos partes: el setup y la carga de datos, y luego la acción que procesa las órdenes seleccionadas.

    export class CentroProcesamiento extends Component {
      static template = "mi_modulo.CentroProcesamiento";
    
      setup() {
        this.state = useState({
          ordenes: [],
          procesando: false,
          ordenesSeleccionadas: new Set(),
          estadisticas: {
            total: 0,
            pendientes: 0,
            procesando: 0,
            completadas: 0
          }
        });
    
        // Inicialización de servicios
        this.orm = useService("orm");
        this.action = useService("action");
        this.notification = useService("notification");
        this.dialog = useService("dialog");
        this.user = useService("user");
    
        // Cargar datos al montar
        useEffect(
          () => {
            this.cargarOrdenes();
            this.cargarEstadisticas();
          },
          () => []
        );
      }
    
      async cargarOrdenes() {
        try {
          const ordenes = await this.orm.searchRead(
            "sale.order",
            [["state", "in", ["draft", "sent", "sale"]]],
            ["name", "partner_id", "amount_total", "state", "date_order"],
            { order: "date_order desc", limit: 100 }
          );
    
          this.state.ordenes = ordenes;
        } catch (error) {
          this.notification.add("Error al cargar órdenes.", { type: "danger" });
        }
      }
    
      async cargarEstadisticas() {
        try {
          const stats = await this.orm.call("sale.order", "get_order_statistics");
          this.state.estadisticas = stats;
        } catch (error) {
          console.error("Error al cargar estadísticas:", error);
        }
      }
    

    La parte interesante es procesarOrdenesSeleccionadas: valida la selección, confirma con el usuario, y luego coordina una llamada ORM por lotes, una notificación de éxito, un refresco de datos, y finalmente abre un reporte — los cinco servicios trabajando juntos alrededor de una sola acción del usuario:

      async procesarOrdenesSeleccionadas() {
        if (this.state.ordenesSeleccionadas.size === 0) {
          this.notification.add("Por favor selecciona órdenes para procesar.", {
            type: "warning"
          });
          return;
        }
    
        const idsOrdenes = Array.from(this.state.ordenesSeleccionadas);
    
        // Diálogo de confirmación
        this.dialog.add(ConfirmationDialog, {
          title: "Procesar Órdenes",
          body: `¿Procesar ${idsOrdenes.length} órdenes seleccionadas?`,
          confirm: async () => {
            try {
              this.state.procesando = true;
    
              // Notificación de progreso
              this.notification.add(`Procesando ${idsOrdenes.length} órdenes...`, {
                type: "info"
              });
    
              // Procesar órdenes por lotes
              await this.orm.call("sale.order", "action_confirm", idsOrdenes);
    
              // Retroalimentación de éxito
              this.notification.add(`¡${idsOrdenes.length} órdenes procesadas exitosamente!`, {
                type: "success"
              });
    
              // Limpiar selección y refrescar
              this.state.ordenesSeleccionadas.clear();
              await this.cargarOrdenes();
              await this.cargarEstadisticas();
    
              // Abrir reporte de procesamiento
              this.action.doAction({
                type: "ir.actions.report",
                report_name: "sale.action_report_saleorder",
                context: { active_ids: idsOrdenes }
              });
    
            } catch (error) {
              console.error("Procesamiento de órdenes falló:", error);
    
              this.notification.add(`Procesamiento falló: ${error.message}`, {
                type: "danger",
                sticky: true
              });
            } finally {
              this.state.procesando = false;
            }
          }
        });
      }
    }
    

    Resumen

    Los servicios integrados de Odoo proporcionan un kit de herramientas integral para crear aplicaciones profesionales e integradas:

    • notification: Retroalimentación del usuario y actualizaciones de estado
    • action: Navegación e integración con Odoo
    • dialog: Interacciones del usuario y confirmaciones
    • user, company, router: Información del sistema y navegación

    Al dominar estos servicios, tus componentes: - Se sentirán nativos a la experiencia de Odoo - Proporcionarán interacciones consistentes del usuario - Manejarán errores graciosamente - Se integrarán perfectamente con los flujos de trabajo de Odoo

    La clave del éxito es entender cuándo y cómo usar cada servicio, combinarlos efectivamente para flujos de trabajo complejos, y siempre priorizar la experiencia del usuario con retroalimentación clara e interacciones intuitivas.

    En el próximo capítulo, exploraremos patrones avanzados de componentes y consideraciones arquitectónicas para construir aplicaciones OWL a gran escala.

    TL;DR: Compón servicios de forma deliberada (una notificación por cada paso de un flujo de varios pasos), aplica debounce a todo lo que se dispara en cada tecla con @web/core/utils/timing, y envuelve las llamadas poco confiables en una estrategia de reintento con backoff en vez de fallar en silencio.

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


    Ejercicios

    1. Flujo compuesto: Escribe un método que cree un partner, y luego cree inmediatamente un contacto relacionado para él, mostrando una notificación de progreso por cada paso (similar al Patrón 3). Asegúrate de que una falla en cualquiera de los pasos muestre cuál falló.
    2. Debounce por tu cuenta: Implementa un debounce de 300ms sobre un input de búsqueda en vivo respaldado por orm.searchRead, sin usar el helper debounce incorporado de Odoo — luego reemplázalo con import { debounce } from "@web/core/utils/timing" y confirma que el comportamiento es el mismo.
    3. Reintento con backoff: Toma realizarOperacionResilientte y adáptalo para reintentar una llamada orm.call hasta 3 veces, luego escribe una prueba rápida (real o en papel) describiendo qué notificación debería ver el usuario después de cada intento fallido.

    ¿Qué Sigue?

    Ya cubriste toda la capa de servicios, desde la primera notificación hasta flujos de trabajo resilientes de varios pasos. El Capítulo 14 pasa a la composición — slots, scoped slots y t-portal — las herramientas para construir componentes reutilizables y flexibles en vez de componentes de un solo uso.

    en Libro OWL
    Capítulo 14: Composición Avanzada con Slots
    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