Toggle navigation

Extendiendo la Interfaz

Ésta guía trata sobre la creación de módulos para la interfaz web de Odoo.

Para crear sitios web con Odoo, ver Construyendo un Website; para añadir capacidades de negocio o ampliar sistemas de negocio existentes de Odoo, ver Construyendo un Módulo.

Un módulo sencillo

Vamos a empezar con un módulo sencillo de Odoo con la configuración básica del componente web, que nos permita probar el framework web.

El módulo ejemplo está disponible en línea y puede descargarse usando el siguiente comando:

$ git clone http://github.com/odoo/petstore

This will create a petstore folder wherever you executed the command. You then need to add that folder to Odoo's addons path, create a new database and install the oepetstore module.

Si navegas a la carpeta petstore , deberías ver el siguiente contenido:

oepetstore
|-- images
|   |-- alligator.jpg
|   |-- ball.jpg
|   |-- crazy_circle.jpg
|   |-- fish.jpg
|   `-- mice.jpg
|-- __init__.py
|-- oepetstore.message_of_the_day.csv
|-- __manifest__.py
|-- petstore_data.xml
|-- petstore.py
|-- petstore.xml
`-- static
    `-- src
        |-- css
        |   `-- petstore.css
        |-- js
        |   `-- petstore.js
        `-- xml
            `-- petstore.xml

El módulo tiene ya varias personalizaciones de servidor. Regresaremos a estos más adelante, por ahora vamos a centrarnos en el contenido relacionado con la web, en la carpeta static.

Los archivos utilizados en el lado "web" de un módulo Odoo deben colocarse en una carpeta static, para que estén disponibles en el explorador web, los archivos fuera de esa carpeta no pueden ser descargados por los navegadores. El nombre que tienen los subdirectorios src/css, src/js and src/xml es por convención y no es estrictamente necesario.

oepetstore/static/css/petstore.css

Actualmente vacío, contendrá los CSS de la tienda de mascotas

oepetstore/static/xml/petstore.xml

En su mayor parte vacío, contendrá las plantillas QWeb

oepetstore/static/js/petstore.js

La parte más importante (e interesante), contiene la lógica de la aplicación (o al menos del lado del navegador) como javascript. Actualmente debería verse como:

odoo.oepetstore = function(instance, local) {
    var _t = instance.web._t,
        _lt = instance.web._lt;
    var QWeb = instance.web.qweb;

    local.HomePage = instance.Widget.extend({
        start: function() {
            console.log("pet store home page loaded");
        },
    });

    instance.web.client_actions.add(
        'petstore.homepage', 'instance.oepetstore.HomePage');
}

El cual sólo imprime un pequeño mensaje en la consola del explorador.

The files in the static folder, need to be defined within the module in order for them to be loaded correctly. Everything in src/xml is defined in __manifest__.py while the contents of src/css and src/js are defined in petstore.xml, or a similar file.

Odoo módulo en JavaScript

JavaScript no tiene módulos incorporados. Como resultado, las variables definidas en archivos diferentes son todos tratadas juntas y pueden entrar en conflicto. Esto ha dado lugar a diversos patrones de módulo usados para construir los espacios de nombres limpios y limitar los riesgos de conflictos de nombres.

El esquema de desarrollo en Odoo utiliza un patrón para definir módulos web, para manejar los espacios de nombres y así cargarlos correctamente.

oepetstore/static/js/petstore.js contiene una declaración de módulo:

odoo.oepetstore = function(instance, local) {
    local.xxx = ...;
}

In Odoo web, modules are declared as functions set on the global odoo variable. The function's name must be the same as the addon (in this case oepetstore) so the framework can find it, and automatically initialize it.

Cuando el cliente web se carga el módulo llama a la función raíz y envía dos parámetros:

  • el primer parámetro es la instancia actual del cliente web de Odoo, da acceso a varias de las capacidades definidas por Odoo (traducciones, servicios de red) así como de objetos definidos por la base o por otros módulos.

  • el segundo parámetro es su propio espacio de nombres local creado automáticamente por el cliente web. Objetos y variables que deben ser accesibles desde fuera de su módulo (o bien porque el cliente de web de Odoo tiene que llamar a los otros para personalizarlos) deben ajustarse dentro de ese espacio de nombres.

Clases

Tanto como módulos y contrario a lenguajes más orientados a objetos, javascript no construye clases 1 aunque provee un mecanismo equivalente (de menor nivel y más detallado).

Por simplicidad y ayuda al desarrollador Odoo web ofrece un sistema de clases basado en Simple JavaScript Inheritance de John Resig.

New classes are defined by calling the extend() method of odoo.web.Class():

var MyClass = instance.web.Class.extend({
    say_hello: function() {
        console.log("hello");
    },
});

The extend() method takes a dictionary describing the new class's content (methods and static attributes). In this case, it will only have a say_hello method which takes no parameters.

Las clases son instanciadas usando el operador new:

var my_object = new MyClass();
my_object.say_hello();
// print "hello" in the console

Y atributos de la instancia pueden accederse a través de this:

var MyClass = instance.web.Class.extend({
    say_hello: function() {
        console.log("hello", this.name);
    },
});

var my_object = new MyClass();
my_object.name = "Bob";
my_object.say_hello();
// print "hello Bob" in the console

Las clases pueden proporcionar un inicializador para realizar la configuración inicial de la instancia, mediante la definición de un método init(). El inicializador recibe los parámetros pasados al usar el operador new:

var MyClass = instance.web.Class.extend({
    init: function(name) {
        this.name = name;
    },
    say_hello: function() {
        console.log("hello", this.name);
    },
});

var my_object = new MyClass("Bob");
my_object.say_hello();
// print "hello Bob" in the console

It is also possible to create subclasses from existing (used-defined) classes by calling extend() on the parent class, as is done to subclass Class():

var MySpanishClass = MyClass.extend({
    say_hello: function() {
        console.log("hola", this.name);
    },
});

var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hola Bob" in the console

Cuando sobreescribes un método usando herencia, puede utilizar this._super() para llamar al método original:

var MySpanishClass = MyClass.extend({
    say_hello: function() {
        this._super();
        console.log("translation in Spanish: hola", this.name);
    },
});

var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hello Bob \n translation in Spanish: hola Bob" in the console

Conceptos básicos de widgets

El cliente web Odoo contiene jQuery para una sencilla manipulación del DOM. Es útil y proporciona una API mejor que el estándar W3C DOM2, pero insuficiente para aplicaciones de estructura compleja complicando su mantenimiento.

Much like object-oriented desktop UI toolkits (e.g. Qt, Cocoa or GTK), Odoo Web makes specific components responsible for sections of a page. In Odoo web, the base for such components is the Widget() class, a component specialized in handling a page section and displaying information for the user.

Tu Widget

El módulo demostración inicial ya proporciona un widget básico:

local.HomePage = instance.Widget.extend({
    start: function() {
        console.log("pet store home page loaded");
    },
});

It extends Widget() and overrides the standard method start(), which — much like the previous MyClass — does little for now.

Esta línea al final del archivo:

instance.web.client_actions.add(
    'petstore.homepage', 'instance.oepetstore.HomePage');

registra nuestro widget básico como una acción del cliente. Las acciones de cliente se explicará más adelante, por ahora esto es sólo lo que permite llamar a nuestro widget y que aparezca cuando seleccionamos la Pet Store ‣ Pet Store ‣ Home Page menú.

Mostrar Contenido

Widgets tienen un número de características y métodos, pero los conceptos básicos son simples:

  • crear un widget

  • formatear los datos del widget

  • Mostrar el widget

The HomePage widget already has a start() method. That method is part of the normal widget lifecycle and automatically called once the widget is inserted in the page. We can use it to display some content.

All widgets have a $el which represents the section of page they're in charge of (as a jQuery object). Widget content should be inserted there. By default, $el is an empty <div> element.

El elemento <div> está generalmente invisible para el usuario si no tiene nada de contenido (o sin estilos específicos, que le dan un tamaño) razón por la cual nada se muestra en la página cuando el HomePage el llamado.

Vamos a añadir algunos contenidos al elemento raíz del widget usando jQuery:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>Hello dear Odoo user!</div>");
    },
});

Este mensaje aparecerá cuando se abre Pet Store ‣ Pet Store ‣ Home Page

El widget del HomePage es utilizado por Odoo Web y gestionado automáticamente. Para aprender a usar un widget “desde cero“ vamos a crear uno nuevo:

local.GreetingsWidget = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>We are so happy to see you again in this menu!</div>");
    },
});

We can now add our GreetingsWidget to the HomePage by using the GreetingsWidget's appendTo() method:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>Hello dear Odoo user!</div>");
        var greeting = new local.GreetingsWidget(this);
        return greeting.appendTo(this.$el);
    },
});
  • HomePage primero agrega su propio contenido a la raíz DOM

  • HomePage luego crea una instancia de GreetingsWidget

  • Finally it tells GreetingsWidget where to insert itself, delegating part of its $el to the GreetingsWidget.

When the appendTo() method is called, it asks the widget to insert itself at the specified position and to display its content. The start() method will be called during the call to appendTo().

To see what happens under the displayed interface, we will use the browser's DOM Explorer. But first let's alter our widgets slightly so we can more easily find where they are, by adding a class to their root elements:

local.HomePage = instance.Widget.extend({
    className: 'oe_petstore_homepage',
    ...
});
local.GreetingsWidget = instance.Widget.extend({
    className: 'oe_petstore_greetings',
    ...
});

Si puedes encontrar la sección relevante del DOM (haga clic derecho sobre el texto a continuación Inspect Element), debe lucir así:

<div class="oe_petstore_homepage">
    <div>Hello dear Odoo user!</div>
    <div class="oe_petstore_greetings">
        <div>We are so happy to see you again in this menu!</div>
    </div>
</div>

Which clearly shows the two <div> elements automatically created by Widget(), because we added some classes on them.

También podemos ver los dos divs que añadimos nosotros mismos

Por último, tenga en cuenta que el elemento <div class="oe_petstore_greetings"> representa la instancia GreetingsWidget que está dentro de <div class="oe_petstore_homepage"> que representa la instancia HomePage, la cual lo agrega

Padres e hijos del widget

En la parte anterior, creamos una instancia de un widget usando esta sintaxis:

new local.GreetingsWidget(this);

El primer argumento es this, que en ese caso fue una instancia de HomePage . Esto indica que el widget se crea otro widget el cual es su padre.

Como hemos visto, widgets generalmente se insertan en el DOM por otro widget y dentro el elemento de raíz de otro widget. Esto significa que la mayoría de los widgets son “parte” de otro widget y existe en el nombre de él. Llamamos el contenedor el padre y el widget contenido el hijo.

Debido a múltiples razones técnicas y conceptuales, es necesaria para un widget saber quién es su padre y cuales son sus hijos.

getParent()

puede utilizarse para obtener el padre de un widget:

local.GreetingsWidget = instance.Widget.extend({
    start: function() {
        console.log(this.getParent().$el );
        // will print "div.oe_petstore_homepage" in the console
    },
});
getChildren()

puede utilizarse para obtener una lista de sus hijos:

local.HomePage = instance.Widget.extend({
    start: function() {
        var greeting = new local.GreetingsWidget(this);
        greeting.appendTo(this.$el);
        console.log(this.getChildren()[0].$el);
        // will print "div.oe_petstore_greetings" in the console
    },
});

When overriding the init() method of a widget it is of the utmost importance to pass the parent to the this._super() call, otherwise the relation will not be set up correctly:

local.GreetingsWidget = instance.Widget.extend({
    init: function(parent, name) {
        this._super(parent);
        this.name = name;
    },
});

Por último, si un widget no tiene un padre (por ejemplo, porque es el widget de la raíz de la aplicación), null puede ser proporcionado como padre:

new local.GreetingsWidget(null);

Destrucción de Widgets

If you can display content to your users, you should also be able to erase it. This is done via the destroy() method:

greeting.destroy();

When a widget is destroyed it will first call destroy() on all its children. Then it erases itself from the DOM. If you have set up permanent structures in init() or start() which must be explicitly cleaned up (because the garbage collector will not handle them), you can override destroy().

El motor de la plantilla de QWeb

En la sección anterior agregamos contenido a nuestros widgets directamente manipulando (y agregando a) su DOM:

this.$el.append("<div>Hello dear Odoo user!</div>");

Esto permite generar y visualizar cualquier tipo de contenido, pero es difícil de manejar cuando se generan cantidades significativas de DOM (mucha duplicación, citando problemas,…)

Como muchos otros ambientes, la solución de Odoo es utilizar un motor de plantilla engine. El motor de plantillas en Odoo se llama QWeb.

QWeb es un lenguaje de plantillas basado en XML, similar a Genshi, Thymeleaf o Facelets. Este tiene las siguientes características:

  • Se ha implementado completamente en JavaScript y es procesado en el navegador

  • Cada archivo de plantilla (archivos XML) contiene varias plantillas

  • It has special support in Odoo Web's Widget(), though it can be used outside of Odoo's web client (and it's possible to use Widget() without relying on QWeb)

Utilizar QWeb

Primero vamos a definir una plantilla de QWeb simple en el archivo casi vacío oepetstore/static/src/xml/petstore.xml:

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="HomePageTemplate">
        <div style="background-color: red;">This is some simple HTML</div>
    </t>
</templates>

Ahora podemos usar esta plantilla en el widget del HomePage. Utilizando la variable del cargador QWeb en la parte superior de la página, podemos llamar a la plantilla definida en el archivo XML:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append(QWeb.render("HomePageTemplate"));
    },
});

QWeb.render() busca la plantilla especifica, y la convierte a una cadena y devuelve el resultado.

However, because Widget() has special integration for QWeb the template can be set directly on the widget via its template attribute:

local.HomePage = instance.Widget.extend({
    template: "HomePageTemplate",
    start: function() {
        ...
    },
});

Aunque el resultado es similar, hay dos diferencias entre estos usos:

  • with the second version, the template is rendered right before start() is called
  • en la primera versión el contenido de la plantilla se agrega al elemento de raíz del widget, mientras que en la segunda versión la raíz de la plantilla queda directamente como elemento de raíz del widget. Razón por la cual el sub-widget "greetings" también tiene un fondo rojo

Contexto QWeb

A las plantillas QWeb le puedes pasar datos y éstos pueden contener lógica de visualización básicas.

Para las llamadas explícitas a QWeb.render(), los datos de la plantilla se pasan como segundo parámetro:

QWeb.render("HomePageTemplate", {name: "Klaus"});

con la plantilla para modificar:

<t t-name="HomePageTemplate">
    <div>Hello <t t-esc="name"/></div>
</t>

dará como resultado:

<div>Hello Klaus</div>

When using Widget()'s integration it is not possible to provide additional data to the template. The template will be given a single widget context variable, referencing the widget being rendered right before start() is called (the widget's state will essentially be that set up by init()):

<t t-name="HomePageTemplate">
    <div>Hello <t t-esc="widget.name"/></div>
</t>
local.HomePage = instance.Widget.extend({
    template: "HomePageTemplate",
    init: function(parent) {
        this._super(parent);
        this.name = "Mordecai";
    },
    start: function() {
    },
});

Resultado:

<div>Hello Mordecai</div>

Declaración de la plantilla

Hemos visto cómo es el procesamiento de plantillas QWeb, ahora veamos la sintaxis de las plantillas.

Una plantilla de QWeb se compone de XML regular mezclado con directivas QWeb . Se declara una directiva de QWeb con atributos XML que comiencen por ‘’ t-‘’.

La directiva más básica es t-name, se utiliza para declarar nuevas plantillas en un archivo de plantillas:

<templates>
    <t t-name="HomePageTemplate">
        <div>This is some simple HTML</div>
    </t>
</templates>

t-name toma el nombre de la plantilla que se está definiendo y declara que pueda llamarse usando QWeb.render(). Sólo se puede utilizar en el nivel superior de un archivo de plantilla.

Escape

La directiva t-esc se puede utilizar para mostrar texto:

<div>Hello <t t-esc="name"/></div>

Toma una expresión de Javascript que se evalúa, el resultado de la expresión es entonces insertado en el documento y escapado de HTML. Ya que es una expresión, es posible proporcionar sólo un nombre de variable como se ve arriba, o una expresión más compleja como un cálculo:

<div><t t-esc="3+5"/></div>

o el método llama:

<div><t t-esc="name.toUpperCase()"/></div>

Produciendo HTML

Para inyectar HTML en la página durante el procesamiento, usas t-raw. Como t-esc toma una expresión Javascript arbitraria como parámetro, pero no realiza un paso de escapar el HTML.

<div><t t-raw="name.link(user_account)"/></div>

Condicionales

QWeb puede tener bloques condicionales utilizando t-if`. La Directiva toma una expresión arbitraria, si la expresión es falsa (``false, null, 0 o una cadena vacía) se suprime el bloque entero, de lo contrario se muestra.

<div>
    <t t-if="true == true">
        true is true
    </t>
    <t t-if="true == false">
        true is not true
    </t>
</div>

Iteración

Para iterar sobre una lista, use t-foreach y t-as. t-foreach toma una expresión devolviendo una lista sobre la que se itera y toma t-as para asignar el nombre de la variable para enlazar a cada elemento de la iteración.

<div>
    <t t-foreach="names" t-as="name">
        <div>
            Hello <t t-esc="name"/>
        </div>
    </t>
</div>

Definir atributos

QWeb proporciona dos directivas relacionadas para definir atributos calculados: t-att-name y t-attf-name. En cualquier caso, name es el nombre del atributo a crear (por ejemplo, t-att-id define el atributo id después de la representación).

t-att- toma una expresión javascript cuyo resultado se define como el valor del atributo, que es más útil si todos el valor del atributo se calcula:

<div>
    Input your name:
    <input type="text" t-att-value="defaultName"/>
</div>

t-attf- toma una formato de cadena. Una cadena de formato es texto literal con bloques de interpolación dentro de él, un bloque de interpolación es una expresión de javascript entre {{ y }}, que será sustituido por el resultado de la expresión. Es más útil para los atributos que son parcialmente literales y parcialmente calculados como una clase:

<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
    insert content here
</div>

Llamar a otras plantillas

Las plantillas se pueden dividir en las plantillas (por simplicidad, mantenibilidad, reusabilidad o para evitar anidamiento excesivo markup).

Esto se hace usando la Directiva de ‘’ t-llamada ‘’, que toma el nombre de la plantilla para representar:

<t t-name="A">
    <div class="i-am-a">
        <t t-call="B"/>
    </div>
</t>
<t t-name="B">
    <div class="i-am-b"/>
</t>

representación de la plantilla de A resultará en:

<div class="i-am-a">
    <div class="i-am-b"/>
</div>

Plantillas secundarias heredan el contexto de representación de su invocador.

Para obtener más información acerca de la

Para referencias acerca de QWeb, vea QWeb.

Ejercicio

Ayudantes de widget

JQuery Selector de Widget

Seleccionar elementos del DOM en un widget puede realizarse llamando al método find() en la raíz del DOM del widget:

this.$el.find("input.my_input")...

But because it's a common operation, Widget() provides an equivalent shortcut through the $() method:

local.MyWidget = instance.Widget.extend({
    start: function() {
        this.$("input.my_input")...
    },
});

Asignar eventos del DOM

Previamente hemos enlazado eventos DOM usando controladores de eventos normales de jQuery (por ejemplo .click() o .change()) a elementos del widget:

local.MyWidget = instance.Widget.extend({
    start: function() {
        var self = this;
        this.$(".my_button").click(function() {
            self.button_clicked();
        });
    },
    button_clicked: function() {
        ..
    },
});

Si bien esto funciona tiene algunos problemas:

  1. es más bien detallado

  2. no admite sustituir el elemento de raíz del widget en tiempo de ejecución sólo ya que solo se realiza la unión cuando start() es ejecutado (durante la inicialización del widget)

  3. requiere tratar con los los problemas de this-binding

Widgets thus provide a shortcut to DOM event binding via events:

local.MyWidget = instance.Widget.extend({
    events: {
        "click .my_button": "button_clicked",
    },
    button_clicked: function() {
        ..
    }
});

events is an object (mapping) of an event to the function or method to call when the event is triggered:

  • la clave es el nombre del evento, posiblemente refinado con un selector CSS en cuyo caso sólo si el evento ocurre en un elemento secundario la función se ejecutará: click se encargará de todos los clics dentro del widget, pero click .my_button ejecutará sólo el clic en elementos que tengan la clase my_button

  • el valor es la acción a realizar cuando se desencadena el evento

    Puede ser una función:

    events: {
        'click': function (e) { /* code here */ }
    }
    

    o el nombre de un método en el objeto (ver ejemplo anterior).

    En cualquier caso, this es la instancia del widget y el controlador pasa un solo parámetro, el evento del objeto jQuery.

Propiedades y eventos de widget

Eventos

Widgets proporciona un sistema de evento (separado del sistema de eventos de DOM/jQuery descrito): un widget puede disparar eventos en sí mismo, y otros widgets (o a sí mismo) puedes atar a ellos mismos para dichos eventos:

local.ConfirmWidget = instance.Widget.extend({
    events: {
        'click button.ok_button': function () {
            this.trigger('user_chose', true);
        },
        'click button.cancel_button': function () {
            this.trigger('user_chose', false);
        }
    },
    start: function() {
        this.$el.append("<div>Are you sure you want to perform this action?</div>" +
            "<button class='ok_button'>Ok</button>" +
            "<button class='cancel_button'>Cancel</button>");
    },
});

Este widget actúa como una fachada, transformando la entrada del usuario (a través de eventos de DOM) en un evento interno documentable al cual el widget padre se pueden enlazar.

trigger() takes the name of the event to trigger as its first (mandatory) argument, any further arguments are treated as event data and passed directly to listeners.

We can then set up a parent event instantiating our generic widget and listening to the user_chose event using on():

local.HomePage = instance.Widget.extend({
    start: function() {
        var widget = new local.ConfirmWidget(this);
        widget.on("user_chose", this, this.user_chose);
        widget.appendTo(this.$el);
    },
    user_chose: function(confirm) {
        if (confirm) {
            console.log("The user agreed to continue");
        } else {
            console.log("The user refused to continue");
        }
    },
});

on() binds a function to be called when the event identified by event_name is. The func argument is the function to call and object is the object to which that function is related if it is a method. The bound function will be called with the additional arguments of trigger() if it has any. Example:

start: function() {
    var widget = ...
    widget.on("my_event", this, this.my_event_triggered);
    widget.trigger("my_event", 1, 2, 3);
},
my_event_triggered: function(a, b, c) {
    console.log(a, b, c);
    // will print "1 2 3"
}

Propiedades

Propiedades son muy similares a atributos normales de objetos que permiten almacenar datos en una instancia del widget, sin embargo tienen la característica adicional que desencadenan eventos cuando son almacenadas:

start: function() {
    this.widget = ...
    this.widget.on("change:name", this, this.name_changed);
    this.widget.set("name", "Nicolas");
},
name_changed: function() {
    console.log("The new value of the property 'name' is", this.widget.get("name"));
}
  • set() sets the value of a property and triggers change:propname (where propname is the property name passed as first parameter to set()) and change
  • get() retrieves the value of a property.

Ejercicio

Modificar las clases y los widgets existentes

The class system of the Odoo web framework allows direct modification of existing classes using the include() method:

var TestClass = instance.web.Class.extend({
    testMethod: function() {
        return "hello";
    },
});

TestClass.include({
    testMethod: function() {
        return this._super() + " world";
    },
});

console.log(new TestClass().testMethod());
// will print "hello world"

Este sistema es similar al mecanismo de la herencia, excepto que alterará la clase objetivo en el mismo lugar sin crear una nueva clase.

In that case, this._super() will call the original implementation of a method being replaced/redefined. If the class already had sub-classes, all calls to this._super() in sub-classes will call the new implementations defined in the call to include(). This will also work if some instances of the class (or of any of its sub-classes) were created prior to the call to include().

Traducciones

El proceso de traducir texto en código Python y JavaScript es muy similar. Usted pudo haber notado estas líneas al principio del archivo petstore.js:

var _t = instance.web._t,
    _lt = instance.web._lt;

Estas líneas se utilizan simplemente para importar las funciones de traducción del módulo actual JavaScript. Se utilizan así:

this.$el.text(_t("Hello user!"));

In Odoo, translations files are automatically generated by scanning the source code. All piece of code that calls a certain function are detected and their content is added to a translation file that will then be sent to the translators. In Python, the function is _(). In JavaScript the function is _t() (and also _lt()).

_t() devolverá la traducción definida para el texto que se da. Si ninguna traducción se define para ese texto, devuelve el texto original.

_lt() ("lazy translate") is similar but somewhat more complex: instead of translating its parameter immediately, it returns an object which, when converted to a string, will perform the translation.

Se utiliza para definir términos traducibles antes de que el sistema de traducción se inicialice, para atributos de la clase por ejemplo (como los módulos se cargan antes de que se configure el idioma del usuario y se descarguen las traducciones lo hace útil).

Comunicación con el servidor de Odoo

Ponerse en contacto con modelos

La mayoría de las operaciones con Odoo implica comunicarse con modelos implementando operaciones de negocio, estos modelos luego (potencialmente) interactuaran con un motor de almacenamiento (generalmente PostgreSQL).

Aunque jQuery provee una función $.ajax para las interacciones de la red, comunicarse con Odoo requiere metadatos adicionales cuya configuración antes de cada llamada puede ser propensa a errores. Como resultado, Odoo web ofrece un método de comunicación primitiva de alto nivel.

Para demostrar esto, el archivo petstore.py contiene un modelo pequeño con un método ejemplo:

class message_of_the_day(models.Model):
    _name = "oepetstore.message_of_the_day"

    @api.model
    def my_method(self):
        return {"hello": "world"}

    message = fields.Text(),
    color = fields.Char(size=20),

Esto declara un modelo con dos campos y un método my_method() que devuelve un diccionario literal.

Aquí está un widget ejemplo que llama a my_method() y muestra el resultado:

local.HomePage = instance.Widget.extend({
    start: function() {
        var self = this;
        var model = new instance.web.Model("oepetstore.message_of_the_day");
        model.call("my_method", {context: new instance.web.CompoundContext()}).then(function(result) {
            self.$el.append("<div>Hello " + result["hello"] + "</div>");
            // will show "Hello world" to the user
        });
    },
});

The class used to call Odoo models is odoo.Model(). It is instantiated with the Odoo model's name as first parameter (oepetstore.message_of_the_day here).

call() can be used to call any (public) method of an Odoo model. It takes the following positional arguments:

name

El nombre del método a llamar, my_method

args

un arreglo de positional arguments a proporcionar al método. Debido a que el ejemplo no tiene ningún argumento posicional para proporcionar, no se proporciona el parámetro args.

Aquí está otro ejemplo con argumentos posicionales:

@api.model
def my_method2(self, a, b, c): ...
model.call("my_method", [1, 2, 3], ...
// with this a=1, b=2 and c=3
kwargs

una asignación de keyword arguments a pasar. El ejemplo proporciona un solo argumento llamado context.

@api.model
def my_method2(self, a, b, c): ...
model.call("my_method", [], {a: 1, b: 2, c: 3, ...
// with this a=1, b=2 and c=3

call() returns a deferred resolved with the value returned by the model's method as first argument.

CompoundContext

La sección anterior utiliza un argumento context que no fue explicado en la llamada al método:

model.call("my_method", {context: new instance.web.CompoundContext()})

El contexto es como un argumento “mágico” que el cliente web siempre le dará al servidor cuando se llama a un método. El contexto es un diccionario que contiene múltiples claves. Uno de los más importantes es el idioma del usuario, el servidor lo utiliza para traducir todos los mensajes de la aplicación. Otra es la zona horaria del usuario, utilizado para calcular correctamente las fechas y horarios si Odoo es utilizado por personas en diferentes países.

The argument is necessary in all methods, otherwise bad things could happen (such as the application not being translated correctly). That's why, when you call a model's method, you should always provide that argument. The solution to achieve that is to use odoo.web.CompoundContext().

CompoundContext() is a class used to pass the user's context (with language, time zone, etc...) to the server as well as adding new keys to the context (some models' methods use arbitrary keys added to the context). It is created by giving to its constructor any number of dictionaries or other CompoundContext() instances. It will merge all those contexts before sending them to the server.

model.call("my_method", {context: new instance.web.CompoundContext({'new_key': 'key_value'})})
@api.model
def my_method(self):
    print self.env.context
    // will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1}

You can see the dictionary in the argument context contains some keys that are related to the configuration of the current user in Odoo plus the new_key key that was added when instantiating CompoundContext().

Consultas

While call() is sufficient for any interaction with Odoo models, Odoo Web provides a helper for simpler and clearer querying of models (fetching of records based on various conditions): query() which acts as a shortcut for the common combination of search() and :read(). It provides a clearer syntax to search and read models:

model.query(['name', 'login', 'user_email', 'signature'])
     .filter([['active', '=', true], ['company_id', '=', main_company]])
     .limit(15)
     .all().then(function (users) {
    // do work with users records
});

versus:

model.call('search', [['active', '=', true], ['company_id', '=', main_company]], {limit: 15})
    .then(function (ids) {
        return model.call('read', [ids, ['name', 'login', 'user_email', 'signature']]);
    })
    .then(function (users) {
        // do work with users records
    });
  • query() takes an optional list of fields as parameter (if no field is provided, all fields of the model are fetched). It returns a odoo.web.Query() which can be further customized before being executed
  • Query() represents the query being built. It is immutable, methods to customize the query actually return a modified copy, so it's possible to use the original and the new version side-by-side. See Query() for its customization options.

When the query is set up as desired, simply call all() to execute it and return a deferred to its result. The result is the same as read()'s, an array of dictionaries where each dictionary is a requested record, with each requested field a dictionary key.

Ejercicios

Componentes de web existentes

El director de acción

En Odoo, muchas operaciones se inicia desde una acción: abrir un elemento de menú (a la vista), imprimir un informe,…

Las acciones son piezas de información que describen cómo un cliente debe reaccionar a la activación de una pieza de contenido. Las acciones pueden almacenarse (y leer a través de un modelo) o pueden generarse sobre la marcha (localmente en los clientes por código javascript, o remotamente por un método en un modelo).

En Odoo Web, el componente responsable de manejar y reaccionar a estas acciones es el Action Manager.

Mediante el administrador de la acciones

El Action Manager puede ser invocado explícitamente desde código javascript mediante la creación de un diccionario que describe una acción del tipo correcto y creando a una instancia Action Manager con el

do_action() is a shortcut of Widget() looking up the "current" action manager and executing the action:

instance.web.TestWidget = instance.Widget.extend({
    dispatch_to_new_action: function() {
        this.do_action({
            type: 'ir.actions.act_window',
            res_model: "product.product",
            res_id: 1,
            views: [[false, 'form']],
            target: 'current',
            context: {},
        });
    },
});

El type más común de acción es ir.actions.act_window que proporciona las vistas de un modelo (recordemos que un modelo se muestra de varias maneras), sus atributos más comunes son:

res_model

El modelo del que se muestran las vistas

res_id (optional)

Vistas de formulario, un registro pre-seleccionado en res_model

views

Muestra las vistas disponibles a través de la acción. Una lista [view_id, view_type], view_id puede ser el identificador de base de datos de una vista del tipo correcto, o false a utilizar la vista predeterminada para el tipo especificado. Tipos de vista pueden no estar presentes varias veces. La acción abrirá la primera vista de la lista por defecto.

target

Cualquiera de los dos current (por defecto) que reemplaza el content de la sección del cliente web por la acción, o new para abrir la acción en un cuadro de diálogo.

context

Datos de contexto adicional para utilizar dentro de la acción.

Acciones de cliente

A lo largo de esta guía, se utilizó un simple widget de HomePage que el cliente web inicia automáticamente cuando selecciona la opción del menú derecho. ¿Pero cómo sabía la Odoo web que tenía que poner este widget? Porque el widget está registrado como una client action (acción del cliente).

Una acción de cliente es (como su nombre lo indica) un tipo de acción definido casi en su totalidad en el cliente, en javascript para Odoo web. El servidor simplemente envía una etiqueta de acción (un nombre arbitrario) y opcionalmente añade unos pocos parámetros, pero más allá todo es manejado por código personalizado del lado del cliente.

Nuestro widget está registrada como el controlador de la acción del cliente a través de esto:

instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');

instance.web.client_actions is a Registry() in which the action manager looks up client action handlers when it needs to execute one. The first parameter of add() is the name (tag) of the client action, and the second parameter is the path to the widget from the Odoo web client root.

Cuando una acción del cliente debe ser ejecutada, el Action Manager busca la etiqueta en el registro, busca la ruta especificada y muestra el widget que encuentra al final.

En el servidor, simplemente habíamos definido una acción ir.actions.client:

<record id="action_home_page" model="ir.actions.client">
    <field name="tag">petstore.homepage</field>
</record>

y un menú que abre la acción:

<menuitem id="home_page_petstore_menu" parent="petstore_menu"
          name="Home Page" action="action_home_page"/>

Arquitectura de las vistas

Gran parte de la utilidad de la web de Odoo (y complejidad) reside en las vistas. Cada tipo de vista es una forma de mostrar un modelo en el cliente.

El “View Manager” (Manejador de vistas)

Cuando una instancia de ActionManager recibe una acción de tipo ir.actions.act_window, Éste delega la sincronización y manejo de sus vistas a un view manager, el cual configura una o varias vistas dependiendo de los requerimientos de la acción original:

Las Vistas

Most Odoo views are implemented through a subclass of odoo.web.View() which provides a bit of generic basic structure for handling events and displaying model information.

La search view es considerada un tipo de vista para el framework Odoo, pero manejado por separado por el cliente web (ya que es un accesorio más permanente y puede interactuar con otras vistas, no regulares).

A view is responsible for loading its own description XML (using fields_view_get) and any other data source it needs. To that purpose, views are provided with an optional view identifier set as the view_id attribute.

Views are also provided with a DataSet() instance which holds most necessary model information (the model name and possibly various record ids).

Views may also want to handle search queries by overriding do_search(), and updating their DataSet() as necessary.

Los campos de la vista formulario

Una necesidad común es la extensión de la vista de formulario web para añadir nuevas maneras de mostrar campos.

All built-in fields have a default display implementation, a new form widget may be necessary to correctly interact with a new field type (e.g. a GIS field) or to provide new representations and ways to interact with existing field types (e.g. validate Char fields which should contain email addresses and display them as email links).

Para especificar explícitamente de que forma un widget debe ser usado para mostrar un campo, utilice simplemente el atributo de widget en la descripción de XML:

<field name="contact_mail" widget="email"/>

Los campos son instanciados por la vista formulario después de leer su descripción desde el XML y construido el HTML correspondiente el cual representa esa descripción. Después de eso, la vista formulario se comunicará con los objetos de campo utilizando algunos métodos. Estos métodos están definidos por la interfaz de ‘’ FieldInterface’’. Casi todos los campos heredan la clase abstracta ‘’ AbstractField’’. Esa clase define algunos mecanismos por defecto que deben aplicarse por la mayoría de los campos.

Estas son algunas de las responsabilidades de una clase de campo:

  • La clase campo debe mostrar y permitir al usuario editar el valor del campo.

  • Esto debe implementar correctamente los 3 atributos disponibles en todos los campos en Odoo. La clase AbstractField ya implementa un algoritmo que calcula de forma dinámica los valores de esos 3 atributos (ellos pueden cambiar en cualquier momento su valor de acuerdo al valor de otros campos). Sus valores son almacenados en las “Propiedades del Widget” (las propiedades de un widget fueron explicadas anteriormente en ésta guía). Es responsabilidad de cada clase “Field” chequear esas propiedades en el widget y dinámicamente adaptarse dependiendo de sus valores. Aquí una descripción de cada uno de esos tributos:

    • required: el campo debe tener un valor antes de guardar. Si required es true y el campo no tiene un valor, el método is_valid() del campo debe devolver false.

    • invisible: cuando éste es true, el campo debe ser invisible. La clase AbstractField ya tiene una implementación básica de este comportamiento que se adapta a la mayoría de los campos.

    • readonly: Cuando es true, el campo no debe ser editable por el usuario. La mayoría de los campos de Odoo tiene un comportamiento totalmente diferente dependiendo del valor de readonly. Como ejemplo, el FieldChar muestra un HTML <input> cuando es modificable y simplemente muestra el texto cuando es de sólo lectura. Esto también significa que tiene mucho más código que si esto tuviese implementado un sólo comportamiento, pero esto es necesario para asegurar una buena experiencia de usuario.

  • Los campos tienen 2 métodos, set_value()``y ``get_value()`, el cual es llamado por la vista formulario para dar el valor a desplegar y obtener el nuevo valor introducido por el usuario. Esos métodos deben ser capaces de manejar los valores como son dado por el servidor cuando el método ``read()``es usado en un modelo y devolver un valor válido para un ``write(). Recuerda que los tipos usados en Javascript/Python para representar los valores dados por el read()``y por el ``write()``no son necesariamente los mismos en Odoo. Por ejemplo, cuando lees un many2one, éste es siempre una dupla la cual el primer valor el el id del registro y el segundo valor es el nombre (por ejemplo ``(15, “Agrolait”). Pero cuando escribes un many2one éste debe tener un valor entero simple, no es una tupla. [UNKNOWN NODE title_reference] tiene una implementación de esos métodos que trabaja bien para tipos de daros simples y los carga en una propiedad llamada value

Nótese que, para entender mejor cómo implementar campos, encarecidamente es recomendable que leas en la definición de FieldInterface``y la clase ``AbstractField directamente en el código de Odoo Web.

Crear un nuevo tipo de campo.

En ésta parte se explicará como crear un nuevo tipo de campo. El ejemplo de acá será la reimplementación del campo FieldChar y explicar progresivamente cada parte.

Campos solo lectura Simples

Aquí está la primera implementación que solo desplegará el texto. El usuario no será capaz de modificar el contenido del campo.

local.FieldChar2 = instance.web.form.AbstractField.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.set("value", "");
    },
    render_value: function() {
        this.$el.text(this.get("value"));
    },
});

instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');

En éste ejemplo, declaramos una clase llamada FiledChar2 heredando de AbstractField. También agregamos ésta clase al registro instance.web.form.widgets con la llave char2. Esto permitirá que usemos éste nuevo campo en cualquier vista formulario especificando widget=“char2” dentro de una etiqueta <field/> en la declaración XML de la vista.

En este ejemplo, definimos solo un método: render_value(). Lo que hace es desplegar la propiedad value del widget. Esas son dos herramientas definidas por la clase AbstractField. Como se explicó antes, la vista formulario llamará al método set_value() del campo para establecer el valor a mostrar. AbstractField también vigila el evento change:value y llama a render_value() cuando éste ocurre. Entonces, es un método conveniente para implementar en las clases hijas y hacer operaciones cada vez que el campo cambie.

En el método init(), también definimos el valor predeterminado del campo si no hay algo especificado es especificado por la vista de formulario (suponemos que el valor predeterminado de un campo char debe ser una cadena vacía).

Campos de Lectura/Escritura

Campos de sólo lectura, que sólo muestran el contenido y no permiten al usuario modificarlo pueden ser útiles, pero la mayoría de campos de Odoo permiten también editar. Esto hace las clases de campos más complicadas, sobre todo porque los campos se supone que deben para manejar ambos tanto editables como no editable, éstos modos son a menudo completamente diferentes (para propósito de diseño y usabilidad) y los campos deben ser capaces de cambiar entre los modos en cualquier momento.

Para saber en qué modo está el campo actual, la clase de AbstractField establece una propiedad de widget llamada effective_readonly. El campo debe observar cambios en esa propiedad del widget y mostrar el modo correcto en consecuencia. Ejemplo:

local.FieldChar2 = instance.web.form.AbstractField.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.set("value", "");
    },
    start: function() {
        this.on("change:effective_readonly", this, function() {
            this.display_field();
            this.render_value();
        });
        this.display_field();
        return this._super();
    },
    display_field: function() {
        var self = this;
        this.$el.html(QWeb.render("FieldChar2", {widget: this}));
        if (! this.get("effective_readonly")) {
            this.$("input").change(function() {
                self.internal_set_value(self.$("input").val());
            });
        }
    },
    render_value: function() {
        if (this.get("effective_readonly")) {
            this.$el.text(this.get("value"));
        } else {
            this.$("input").val(this.get("value"));
        }
    },
});

instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
<t t-name="FieldChar2">
    <div class="oe_field_char2">
        <t t-if="! widget.get('effective_readonly')">
            <input type="text"></input>
        </t>
    </div>
</t>

En el método start() (el cual es llamado inmediatamente después de que un widget ha sido agregado al DOM), se enlaza el evento change:effective_readonly. Esto nos permite volver a mostrar el campo cada vez que la propiedad del widget effective_readonly``cambia. Éste manejado de evento llamará ``display_field(), el cual es llamado también directamente en start(). El display_field() fué creado específicamente para éste campo, no es un método definido en AbstractField o cualquier otra clase. Podemos usar éste método para desplegar el contenido del campo en el modo actual.

De ahora en adelante la concepción de este campo es típica, excepto que hay un montón de comprobaciones para saber el estado de la propiedad effective_readonly :

  • En la plantilla de QWeb usada para mostrar el contenido del widget, se muestra un <input type="text" /> si estamos en modo de lectura-escritura y nada en particular en modo de sólo lectura.

  • En el método display_field(), tenemos que enlazar el evento change a el elemento <input type=text/>``para saber cuando el usuario ha cambiado el valor. Cuando esto suceda, llamado a método ``internal_set_value() con el nuevo valor en el campo. Esto es un método conveniente provisto por la clase AbstractField. Dicho método colocará un nuevo valor en la propiedad value``pero no hará una llamada a ``render_value() (la cual no es necesaria ya que <input type=“text”/> contiene el valor correcto)

  • En render_value(), utilizamos un código totalmente diferente para mostrar el valor del campo en función de si estamos en sólo lectura o en modo de lectura y escritura.

Los Widgets personalizados en los formularios

Los campos de formulario se utilizan para editar un campo y están intrínsecamente ligados a un campo. Porque esto podría ser una limitante, también es posible crear form widgets que no están tan restringidos y tienen menos vínculos a un ciclo de vida específico.

Widgets de formulario personalizada se pueden agregar a una vista de formulario a través de la etiqueta de widget:

<widget type="xxx" />

Este tipo de widget simplemente creará la vista de formulario durante la creación del HTML según la definición de XML. Tienen propiedades en común con los campos (como la propiedad de effective_readonly) pero que no se les asigna un campo preciso. Y por lo que no tienen métodos como get_value() y set_value(). Debe heredar de la clase abstracta FormWidget.

Form widgets can interact with form fields by listening for their changes and fetching or altering their values. They can access form fields through their field_manager attribute:

local.WidgetMultiplication = instance.web.form.FormWidget.extend({
    start: function() {
        this._super();
        this.field_manager.on("field_changed:integer_a", this, this.display_result);
        this.field_manager.on("field_changed:integer_b", this, this.display_result);
        this.display_result();
    },
    display_result: function() {
        var result = this.field_manager.get_field_value("integer_a") *
                     this.field_manager.get_field_value("integer_b");
        this.$el.text("a*b = " + result);
    }
});

instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication');

FormWidget is generally the FormView() itself, but features used from it should be limited to those defined by FieldManagerMixin(), the most useful being:

  • get_field_value(field_name)() which returns the value of a field.
  • set_values(values)() sets multiple field values, takes a mapping of {field_name: value_to_set}
  • Un evento field_changed:field_name se dispara en cualquier momento que se cambia el valor del campo llamado field_name

[1]

como un concepto separado de las instancias. En muchos lenguajes las clases son objetos (metaclases) y a su vez tienes instancias de si mismos sin embargo siguen siendo dos jerarquías bastante separadas entre clases e instancias

[2]

como empapelar sobre las diferencias entre navegadores, aunque esto es menos necesario en el tiempo