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 17 Part 1: The Legacy-OWL Bridge - Understanding and Basic Implementation

  • All Blogs
  • OWL Book
  • Chapter 17 Part 1: The Legacy-OWL Bridge - Understanding and Basic Implementation
  • August 25, 2026 by
    Chapter 17 Part 1: The Legacy-OWL Bridge - Understanding and Basic Implementation
    Grover Menacho

    Why this chapter? Because almost no real Odoo project is a blank slate — you will spend far more time working inside a codebase that already has years of legacy widgets than building something brand new in pure OWL. What is Odoo trying to solve with this? Odoo needs a way to let old and new frontend code coexist during the multi-year migration from the legacy widget system to OWL, without forcing a risky, all-at-once rewrite of every client's customizations. Real-world application: This is exactly the situation that pushed me to write this book: a client upgrade broke every custom widget we had built for them, and the bridge techniques in this chapter are what let you modernize a module piece by piece instead of freezing a project until a full rewrite is done.

    Congratulations on reaching the final core chapter! You've mastered modern JavaScript, built sophisticated OWL components, implemented global state management, and learned professional testing practices. You're fully equipped to build cutting-edge applications in Odoo from scratch.

    However, the reality of professional Odoo development is more nuanced. Most of your work won't involve green-field projects where you can use pure OWL throughout. Instead, you'll be working with existing Odoo databases filled with years of customizations, third-party modules, and user interfaces built with Odoo's older JavaScript framework—the legacy widget system.

    This creates a critical challenge: How do you modernize applications incrementally? How do you introduce powerful OWL components into existing legacy interfaces without breaking everything? How do you leverage existing legacy widgets in new OWL applications when rebuilding them would take months?

    The answer lies in Odoo's legacy-OWL bridge—a sophisticated compatibility layer that enables seamless interoperability between old and new code. This isn't just a technical curiosity; it's an essential tool for any professional Odoo developer working on real-world projects.


    Understanding the Legacy Landscape

    Before diving into bridge techniques, let's understand what we're bridging between.

    The Legacy Widget System

    Odoo's legacy JavaScript framework, used from version 8 through parts of version 16, was built around:

    Widget-Based Architecture: Components were classes extending Widget

    const MyWidget = Widget.extend({
        template: 'my_module.MyTemplate',
    
        start: function() {
            // Initialization logic
            return this._super.apply(this, arguments);
        },
    
        destroy: function() {
            // Cleanup logic
            this._super.apply(this, arguments);
        }
    });
    

    jQuery-Heavy: Direct DOM manipulation and event handling

    events: {
        'click .my-button': '_onButtonClick',
    },
    
    _onButtonClick: function(event) {
        this.$('.result').text('Button clicked!');
    }
    

    QWeb Templates: Server-side template compilation

    <t t-name="my_module.MyTemplate">
        <div class="my-widget">
            <button class="my-button">Click me</button>
            <div class="result"></div>
        </div>
    </t>
    

    Why Bridge Instead of Rewrite?

    Practical Reality: Large Odoo installations have hundreds of custom widgets representing years of business logic and user interface refinements.

    Risk Management: A complete rewrite introduces significant risk of breaking existing functionality that users depend on daily.

    Resource Constraints: Rewriting everything at once would require massive development resources and extended project timelines.

    Business Continuity: Users need new features while existing functionality continues to work reliably.

    The bridge approach allows for gradual modernization—introducing OWL components incrementally while maintaining system stability.


    Bridge Architecture Overview

    The legacy-OWL bridge works in both directions:

    Direction 1 mounts OWL inside the widget; direction 2 wraps the widget so it can be used from OWL.{width=100%}

    Direction 1: OWL in Legacy - Mount modern OWL components inside existing legacy widgets - Add new features without rewriting entire screens - Gradually modernize user interfaces

    Direction 2: Legacy in OWL - Use existing legacy widgets within new OWL applications - Leverage complex legacy functionality without rebuilding - Maintain compatibility with third-party widgets


    Scenario 1: Embedding OWL Components in Legacy Widgets

    This is the most common modernization pattern. You have an existing legacy form or dashboard, and you want to add a sophisticated new feature built with OWL.

    Example: Adding a Modern Chart to a Legacy Dashboard

    Let's say you have a legacy sales dashboard widget, and you want to add an interactive revenue chart built with OWL.

    1. The Legacy Dashboard Widget

    odoo.define('sales_dashboard.LegacyDashboard', function (require) {
        "use strict";
    
        const Widget = require('web.Widget');
        const core = require('web.core');
        const { mount } = require("@odoo/owl");
    
        // Import our modern OWL component
        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 = {}; // Track mounted OWL components
            },
    
            async start() {
                await this._super(...arguments);
    
                // Initialize legacy functionality
                this._setupLegacyFeatures();
    
                // Mount OWL components
                await this._mountOWLComponents();
    
                // Load initial data
                await this._loadDashboardData();
            },
    
            _setupLegacyFeatures: function() {
                // Legacy jQuery-based functionality
                this.$('.legacy-counter').each(function(index, element) {
                    $(element).data('value', 0);
                });
    
                // Initialize legacy date picker
                this.$('.date-filter').datepicker({
                    format: 'yyyy-mm-dd',
                    onSelect: this._onDateFilterChange.bind(this)
                });
            },
    
            async _mountOWLComponents() {
                try {
                    // Mount the revenue chart in its designated container
                    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),
                                // Pass legacy widget reference for complex interactions
                                legacyWidget: this,
                            }
                        });
                    }
    
                    // Mount additional OWL components as needed
                    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('Failed to mount OWL components:', error);
                    // Graceful degradation - show error message or fallback UI
                    this._showComponentError(error);
                }
            },
    
    

    Once the OWL components are mounted, the widget loads its data and pushes it into both the legacy counters and the OWL components' props:

            async _loadDashboardData() {
                try {
                    const data = await this._rpc({
                        model: 'sale.order',
                        method: 'get_dashboard_data',
                        args: [this._getDateRange()],
                    });
    
                    // Update legacy widgets
                    this._updateLegacyCounters(data.counters);
    
                    // Update OWL components
                    await this._updateOWLComponents(data);
    
                } catch (error) {
                    console.error('Failed to load dashboard data:', error);
                    this._showError('Failed to load dashboard data');
                }
            },
    
            async _updateOWLComponents(data) {
                // Update OWL component props
                if (this.owlComponents.revenueChart) {
                    this.owlComponents.revenueChart.props.data = data.revenue;
                    // Trigger re-render by updating 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) {
                // Legacy jQuery-based updates
                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);
                    }
                });
            },
    
            // Event handlers for legacy functionality
            _onRefreshData: function(event) {
                event.preventDefault();
                this._loadDashboardData();
            },
    
            _onDateFilterChange: function(event) {
                const newDateRange = this._getDateRange();
                this._loadDashboardData();
            },
    
            // Event handlers for OWL component interactions
            _onChartDataPointClick: function(dataPoint) {
                // Handle clicks from OWL chart - maybe show legacy drill-down dialog
                this._showDrillDownDialog(dataPoint);
            },
    
            _onChartDateRangeChange: function(dateRange) {
                // Update legacy date filters when OWL chart changes date range
                this.$('.date-filter').datepicker('setDate', dateRange.start);
                this._loadDashboardData();
            },
    
            _onMetricClick: function(metric) {
                // Navigate to legacy list view
                this.do_action({
                    type: 'ir.actions.act_window',
                    res_model: 'sale.order',
                    views: [[false, 'list']],
                    domain: metric.domain,
                    context: metric.context,
                });
            },
    
            // Utility methods
            _getDateRange: function() {
                return {
                    start: this.$('.date-start').val(),
                    end: this.$('.date-end').val(),
                };
            },
    
            _showDrillDownDialog: function(dataPoint) {
                // Legacy dialog implementation
                const dialog = new Dialog(this, {
                    title: `Details for ${dataPoint.label}`,
                    size: 'medium',
                    $content: $(`<div>Revenue: ${dataPoint.value}</div>`),
                });
                dialog.open();
            },
    
            _showComponentError: function(error) {
                this.$('.owl-error-container').show().find('.error-message').text(
                    'Failed to load interactive components. Please refresh the page.'
                );
            },
    
            _showError: function(message) {
                // Legacy error display
                this.$('.error-container').show().find('.error-text').text(message);
            },
    

    The last piece is the most important one for this chapter: cleaning up the mounted OWL components. Because the legacy Widget class has no automatic lifecycle hook for this, you must do it by hand inside destroy, or every mounted OWL component leaks memory every time the legacy widget is closed and reopened.

            // Critical: Cleanup OWL components to prevent memory leaks
            destroy: function() {
                // Unmount all OWL components
                Object.values(this.owlComponents).forEach(component => {
                    if (component && component.destroy) {
                        component.destroy();
                    }
                });
                this.owlComponents = {};
    
                // Call parent destroy
                this._super.apply(this, arguments);
            },
        });
    
        return LegacyDashboard;
    });
    

    2. The Legacy Template

    <t t-name="sales_dashboard.LegacyDashboardTemplate">
        <div class="legacy-sales-dashboard">
            <!-- Legacy header with controls -->
            <div class="dashboard-header">
                <h2>Sales Dashboard (Legacy)</h2>
                <div class="dashboard-controls">
                    <input type="text" class="date-filter date-start" placeholder="Start Date"/>
                    <input type="text" class="date-filter date-end" placeholder="End Date"/>
                    <button class="btn btn-primary refresh-data">Refresh</button>
                </div>
            </div>
    
            <!-- Legacy counters -->
            <div class="legacy-counters row">
                <div class="col-md-3">
                    <div class="counter-widget">
                        <h4>Total Sales</h4>
                        <div class="counter" data-metric="total_sales">0</div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="counter-widget">
                        <h4>New Customers</h4>
                        <div class="counter" data-metric="new_customers">0</div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="counter-widget">
                        <h4>Active Deals</h4>
                        <div class="counter" data-metric="active_deals">0</div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="counter-widget">
                        <h4>Conversion Rate</h4>
                        <div class="counter" data-metric="conversion_rate">0%</div>
                    </div>
                </div>
            </div>
    
            <!-- Modern OWL components mounted here -->
            <div class="modern-components">
                <div class="row">
                    <div class="col-md-8">
                        <div class="card">
                            <div class="card-header">
                                <h5>Revenue Trends (Modern OWL Component)</h5>
                            </div>
                            <div class="card-body">
                                <!-- OWL component will be mounted here -->
                                <div class="revenue-chart-container"></div>
                                <!-- Fallback for when OWL component fails -->
                                <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>Key Metrics (Modern OWL Component)</h5>
                            </div>
                            <div class="card-body">
                                <!-- Another OWL component mounted here -->
                                <div class="metrics-widget-container"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
    
            <!-- Legacy table -->
            <div class="legacy-table-section">
                <h4>Recent Orders (Legacy Table)</h4>
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>Order #</th>
                            <th>Customer</th>
                            <th>Amount</th>
                            <th>Date</th>
                        </tr>
                    </thead>
                    <tbody class="recent-orders-tbody">
                        <!-- Populated by legacy JavaScript -->
                    </tbody>
                </table>
            </div>
    
            <!-- Error container -->
            <div class="error-container" style="display: none;">
                <div class="alert alert-danger">
                    <span class="error-text"></span>
                </div>
            </div>
        </div>
    </t>
    

    3. The Modern OWL Revenue Chart Component

    /** @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">Line Chart</option>
                        <option value="bar">Bar Chart</option>
                        <option value="area">Area Chart</option>
                    </select>
                    <button class="btn btn-sm btn-secondary" t-on-click="exportChart">
                        Export
                    </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">
                        Showing <t t-esc="chartSummary.totalDataPoints"/> data points.
                        Highest: <t t-esc="chartSummary.highest"/>
                        Average: <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 }, // Reference to legacy widget
        };
    
        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() {
            // Initialize Chart.js or similar charting library
            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 `Revenue: $${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: 'Revenue',
                    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("Chart exported successfully", { 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;
        }
    }
    

    This example demonstrates several key principles:

    1. Clear Separation: Legacy and OWL code remain separate but communicate through well-defined interfaces
    2. Graceful Degradation: If OWL components fail to load, the legacy functionality continues working
    3. Bidirectional Communication: OWL components can notify legacy widgets of events, and vice versa
    4. Memory Management: Proper cleanup prevents memory leaks
    5. Error Handling: Robust error handling ensures system stability

    Key Benefits of This Approach

    1. Incremental Modernization: We replaced the simple chart with a sophisticated OWL component while keeping complex legacy features

    2. Risk Management: Legacy export and dialog functionality continues working

    3. Enhanced User Experience: Modern, interactive charts and tables

    4. Maintainability: New features can be built in OWL while legacy features remain stable

    5. Performance: Only load modern components when needed


    Best Practices for Scenario 1

    1. Component Isolation

    Keep OWL components self-contained:

    // Good: OWL component is independent
    const owlComponent = await mount(ChartComponent, target, {
        props: {
            data: this.salesData,
            onEvent: this.handleEvent.bind(this)
        }
    });
    
    // Bad: OWL component depends on legacy widget internals
    const owlComponent = await mount(ChartComponent, target, {
        props: {
            legacyWidget: this, // Too much coupling
            domElement: this.$('.some-element') // Direct DOM dependency
        }
    });
    

    2. Error Boundaries

    Always implement graceful fallbacks:

    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(`Failed to mount ${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>Component Unavailable</h6>
                    <p>This feature is temporarily unavailable. Please refresh the page.</p>
                    <button class="btn btn-sm btn-secondary" onclick="location.reload()">
                        Refresh Page
                    </button>
                </div>
            `;
        }
    }
    

    3. Memory Management

    Critical for preventing leaks:

    destroy: function() {
        // Clean up OWL components first
        this._destroyOWLComponents();
    
        // Then call parent destroy
        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 destroying component ${key}:`, error);
            }
        });
        this.owlComponents = {};
    }
    

    TL;DR: Wrap legacy widgets from inside an OWL component with useRef + onMounted/onWillUnmount, or mount OWL components inside a legacy widget's start()/destroy() — either direction works as long as you keep the interface between the two explicit.

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

    Common Pitfalls

    Forgetting to destroy mounted OWL components. If you mount an OWL component inside _mountOWLComponents but don't unmount it in destroy, the component (and everything it holds onto — event listeners, timers, service subscriptions) leaks every time the legacy widget is closed and reopened.

    Interacting with the legacy widget's DOM before it's ready. The legacy widget's this.el only exists once start() has run. Mounting an OWL component or querying this.el.querySelector(...) from init() will fail silently or throw — always do this from inside (or after) start().

    Mixing the two event systems without a clear boundary. It's tempting to have OWL components call legacy this.trigger_up(...) directly, or have the legacy widget reach into OWL internals. Keep the interface between them explicit — callback props going one way, the legacy widget's public methods going the other — so you don't end up with two objects that both think they own the same piece of state.

    Assuming the OWL component's props are "live". Legacy widgets mutate plain objects; OWL only re-renders when it detects a change through its reactivity system. Directly poking a value into this.owlComponents.revenueChart.props.data (like the example above does) does not automatically trigger a re-render — that's why the example calls .render() explicitly afterward.

    Exercises

    1. Take the LegacyDashboard widget and add a third OWL component (e.g., a simple TopCustomersList) mounted into a new container. Make sure it's properly destroyed alongside the other two.
    2. Write the _destroyOWLComponents cleanup by hand (without looking at the "Memory Management" example) for a widget that tracks two mounted OWL components in this.owlComponents.
    3. Modify _onChartDataPointClick so that instead of opening a legacy dialog, it calls a callback prop passed down from an OWL parent — describe in a sentence why that's not possible in this direction (legacy widgets aren't OWL components and don't receive props).

    In Part 2, we'll explore Scenario 2 (using legacy widgets in OWL components), advanced bridge patterns, testing strategies, and migration planning. This foundational understanding of Scenario 1 will prepare you for the more complex integration challenges ahead.

    What's Next?

    Part 2 flips the direction you just learned — instead of mounting OWL inside a legacy widget, you'll wrap a legacy widget inside an OWL component, plus cover advanced bridge patterns and a practical migration timeline.

    in OWL Book
    Chapter 17 Part 2: Advanced Bridge Patterns and Migration Strategies
    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