¿Por qué este capítulo? Porque casi ningún proyecto real de Odoo parte de cero — vas a pasar mucho más tiempo trabajando dentro de una base de código que ya tiene años de widgets legado que construyendo algo nuevo en OWL puro. ¿Qué trata de resolver Odoo con esto? Odoo necesita una forma de que el código frontend viejo y nuevo convivan durante la migración de varios años del sistema de widgets legado a OWL, sin forzar una reescritura riesgosa y de una sola vez de todas las personalizaciones de cada cliente. Aplicación en la vida real: Esta es exactamente la situación que me llevó a escribir este libro: la actualización de un cliente rompió todos los widgets personalizados que habíamos construido para ellos, y las técnicas de puente de este capítulo son las que te permiten modernizar un módulo pieza por pieza en vez de congelar un proyecto hasta terminar una reescritura completa.
¡Felicidades por llegar al último capítulo central del libro! Has dominado JavaScript moderno, construido componentes OWL sofisticados, implementado gestión de estado global y aprendido prácticas profesionales de pruebas. Estás completamente equipado para construir aplicaciones de vanguardia en Odoo desde cero.
Sin embargo, la realidad del desarrollo profesional de Odoo es más matizada. La mayor parte de tu trabajo no involucrará proyectos desde cero donde puedas usar OWL puro en todas partes. En su lugar, trabajarás con bases de datos de Odoo existentes llenas de años de personalizaciones, módulos de terceros e interfaces de usuario construidas con el framework JavaScript más antiguo de Odoo: el sistema de widgets legado.
Esto crea un desafío crítico: ¿Cómo modernizar aplicaciones de forma incremental? ¿Cómo introducir componentes OWL poderosos en interfaces legado existentes sin romper todo? ¿Cómo aprovechar widgets legado existentes en nuevas aplicaciones OWL cuando reconstruirlos tomaría meses?
La respuesta está en el puente legacy-OWL de Odoo: una capa de compatibilidad sofisticada que permite interoperabilidad perfecta entre código viejo y nuevo. Esto no es solo una curiosidad técnica; es una herramienta esencial para cualquier desarrollador profesional de Odoo trabajando en proyectos del mundo real.
Entendiendo el Panorama Legado
Antes de sumergirnos en las técnicas del puente, entendamos entre qué estamos haciendo el puente.
El Sistema de Widgets Legado
El framework JavaScript legado de Odoo, usado desde la versión 8 hasta partes de la versión 16, fue construido alrededor de:
Arquitectura Basada en Widgets: Los componentes eran clases que extendían Widget
const MyWidget = Widget.extend({
template: 'my_module.MyTemplate',
start: function() {
// Lógica de inicialización
return this._super.apply(this, arguments);
},
destroy: function() {
// Lógica de limpieza
this._super.apply(this, arguments);
}
});
Uso Intensivo de jQuery: Manipulación directa del DOM y manejo de eventos
events: {
'click .my-button': '_onButtonClick',
},
_onButtonClick: function(event) {
this.$('.result').text('¡Botón clickeado!');
}
Plantillas QWeb: Compilación de plantillas del lado del servidor
<t t-name="my_module.MyTemplate">
<div class="my-widget">
<button class="my-button">Haz clic</button>
<div class="result"></div>
</div>
</t>
¿Por Qué Hacer un Puente en Lugar de Reescribir?
Realidad Práctica: Las instalaciones grandes de Odoo tienen cientos de widgets personalizados que representan años de lógica de negocio y refinamientos de interfaz de usuario.
Gestión de Riesgos: Una reescritura completa introduce riesgo significativo de romper funcionalidad existente de la que los usuarios dependen diariamente.
Limitaciones de Recursos: Reescribir todo de una vez requeriría recursos masivos de desarrollo y cronogramas de proyecto extendidos.
Continuidad del Negocio: Los usuarios necesitan nuevas características mientras la funcionalidad existente continúa funcionando de manera confiable.
El enfoque del puente permite modernización gradual: introducir componentes OWL incrementalmente mientras se mantiene la estabilidad del sistema.
Visión General de la Arquitectura del Puente
El puente legacy-OWL funciona en ambas direcciones:
{width=100%}
Dirección 1: OWL en Legado - Montar componentes OWL modernos dentro de widgets legado existentes - Agregar nuevas características sin reescribir pantallas completas - Modernizar gradualmente las interfaces de usuario
Dirección 2: Legado en OWL - Usar widgets legado existentes dentro de nuevas aplicaciones OWL - Aprovechar funcionalidad legado compleja sin reconstruir - Mantener compatibilidad con widgets de terceros
Escenario 1: Incrustando Componentes OWL en Widgets Legado
Este es el patrón de modernización más común. Tienes un formulario o dashboard legado existente, y quieres agregar una nueva característica sofisticada construida con OWL.
Ejemplo: Agregando un Gráfico Moderno a un Dashboard Legado
Digamos que tienes un widget de dashboard de ventas legado, y quieres agregar un gráfico de ingresos interactivo construido con OWL.
1. El Widget Dashboard Legado
odoo.define('sales_dashboard.LegacyDashboard', function (require) {
"use strict";
const Widget = require('web.Widget');
const core = require('web.core');
const { mount } = require("@odoo/owl");
// Importar nuestro componente OWL moderno
const { RevenueChart } = require('sales_dashboard.RevenueChart');
const LegacyDashboard = Widget.extend({
template: 'sales_dashboard.LegacyDashboardTemplate',
events: {
'click .refresh-data': '_onRefreshData',
'change .date-filter': '_onDateFilterChange',
},
init: function(parent, options) {
this._super.apply(this, arguments);
this.salesData = options.salesData || [];
this.owlComponents = {}; // Rastrear componentes OWL montados
},
async start() {
await this._super(...arguments);
// Inicializar funcionalidad legado
this._setupLegacyFeatures();
// Montar componentes OWL
await this._mountOWLComponents();
// Cargar datos iniciales
await this._loadDashboardData();
},
_setupLegacyFeatures: function() {
// Funcionalidad basada en jQuery legado
this.$('.legacy-counter').each(function(index, element) {
$(element).data('value', 0);
});
// Inicializar selector de fecha legado
this.$('.date-filter').datepicker({
format: 'yyyy-mm-dd',
onSelect: this._onDateFilterChange.bind(this)
});
},
async _mountOWLComponents() {
try {
// Montar el gráfico de ingresos en su contenedor designado
const chartTarget = this.el.querySelector('.revenue-chart-container');
if (chartTarget) {
this.owlComponents.revenueChart = await mount(RevenueChart, chartTarget, {
props: {
initialData: this.salesData,
onDataPointClick: this._onChartDataPointClick.bind(this),
onDateRangeChange: this._onChartDateRangeChange.bind(this),
// Pasar referencia del widget legado para interacciones complejas
legacyWidget: this,
}
});
}
// Montar componentes OWL adicionales según sea necesario
const metricsTarget = this.el.querySelector('.metrics-widget-container');
if (metricsTarget) {
const { SalesMetrics } = require('sales_dashboard.SalesMetrics');
this.owlComponents.salesMetrics = await mount(SalesMetrics, metricsTarget, {
props: {
data: this.salesData,
onMetricClick: this._onMetricClick.bind(this)
}
});
}
} catch (error) {
console.error('Falló al montar componentes OWL:', error);
// Degradación elegante - mostrar mensaje de error o UI de respaldo
this._showComponentError(error);
}
},
Una vez montados los componentes OWL, el widget carga sus datos y los envía tanto a los contadores legados como a los props de los componentes OWL:
async _loadDashboardData() {
try {
const data = await this._rpc({
model: 'sale.order',
method: 'get_dashboard_data',
args: [this._getDateRange()],
});
// Actualizar widgets legado
this._updateLegacyCounters(data.counters);
// Actualizar componentes OWL
await this._updateOWLComponents(data);
} catch (error) {
console.error('Falló al cargar datos del dashboard:', error);
this._showError('Falló al cargar datos del dashboard');
}
},
async _updateOWLComponents(data) {
// Actualizar props del componente OWL
if (this.owlComponents.revenueChart) {
this.owlComponents.revenueChart.props.data = data.revenue;
// Disparar re-renderizado actualizando props
await this.owlComponents.revenueChart.render();
}
if (this.owlComponents.salesMetrics) {
this.owlComponents.salesMetrics.props.data = data.metrics;
await this.owlComponents.salesMetrics.render();
}
},
_updateLegacyCounters: function(counters) {
// Actualizaciones basadas en jQuery legado
Object.keys(counters).forEach(key => {
const $counter = this.$(`.counter[data-metric="${key}"]`);
this._animateCounter($counter, counters[key]);
});
},
_animateCounter: function($element, targetValue) {
const currentValue = $element.data('value') || 0;
$({ value: currentValue }).animate({ value: targetValue }, {
duration: 1000,
step: function() {
$element.text(Math.floor(this.value));
},
complete: function() {
$element.data('value', targetValue);
}
});
},
// Manejadores de eventos para funcionalidad legado
_onRefreshData: function(event) {
event.preventDefault();
this._loadDashboardData();
},
_onDateFilterChange: function(event) {
const newDateRange = this._getDateRange();
this._loadDashboardData();
},
// Manejadores de eventos para interacciones de componentes OWL
_onChartDataPointClick: function(dataPoint) {
// Manejar clics desde gráfico OWL - tal vez mostrar diálogo de desglose legado
this._showDrillDownDialog(dataPoint);
},
_onChartDateRangeChange: function(dateRange) {
// Actualizar filtros de fecha legado cuando el gráfico OWL cambia el rango de fecha
this.$('.date-filter').datepicker('setDate', dateRange.start);
this._loadDashboardData();
},
_onMetricClick: function(metric) {
// Navegar a vista de lista legado
this.do_action({
type: 'ir.actions.act_window',
res_model: 'sale.order',
views: [[false, 'list']],
domain: metric.domain,
context: metric.context,
});
},
// Métodos utilitarios
_getDateRange: function() {
return {
start: this.$('.date-start').val(),
end: this.$('.date-end').val(),
};
},
_showDrillDownDialog: function(dataPoint) {
// Implementación de diálogo legado
const dialog = new Dialog(this, {
title: `Detalles para ${dataPoint.label}`,
size: 'medium',
$content: $(`<div>Ingresos: ${dataPoint.value}</div>`),
});
dialog.open();
},
_showComponentError: function(error) {
this.$('.owl-error-container').show().find('.error-message').text(
'Falló al cargar componentes interactivos. Por favor, recarga la página.'
);
},
_showError: function(message) {
// Visualización de error legado
this.$('.error-container').show().find('.error-text').text(message);
},
La última pieza es la más importante de este capítulo: limpiar los componentes OWL montados. Como la clase legada Widget no tiene ningún hook de ciclo de vida automático para esto, hay que hacerlo a mano dentro de destroy, o cada componente OWL montado generará una fuga de memoria cada vez que el widget legado se cierre y se vuelva a abrir.
// Crítico: Limpiar componentes OWL para prevenir fugas de memoria
destroy: function() {
// Desmontar todos los componentes OWL
Object.values(this.owlComponents).forEach(component => {
if (component && component.destroy) {
component.destroy();
}
});
this.owlComponents = {};
// Llamar al destroy padre
this._super.apply(this, arguments);
},
});
return LegacyDashboard;
});
2. La Plantilla Legado
<t t-name="sales_dashboard.LegacyDashboardTemplate">
<div class="legacy-sales-dashboard">
<!-- Encabezado legado con controles -->
<div class="dashboard-header">
<h2>Dashboard de Ventas (Legado)</h2>
<div class="dashboard-controls">
<input type="text" class="date-filter date-start" placeholder="Fecha Inicio"/>
<input type="text" class="date-filter date-end" placeholder="Fecha Fin"/>
<button class="btn btn-primary refresh-data">Actualizar</button>
</div>
</div>
<!-- Contadores legado -->
<div class="legacy-counters row">
<div class="col-md-3">
<div class="counter-widget">
<h4>Ventas Totales</h4>
<div class="counter" data-metric="total_sales">0</div>
</div>
</div>
<div class="col-md-3">
<div class="counter-widget">
<h4>Clientes Nuevos</h4>
<div class="counter" data-metric="new_customers">0</div>
</div>
</div>
<div class="col-md-3">
<div class="counter-widget">
<h4>Negocios Activos</h4>
<div class="counter" data-metric="active_deals">0</div>
</div>
</div>
<div class="col-md-3">
<div class="counter-widget">
<h4>Tasa de Conversión</h4>
<div class="counter" data-metric="conversion_rate">0%</div>
</div>
</div>
</div>
<!-- Componentes OWL modernos montados aquí -->
<div class="modern-components">
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5>Tendencias de Ingresos (Componente OWL Moderno)</h5>
</div>
<div class="card-body">
<!-- El componente OWL será montado aquí -->
<div class="revenue-chart-container"></div>
<!-- Respaldo cuando el componente OWL falla -->
<div class="owl-error-container" style="display: none;">
<div class="alert alert-warning">
<span class="error-message"></span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5>Métricas Clave (Componente OWL Moderno)</h5>
</div>
<div class="card-body">
<!-- Otro componente OWL montado aquí -->
<div class="metrics-widget-container"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla legado -->
<div class="legacy-table-section">
<h4>Órdenes Recientes (Tabla Legado)</h4>
<table class="table table-striped">
<thead>
<tr>
<th>Orden #</th>
<th>Cliente</th>
<th>Monto</th>
<th>Fecha</th>
</tr>
</thead>
<tbody class="recent-orders-tbody">
<!-- Poblado por JavaScript legado -->
</tbody>
</table>
</div>
<!-- Contenedor de errores -->
<div class="error-container" style="display: none;">
<div class="alert alert-danger">
<span class="error-text"></span>
</div>
</div>
</div>
</t>
3. El Componente OWL Moderno RevenueChart
/** @odoo-module **/
import { Component, useState, onMounted, onWillUpdateProps } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class RevenueChart extends Component {
static template = xml`
<div class="revenue-chart-owl">
<div class="chart-controls">
<select t-model="state.chartType" t-on-change="onChartTypeChange">
<option value="line">Gráfico de Línea</option>
<option value="bar">Gráfico de Barras</option>
<option value="area">Gráfico de Área</option>
</select>
<button class="btn btn-sm btn-secondary" t-on-click="exportChart">
Exportar
</button>
</div>
<div class="chart-container" t-ref="chartContainer">
<canvas t-ref="chartCanvas"></canvas>
</div>
<div class="chart-summary" t-if="chartSummary">
<small class="text-muted">
Mostrando <t t-esc="chartSummary.totalDataPoints"/> puntos de datos.
Mayor: <t t-esc="chartSummary.highest"/>
Promedio: <t t-esc="chartSummary.average"/>
</small>
</div>
</div>
`;
static props = {
initialData: { type: Array, optional: true },
onDataPointClick: { type: Function, optional: true },
onDateRangeChange: { type: Function, optional: true },
legacyWidget: { type: Object, optional: true }, // Referencia al widget legado
};
setup() {
this.notification = useService("notification");
this.state = useState({
chartType: 'line',
data: this.props.initialData || [],
loading: false,
});
this.chart = null;
this.chartCanvasRef = useRef("chartCanvas");
onMounted(() => {
this.initializeChart();
});
onWillUpdateProps((nextProps) => {
if (JSON.stringify(nextProps.initialData) !== JSON.stringify(this.props.initialData)) {
this.updateChartData(nextProps.initialData);
}
});
}
async initializeChart() {
// Inicializar Chart.js o librería de gráficos similar
const ctx = this.chartCanvas.getContext('2d');
this.chart = new Chart(ctx, {
type: this.state.chartType,
data: this.getChartData(),
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
onClick: (event, elements) => {
if (elements.length > 0 && this.props.onDataPointClick) {
const dataPoint = this.state.data[elements[0].index];
this.props.onDataPointClick(dataPoint);
}
},
plugins: {
legend: {
display: true,
position: 'top',
},
tooltip: {
callbacks: {
label: (context) => {
return `Ingresos: $${context.parsed.y.toLocaleString()}`;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: 'day'
}
},
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
}
}
}
}
});
}
getChartData() {
return {
labels: this.state.data.map(point => point.date),
datasets: [{
label: 'Ingresos',
data: this.state.data.map(point => ({
x: point.date,
y: point.revenue
})),
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1
}]
};
}
updateChartData(newData) {
if (this.chart && newData) {
this.state.data = newData;
this.chart.data = this.getChartData();
this.chart.update();
}
}
onChartTypeChange() {
if (this.chart) {
this.chart.config.type = this.state.chartType;
this.chart.update();
}
}
exportChart() {
if (this.chart) {
const url = this.chart.toBase64Image();
const link = document.createElement('a');
link.download = 'revenue-chart.png';
link.href = url;
link.click();
this.notification.add("Gráfico exportado exitosamente", { type: "success" });
}
}
get chartSummary() {
if (!this.state.data.length) return null;
const revenues = this.state.data.map(d => d.revenue);
return {
totalDataPoints: this.state.data.length,
highest: Math.max(...revenues).toLocaleString(),
average: (revenues.reduce((a, b) => a + b, 0) / revenues.length).toFixed(0),
};
}
get chartCanvas() {
return this.chartCanvasRef.el;
}
}
Este ejemplo demuestra varios principios clave:
- Separación Clara: El código legado y OWL permanece separado pero se comunica a través de interfaces bien definidas
- Degradación Elegante: Si los componentes OWL fallan al cargar, la funcionalidad legado continúa funcionando
- Comunicación Bidireccional: Los componentes OWL pueden notificar a widgets legado de eventos, y viceversa
- Gestión de Memoria: Limpieza apropiada previene fugas de memoria
- Manejo de Errores: Manejo robusto de errores asegura estabilidad del sistema
Beneficios Clave de Este Enfoque
1. Modernización Incremental: Reemplazamos el gráfico simple con un componente OWL sofisticado mientras mantenemos características legado complejas
2. Gestión de Riesgos: La funcionalidad de exportación y diálogos legado continúa funcionando
3. Experiencia de Usuario Mejorada: Gráficos y tablas modernos e interactivos
4. Mantenibilidad: Las nuevas características pueden construirse en OWL mientras las características legado permanecen estables
5. Rendimiento: Solo cargar componentes modernos cuando sea necesario
Mejores Prácticas para el Escenario 1
1. Aislamiento de Componentes
Mantén los componentes OWL autocontenidos:
// Bueno: El componente OWL es independiente
const owlComponent = await mount(ChartComponent, target, {
props: {
data: this.salesData,
onEvent: this.handleEvent.bind(this)
}
});
// Malo: El componente OWL depende de las partes internas del widget legado
const owlComponent = await mount(ChartComponent, target, {
props: {
legacyWidget: this, // Demasiado acoplamiento
domElement: this.$('.some-element') // Dependencia directa del DOM
}
});
2. Límites de Error
Siempre implementa respaldos elegantes:
async _mountOWLComponents() {
const targets = [
{ selector: '.chart-container', component: ChartComponent },
{ selector: '.metrics-container', component: MetricsComponent }
];
for (const { selector, component } of targets) {
try {
const target = this.el.querySelector(selector);
if (target) {
this.owlComponents[selector] = await mount(component, target, {
props: this.getComponentProps(component)
});
}
} catch (error) {
console.error(`Falló al montar ${component.name}:`, error);
this._showFallbackUI(selector, error);
}
}
}
_showFallbackUI(selector, error) {
const target = this.el.querySelector(selector);
if (target) {
target.innerHTML = `
<div class="alert alert-warning">
<h6>Componente No Disponible</h6>
<p>Esta característica está temporalmente no disponible. Por favor, recarga la página.</p>
<button class="btn btn-sm btn-secondary" onclick="location.reload()">
Recargar Página
</button>
</div>
`;
}
}
3. Gestión de Memoria
Crítico para prevenir fugas:
destroy: function() {
// Limpiar componentes OWL primero
this._destroyOWLComponents();
// Luego llamar al destroy padre
this._super.apply(this, arguments);
},
_destroyOWLComponents: function() {
Object.entries(this.owlComponents).forEach(([key, component]) => {
try {
if (component && typeof component.destroy === 'function') {
component.destroy();
}
} catch (error) {
console.error(`Error destruyendo componente ${key}:`, error);
}
});
this.owlComponents = {};
}
TL;DR: Envuelve widgets legado desde dentro de un componente OWL con useRef + onMounted/onWillUnmount, o monta componentes OWL dentro del start()/destroy() de un widget legado — cualquiera de las dos direcciones funciona mientras mantengas explícita la interfaz entre ambos.
Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch17_1_ex1. Las instrucciones de instalación están en el README del repositorio.
Errores Comunes
Olvidar destruir los componentes OWL montados. Si montas un componente OWL dentro de _mountOWLComponents pero no lo desmontas en destroy, el componente (y todo lo que retiene: listeners de eventos, timers, suscripciones a servicios) genera una fuga cada vez que el widget legado se cierra y se vuelve a abrir.
Interactuar con el DOM del widget legado antes de que esté listo. El this.el del widget legado solo existe una vez que start() se ha ejecutado. Montar un componente OWL o hacer this.el.querySelector(...) desde init() fallará silenciosamente o lanzará un error — hazlo siempre desde dentro (o después) de start().
Mezclar los dos sistemas de eventos sin un límite claro. Es tentador hacer que los componentes OWL llamen directamente a this.trigger_up(...) del widget legado, o que el widget legado acceda a los internos de OWL. Mantén la interfaz entre ambos explícita — callback props en una dirección, métodos públicos del widget legado en la otra — para no terminar con dos objetos que ambos creen ser dueños del mismo estado.
Asumir que los props del componente OWL son "reactivos" automáticamente. Los widgets legados mutan objetos planos; OWL solo vuelve a renderizar cuando detecta un cambio a través de su sistema de reactividad. Modificar directamente un valor en this.owlComponents.revenueChart.props.data (como hace el ejemplo anterior) NO dispara automáticamente un re-render — por eso el ejemplo llama explícitamente a .render() después.
Ejercicios
- Toma el widget
LegacyDashboardy añade un tercer componente OWL (por ejemplo, unTopCustomersListsimple) montado en un nuevo contenedor. Asegúrate de que se destruya correctamente junto con los otros dos. - Escribe a mano el método
_destroyOWLComponents(sin mirar el ejemplo de "Gestión de Memoria") para un widget que rastrea dos componentes OWL montados enthis.owlComponents. - Modifica
_onChartDataPointClickpara que, en vez de abrir un diálogo legado, llame a un callback prop pasado desde un padre OWL — explica en una frase por qué esto no es posible en esta dirección (los widgets legados no son componentes OWL y no reciben props).
En la Parte 2, exploraremos el Escenario 2 (usar widgets legado en componentes OWL), patrones de puente avanzados, estrategias de pruebas y planificación de migración. Esta comprensión fundamental del Escenario 1 te preparará para los desafíos de integración más complejos que vienen.
¿Qué Sigue?
La Parte 2 invierte la dirección que acabas de aprender — en vez de montar OWL dentro de un widget legado, vas a envolver un widget legado dentro de un componente OWL, además de cubrir patrones de puente avanzados y un cronograma práctico de migración.