É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.
Advertencia
Este manual supone los siguientes conocimientos previos:
Conceptos básicos de JavaScript y buenas prácticas
- jQuery
- Underscore.js
También requiere tener Odoo instalado y Git.
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.
Advertencia
Se concatenan todos los archivos JavaScript y son minificados (minified) para reducir el tiempo de carga de la aplicación.
Uno de los inconvenientes es que la depuración se hace más difícil cuando los archivos individuales desaparecen y por ende el código se hace significativamente menos legible. Es posible deshabilitar este proceso activando el "modo desarrollador": para ello inicie sesión en la instancia Odoo (usuario admin contraseña admin por defecto), abra el menú de usuario (en la esquina superior derecha de la pantalla de Odoo) y seleccione About, luego haz click en Activate the developer mode:


Esto volverá a cargar al cliente web con las optimizaciones deshabilitadas, y hará que el desarrollo y la depuración sea más cómodo.
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
Advertencia
_super
no es un método estándar, es creado sobre la marcha para el próximo método de la cadena actual de la herencia, si hay alguno. Sólo se define durante la parte síncrona de una llamada al método, para su uso en controladores asincrónicos (después de llamadas de red o en las devoluciones de llamada setTimeout
) una referencia a su valor será retenida, no debe accederse a través de this
:
// broken, will generate an error
say_hello: function () {
setTimeout(function () {
this._super();
}.bind(this), 0);
}
// correct
say_hello: function () {
// don't forget .bind()
var _super = this._super.bind(this);
setTimeout(function () {
_super();
}.bind(this), 0);
}
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
menú.Advertencia
ya que el widget se llama desde fuera de nuestro módulo, el cliente web necesita su nombre "fully qualified", no la versión local.
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
Nota
para actualizar el código javascript que se cargan en Odoo Web, solo necesitas volver a cargar la página. No hay que reiniciar el servidor de Odoo.
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 DOMHomePage
luego crea una instancia deGreetingsWidget
- Finally it tells
GreetingsWidget
where to insert itself, delegating part of its$el
to theGreetingsWidget
.
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()
.
Peligro
when overriding destroy()
, _super()
must always be called otherwise the widget and its children are not
correctly cleaned up leaving possible memory leaks and "phantom events",
even if no error is displayed
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 useWidget()
without relying on QWeb)
Nota
La razón de utilizar QWeb en vez de los motores de plantilla javascript es la extensibilidad de plantillas preexistentes (tercero), al igual que las views en Odoo.
La mayoría de los motores de plantilla javascript son basados en texto que impide la fácil extensibilidad estructural donde un motor de plantillas basado en XML puede ser genéricamente alterada usando por ejemplo usando XPath o CSS y alteración de árbol DSL (o incluso sólo XSLT). Esta flexibilidad y extensibilidad es una característica del núcleo de Odoo y perderlo era considerado inaceptable.
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
Advertencia
templates should have a single non-t
root element, especially if
they're set as a widget's template
. If there are
multiple "root elements", results are undefined (usually only the first
root element will be used and the others will be ignored)
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>
Peligro
t-raw
no debe ser utilizado en datos que pueda tener contenido no escapado proporcionado por el usuario porque esto conduce a vulnerabilidades cross-site scripting
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>
Nota
QWeb no tiene una sentencia “else”, utilice un segundo t-if`
con la condición original invertida. Puede que desee guardar la condición en una variable local si se trata de una expresión compleja o costosa.
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>
Nota
t-foreach
puede utilizarse con números y objetos (diccionarios)
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
Exercise
Uso de QWeb en Widgets
Crear un widget cuyo constructor toma dos parámetros aparte de padre
: product_names
y color
.
product_names
debe ser un arreglo de cadenas, cada una el nombre de un productocolor
es una cadena que contiene un color en CSS color formato (es decir:#000000
para el negro).
El widget debe mostrar los nombres de producto uno bajo el otro, cada uno en una caja aparte con un color de fondo con el valor de color
y una frontera. Debe utilizar QWeb para representar el HTML. Cualquier CSS necesario debe estar en oepetstore/static/src/css/petstore.css`
.
Utilice el widget en HomePage
con media docena productos.
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() {
var products = new local.ProductsWidget(
this, ["cpu", "mouse", "keyboard", "graphic card", "screen"], "#00FF00");
products.appendTo(this.$el);
},
});
local.ProductsWidget = instance.Widget.extend({
template: "ProductsWidget",
init: function(parent, products, color) {
this._super(parent);
this.products = products;
this.color = color;
},
});
instance.web.client_actions.add(
'petstore.homepage', 'instance.oepetstore.HomePage');
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="ProductsWidget">
<div>
<t t-foreach="widget.products" t-as="product">
<span class="oe_products_item"
t-attf-style="background-color: {{ widget.color }};">
<t t-esc="product"/>
</span>
<br/>
</t>
</div>
</t>
</templates>
.oe_products_item {
display: inline-block;
padding: 3px;
margin: 5px;
border: 1px solid black;
border-radius: 3px;
}

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")...
},
});
Advertencia
La función de jQuery global $()
nunca debe ser usada a menos que sea absolutamente necesario: la selección en la raíz de un widget son contextualizadas al widget, pero las selecciones con $()
son globales a la página/aplicación y pueden coincidir con piezas de otros widgets y vistas, conduciendo a efectos secundarios peligrosos o no esperados. Desde un widget generalmente debes actuar solamente en la sección del DOM, no debería haber razón alguna para una selección global.
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:
es más bien detallado
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)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, peroclick .my_button
ejecutará sólo el clic en elementos que tengan la clasemy_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"
}
Nota
Triggering events on an other widget is generally a bad idea. The main
exception to that rule is odoo.web.bus
which exists specifically
to broadcasts evens in which any widget could be interested throughout
the Odoo web application.
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 triggerschange:propname
(where propname is the property name passed as first parameter toset()
) andchange
get()
retrieves the value of a property.
Ejercicio
Exercise
Eventos y propiedades del widget
Crea un widget ColorInputWidget
que desplegará 3 <input type="text">
. Cada uno de dichos <input>
es dedicado a un número hexadecimal de 00 a FF. Cuando cualquiera de esos <input>``es modificado por el usuario el widget de consultar el contenido de esos tres ``<input>
concatenar sus valores y lograr un código CSS completo (por ejemplo #00FF00
) y colocar el resultado en la propiedad llamada color
. Considere que el evento jQuery change()
puedes enlazarlo a cualquier elemento HTML input()
y el método val()
puede consultar el valor actual de dichos <input>
, esto será útil para el ejercicio.
A continuación, modificar el widget HomePage
para crear una instancia de ColorInputWidget
y mostrarla. El widget HomePage
también debe mostrar un rectángulo vacío. Ese rectángulo debe siempre, en cualquier momento tener el mismo color de fondo que el color en la propiedad de ColorInputWidget
de la instancia de ColorInputWidget
.
Utilizar QWeb para generar todos los HTML.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.ColorInputWidget = instance.Widget.extend({
template: "ColorInputWidget",
events: {
'change input': 'input_changed'
},
start: function() {
this.input_changed();
return this._super();
},
input_changed: function() {
var color = [
"#",
this.$(".oe_color_red").val(),
this.$(".oe_color_green").val(),
this.$(".oe_color_blue").val()
].join('');
this.set("color", color);
},
});
local.HomePage = instance.Widget.extend({
template: "HomePage",
start: function() {
this.colorInput = new local.ColorInputWidget(this);
this.colorInput.on("change:color", this, this.color_changed);
return this.colorInput.appendTo(this.$el);
},
color_changed: function() {
this.$(".oe_color_div").css("background-color", this.colorInput.get("color"));
},
});
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="ColorInputWidget">
<div>
Red: <input type="text" class="oe_color_red" value="00"></input><br />
Green: <input type="text" class="oe_color_green" value="00"></input><br />
Blue: <input type="text" class="oe_color_blue" value="00"></input><br />
</div>
</t>
<t t-name="HomePage">
<div>
<div class="oe_color_div"></div>
</div>
</t>
</templates>
.oe_color_div {
width: 100px;
height: 100px;
margin: 10px;
}
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.
Nota
Para inyectar valores proporcionado por el usuario en cadenas traducibles, es aconsejable utilizar _.str.sprintf con los argumentos después de la traducción:
this.$el.text(_.str.sprintf(
_t("Hello, %(user)s!"), {
user: "Ed"
}));
Esto hace más legible las cadenas traducibles a los traductores y le da más flexibilidad para ordenar u omitir parámetros.
_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 aodoo.web.Query()
which can be further customized before being executedQuery()
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. SeeQuery()
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
Exercise
Mensaje del día
Crear un widget MessageOfTheDay
que muestre el último registro del modelo oepetstore.message_of_the_day
. El widget debe obtener su registro tan pronto como se muestra.
Mostrar el widget en la página de la tienda de mascotas.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.HomePage = instance.Widget.extend({
template: "HomePage",
start: function() {
return new local.MessageOfTheDay(this).appendTo(this.$el);
},
});
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
local.MessageOfTheDay = instance.Widget.extend({
template: "MessageOfTheDay",
start: function() {
var self = this;
return new instance.web.Model("oepetstore.message_of_the_day")
.query(["message"])
.order_by('-create_date', '-id')
.first()
.then(function(result) {
self.$(".oe_mywidget_message_of_the_day").text(result.message);
});
},
});
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="HomePage">
<div class="oe_petstore_homepage">
</div>
</t>
<t t-name="MessageOfTheDay">
<div class="oe_petstore_motd">
<p class="oe_mywidget_message_of_the_day"></p>
</div>
</t>
</templates>
.oe_petstore_motd {
margin: 5px;
padding: 5px;
border-radius: 3px;
background-color: #F0EEEE;
}
Exercise
Lista de juguetes para mascotas
Crear un widget PetToysList
mostrando 5 juguetes (usando su nombre y sus imágenes).
Los juguetes para mascotas no se almacenan en un nuevo modelo, en su lugar se almacenan en la product.product
con una categoría especial Pet Toys. Podrás ver los juguetes previamente generados y añadir nuevos yendo a . Usted probablemente necesitará explorar product.product
para crear el dominio adecuado para seleccionar juguetes correctos.
En Odoo, las imágenes se almacenan generalmente en campos regulares codificados como base64, HTML soporta mostrar imágenes directamente base64 con <img src="data:mime_type;base64,base64_image_data"/>
El widget de PetToysList
debe ser exhibido en la página a la derecha del widget MessageOfTheDay
. Usted necesitará hacer algún diseño con CSS para lograrlo.
odoo.oepetstore = function(instance, local) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
local.HomePage = instance.Widget.extend({
template: "HomePage",
start: function () {
return $.when(
new local.PetToysList(this).appendTo(this.$('.oe_petstore_homepage_left')),
new local.MessageOfTheDay(this).appendTo(this.$('.oe_petstore_homepage_right'))
);
}
});
instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');
local.MessageOfTheDay = instance.Widget.extend({
template: 'MessageOfTheDay',
start: function () {
var self = this;
return new instance.web.Model('oepetstore.message_of_the_day')
.query(["message"])
.order_by('-create_date', '-id')
.first()
.then(function (result) {
self.$(".oe_mywidget_message_of_the_day").text(result.message);
});
}
});
local.PetToysList = instance.Widget.extend({
template: 'PetToysList',
start: function () {
var self = this;
return new instance.web.Model('product.product')
.query(['name', 'image'])
.filter([['categ_id.name', '=', "Pet Toys"]])
.limit(5)
.all()
.then(function (results) {
_(results).each(function (item) {
self.$el.append(QWeb.render('PetToy', {item: item}));
});
});
}
});
}
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="HomePage">
<div class="oe_petstore_homepage">
<div class="oe_petstore_homepage_left"></div>
<div class="oe_petstore_homepage_right"></div>
</div>
</t>
<t t-name="MessageOfTheDay">
<div class="oe_petstore_motd">
<p class="oe_mywidget_message_of_the_day"></p>
</div>
</t>
<t t-name="PetToysList">
<div class="oe_petstore_pettoyslist">
</div>
</t>
<t t-name="PetToy">
<div class="oe_petstore_pettoy">
<p><t t-esc="item.name"/></p>
<p><img t-att-src="'data:image/jpg;base64,'+item.image"/></p>
</div>
</t>
</templates>
.oe_petstore_homepage {
display: table;
}
.oe_petstore_homepage_left {
display: table-cell;
width : 300px;
}
.oe_petstore_homepage_right {
display: table-cell;
width : 300px;
}
.oe_petstore_motd {
margin: 5px;
padding: 5px;
border-radius: 3px;
background-color: #F0EEEE;
}
.oe_petstore_pettoyslist {
padding: 5px;
}
.oe_petstore_pettoy {
margin: 5px;
padding: 5px;
border-radius: 3px;
background-color: #F0EEEE;
}
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, ofalse
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 elcontent
de la sección del cliente web por la acción, onew
para abrir la acción en un cuadro de diálogo.context
Datos de contexto adicional para utilizar dentro de la acción.
Exercise
Ir al producto
Modificar el componente PetToysList
para hacer clic en un juguete y que reemplace la página principal por vista de formulario de juguete.
local.PetToysList = instance.Widget.extend({
template: 'PetToysList',
events: {
'click .oe_petstore_pettoy': 'selected_item',
},
start: function () {
var self = this;
return new instance.web.Model('product.product')
.query(['name', 'image'])
.filter([['categ_id.name', '=', "Pet Toys"]])
.limit(5)
.all()
.then(function (results) {
_(results).each(function (item) {
self.$el.append(QWeb.render('PetToy', {item: item}));
});
});
},
selected_item: function (event) {
this.do_action({
type: 'ir.actions.act_window',
res_model: 'product.product',
res_id: $(event.currentTarget).data('id'),
views: [[false, 'form']],
});
},
});
<t t-name="PetToy">
<div class="oe_petstore_pettoy" t-att-data-id="item.id">
<p><t t-esc="item.name"/></p>
<p><img t-attf-src="data:image/jpg;base64,{{item.image}}"/></p>
</div>
</t>
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.
Nota
un controlador de acción de cliente también puede ser una función regular, en dado caso será llamado y su resultado (si existe) se interpretará como la próxima acción a ejecutar.
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"/>
Nota
el mismo widget se utiliza en "view" (sólo lectura) y modo "edition" de una vista de formulario, no es posible utilizar un widget en uno y otro widget en el otro
y un campo dado no puede ser utilizado varias veces en el mismo formulario
un widget puede ignorar el modo de la vista formulario y continuar siendo los mismos en vista y edición
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. Sirequired
estrue
y el campo no tiene un valor, el métodois_valid()
del campo debe devolverfalse
.invisible
: cuando éste estrue
, el campo debe ser invisible. La claseAbstractField
ya tiene una implementación básica de este comportamiento que se adapta a la mayoría de los campos.readonly
: Cuando estrue
, 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 dereadonly
. Como ejemplo, elFieldChar
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 elread()``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 llamadavalue
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 eventochange
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 claseAbstractField
. Dicho método colocará un nuevo valor en la propiedadvalue``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.
Exercise
Crear un campo de Color
Crear una clase de FieldColor
. El valor de este campo debe ser una cadena que contiene un código de color como los que se usan en CSS (ejemplo: ‘’ #FF0000’’ para el rojo). En modo de sólo lectura, este campo debe mostrar un bloque pequeño cuyo color corresponde al valor del campo. En modo de lectura y escritura, se debe mostrar un <input type=“color”>
. Ese tipo de <input>
es un componente de HTML5 que no funciona en todos los navegadores pero funciona bien en Google Chrome. Así que está bien para usar como un ejercicio.
Puede usar este widget en la vista Formulario del modelo message_of_the_day
para su campo denominado color
. Como un bono, puede cambiar el widget de MessageOfTheDay
en la parte anterior de esta guía para mostrar el mensaje de la jornada con el color de fondo indicado en el campo de color
.
local.FieldColor = instance.web.form.AbstractField.extend({
events: {
'change input': function (e) {
if (!this.get('effective_readonly')) {
this.internal_set_value($(e.currentTarget).val());
}
}
},
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() {
this.$el.html(QWeb.render("FieldColor", {widget: this}));
},
render_value: function() {
if (this.get("effective_readonly")) {
this.$(".oe_field_color_content").css("background-color", this.get("value") || "#FFFFFF");
} else {
this.$("input").val(this.get("value") || "#FFFFFF");
}
},
});
instance.web.form.widgets.add('color', 'instance.oepetstore.FieldColor');
<t t-name="FieldColor">
<div class="oe_field_color">
<t t-if="widget.get('effective_readonly')">
<div class="oe_field_color_content" />
</t>
<t t-if="! widget.get('effective_readonly')">
<input type="color"></input>
</t>
</div>
</t>
.oe_field_color_content {
height: 20px;
width: 50px;
border: 1px solid black;
}
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 llamadofield_name
Exercise
Mostrar coordenadas en Google Maps
Agregue dos campos a product.product
grabando una latitud y una longitud, luego crear un nuevo widget de formulario para mostrar la latitud y longitud de origen de un producto en el mapa
Para mostrar el mapa, utilice Incrustar Google Map:
<iframe width="400" height="300" src="https://maps.google.com/?ie=UTF8&ll=XXX,YYY&output=embed">
</iframe>
donde XXX
debe reemplazarse por la latitud y la YYY
por la longitud.
Mostrar los campos de dos posiciones y un widget de mapa utilizando en una nueva pestaña del notebook en la vista de formulario del producto.
local.WidgetCoordinates = instance.web.form.FormWidget.extend({
start: function() {
this._super();
this.field_manager.on("field_changed:provider_latitude", this, this.display_map);
this.field_manager.on("field_changed:provider_longitude", this, this.display_map);
this.display_map();
},
display_map: function() {
this.$el.html(QWeb.render("WidgetCoordinates", {
"latitude": this.field_manager.get_field_value("provider_latitude") || 0,
"longitude": this.field_manager.get_field_value("provider_longitude") || 0,
}));
}
});
instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates');
<t t-name="WidgetCoordinates">
<iframe width="400" height="300"
t-attf-src="https://maps.google.com/?ie=UTF8&ll={{latitude}},{{longitude}}&output=embed">
</iframe>
</t>
Exercise
Actualizar a la coordenada actual
Agregar un botón Restablecer coordenadas del producto a la ubicación del usuario, usted puede conseguir estas coordenadas usando el javascript geolocation API.
Ahora nos gustaría mostrar un botón adicional para configurar automáticamente las coordenadas para la ubicación del usuario actual.
Para obtener las coordenadas del usuario, una manera fácil es utilizar la API de geolocalización de JavaScript. Ver la documentación online para entender cómo usarla.
Tenga en cuenta que el usuario no debe poder hacer clic en ese botón cuando la vista de formulario está en modo de sólo lectura. Por lo tanto, este widget personalizado debe manejar correctamente la propiedad effective_readonly
como cualquier campo. Una manera de hacer esto sería hacer el botón desaparece cuando effective_readonly
es [UNKNOWN NODE title_reference].
local.WidgetCoordinates = instance.web.form.FormWidget.extend({
events: {
'click button': function () {
navigator.geolocation.getCurrentPosition(
this.proxy('received_position'));
}
},
start: function() {
var sup = this._super();
this.field_manager.on("field_changed:provider_latitude", this, this.display_map);
this.field_manager.on("field_changed:provider_longitude", this, this.display_map);
this.on("change:effective_readonly", this, this.display_map);
this.display_map();
return sup;
},
display_map: function() {
this.$el.html(QWeb.render("WidgetCoordinates", {
"latitude": this.field_manager.get_field_value("provider_latitude") || 0,
"longitude": this.field_manager.get_field_value("provider_longitude") || 0,
}));
this.$("button").toggle(! this.get("effective_readonly"));
},
received_position: function(obj) {
this.field_manager.set_values({
"provider_latitude": obj.coords.latitude,
"provider_longitude": obj.coords.longitude,
});
},
});
instance.web.form.custom_widgets.add('coordinates', 'instance.oepetstore.WidgetCoordinates');
<t t-name="WidgetCoordinates">
<iframe width="400" height="300"
t-attf-src="https://maps.google.com/?ie=UTF8&ll={{latitude}},{{longitude}}&output=embed">
</iframe>
<button>Get My Current Coordinate</button>
</t>
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
como empapelar sobre las diferencias entre navegadores, aunque esto es menos necesario en el tiempo