Você está na página 1de 215

capitulo--1.

md 07/02/2018

1 # odoo essentials
2
3 ## Capítulo 1. Iniciando con desarrollo Odoo
4
5 Antes de sumergirnos en el desarrollo en Odoo, necesitamos armar
nuestro ambiente de desarrollo y aprender las tareas de administraciión
básicas para ello.
6
7 Oddo se construye utilizando el lenguaje de programación Phyton, y
utiliza la base de datos PostgreSQL para almacenamiento de datos; estos
son los dos principales requerimientos de un huesped Odoo. Para correr
Odoo desde la fuente, necesitaremos primero instalar las librerias de
Phyton de as cuales depende. El código de fuente de Odoo puede
posteriormente ser descargado desde GitHub. Mientras podemos descargar
un archivo ZIP o tarball, veremos que es mejor si obtenemos las fuentes
utilizando la versión Git de aplicación de control.; Nos ayudará a
tenerlo instalado en nuestro huesped Odoo también.
8
9 ## Creando un huesped para el servidor Odoo
10
11 Se recomienda para el sistema Debian/Ubuntu para el servidor Odoo. Aún
serás capaz de trabajar desde tu sistema de escritorio favorito, bien
sea Windows, Mac, o Linux.
12
13 Odoo puede correr con gran variedad de sistemas operativos, entonces,
por qué escoger Debian a expensas de otros sistemas operativos?: Porque
Debian es considerado la plataforma de despliegue de referencia por el
equipo Odoo; tiene el mejor soporte. Será más facil hallar ayuda y
recursos adicionales si trabajamos con Debian/Ubuntu.
14
15 También es la plataforma con la que la mayoría de desarrolladores
trabajan y donde se implementan más despliegues. Así que
inevitablemente, se espera que los desarrolladores de Odoo se sientan
cómodos con la plataforma Debian/Ubuntu. Incluso si tienes un pasado
Windows, será importante que tengas algún conocimiento acerca de esto.
16
17 En este capítulo, aprenderás cómo armar y trabajar con Odoo hospedado
en un sistema Debian, usando sólo la linea de comando. Para aquellos en
casa con un sistema Windows, cubriremos cómo configurar una máquina
virtual para hospedar el servidor Odoo. Como ventaja, las técnicas que
aprenderás aquí también te permitirán administrar Odoo en servidores en
la nube, donde tu único acceso será a través de Secure Shel ( SSH).
18
19 ### Nota
20
21 Manten en mente que estas instrucciones están destinadas a organizar un
nuevo sistemas para desarrollo. Si deseas probar algunos de ellos en un
sistema existente, siempre toma una copia de seguridad antes de tiempo
para poder restaurarlo en caso de que algo salga mal.
22
23 ## Provisión para un huesped Debian
24
25 Como se explicó antes, necesitaremos un huesped basado en Debian para
nuestro servidor Odoo. Si estos son tus primeros pasos con Linux, puede
gustarte notar que Ubuntu es una distribución Linux basada en Debian,
así que son muy similares.
26
27 Está garantizado que Odoo trabaje con la versión actual estable de
Debian o Ubuntu. Al tiempo de escribir, estos son Debian 8 "Jessie" y
Ubuntu 16.04.1 LTS (Xenial Xerus). Ambos, vienen con Python 2.7, el
cual es necesario para correr Odoo. Es importante señalar que Odoo no
Page 1
capitulo--1.md 07/02/2018

soporta Python 3 aún, así que Python 2 es requerido.


28
29 Si ya estás ejecutando Ubuntu o alguna otra distribución con basada en
Debian, ¡estas listo!; Esto también puede ser utilizado como un huesped
para Odoo.
30
31 Para los sitemas operativos Windows y Mac, instala Python, PostgreSQL,
y todas las dependencias ; luego, ejecuta Odoo desde la fuente nativa.
Sin embargo, esto puede ser un reto, así que nuestro consejo es que
uses una máquina virtual que ejecute un servidor Debian o Ubuntu. Eres
bienvenido a utilizar tu software de virtualización preferido para
obtener un sistema de trabajo Debian en una máquina virtual.
32
33 En caso de que necesites alguna guía, aquí hay unos consejos con
respecto al software de visualización. Existen varias opciones, tales
como Microsoft Hyper-V (disponíble en algunas versiones de Windows
recientes), Oracle VirtualBox y VMWare Workstation Player (VMWare
Fusion para MAC). La VMWare Workstation Player es probablemente más
fácil de utilizar y descargas fáciles de usar pueden ser halladas en
https://my.vmware.com/wev/vmware/downloads.
34
35 Con respecto a la imagen de Linux a utilizar, será más amigable para el
usuario instalar Ubuntu Server que Debian. Si estás empezando con
Linux, te recomiendo que pruebes una imagen lista para usar. TurnKey
Linux proporciona imágenes preinstaladas fáciles de usar en varios
formatos, incluyendo ISO. El formato ISO funcionará con cualquier
programa de visualización que elijas, incluso en una máquina de metal
desnudo que puedas tener. Una buena opción podría ser la imagen LAPP,
que incluye Python y PostgreSQL, y se puede encontrar en
http://www.turnkeylinux.org/lapp.
36
37 Una vez instalada y arrancada, deberías ser capaz de iniciar sesión en
una línea de comando shell.
38
39
40 ## Creando una cuenta de usuario para Oddo
41
42 Si has iniciado sesión usando la cuenta de super usuario `root`, tu
primera tarea debe ser crear una cuenta de usuario normal para tu
trabajo, ya que se considera una mala práctica trabajar como `root`. En
particular, el operador de Odoo se rehusará a correr si lo inicias como
`root`.
43
44 Si has inicaido sesión usando Ubuntu, probablemente no necesitaras
esto, ya que el proceso de instalación debe haberte guiado para la
creación de un usuario.
45
46 Primero, asegurate de que `sudo` esté instalado. Nuestro usuario de
trabajo lo necesitará. Si se inició sesión como `root`, ejecuta los
siguientes comandos:
47
48 ```
49 # apt-get update && apt-get upgrade # Install system updates
50 # apt-get install sudo # Make sure 'sudo' is installed
51
52 ```
53
54
55 El siguiente set de comandos creará un usuario `odoo`:
56
57 ```
Page 2
capitulo--1.md 07/02/2018

58 # useradd -m -g sudo -s /bin/bash odoo # Create an 'odoo' user with


sudo powers
59
60 # passwd odoo # Ask and set a password for the new user
61
62 ```
63 Puedes cambiar el nombre de usuario `odoo` al que tu quieras. La
opción `-m`asegura que su directorio de inicio sea creado. La opción
`-g sudo`
64
65
66 Ahora podemos iniciar sesión como el nuevo usuario y organizar Odoo.
67
68
69 ## Instalando Odoo desde la fuente
70
71 Los paquetes de instalación rápida de Odoo, pueden hallarse en
nigthly.odoo.com, disponíble como Windows `(.exe)`, Debian (`.deb`),
CentOS (`.rpm`), y código fuente en formato tarballs (`. tar .gz`).
72
73 Como desarrolladores, preferiremos instalarlos directamente del
repositorio GitHub. Esto terminará dándonos más control sobre versiones
y actualizaciones.
74
75 Para mantener las cosas ordenadas, vamos a trabajar en un directorio
`/odoo-dev` dentro de nuestro directorio `home`.
76
77 ### Nota
78
79 A lo largo del libro, asumiremos que `/odoo-dev` es el directorio donde
tu servidor de Odoo está instalado.
80
81 Primero, asegúrate de haber iniciado sesión como el usuario creado
ahora o durante el proceso de instalación, no como el usuario `root`.
Asumiendo que tu usuario es `odoo`, confírmalo con el siguiente comando:
82
83 ```
84 odoo
85
86 $ echo $HOME
87
88 /home/odoo
89 $ whoami
90
91 odoo
92
93 $ echo $HOME
94
95 /home/odoo
96 ```
97
98 Ahora podemos utilizar este script. Nos muestra cómo instalar Odoo
desde la fuente a un sistema Debian/Ubuntu.
99
100 Primero, intala las dependencias básicas para comenzar:
101
102 ```
103 $ sudo apt-get update && sudo apt-get upgrade #Install system updates
104
105 $ sudo apt-get install git # Install Git
106
Page 3
capitulo--1.md 07/02/2018

107 $ sudo apt-get install npm # Install NodeJs and its package manager
108
109 $ sudo ln -s /usr/bin/nodejs /usr/bin/node # call node runs nodejs
110
111 $ sudo npm install -g less less-plugin-clean-css #Install less compiler
112
113 ```
114
115 Partiendo de la versión 9.0, el cliente web de Odoo requiere que el
preprocesador `less` CSS esté instalado en el sistema para que las
páginas web puedan ser renderizadas correctamente. Para instalar esto,
necesitamos Node.js y npm.
116
117 Luego, necesitamos obtener el código fuente Odoo e instalar sus
dependencias. El código fuente Odoo incluye un script de utilidades,
dentro del directorio `odoo/setup/`, para ayudarnos a instalar las
dependencias requeridas en un sistema Debian/Ubuntu:
118
119 ```
120 $ mkdir ~/odoo-dev # Create a directory to work in
121
122 $ cd ~/odoo-dev # Go into our work directory
123
124 $ git clone https://github.com/odoo/odoo.git -b 10.0 --depth=1 # Get
Odoo source code
125
126 $ ./odoo/setup/setup_dev.py setup_deps # Installs Odoo system
dependencies
127
128 $ ./odoo/setup/setup_dev.py setup_pg # Installs PostgreSQL & db
superuser for unix user
129 ```
130
131 Al final, Odoo debería estar listo para utilizarse. El símbolo ~ es n
atajo para nuestro directorio `home` (por ejemplo, `/home/odoo`).
132 La opción `git -b 10.0` indíca a Git que descargue específicamente la
rama 10.0 de Odoo. Al tiempo de la escritura, esto es redundante ya que
10.0 es la rama por defecto; sin embargo, esto puede cambiar, entonces,
puede hacer el script a prueba del futuro. La opción `--depth=1` indica
a Git que descargue sólo la última revisión, en vez del último
historial de cambio completo, haciendo la descarga más pequeña y más
veloz.
133
134 Para iniciar un servidor Odoo, solo ejecuta:
135
136 ```
137 $ ~/odoo-dev/odoo/odoo-bin
138 ```
139
140 ### Tip
141
142 En Odoo 10, el script `odoo.py`, utilizado en versiones previas para
iniciar el servidor, fue reemplazado con `odoo-bin`.
143
144 De forma predeterminada, las instancias Odoo escuchan en el puerto
8069, por lo que si apuntamos un navegador a `http: //
<dirección-servidor>: 8069`, llegaremos a estas instancias. Cuando lo
accedemos por primera vez, nos muestra un asistente para crear una
nueva base de datos, como se muestra en la siguiente captura de pantalla:
145
146 ![Database](file:img/1-01.jpg)
Page 4
capitulo--1.md 07/02/2018

147
148 Como desarrolladores, necesitaremos trabajar con varias bases de datos,
así que es más convenientes más conveniente crearlos desde la línea de
comandos, así que aprenderemos cómo hacerlo. Ahora presione ***Ctrl +
C*** en el terminal para detener el servidor Odoo y volver al prompt de
comando.
149
150 ## Inicializando una nueva database Odoo
151
152 Para ser capaces de crear una nueva database, tu usuario debe ser un
super usuario PostgreSQL. El sigiente comando crea un superusuario
PostgreSQL para el usuario actual Unix.
153
154 ```
155 $ sudo createuser --superuser $(whoami)
156 ```
157 Para crear una nueva database, usa el comando `createdb`. Creeamos una
database `demo`:
158
159 ```
160 $ createdb demo
161 ```
162
163 Para inicializar esta database con el esquema de datos Odoo, debemos
ejecutar Odoo en la database vacía, usando la opción `-d`:
164
165 ```
166 $ ~/odoo-dev/odoo/odoo-bin -d demo
167 ```
168
169 Esto tomará un par de minutos para inicializar una database `demo`, y
terminará con un mensaje de registro INFO, **Módulos cargados**.
170
171 ### Nota
172 Ten en cuenta que puede no ser el último mensaje de registro, y puede
estar en las últimas tres o cuatro líneas. Con esto, el servidor estará
listo para escuchar las peticiones del cliente.
173
174 De forma predeterminada, esto inicializará la database con datos de
demostración, que a menudo es útil para las databases de desarrollo.
Para inicializar una database sin datos de demostración, agregue la
opción `--without-demo-data = all` al comando.
175
176 Ahora abre `http: // <server-name>: 8069` con tu navegador para que se
presente la pantalla de inicio de sesión. Si no conoces el nombre del
servidor, escribe el comando `hostname` en el terminal para encontrarlo
o el comando `ifconfig` para encontrar la dirección IP.
177
178 Si estás hospedando Odoo en una máquina virtual, es posible que debas
establecer algunas configuraciones de red para poder acceder desde tu
sistema huesped. La solución más simple es cambiar el tipo de red de la
máquina virtual de NAT a Bridged. Con esto, en lugar de compartir la
dirección IP del huesped, la máquina virtual invitada tendrá su propia
dirección IP. También es posible utilizar NAT, pero eso requiere que
configures el reenvío de puertos para que su sistema sepa que algunos
puertos, como `8069`, deben ser manejados por la máquina virtual. En
caso de que tengas problemas, esperamos que estos detalles te ayuden a
encontrar información relevante en la documentación del software de
virtualización elegido.
179
180 La cuenta de administrador predeterminada es `admin` con su contraseña
Page 5
capitulo--1.md 07/02/2018

`admin`. Al iniciar sesión, se le presenta el menú **Apps**, que


muestra las aplicaciones disponibles:
181
182 ![Database](file:img/1-02.jpg)
183
184 Siempre que desee detener la instancia del servidor Odoo y volver a la
línea de comandos, presione ***Ctrl + C*** en el indicador de bash. Al
presionar la tecla de flecha hacia arriba le llevará el comando de
shell anterior, por lo que es una forma rápida de iniciar Odoo de nuevo
con las mismas opciones. Las teclas ***Ctrl + C*** seguido por la tecla
de flecha hacia arriba y ***Enter*** son una combinación utilizada con
frecuencia para reiniciar el servidor Odoo durante el desarrollo.
185
186 ## Administrar sus bases de datos
187
188 Hemos visto cómo crear e inicializar nuevas bases de datos Odoo desde
la línea de comandos. Hay más comandos que vale la pena saber para
administrar las bases de datos.
189
190 Ya sabemos cómo usar el comando `createdb` para crear bases de datos
vacías, pero también podemos crear una nueva base de datos copiando una
existente, usando la opción `--template`.
191
192 Asegúrate de que tu instancia de Odoo está detenida y no tiene ninguna
otra conexión abierta en la base de datos `demo` que acabamos de crear
y, a continuación, ejecuta esto:
193
194 ```
195 $ Createdb --template = demo demo-test
196 ```
197 De hecho, cada vez que creamos una base de datos, se utiliza una
plantilla. Si no se especifica ninguna, se utiliza una predeterminada
llamada `template1`.
198
199 Para enumerar las bases de datos existentes en su sistema, utiliza la
utilidad `psq`l de PostgreSQL con la opción `-l`:
200
201 ```
202 $ Psql -l
203 ```
204
205 Al ejecutarlo se listarán las dos bases de datos que hemos creado hasta
ahora: `demo` y `demo-test`. La lista también mostrará la codificación
utilizada en cada base de datos. El valor predeterminado es UTF-8, que
es la codificación necesaria para las bases de datos Odoo.
206
207 Para eliminar una base de datos que ya no necesitas (o quieres crear
nuevamente) para utilizar el comando `dropdb`:
208
209 ```
210 $ Dropdb demo-test
211 ```
212
213 Ahora ya sabes lo básico para trabajar con bases de datos. Para obtener
más información sobre PostgreSQL, consulta la documentación oficial en
http://www.postgresql.org/docs/.
214
215 ### Nota
216
217 **ADVERTENCIA:**
218
Page 6
capitulo--1.md 07/02/2018

219 El comando drop de la base de datos destruirá irrevocablemente tus


datos. Ten cuidado al usarlo y manten siempre copias de seguridad de
bases de datos importantes antes de usar este comando.
220
221 ## Una palabra sobre las versiones de productos Odoo
222
223 Al momento de la redacción de este texto, la última versión estable de
Odoo es la versión 10, marcada en GitHub como rama 10.0. Esta es la
versión con la que trabajaremos a lo largo del libro.
224
225 ### Nota
226 Es importante notar que las bases de datos de Odoo son incompatibles
entre las versiones principales de Odoo. Esto significa que si ejecutas
un servidor Odoo 10 contra una base de datos creada para una versión
principal anterior de Odoo, no funcionará.
227
228 El trabajo de migración no trivial es necesario antes de que una base
de datos pueda ser usada con una versión más reciente del producto.
229
230 Lo mismo ocurre con los módulos addon: como regla general, un módulo
addon desarrollado para una versión Odoo major no funcionará con otras
versiones. Cuando descargue un módulo de la comunidad desde la Web,
asegúrese de que esté orientado a la versión Odoo que está utilizando.
231
232 Por otra parte, se espera que las versiones principales (9.0, 10.0)
reciban actualizaciones frecuentes, pero éstas deben ser en su mayoría
correcciones de errores.
233 Se asegura que son "API estable", lo que significa que las estructuras
de datos del modelo y los identificadores de elementos de vista se
mantendrán estables. Esto es importante porque significa que no habrá
ningún riesgo de ruptura de módulos personalizados debido a cambios
incompatibles en los módulos de núcleo ascendentes.
234
235 Tenga en cuenta que la versión en la rama `master` resultará en la
siguiente versión estable principal, pero hasta entonces, no es "API
estable" y no debes utilizarla para crear módulos personalizados.
Hacerlo es como moverse en arena movediza: no puedes estar seguro de
cuándo se introducirán algunos cambios que romperán tu módulo
personalizado.
236
237 ## Más opciones de configuración del servidor
238
239 El servidor Odoo soporta bastantes otras opciones. Podemos comprobar
todas las opciones disponibles con Más opciones de configuración del
servidor
240 El servidor Odoo soporta bastantes otras opciones. Podemos comprobar
todas las opciones disponibles con Más opciones de configuración del
servidor
241 El servidor Odoo soporta bastantes otras opciones. Podemos comprobar
todas las opciones disponibles con `--help`:
242
243 ```
244 $ ./odoo-bin --help
245 ```
246
247 Revisaremos algunas de las opciones más importantes en las siguientes
secciones. Comencemos por ver cómo se pueden guardar las opciones
actualmente activas en un archivo de configuración.
248
249 ### Archivos de configuración del servidor Odoo
250
Page 7
capitulo--1.md 07/02/2018

251 La mayoría de las opciones se pueden guardar en un archivo de


configuración. De forma predeterminada, Odoo utilizará el archivo
`.odoorc` en su directorio personal. En sistemas Linux su ubicación
predeterminada está en el directorio de inicio (`$ HOME`) y en la
distribución de Windows está en el mismo directorio que el ejecutable
utilizado para iniciar Odoo.
252
253 ## Nota
254
255 En versiones anteriores de Odoo / OpenERP, el nombre del archivo de
configuración predeterminado era `.openerp-serverrc`. Para
compatibilidad con versiones anteriores, Odoo 10 seguirá utilizando
esto si está presente y no se encuentra ningún archivo `.odoorc`.
256
257 En una instalación limpia, el archivo de configuración `.odoorc` no se
crea automáticamente. Debemos usar la opción `--save` para crear el
archivo de configuración predeterminado, si aún no existe, y almacenar
la configuración actual de la instancia en el:
258
259 ```
260 $ ~ / Odoo-dev / odoo / odoo-bin --save --stop-after-init #servir
configuración al archivo
261 ```
262
263 Aquí, también usamos la opción `--stop-after-init` para detener el
servidor después de que termine sus acciones. Esta opción se utiliza
con frecuencia cuando se ejecutan pruebas o se solicita ejecutar una
actualización de módulo para comprobar si está instalada correctamente.
264
265 Ahora podemos inspeccionar lo que se guardó en este archivo de
configuración predeterminado:
266
267 ```
268 $ More ~ / .odoorc # show the configuration file
269 ```
270
271 Esto mostrará todas las opciones de configuración disponibles con sus
valores predeterminados. Su edición será efectiva la próxima vez que
inicie una instancia de Odoo. Escriba `q` para salir y volver al prompt.
272
273 También podemos optar por usar un archivo de configuración específico,
usando la opción `--conf = <filepath>`. Los archivos de configuración
no necesitan tener todas las opciones que acabas de ver. Sólo los que
realmente cambian un valor por defecto deben estar allí.
274
275 ## Cambiando el puerto de escucha
276
277 La opción de comando `--xmlrpc-port = <port>` nos permite cambiar el
puerto de escucha de una instancia de servidor desde el predeterminado
8069. Esto se puede usar para ejecutar más de una instancia al mismo
tiempo, en la misma máquina.
278
279 Vamos a probar esto. Abre dos ventanas de terminal. En el primero,
ejecuta esto:
280
281 ```
282 $ ~ / Odoo-dev / odoo / odoo-bin --xmlrpc-port = 8070
283 ```
284
285 Ejecuta el siguiente comando en el segundo terminal:
286
Page 8
capitulo--1.md 07/02/2018

287 ```
288 $ ~ / Odoo-dev / odoo / odoo-bin --xmlrpc-port = 8071
289 ```
290
291 Ahí lo tienes: dos instancias Odoo en el mismo servidor de escucha en
diferentes puertos! Las dos instancias pueden utilizar bases de datos
iguales o diferentes, dependiendo de los parámetros de configuración
utilizados. Y los dos podrían estar ejecutando las mismas o diferentes
versiones de Odoo.
292
293 ### La opción filtro de la base de datos
294
295 Cuando se desarrolla con Odoo, es frecuente trabajar con varias bases
de datos, ya veces incluso con diferentes versiones de Odoo. Detener e
iniciar diferentes instancias de servidor en el mismo puerto y cambiar
entre distintas bases de datos puede provocar que las sesiones de
cliente web se comporten de forma incorrecta.
296
297 El acceso a nuestra instancia utilizando una ventana del navegador que
se ejecuta en modo privado puede ayudar a evitar algunos de estos
problemas.
298
299 Otra buena práctica es habilitar un filtro de base de datos en la
instancia del servidor para asegurarse de que sólo permite las
solicitudes de la base de datos con la que queremos trabajar, ignorando
todos las demás. Esto se hace con la opción `--db-filter`. Acepta una
expresión regular que se utiliza como filtro para los nombres de base
de datos válidos. Para que coincida con un nombre exacto, la expresión
debe comenzar con un `^` y terminar con `$`.
300
301 Por ejemplo, para permitir sólo la base de datos `demo` utilizaríamos
este comando:
302
303 ```
304 $ ~ / Odoo-dev / odoo / odoo-bin --db-filter = ^ demo $
305 ```
306
307 ### Administrar los mensajes de registro del servidor
308
309 La opción `--log-level` nos permite establecer la verbosidad del
registro. Esto puede ser muy útil para entender lo que está sucediendo
en el servidor. Por ejemplo, para habilitar el nivel de registro de
depuración, use la opción `--log-level=debug`.
310
311 Los siguientes niveles de registro pueden ser particularmente
interesantes:
312
313 + `Debug_sql` para inspeccionar consultas SQL generadas por el servidor
314 + `Debug_rp`c para detallar las peticiones recibidas por el servidor
315 + `Debug_rpc_answer` para detallar las respuestas enviadas por el
servidor
316
317 De forma predeterminada, la salida del registro se dirige a la salida
estándar (la pantalla de la consola), pero se puede dirigir a un
archivo de registro con la opción `--logfile=<filepath>`.
318
319 Finalmente, la opción `--dev=all` mostrará el depurador de Python
(`pdb`) cuando se genera una excepción. Es útil hacer un análisis
post-mortem de un error de servidor. Ten en cuenta que no tiene ningún
efecto en la verbosidad del registrador. Puedes encontrar más detalles
sobre los comandos del depurador de Python en
Page 9
capitulo--1.md 07/02/2018

https://docs.python.org/2/library/pdb.html#debugger-commands.
320
321
322 ### Desarrollando desde tu estación de trabajo
323 Puedes estar ejecutando Odoo con un sistema Debian / Ubuntu en una
máquina virtual local o en un servidor a través de la red. Pero puede
que prefieras hacer el trabajo de desarrollo en tu estación de trabajo
personal, utilizando tu editor de texto favorito o IDE. Este suele ser
el caso de los desarrolladores que trabajan desde estaciones de trabajo
Windows. Pero también puede ser el caso de los usuarios de Linux que
necesitan trabajar en un servidor Odoo a través de la red local.
324
325 Una solución para esto es para permitir el uso compartido de archivos
en el huesped Odoo para que los archivos sean fáciles de editar desde
nuestra estación de trabajo. Para las operaciones del servidor Odoo,
como un reinicio del servidor, podemos usar un shell SSH (como PuTTY en
Windows) junto con nuestro editor favorito.
326
327 #### Usando un editor de texto Linux
328 Tarde o temprano, necesitaremos editar archivos desde la línea de
comandos del shell. En muchos sistemas Debian, el editor de texto
predeterminado es vi. Si no te sientes cómodo con él, probablemente
podrías usar una alternativa más amigable. En los sistemas Ubuntu, el
editor de texto predeterminado es nano. Es posible que prefieras este,
ya que es más fácil de usar. En caso de que no esté disponible en tu
servidor, se puede instalar con:
329
330 ```
331 $ sudo apt-get install nano
332 ```
333
334 En las siguientes secciones, asumiremos nano como el editor preferido.
Si prefieres cualquier otro editor, siéntete libre de adaptar los
comandos en consecuencia.
335
336 ## Instalando y configurando Samba
337 El servicio Samba ayuda a que los servicios de compartición de archivos
de Linux sean compatibles con los sistemas Microsoft Windows. Podemos
instalarlo en nuestro servidor Debian / Ubuntu con este comando:
338
339
340 ```
341
342 $ Sudo apt-get instalar samba samba-common-bin
343
344 ```
345
346
347
348 El paquete `samba` instala los servicios de intercambio de archivos y
el paquete `samba-common-bin` es necesario para la herramienta
`smbpasswd`. De forma predeterminada, los usuarios autorizados a
acceder a archivos compartidos deben registrarse con él. Necesitamos
registrar a nuestro usuario, `odoo` por ejemplo, y establecer una
contraseña para su acceso a compartir archivos:
349 ```
350
351
352 $ Sudo smbpasswd -a odoo
353 ```
354
Page 10
capitulo--1.md 07/02/2018

355
356
357
358
359 Después de esto, se nos pedirá una contraseña para usar para acceder al
directorio compartido, y el usuario `odoo` podrá acceder a los archivos
compartidos para su directorio personal, aunque será de sólo lectura.
Queremos tener acceso de escritura, por lo que necesitamos editar el
archivo de configuración de Samba para cambiarlo de la siguiente manera:
360
361 ```
362
363
364 $ Sudo nano /etc/samba/smb.conf
365
366 ```
367
368 En el archivo de configuración, busque la sección `[homes]`. Edita sus
líneas de configuración para que coincidan con la configuración de la
siguiente manera:
369
370 ```
371 [homes]
372 comment = Home Directories
373 browseable = yes
374 read only = no
375 create mask = 0640
376 directory mask = 0750
377 ```
378
379 Para que los cambios de configuración tengan efecto, reinicia el
servicio:
380
381 ```
382 $ sudo /etc/init.d/smbd restart
383 ```
384 #### Tip
385 ##### Descargando el código ejemplo
386
387 Puedes descargar los archivos de códigos de ejemplo para todos los
libros Packt que hayas comprado desde tu cuenta en
http://www.packtpub.com. Si compraste este libro en algun otro sitio,
puedes entrar a http://www.packtpub.com/support y registrate para que
te envien los archivos directamente por correo electrónico.
388
389 Para acceder a los archivos desde Windows, podemos asignar una unidad
de red para la ruta `\\ <my-server-name>\odoo`
390 utilizando el nombre de usuario y la contraseña específicos definidos
con `smbpasswd` Al intentar iniciar sesión con el usuario `odoo`,
podría encontrar problemas con Windows agregando el dominio del equipo
al nombre de usuario (por ejemplo, `MYPC \ odoo`). Para evitar esto,
utilice un dominio vacío añadiendo un caracter`\` al inicio de sesión
(por ejemplo, `\ odoo`):
391
392 ![Directory](file:img/1-03.jpg)
393
394
395 Si ahora abrimos la unidad asignada con el Explorador de Windows,
podremos acceder y editar el contenido del directorio home del usuario
`odoo`:
396
Page 11
capitulo--1.md 07/02/2018

397 ![Home](file:img/1-04.jpg)
398
399 Odoo incluye un par de herramientas que son muy útiles para los
desarrolladores, y vamos a hacer uso de ellAs a lo largo del libro. Son
características técnicas y el modo de desarrollo. Estos están
desactivados por defecto, por lo que este es un buen momento para
aprender a habilitarlos.
400
401 ### Activación de las herramientas de desarrollo
402 Las herramientas de desarrollo proporcionan una configuración y
funciones avanzadas de servidor. Estos incluyen un menú de depuración
en la barra de menú superior junto con opciones de menú adicionales en
el menú **Settings**, en particular el menú **Technical**.
403
404 Estas herramientas vienen deshabilitadas de forma predeterminada y,
para habilitarlas, debemos iniciar sesión como administrador. En la
barra de menú superior, seleccione el menú **Settings**. En la parte
inferior derecha, debajo de la versión Odoo, encontrará dos opciones
para habilitar el modo de desarrollo; cualquiera de ellas habilitará
los menús **Debug** y **Technical**. La segunda opción, **Activate the
developer mode (whit assest)**, también deshabilita la minificación de
JavaScript y CSS utilizada por el cliente web, facilitando la
depuración del comportamiento del cliente:
405
406 ![Depuracion](file:img/1-05.jpg)
407
408 Después de eso, la página se vuelve a cargar y debería verse un icono
de error en la barra de menú superior, justo antes del avatar y nombre
de usuario de la sesión que proporciona las opciones de modo de
depuración. Y en la opción **Settings** en el menú superior, deberíamos
ver una nueva sección del menú **Technical** que da acceso a muchos
internos de la instancia de Odoo:
409
410 ![Cliente](file:img/1-06.jpg)
411
412 #### Tip
413 La opción **Technical** del menú nos permite inspeccionar y editar
todas las configuraciones Odoo almacenadas en la base de datos, desde
la interfaz de usuario hasta la seguridad y otros parámetros del
sistema. Aprenderás más sobre muchos de estos a lo largo del libro.
414
415 ## Instalación de módulos de terceros
416 Hacer nuevos módulos disponibles en una instancia Odoo para que puedan
instalarse es algo que los recién llegados a Odoo suelen encontrar
confuso. Pero no tiene que ser así, así que vamos a desmitificarlo.
417
418 ## Encontrar módulos comunitarios
419 Hay muchos módulos Odoo disponibles en Internet. La tienda de
aplicaciones de Odoo en apps.odoo.com es un catálogo de módulos que se
pueden descargar e instalar en su sistema. La **Odoo Community
Association (OCA)** coordina las contribuciones de la comunidad y
mantiene bastantes repositorios de módulos en GitHub en
https://github.com/OCA/.
420
421 Para agregar un módulo a una instalación de Odoo, podríamos copiarlo en
el directorio `addons` junto con los módulos oficiales. En nuestro
caso, el directorio `addons` está en `~ / odoo-dev / odoo / addons /`.
Esto podría no ser la mejor opción para nosotros, ya que nuestra
instalación de Odoo se basa en un repositorio de código controlado por
versiones, y queremos mantenerlo sincronizado con el repositorio de
GitHub.
Page 12
capitulo--1.md 07/02/2018

422
423 Afortunadamente, podemos usar ubicaciones adicionales para los módulos
para que podamos mantener nuestros módulos personalizados en un
directorio diferente, sin tenerlos mezclados con los oficiales.
424
425 Como ejemplo, vamos a descargar el código de este libro, disponible en
GitHub, y hacer disponíbles esos módulos addon en nuestra instalación
de Odoo.
426
427 Para obtener el código fuente de GitHub, ejecute los siguientes comandos:
428
429 ```
430 $ cd ~/odoo-dev
431
432 $ git clone https://github.com/dreispt/todo_app.git -b 10.0
433 ```
434 Usamos la opción `-b` para asegurarnos de que estamos escargando los
módulos para la versión 10.0.
435
436 Después de esto, tendremos un directorio nuevo `/ todo_app` junto al
directorio `/ odoo`, que contiene los módulos. Ahora debemos informar a
Odoo sobre este nuevo directorio de módulos.
437
438 ### Configurandola ruta addons
439 El servidor Odoo tiene una opción de configuración llamada
`addons_path` para establecer dónde el servidor debe buscar módulos. De
forma predeterminada, esto apunta al directorio `/ addons`, donde se
ejecuta el servidor Odoo.
440
441 Podemos proporcionar no sólo una, sino una lista de directorios donde
se pueden encontrar módulos. Esto nos permite mantener nuestros módulos
personalizados en un directorio diferente, sin tenerlos mezclados con
los addons oficiales.
442
443 Vamos a iniciar el servidor con una ruta addons que incluye nuestro
nuevo directorio de módulos:
444 ```
445 $ cd ~/odoo-dev/odoo
446
447 $ ./odoo-bin -d demo --addons-path="../todo_app,./addons"
448 ```
449
450 Si miras más de cerca el registro del servidor, notarás una línea que
informa de la ruta de complementos en uso: `INFO? Odoo: addons paths:
[...]`. Confirma que contiene nuestro directorio `todo_app`.
451
452 ### Actualizando la lista de aplicaciones
453 Todavía necesitamos pedirle a Odoo que actualice su lista de módulos
antes de que estos nuevos módulos estén disponibles para la instalación.
454
455 Para ello, necesitamos activar el modo desarrollador, ya que
proporciona la opción de menú **Actualizar Lista de Aplicaciones**. Se
puede encontrar en el menú superior de **Aplicaciones**.
456
457 Después de actualizar la lista de módulos, podemos confirmar que los
nuevos módulos están disponibles para la instalación. Utilice la opción
de menú **Aplicaciones** para ver la lista de módulos locales. Busca
`todo` y deberías ver los nuevos módulos disponibles.
458
459 Ten en cuenta que la segunda opción de menú **App Store** muestra la
lista de módulos del almacén de aplicaciones Odoo en lugar de los
Page 13
capitulo--1.md 07/02/2018

módulos locales:
460
461 ![Apps](file:img/1-07.jpg)
462
463 ## Resumen
464 En este capítulo, aprendimos a configurar un sistema Debian para alojar
Odoo e instalarlo desde el código fuente de GitHub. También aprendimos
a crear bases de datos Odoo y ejecutar instancias de Odoo. Para
permitir a los desarrolladores utilizar sus herramientas favoritas en
su estación de trabajo personal, explicamos cómo configurar el uso
compartido de archivos en el huesped Odoo.
465
466 Ahora deberíamos tener un ambiente Odoo en funcionamiento para trabajar
y estar cómodos con la administración de bases de datos e instancias.
467
468 Con esto en su lugar, estamos listos para ir directamente a la acción.
En el próximo capítulo, crearemos nuestro primer módulo Odoo desde cero
y entenderemos los principales elementos que involucra.
469
470 ¡Entonces empecemos!

Page 14
capitulo-2.md 07/02/2018

1 # Capítulo 2. Creación de su primera aplicación Odoo


2 Desarrollar en Odoo la mayor parte del tiempo significa crear nuestros
propios módulos. En este capítulo, crearemos nuestra primera aplicación
Odoo y aprenderemos los pasos necesarios para ponerla a disposición de
Odoo e instalarla.
3
4 Inspirado por el notable proyecto http://todomvc.com/, vamos a
construir una simple aplicación de tareas pendientes. Debería
permitirnos agregar nuevas tareas, marcarlas como completadas y,
finalmente, borrar la lista de tareas de todas las tareas ya completadas.
5
6 Comenzaremos aprendiendo los conceptos básicos del desarrollo de un
flujo de trabajo: configura una nueva instancia para tu trabajo, crea e
instalA un nuevo módulo y actualízalo para aplicar los cambios que
realices junto con las iteraciones de desarrollo.
7
8 Odoo sigue una arquitectura similar a MVC, y pasaremos por las capas
durante nuestra implementación de la aplicación de tareas pendientes:
9
10 + La capa del **modelo**, que define la estructura de los datos de la
aplicación
11 + La capa de ***vista**, que describe la interfaz de usuario
12 + La capa del **controlador**, que soporta la lógica de negocio de la
aplicación
13
14 A continuación, aprenderemos cómo configurar la seguridad de control de
acceso y, finalmente, agregaremos información sobre la descripción y la
marca al módulo.
15
16 #### Nota
17 Ten en cuenta que el concepto del término controlador mencionado aquí
es diferente de los controladores de desarrollo web Odoo. Estos son
puntos finales del programa que las páginas web pueden llamar para
realizar acciones.
18
19 Con este enfoque, podrás aprender gradualmente sobre los bloques
básicos de construcción que conforman una aplicación y experimentar el
proceso iterativo de construir un módulo Odoo desde cero.
20
21 ## Conceptos esenciales
22 Es probable que estés empezando con Odoo, así que ahora es obviamente
un buen momento para explicar los módulos de Odoo y cómo se utilizan en
un desarrollo Odoo.
23
24 ### Descripción de aplicaciones y módulos
25 Es común oír hablar de los módulos y aplicaciones Odoo. Pero, ¿cuál es
exactamente la diferencia entre ellos?
26
27 Los **Complementos de Módulos** son los componentes básicos para las
aplicaciones Odoo. Un módulo puede agregar nuevas características a
Odoo, o modificar las existentes. Es un directorio que contiene un
manifiesto, o archivo descriptor, llamado `__manifest__.py`, más los
archivos restantes que implementan sus características.
28
29 Las **Aplicaciones** son la forma en que se añaden las principales
características a Odoo. Proporcionan los elementos básicos para un área
funcional, como Contabilidad o RH, en función de qué características de
módulos complementarios modifican o amplían. Debido a esto, se destacan
en el menú **Apps** de Odoo.
30
31 Si su módulo es complejo y agrega funcionalidad nueva o mayor a Odoo,
Page 1
capitulo-2.md 07/02/2018

podrías considerar crearlo como una aplicación. Si tu módulo sólo hace


cambios a la funcionalidad existente en Odoo, es probable que no sea
una aplicación.
32
33 Si un módulo es una aplicación o no, se define en el manifiesto.
Técnicamente no tiene ningún efecto particular sobre cómo se comporta
el módulo addon. Sólo se utiliza para resaltar en la lista de
**Aplicaciones**.
34
35 ### Modificando y extendiendo módulos
36 En el ejemplo que vamos a seguir, crearemos un nuevo módulo con el
menor número posible de dependencias.
37
38 Sin embargo, este no será el caso típico. Principalmente, modificaremos
o extenderemos un módulo ya existente.
39
40 Como regla general, se considera una mala práctica modificar los
módulos existentes al cambiar su código fuente directamente. Esto es
especialmente cierto para los módulos oficiales proporcionados por
Odoo. Hacerlo no te permite tener una separación clara entre el código
del módulo original y las modificaciones, y esto hace que sea difícil
aplicar actualizaciones ya que sobrescribirían las modificaciones.
41
42 En su lugar, debemos crear los módulos de extensión que se instalarán
junto a los módulos que queremos modificar, implementando los cambios
que necesitamos. De hecho, uno de los principales puntos fuertes de
Odoo es el mecanismo de **herencia**, que permite módulos
personalizados para extender los módulos existentes, ya sea
oficialmente o desde la comunidad. La herencia es posible en todos los
niveles: modelos de datos, lógica empresarial y capas de interfaz de
usuario.
43
44 En este capítulo, crearemos un módulo completamente nuevo, sin extender
ningún módulo existente, para enfocarnos en las diferentes partes y
pasos involucrados en la creación del módulo. Vamos a tener sólo una
breve mirada a cada parte ya que cada uno de ellos será estudiado con
más detalle en los capítulos posteriores.
45
46 Una vez que estemos cómodos con la creación de un nuevo módulo, podemos
sumergirnos en el mecanismo de herencia, que será introducido en el
Capítulo 3, *Herencia - Extendiendo Aplicaciones Existentes*.
47
48 Para obtener desarrollo productivo para Odoo debemos estar cómodos con
el flujo de trabajo de desarrollo: administrar el entorno de
desarrollo, aplicar cambios de código y comprobar los resultados. Esta
sección le guiará a través de estos fundamentos.
49
50 ### Creando el esqueleto básico del módulo
51 Siguiendo las instrucciones del Capítulo 1, *Iniciando con desarrollo
Odoo*, deberíamos tener el servidor Odoo en `~ / odoo-dev / odoo /`.
Para mantener las cosas ordenadas, crearemos un nuevo directorio junto
con él para alojar nuestros módulos personalizados, en `~ / odoo-dev /
custom-addons`.
52
53 Odoo incluye un comando `scaffold` para crear automáticamente un nuevo
directorio de módulo, con una estructura básica ya establecida. Puedes
obtener más información al respecto con el siguiente comando:
54 ```
55 $ ~/odoo-dev/odoo/odoo-bin scaffold --help
56 ```
57
Page 2
capitulo-2.md 07/02/2018

58 Es posible que desees tener esto en cuenta cuando empieces a trabajar


en tu próximo módulo, pero no lo usaremos ahora, ya que preferiremos
crear manualmente toda la estructura de nuestro módulo.
59
60 Un módulo addon Odoo es un directorio que contiene un archivo
descriptor `__manifest__.py`.
61
62 #### Nota
63 En versiones anteriores, este archivo descriptor se denominó
`__openerp__.py`. Este nombre aún se admite pero está obsoleto.
64
65 También necesita ser Python importable, por lo que también debe tener
un archivo `__init__.py`.
66
67 El nombre del directorio del módulo es su nombre técnico. Usaremos
`todo_app` para ello. El nombre técnico debe ser un identificador
Python válido: debe comenzar con una letra y sólo puede contener
letras, números y el carácter de subrayado.
68
69 Los siguientes comandos crearán el directorio del módulo y crearán un
archivo `__init__.py ` vacío en él, `~ / odoo-dev / custom-addons /
todo_app / __ init__.py`.
70
71 En caso de que desee hacerlo directamente desde la línea de comandos,
esto es lo que debes usar:
72 ```
73 $ mkdir ~/odoo-dev/custom-addons/todo_app
74
75 $ touch ~/odoo-dev/custom-addons/todo_app/__init__.py
76 ```
77
78 A continuación, necesitamos crear el archivo de manifiesto. Debería
contener sólo un diccionario Python con una docena de posibles
atributos; De esto, solo se requiere el atributo de `name`. El atributo
`description`, para una descripción más larga, y el atributo `author`
proporcionan una mejor visibilidad y son recomendados.
79
80 Ahora debemos añadir un archivo `__manifest__.py` junto al archivo
`__init__.py` con el siguiente contenido:
81
82 ```
83
84 {
85 'name': 'To-Do Application',
86 'description': 'Manage your personal
87 To-Do
88 tasks.',
89 'author': 'Daniel Reis',
90 'depends': ['base'],
91 'application': True,
92 }
93
94 ```
95 El atributo `depends` puede tener una lista de otros módulos que se
requieren. Odoo los instalará automáticamente cuando este módulo esté
instalado. No es un atributo obligatorio, pero se aconseja tenerlo
siempre. Si no se necesitan dependencias en particular, debemos
depender del módulo básico `base`.
96
97 Debes tener cuidado de asegurarte de que todas las dependencias se
establecen explícitamente aquí; De lo contrario, el módulo puede fallar
Page 3
capitulo-2.md 07/02/2018

al instalar en una base de datos limpia (debido a las dependencias que


faltan) o tener errores de carga si por casualidad los otros módulos
necesarios se cargan después.
98
99 Para nuestra aplicación, no necesitamos dependencias específicas, por
lo que dependemos únicamente del módulo `base`.
100
101 Para ser conciso, elegimos utilizar muy pocas claves de descriptor,
pero, en un escenario real, te recomendamos que también uses las claves
adicionales, ya que son relevantes para la tienda de aplicaciones Odoo.
102
103 + `summary` se muestra como un subtítulo para el módulo.
104 + `version`, por defecto, es 1.0. Debe seguir reglas de versiones
semánticas (vea http://semver.org/ para más detalles).
105 + El Identificador de `license`, por defecto es `LGPL-3`.
106 + `website` es una URL para encontrar más información sobre el módulo.
Esto puede ayudar a la gente a encontrar más documentación o al
rastreador de incidencias para registrar bugs y sugerencias.
107 + `category` es la categoría funcional del módulo, que por defecto es
`Uncategorized`. La lista de categorías existentes se encuentra en el
formulario grupos de seguridad (**Settings | User | Groups**), en la
lista desplegable del campo **Application**.
108
109 Estas otras teclas descriptoras también están disponibles:
110
111 + `installable` es por defecto `True` pero se puede establecer como
`False` para deshabilitar un módulo.
112 + `Auto_install` si se establece en `True`, este módulo se instalará
automáticamente, siempre que todas sus dependencias ya estén
instaladas. Se utiliza para los módulos de pegamento.
113
114 Desde Odoo 8.0, en lugar de la clave de `description`, podemos utilizar
un archivo `README.rst` o `README.md` en el directorio superior del
módulo.
115
116 ### Una palabra sobre las licencias
117 Elegir una licencia para tu trabajo es muy importante, y debes
considerar cuidadosamente cuál es la mejor opción para tí y sus
implicaciones. Las licencias más utilizadas para los módulos Odoo son
la **Licencia Pública General Menor de GNU (LGLP¨** y la **Licencia
Pública General de Affero (AGPL)**. La LGPL es más permisiva y permite
el trabajo derivado comercial, sin la necesidad de compartir el código
fuente correspondiente. La AGPL es una licencia de código abierto más
fuerte, y requiere trabajo derivado y alojamiento de servicio para
compartir su código fuente. Obten más información acerca de las
licencias GNU en https://www.gnu.org/licenses/.
118
119 ### Añadiendo a la ruta addons
120 Ahora que tenemos un nuevo módulo minimalista, queremos ponerlo a
disposición de la instancia de Odoo.
121
122 Para ello, debemos asegurarnos de que el directorio que contiene el
módulo está en la ruta addons, entonces actualiza la lista de módulos
Odoo.
123
124 Ambas acciones se han explicado en detalle en el capítulo anterior,
pero aquí, continuaremos con un breve resumen de lo que se necesita.
125
126 Posicionaremos en nuestro directorio de trabajo e iniciaremos el
servidor con la configuración de ruta de addons apropiada:
127
Page 4
capitulo-2.md 07/02/2018

128 ```
129 $ cd ~/odoo-dev
130
131
132 $ ./odoo/odoo-bin -d todo --addons-path="custom-addons,odoo/addons"
--save
133 ```
134 La opción `--save` guarda las opciones que utilizaste en un archivo de
configuración. Esto nos evita repetirlas cada vez que reiniciamos el
servidor: solo se ejecuta `./odoo-bin` y se utilizará la última opción
guardada.
135
136 Observa atentamente el registro del servidor. Debe tener una línea
`INFO? Odoo: addons paths: [...]`. Debe incluir nuestro directorio de
`custom-addons`.
137
138 Recuerda incluir también cualquier otro directorio de complementos que
puedas estar utilizando. Por ejemplo, si también tienes un directorio
`~ / odoo-dev / extra` que contiene módulos adicionales que se
utilizarán, es posible que desees incluirlos también utilizando la
opción `--addons-path`:
139 ```
140
141 --addons-path = "custom-addons, extra, odoo / addons"
142
143 ```
144
145
146 Ahora necesitamos la instancia Odoo para reconocer el nuevo módulo que
acabamos de agregar.
147
148 ### Instalando el nuevo módulo
149 En el menú superior de **Aplicaciones**, seleccione la opción
**Actualizar Lista de Aplicaciones**. Esto actualizará la lista de
módulos, añadiendo los módulos que se hayan agregado desde la última
actualización a la lista. Recuerda que necesitamos activar el modo
desarrollador para que esta opción sea visible. Esto se hace en el
panel de **Configuración**, en el enlace de abajo a la derecha, debajo
de la información del número de versión de Odoo.
150
151 #### Tip
152 Asegúrate de que tu sesión de cliente web está funcionando con la base
de datos correcta. Puedes comprobarlo en la parte superior derecha: el
nombre de la base de datos se muestra entre paréntesis, justo después
del nombre de usuario. Una manera de aplicar la base de datos correcta
es iniciar la instancia del servidor con la opción adicional
`--db-filter = ^ MYDB $`.
153
154 La opción **Aplicaciones** nos muestra la lista de módulos disponibles.
De forma predeterminada, muestra sólo los módulos de aplicación. Ya que
hemos creado un módulo de aplicación, no necesitamos eliminar ese
filtro para verlo. Escribe `todo` en la búsqueda y debes ver nuestro
nuevo módulo, listo para ser instalado:
155
156 ![Installed]file:img/2-01.jpg)
157
158 Ahora haZ clic en el botón **Instalar** del módulo y ¡estamos listos!
159
160 ### Actualizando un módulo
161 El desarrollo de un módulo es un proceso iterativo, y querrás que los
cambios hechos en los archivos fuente sean aplicados y hechos visibles
Page 5
capitulo-2.md 07/02/2018

en Odoo.
162
163 En muchos casos, esto se realiza actualizando el módulo: busca el
módulo en la lista de **Aplicaciones** y una vez que ya esté instalado,
tendrás disponible un botón de **Actualización**.
164
165 Sin embargo, cuando los cambios son sólo en código Python, la
actualización puede no tener un efecto. En lugar de una actualización
de módulo, es necesario reiniciar el servidor de aplicaciones. Dado que
Odoo carga el código Python sólo una vez, cualquier cambio posterior en
el código requiere que se aplique un reinicio del servidor.
166
167 En algunos casos, si los cambios de módulo estuvieran en los archivos
de datos y en el código de Python, es posible que necesites ambas
operaciones. Esta es una fuente común de confusión para los nuevos
desarrolladores Odoo.
168 Pero afortunadamente, hay una mejor manera. La manera más segura y
rápida de hacer que todos nuestros cambios en un módulo sean efectivos
es detener y reiniciar la instancia del servidor, solicitando que
nuestros módulos sean actualizados a nuestra base de datos de trabajo.
169
170 En el terminal donde se ejecuta la instancia de servidor, utiliza
**Ctrl + C** para detenerla. A continuación, inicie el servidor y
actualice el módulo `todo_app` mediante el siguiente comando:
171
172 ```
173 $ ./odoo-bin -d todo -u todo_app
174
175 ```
176
177
178 La opción `-u` (o `--update` en el forma larga) requiere la opción `-d`
y acepta una lista de módulos separados por comas para actualizar. Por
ejemplo, podríamos usar `-u todo_app, mail`. Cuando se actualiza un
módulo, también se actualizan todos los módulos instalados que dependen
de él. Esto es esencial para mantener la integridad de los mecanismos
de herencia, utilizados para extender características.
179
180 A lo largo del libro, cuando necesites aplicar el trabajo realizado en
módulos, la forma más segura es reiniciar la instancia Odoo con el
comando anterior. Al presionar la tecla de flecha hacia arriba, se
obtiene el comando anterior que se utilizó. Por lo tanto, la mayoría de
las veces, te encontrará usando la combinación de teclas _**Ctrl +
C**_, arriba y _**Enter**_.
181
182 Desafortunadamente, tanto la actualización de la lista de módulos como
la desinstalación de módulos son acciones que no están disponibles a
través de la línea de comandos. Estos deben hacerse a través de la
interfaz web en el menú de **Aplicaciones**.
183
184 ### El modo de desarrollo del servidor
185 En Odoo 10 se introdujo una nueva opción que proporciona
características amigables para los desarrolladores. Para usarla, inicia
la instancia del servidor con la opción adicional `--dev = all`.
186 Esto permite que algunas características prácticas aceleren nuestro
ciclo de desarrollo. Los más importantes son:
187 + Recargar código Python automáticamente, una vez que se guarda un
archivo Python, evitando un reinicio manual del servidor
188 + Leer las definiciones de vista directamente desde los archivos XML,
evitando actualizaciones manuales del módulo
189
Page 6
capitulo-2.md 07/02/2018

190 La opción `--dev` acepta una lista de opciones separadas por comas,
aunque la opción `all` será adecuada la mayor parte del tiempo. También
podemos especificar el depurador que preferimos usar. De forma
predeterminada, se utiliza el depurador Python, `pdb`. Algunas personas
pueden preferir instalar y usar depuradores alternativos. Aquí también
se admiten `ipdb` y `pudb`.
191
192 ## La capa modelo
193 Ahora que Odoo conoce nuestro nuevo módulo, comencemos agregándole un
modelo simple.
194
195 Los modelos describen objetos de negocio, como una oportunidad, ordenes
de clientes o socios (cliente, proveedor, etc.). Un modelo tiene una
lista de atributos y también puede definir su negocio específico.
196
197 Los modelos se implementan utilizando una clase Python derivada de una
clase de plantilla Odoo. Se traducen directamente a objetos de base de
datos, y Odoo se encarga de esto automáticamente al instalar o
actualizar el módulo. El mecanismo responsable de esto es el **Modelo
Relacional de Objetos (ORM)**.
198
199 Nuestro módulo será una aplicación muy simple para mantener las tareas
pendientes. Estas tareas tendrán un solo campo de texto para la
descripción y una casilla de verificación para marcarlas como
completas. Más adelante deberíamos añadir un botón para limpiar la
lista de tareas de las tareas completas.
200
201 ### Creando el modelo de datos
202 Las directrices de desarrollo de Odoo establecen que los archivos
Python para los modelos deben colocarse dentro de un subdirectorio
`models`. Para simplificar, no lo seguiremos aquí, así que vamos a crar
un archivo `todo_model.py` en el directorio principal del módulo
`todo_app`.
203
204 Añade el siguiente contenido:
205
206 ```
207 # -*- coding: utf-8 -*-
208 from odoo import models, fields
209 class TodoTask(models.Model):
210 _name = 'todo.task'
211 _description = 'To-do Task'
212 name = fields.Char('Description', required=True)
213 is_done = fields.Boolean('Done?')
214 active = fields.Boolean('Active?', default=True)
215
216 ```
217 La primera línea es un marcador especial que indica al intérprete de
Python que este archivo tiene UTF-8 para que pueda esperar y manejar
caracteres no ASCII. No usaremos ninguno, pero es una buena práctica
tenerlo de todos modos.
218
219 La segunda línea es una instrucción de importación de código Python,
haciendo disponibles los objetos `models` y `fields` del núcleo Odoo.
220
221 La tercera línea declara nuestro nuevo modelo. Es una clase derivada de
`models.Model`.
222
223 La siguiente línea establece el atributo `_name` que define el
identificador que se utilizará en Odoo para referirse a este modelo.
Toma en cuenta que el nombre real de la clase Python, `TodoTask` en
Page 7
capitulo-2.md 07/02/2018

este caso, carece de significado para otros módulos Odoo. El valor


`_name` es lo que se utilizará como identificador.
224
225 Observa que esta y las siguientes líneas tienen sangría. Si no estás
familiarizado con Python, debes saber que esto es importante: la
sangría define un bloque de código anidado, por lo que estas cuatro
líneas deben tener estar todas igual sangría.
226
227 Luego, tenemos el atributo modelo `_description `. No es obligatorio,
pero proporciona un nombre fácil de usar para los registros del modelo,
que puede utilizarse para mejores mensajes de usuario.
228
229 Las tres últimas líneas definen los campos del modelo. Vale la pena
señalar que `name` y `active` son nombres de campos especiales. De
forma predeterminada, Odoo usará el campo de `name` como el título del
registro al referenciarlo de otros modelos. El campo `active` se
utiliza para inactivar los registros y, por defecto, sólo los registros
activos serán mostrados. Lo utilizaremos para borrar las tareas
completadas sin eliminarlas de la base de datos.
230
231 En este momento, este archivo aún no es utilizado por el módulo.
Debemos decirle a Python que lo cargue con el módulo en el archivo
`__init__.py`. Vamos a editarlo para agregar la siguiente línea:
232 ```
233 from . Importar todo_modelo
234 ```
235 ¡Eso es! Para que nuestros cambios de código de Python entren en vigor,
la instancia de servidor debe reiniciarse (a menos que esté utilizando
el modo `--dev`).
236
237 No veremos ninguna opción de menú para acceder a este nuevo modelo ya
que no los hemos añadido aún. Sin embargo, podemos inspeccionar el
modelo recién creado usando el menú **Technical**. En el menú superior
**Settings**, ve a **Technical | Database Structure | Models**, busca
el modelo `todo.task` en la lista y haz clic en él para ver su
definición:
238
239 ![Settings](file:img/2-02.jpg)
240
241 Si todo va bien, se confirma que el modelo y los campos fueron creados.
Si no puedes verlos aquí, intenta reiniciar el servidor con una
actualización de módulo, como se describió anteriormente.
242
243 También podemos ver algunos campos adicionales que no declaramos. Estos
son campos reservados que Odoo agrega automáticamente a cada modelo
nuevo. Estos son los siguientes:
244
245 + `id` es un identificador numérico único para cada registro del modelo.
246 + `create_date` y `create_uid` especifican cuándo se creó el registro y
quién lo creó respectivamente.
247 + `write_date` y `write_uid` confirman cuándo el registro fue
modificado por última vez y quien lo modificó respectivamente.
248 + `__last_update` es un ayudante que en realidad no se almacena en la
base de datos. Se utiliza para verificaciones de concurrencia.
249
250 ### Añadiendo pruebas automatizadas
251 Las mejores prácticas de programación incluyen tener pruebas
automatizadas para tu código. Esto es aún más importante para lenguajes
dinámicos como Python. Como no hay ningún paso de compilación, no puede
estar seguro de que no haya errores sintácticos hasta que el intérprete
realmente ejecute el código. Un buen editor puede ayudarnos a detectar
Page 8
capitulo-2.md 07/02/2018

estos problemas con antelación, pero no puede ayudarnos a asegurar que


el código se ejecute como lo desean las pruebas automatizadas.
252
253 Odoo soporta dos formas de describir las pruebas: ya sea utilizando
archivos de datos YAML o utilizando código Python, basado en la
biblioteca `Unittest2`. Las pruebas YAML son un legado de versiones
anteriores, y no se recomiendan. Preferiremos usar pruebas de Python y
añadiremos un caso básico de prueba a nuestro módulo.
254
255
256 Los archivos de código de prueba deben tener un nombre que empiece por
`test_` y se debe importar desde `tests / __ init__.py`. Pero el
directorio de `test` (o submódulo Python) no se debe importar desde la
parte superior del módulo `__init__.py`, ya que se descubrirá y
cargará automáticamente sólo cuando se ejecuten pruebas.
257
258 Las pruebas deben colocarse en un subdirectorio `test/`. Añade un
archivo `tests / __ init__.py` con lo siguiente:
259 ```
260 from . import test_todo
261
262 ```
263 Ahora, añade el código de prueba real disponíble en el archivo
`tests/test_todo.py`:
264 ```
265 # -*- coding: utf-8 -*-
266 from odoo.tests.common import TransactionCase
267
268 class TestTodo(TransactionCase):
269
270 def test_create(self):
271 "Create a simple Todo"
272 Todo = self.env['todo.task']
273 task = Todo.create({'name': 'Test Task'})
274 self.assertEqual(task.is_done, False)
275
276 ```
277 Esto agrega un caso simple de prueba para crear una nueva tarea y
verifica que el campo ** Is Done?** Tiene el valor predeterminado
correcto.
278
279 Ahora queremos hacer nuestras pruebas. Esto se hace agregando la opción
`--test-enable` durante la instalación del módulo:
280
281 ```
282
283 $ ./odoo-bin -d todo -i todo_app --test-enable
284
285 ```
286
287
288
289 El servidor Odoo buscará un subdirectorio tests/ en los módulos
actualizados y los ejecutará. Si alguna de las pruebas falla, el
registro del servidor te mostrará eso.
290
291 ## La capa de vista
292
293 La capa de vista describe la interfaz de usuario. Las vistas se definen
mediante XML, que es utilizado por el marco de cliente web para generar
vistas HTML con datos.
Page 9
capitulo-2.md 07/02/2018

294
295 Tenemos elementos de menú que pueden activar acciones que pueden hacer
vistas. Por ejemplo, la opción de menú **Usuarios** procesa una acción
también denominada **Usuarios**, que a su vez genera una serie de
vistas. Existen varios tipos de vista disponibles, como las vistas de
lista y formulario y las opciones de filtro también disponíbles, están
definidas por un tipo particular de vista, la vista de búsqueda.
296
297 Las directrices de desarrollo de Odoo establecen que los archivos XML
que definen la interfaz de usuario deben colocarse dentro de un
subdirectorio `views /` subdirectorio
298 Comencemos a crear la interfaz de usuario para nuestra aplicación de
tareas pendientes.
299 ###Agregar elementos de menú
300
301 Ahora que tenemos un modelo para almacenar nuestros datos, debemos
hacerlo disponible en la interfaz de usuario.
302
303 Para ello, debemos añadir una opción de menú para abrir el modelo
`To–do Task` para que pueda utilizarse.
304
305 Cree el archivo `views / todo_menu.xml` para definir un elemento de
menú y la acción realizada por él:
306
307
308 ```
309
310 <?xml version="1.0"?>
311 <odoo>
312 <!-- Action to open To-do Task list -->
313 <act_window id="action_todo_task"
314 name="To-do Task"
315 res_model="todo.task"
316 view_mode="tree,form" />
317 <!-- Menu item to open To-do Task list -->
318 <menuitem id="menu_todo_task"
319 name="Todos"
320 action="action_todo_task" />
321 </odoo>
322
323 ```
324
325 La interfaz de usuario, incluidas las opciones y las acciones de menú,
se almacena en las tablas de la base de datos. El archivo XML es un
archivo de datos utilizado para cargar esas definiciones en la base de
datos cuando el módulo se instala o actualiza. El código anterior es un
archivo de datos Odoo, que describe dos registros para añadir a Odoo:
326
327 + El elemento `<act_window>` define una acción de ventana del lado del
cliente que abrirá el modelo `todo.task` con las vistas de árbol y
formulario habilitadas, en ese orden.
328 + El `<menuitem>` define un elemento de menú superior que llama a la
acción `action_todo_task`, que se definió anteriormente.
329
330 Ambos elementos incluyen un atributo id. Este atributo id también
llamado **XML ID**, es muy importante: se utiliza para identificar de
forma única cada elemento de datos dentro del módulo, y puede ser
utilizado por otros elementos para referenciarlo. En este caso, el
elemento `<menuitem>` necesita hacer referencia a la acción para
procesar, y necesita hacer uso de la <act_window> ID para eso. Los ID
XML se tratan con mayor detalle en el Capítulo 4, *Datos del módulo*
Page 10
capitulo-2.md 07/02/2018

331
332 Nuestro módulo aún no conoce el nuevo archivo de datos XML. Esto se
hace agregándolo al atributo de datos en el archivo `__manifest__.py`.
Este, contiene la lista de archivos a cargar por el módulo. Agregue
este atributo al diccionario del manifiesto:
333 ```
334
335 'Data': ['views / todo_menu.xml'],
336 ```
337
338 Ahora necesitamos actualizar el módulo de nuevo para que estos cambios
surtan efecto. Vaya al menú superior de **Todos** y debe ver nuestra
nueva opción de menú disponible:
339
340 ![Save](file:img/2-03.jpg)
341
342 Aunque no hemos definido nuestra vista de interfaz de usuario, al hacer
clic en el menú **Todos** se abrirá un formulario generado
automáticamente para nuestro modelo, lo que nos permitirá agregar y
editar registros.
343
344 Odoo es lo suficientemente agradable como para generarlos
automáticamente para que podamos empezar a trabajar con nuestro modelo
de inmediato.
345
346 ¡Hasta aquí todo bien! Vamos a mejorar nuestra interfaz de usuario
ahora. Trata de hacer mejoras graduales como se muestra en las próximas
secciones, haciendo actualizaciones de módulos frecuentes, y no tengas
miedo de experimentar. También puedes intentar la opción de servidor
`--dev = all`. Usándolo, las definiciones de vista se leen directamente
desde los archivos XML para que tus cambios puedan estar inmediatamente
disponibles para Odoo sin necesidad de una actualización de módulo.
347
348 ### Tip
349
350 Si una actualización falla debido a un error de XML, no te preocupe!
Comenta las últimas porciones XML editadas o elimina el archivo XML de
`__manifest__.py` y repita la actualización. El servidor debe iniciarse
correctamente. Ahora lee el mensaje de error en el registro del
servidor con cuidado: debe señalarte dónde está el problema.
351
352 Odoo admite varios tipos de vistas, pero las tres más importantes son:
`tree` (generalmente llamado vistas de lista), `form` y `search views`.
Vamos a añadir un ejemplo de cada uno a nuestro módulo.
353 ### Creando la vista de formulario
354
355 Todas las vistas se almacenan en la base de datos, en el modelo
`ir.ui.view`. Para añadir una vista a un módulo, declaramos un elemento
`<record>` que describe la vista en un archivo XML, que se va a cargar
en la base de datos cuando se instala el módulo.
356
357 Agregue este nuevo archivo `views / todo_view.xml` para definir nuestra
vista de formulario:
358 ```
359 <?xml version="1.0"?>
360 <odoo>
361 <record id="view_form_todo_task" model="ir.ui.view">
362 <field name="name">To-do Task Form</field>
363 <field name="model">todo.task</field>
364 <field name="arch" type="xml">
365
Page 11
capitulo-2.md 07/02/2018

366 <form string="To-do Task">


367
368
369
370
371
372 <group>
373 <field name="name"/>
374 <field name="is_done"/>
375 <field name="active" readonly="1"/>
376
377
378
379
380
381 </group>
382 </form>
383
384
385
386
387 </field>
388 </record>
389 </odoo>
390 ```
391 Recuerde agregar este nuevo archivo a la clave de datos en el archivo
de manifiesto, de lo contrario, nuestro módulo no lo sabrá y no se
cargará.
392
393 Esto agregará un registro al modelo `ir.ui.view` con el identificador
`view_form_todo_task`. La vista es para el modelo `todo.task` y se
denomina `To-do Task Form`. El nombre es solo para información; No
tiene que ser único, pero debe permitir que uno identifique fácilmente
a qué registro se refiere. De hecho, el nombre puede ser totalmente
omitido, en ese caso, se generará automáticamente a partir del nombre
del modelo y el tipo de vista.
394
395 El atributo más importante es `arch`, y contiene la definición de la
vista, resaltada en el código XML anterior. La etiqueta `<form>` define
el tipo de vista y, en este caso, contiene tres campos. También
agregamos un atributo al campo `active` para que sea solo de lectura.
396
397 ### Vistas del formulario de documento empresarial
398
399 La sección anterior proporcionó una vista de formulario básica, pero
podemos hacer algunas mejoras en ella. Para los modelos de documentos,
Odoo tiene un estilo de presentación que imita una página en papel.
Este formulario contiene dos elementos: `<header>` para contener los
botones de acción y `<sheet>` para contener los campos de datos.
400
401 Ahora podemos reemplazar el `<form>` básico definido en la sección
anterior por éste:
402 ```
403 <header>
404
405
406
407
408 <!-- Buttons go here-->
409
410 </header>
Page 12
capitulo-2.md 07/02/2018

411
412
413
414
415
416
417 <sheet>
418
419
420
421
422 <!-- Content goes here: -->
423 <group>
424 <field name="name"/>
425 <field name="is_done"/>
426 <field name="active" readonly="1"/>
427 </group>
428
429 </sheet>
430
431
432
433
434 </form>
435
436 ```
437
438 ### Añadiendo botones de acción
439
440 Los formularios pueden tener botones para realizar acciones. Estos
botones pueden ejecutar acciones de ventana como abrir otro formulario
o ejecutar funciones de Python definidas en el modelo.
441
442 Pueden colocarse en cualquier lugar dentro de un formulario, pero para
los formularios de estilo de documento, el lugar recomendado para ellos
es la sección `<header>`.
443
444 Para nuestra aplicación, agregaremos dos botones para ejecutar los
métodos del modelo `todo.task`:
445 ```
446
447 <header>
448
449 <button name="do_toggle_done" type="object"
450 string="Toggle Done" class="oe_highlight" />
451 <button name="do_clear_done" type="object"
452 string="Clear All Done" />
453
454
455
456
457 </header>
458 ```
459
460
461 Los atributos básicos de un botón comprenden lo siguiente:
462
463 + `stryng` con el texto a mostrar en el botón
464 + `type` de acción que realiza
465 + `name` es el identificador de esa acción
466 + `class` es un atributo opcional para aplicar estilos CSS, como en
Page 13
capitulo-2.md 07/02/2018

HTML normal
467
468 ### Uso de grupos para organizar formularios
469
470 La etiqueta `<group> `te permite organizar el contenido del formulario.
Colocar elementos `<group>` dentro de un elemento `<group>` crea un
diseño de dos columnas dentro del grupo externo. Se aconseja que los
elementos del grupo tengan un atributo de nombre para que sea más fácil
para otros módulos extenderlos.
471
472 Usaremos esto para organizar mejor nuestro contenido. Cambiemos el
contenido `<sheet>` de nuestro formulario para que coincida con este:
473
474 ```
475 <sheet>
476
477 <group name="group_top">
478 <group name="group_left">
479
480
481
482
483 <field name="name"/>
484
485 </group>
486 <group name="group_right">
487
488
489
490
491 <field name="is_done"/>
492 <field name="active" readonly="1"/>
493
494 </group>
495 </group>
496
497
498
499
500 </sheet>
501 ```
502
503 ### La vista de formulario completa
504
505 En este punto, nuestro formulario `todo.task` debe verse así:
506
507 ```
508 <form>
509 <header>
510 <button name="do_toggle_done" type="object"
511 string="Toggle Done" class="oe_highlight" />
512 <button name="do_clear_done" type="object"
513 string="Clear All Done" />
514 </header>
515 <sheet>
516 <group name="group_top">
517 <group name="group_left">
518 <field name="name"/>
519 </group>
520 <group name="group_right">
521 <field name="is_done"/>
Page 14
capitulo-2.md 07/02/2018

522 <field name="active" readonly="1" />


523 </group>
524 </group>
525 </sheet>
526 </form>
527 ```
528 ### Tip
529 Recuerda que para que los cambios se carguen en nuestra base de datos
Odoo, se necesita una actualización del módulo. Para ver los cambios en
el cliente web, el formulario debe ser recargado: haz clic de nuevo en
la opción de menú que lo abre o vuelve a cargar la página del navegador
(_**F5**_ en la mayoría de los navegadores).
530
531 Los botones de acción no funcionarán aún, ya que todavía necesitamos
agregar su lógica de negocio.
532 ### Adición de vistas de lista y de búsqueda
533
534 Cuando se visualiza un modelo en modo de lista, se utiliza una vista
`<tree>`. Las vistas de árbol son capaces de mostrar líneas organizadas
en jerarquías, pero la mayoría de las veces, se utilizan para mostrar
listas sin formato.
535
536 Podemos agregar la siguiente definición de vista `tree` a
`todo_view.xml`:
537 ```
538 <record id="view_tree_todo_task" model="ir.ui.view">
539 <field name="name">To-do Task Tree</field>
540 <field name="model">todo.task</field>
541 <field name="arch" type="xml">
542 <tree colors="decoration-muted:is_done==True">
543 <field name="name"/>
544 <field name="is_done"/>
545 </tree>
546 </field>
547 </record>
548 ```
549
550 Esto define una lista con sólo dos columnas: `name` y `is_done`.
También añadimos un toque agradable: las líneas para las tareas hechas
(`is_done == True`) se muestran en gris. Esto se hace aplicando la
clase silenciada Bootstrap. Consulta
http://getbootstrap.com/css/#helper-classes-colors para obtener más
información sobre Bootstrap y sus colores contextuales.
551
552 En la esquina superior derecha de la lista, Odoo muestra un cuadro de
búsqueda. Los campos que busca y los filtros disponibles se definen
mediante una vista `<search>`.
553
554 Como antes, agregamos esto a `todo_view.xml`:
555 ```
556 <record id="view_filter_todo_task" model="ir.ui.view">
557 <field name="name">To-do Task Filter</field>
558 <field name="model">todo.task</field>
559 <field name="arch" type="xml">
560
561 <search>
562 <field name="name"/>
563 <filter string="Not Done"
564 domain="[('is_done','=',False)]"/>
565 <filter string="Done"
566 domain="[('is_done','!=',False)]"/>
Page 15
capitulo-2.md 07/02/2018

567 </search>
568
569
570
571
572 </field>
573 </record>
574 ```
575
576 Los elementos `<field>` definen campos que también se buscan al
escribir en el cuadro de búsqueda. Los elementos `<filter>` añaden
condiciones de filtro predefinidas, que se pueden alternar con un clic
de usuario, definido mediante el uso de una sintaxis específica.
577
578 ## La capa de lógica de negocio
579
580 Ahora vamos a añadir algo de lógica a nuestros botones. Esto se hace
con código Python, utilizando los métodos de la clase de modelos Python.
581 ### Añadiendo lógica de negocio
582
583 Debemos editar el archivo Python `todo_model.py` para agregar a la
clase los métodos llamados por los botones. Primero, necesitamos
importar la nueva API, así que agréguala a la declaración de
importación en la parte superior del archivo Python:
584 ```
585 from odoo import models, fields, api
586 ```
587
588 La acción del botón **Toggle Done** será muy simple: solo cambia la
bandera **Is Done?**. Para la lógica de los registros, utiliza el
decorador `@api.multi`. Aquí, `self` representará un conjunto de
registros, y entonces deberíamos hacer un bucle a través de cada
registro.
589
590 Dentro de la clase TodoTask, añade esto:
591 ```
592 @api.multi
593 def do_toggle_done(self):
594 for task in self:
595 task.is_done = not task.is_done
596 return True
597 ```
598 El código pasa por todos los registros de tarea y, para cada uno,
modifica el campo `is_done`, invirtiendo su valor. El método no
necesita devolver nada, pero debemos tenerlo al menos para devolver un
valor `True`. La razón es que los clientes pueden utilizar XML-RPC para
llamar a estos métodos y este protocolo no admite funciones de servidor
devolviendo sólo un valor `None`.
599
600 Para el botón **Clear All Done**, queremos ir un poco más lejos. Debe
buscar todos los registros activos que están hechos, y hacerlos
inactivos. Normalmente, se espera que los botones de formulario actúen
sólo en el registro seleccionado, pero en este caso, queremos que actúe
también en registros distintos del actual:
601 ```
602 @api.model
603 def do_clear_done(self):
604 dones = self.search([('is_done', '=', True)])
605 dones.write({'active': False})
606 return True
607 ```
Page 16
capitulo-2.md 07/02/2018

608
609
610 En los métodos decorados con `@ api.model`, la variable `self`
representa el modelo sin registro en particular. Construiremos un
conjunto de registros `dones` que contenga todas las tareas marcadas
como terminadas. A continuación, establecemos el indicador `active`
para `False` en ellos.
611
612 El método de búsqueda es un método API que devuelve los registros que
cumplen algunas condiciones. Estas condiciones están escritas en un
dominio, que es una lista de tripletes. Exploraremos los dominios con
más detalle en el Capítulo 6, *Vistas – Diseñando la interfaz de
usuario*.
613
614 El método `write` establece los valores de una vez en todos los
elementos del conjunto de registros. Los valores a escribir se
describen utilizando un diccionario. Usar `write here` es más eficiente
que iterar a través del conjunto de registros para asignar el valor a
cada uno de ellos uno por uno.
615 ### Añadiendo de pruebas
616
617 Ahora debemos agregar pruebas para la lógica de negocio. Idealmente,
queremos que cada línea de código sea cubierta por al menos un caso de
prueba. En `tests / test_todo.py`, agregua unas cuantas líneas más de
código al método `test_create ()`:
618 ```
619 # def test_create(self):
620 # ...
621
622 # Test Toggle Done
623 task.do_toggle_done()
624 self.assertTrue(task.is_done)
625 # Test Clear Done
626 Todo.do_clear_done()
627 self.assertFalse(task.active)
628
629 ```
630
631
632 Si ahora ejecutamos las pruebas y los métodos del modelo están
correctamente escritos, no deberíamos ver ningún mensaje de error en el
registro del servidor:
633
634 ```
635 $ ./odoo-bin -d todo -i todo_app --test-enable
636
637 ```
638 ## Configurando la seguridad de acceso
639
640 Es posible que haya notado que, al cargar, nuestro módulo recibe un
mensaje de advertencia en el registro del servidor:
641
642 **The model todo.task has no access rules, consider adding one.**
643
644
645 (**El modelo todo.task no tiene reglas de acceso, considere agregar
una.**)
646
647
648
649
Page 17
capitulo-2.md 07/02/2018

650 El mensaje es bastante claro: nuestro nuevo modelo no tiene reglas de


acceso, por lo que no puede ser utilizado por nadie que no sea el
superusuario de admin. Como superusuario, el admin ignora las reglas de
acceso a datos, y es por eso que hemos podido utilizar el formulario
sin errores. Pero debemos corregir esto antes de que otros usuarios
puedan usar nuestro modelo.
651
652 Otra cuestión que todavía tenemos que abordar es que queremos que las
tareas pendientes sean privadas para cada usuario. Odoo soporta reglas
de acceso a nivel de fila, que usaremos para implementar eso.
653
654 ### Probando la seguridad de acceso
655
656 De hecho, nuestras pruebas deben estar fallando en este momento debido
a las reglas de acceso que faltan. Ellas no están porque se hacen con
el usuario admin. Por lo tanto, debemos cambiarlos para que utilicen el
usuario Demo en su lugar.
657
658 Para ello, debemos editar el archivo `tests / test_todo.py` para añadir
un método `setUp`:
659 ```
660 # class TestTodo(TransactionCase):
661
662 def setUp(self, *args, **kwargs):
663 result = super(TestTodo, self).setUp(*args, \
664 **kwargs)
665 user_demo = self.env.ref('base.user_demo')
666 self.env= self.env(user=user_demo)
667 return result
668 ```
669
670
671 Esta primera instrucción llama al código `setUp` de la clase padre. Los
siguientes cambian el entorno utilizado para ejecutar las pruebas,
`self.env`, a una nueva usando el usuario `Demo`. No se necesitan más
cambios en las pruebas que ya escribimos.
672
673 También debemos añadir un caso de prueba para asegurarnos de que los
usuarios sólo pueden ver sus propias tareas. Para ello, primero,
agregua una importación adicional en la parte superior:
674 ```
675 from odoo.exceptions import AccessError
676
677 ```
678 A continuación, agregua un método adicional a la clase de prueba:
679 ```
680 def test_record_rule(self):
681 "Test per user record rules"
682 Todo = self.env['todo.task']
683 task = Todo.sudo().create({'name': 'Admin Task'})
684 with self.assertRaises(AccessError):
685 Todo.browse([task.id]).name
686
687 ```
688
689 Dado que nuestro método `env` ahora está utilizando el usuario de Demo,
usamos el método `sudo ()` para cambiar el contexto al usuario admin. A
continuación, lo usamos para crear una tarea que no debería ser
accesible para el usuario Demo.
690
691 Al intentar acceder a los datos de esta tarea, esperamos que se genere
Page 18
capitulo-2.md 07/02/2018

una excepción `AccessError`.


692
693 Si ejecutamos las pruebas ahora, deberían fallar, así que nos
encargamos de eso.
694
695 ### Añadiendo seguridad de control de acceso
696
697 Para obtener una imagen de qué información se necesita para agregar
reglas de acceso a un modelo, utiliza el cliente web y ve a **Settings
| Technical | Security | Access Controls List** :
698
699 ![Create](file:img/2-04.jpg)
700
701 ### Nota
702
703 Aquí podemos ver la ACL de algunos modelos. Indica, por grupo de
seguridad, qué acciones se permiten en los registros.
704
705 Esta información debe ser proporcionada por el módulo utilizando un
archivo de datos para cargar las líneas en el modelo `ir.model.access`.
Vamos a agregar acceso completo al grupo de empleados en el modelo. El
empleado es el grupo básico de acceso al que casi todos pertenecen.
706
707 Esto se hace utilizando un archivo CSV denominado `security /
ir.model.access.csv`. Vamos a agregarlo con el siguiente contenido:
708 ```
709 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unl
ink
710 acess_todo_task_group_user,todo.task.user,model_todo_task,base.group_user
,1,1,1,1
711
712 ```
713
714 El nombre de archivo corresponde al modelo para cargar los datos, y la
primera línea del archivo tiene los nombres de columna. Estas son las
columnas proporcionadas en nuestro archivo CSV:
715
716 + `id` es el identificador externo del registro (también conocido como
XML ID). Debe ser único en nuestro módulo.
717 + `name` es un título de descripción. Es sólo informativo y es mejor si
se mantiene único. Los módulos oficiales normalmente usan una cadena
separada por puntos con el nombre del modelo y el grupo. Siguiendo esta
convención, utilizamos `todo.task.user`.
718 + `model_id` es el identificador externo del modelo al que estamos
dando acceso. Los modelos tienen XML IDs generados automáticamente por
el ORM: para `todo.task`, el identificador es `model_todo_task`.
719 + `group_id` identifica el grupo de seguridad para dar permisos. Los
más importantes son proporcionados por el módulo base. El grupo
Empleado es un caso así y tiene el identificador `base.group_user`.
720 + Los campos `perm` marcan el acceso a garantizar `read`, `write`, `
create` o `un link` (borrar) el acceso.
721
722 No debemos olvidar añadir la referencia a este nuevo archivo en el
atributo de datos del descriptor `__manifest__.py`. Debe tener un
aspecto como este:
723 ```
724 'data': [
725 'security/ir.model.access.csv',
726 'views/todo_view.xml',
727 'views/todo_menu.xml',
728 ],
Page 19
capitulo-2.md 07/02/2018

729
730 ```
731 Como antes, actualice el módulo para que estas adiciones entren en
vigor. El mensaje de advertencia debe desaparecer, y podemos confirmar
que los permisos están bien iniciando sesión con el usuario `demo` (la
contraseña también es `demo`). Si ejecutamos nuestras pruebas ahora
solo deberían fallar el caso de prueba `test_record_rule`.
732 ### Reglas de acceso a nivel de fila
733
734 Podemos encontrar la opción **Record Rules** en el menú **Technical**,
junto con **Access Control List*.
735
736 Las reglas de registro se definen en el modelo `ir.rule`. Como de
costumbre, necesitamos proporcionar un nombre distintivo. También
necesitamos el modelo en el que operan y el filtro de dominio que se
utilizará para la restricción de acceso. El filtro de dominio utiliza
la lista usual de tuplas sintáctica utilizada en Odoo.
737
738 Por lo general, las reglas se aplican a algunos grupos de seguridad en
particular. En nuestro caso, lo haremos aplicable al grupo Empleados.
Si no se aplica a ningún grupo de seguridad en particular, se considera
global (el campo `global` se establece automáticamente en `True`). Las
reglas globales son diferentes porque imponen restricciones que las
reglas no globales no pueden anular.
739
740 Para agregar la regla de registro, debemos crear un archivo `security /
todo_access_rules.xml` con el siguiente contenido:
741 ```
742 <?xml version="1.0" encoding="utf-8"?>
743 <odoo>
744 <data noupdate="1">
745 <record id="todo_task_user_rule" model="ir.rule">
746 <field name="name">ToDo Tasks only for owner</field>
747 <field name="model_id" ref="model_todo_task"/>
748 <field name="domain_force">
749 [('create_uid','=',user.id)]
750 </field>
751 <field name="groups" eval="
752 [(4,ref('base.group_user'))]"/>
753 </record>
754 </data>
755 </odoo>
756
757 ```
758
759 ### Nota
760
761 Observa el atributo `noupdate = "1"`. Significa que estos datos no se
actualizarán en actualizaciones de módulos. Esto le permitirá ser
personalizado más adelante ya que las actualizaciones de módulos no
destruirán los cambios realizados por el usuario. Pero ten en cuenta
que esto también será el caso durante el desarrollo, por lo que es
posible que desees establecer `noupdate = "0" ` durante el desarrollo
hasta que estéss satisfecho con el archivo de datos.
762
763 En el campo de grupos, también encontrarás una expresión especial. Es
un campo relacional de uno a muchos, y tienen una sintaxis especial
para operar. En este caso, la tupla (4, x) indica anexar `x` a los
registros, y aquí `x` es una referencia al grupo Empleados,
identificado por `base.group_user`. Esta sintaxis especial de escritura
de uno-a-muchos se discute con más detalle en el Capítulo 4, *Datos de
Page 20
capitulo-2.md 07/02/2018

Módulo*.
764
765 Como antes, debemos añadir el archivo a `__manifest__.py` antes de
poder cargarlo en el módulo:
766 ```
767 'data': [
768 'security/ir.model.access.csv',
769 'security/todo_access_rules.xml',
770 'todo_view.xml',
771 'todo_menu.xml',
772 ],
773
774 ```
775
776 Si lo hicimos bien, podemos ejecutar las pruebas de módulo y ahora
deben pasar.
777
778 ## Describiendo mejor el módulo
779
780 Nuestro módulo se ve bien. ¿Por qué no añadir un icono para que se vea
aún mejor? Para esto, solo necesitamos agregar al módulo un archivo
`static / description / icon.png` con el icono que se va a usar.
781
782 Estaremos reutilizando el icono de la aplicación existente **Notes**,
por lo que deberíamos copiar el archivo `odoo / addons / static /
description / icon.png` en el directorio `addons / todo_app / static /
description`.
783
784 Los siguientes comandos deben hacer ese truco para nosotros:
785
786 ```
787 $ mkdir -p ~/odoo-dev/custom-addons/todo_app/static/description
788 $ cp ~/odoo-dev/odoo/addons/note/static/description/icon.png
~/odoo-dev/custom-addons/todo_app/static/description
789
790 ```
791
792 Ahora, si actualizamos la lista de módulos, nuestro módulo debe
mostrarse con el nuevo icono.
793
794 También podemos añadir una descripción mejor para explicar lo que hace
y lo grandioso que es. Esto se puede hacer en la clave `description`
del archivo `__manifest__.py`. Sin embargo, la forma preferida es
agregar un archivo `README.rst` al directorio raíz del módulo.
795
796 ## Resumen
797
798 Hemos creado un nuevo módulo desde el principio, cubriendo los
elementos más utilizados en un módulo: modelos, los tres tipos básicos
de vistas (formulario, lista y búsqueda), lógica empresarial en los
métodos de modelo y seguridad de acceso.
799
800 En el proceso, nos familiarizamos con el proceso de desarrollo de
módulos, que implica actualizaciones de módulos y reinicios del
servidor de aplicaciones para hacer que los cambios graduales sean
efectivos en Odoo.
801
802 Recuerde siempre, cuando se agregan campos del modelo, se necesita una
actualización. Al cambiar el código de Python, incluyendo el archivo de
manifiesto, se necesita un reinicio. Al cambiar archivos XML o CSV, se
necesita una actualización; También, en caso de duda, haz lo siguiente:
Page 21
capitulo-2.md 07/02/2018

reinicia el servidor y actualiza los módulos.


803
804 En el siguiente capítulo, aprenderás cómo construir módulos que se
apilarán en los existentes para agregar características.
805
806
807
808
809

Page 22
capitulo-3.md 07/02/2018

1 # Capítulo 3. Herencia - Ampliando aplicaciones existentes.


2 Una de las características más potentes de Odoo es la capacidad de
agregar características sin modificar directamente los objetos
subyacentes.
3
4 Esto se logra a través de mecanismos de herencia, funcionando como
capas de modificación sobre objetos existentes. Estas modificaciones
pueden ocurrir en todos los niveles: modelos, vistas y lógica de
negocio. En lugar de modificar directamente un módulo existente,
creamos un nuevo módulo para agregar las modificaciones deseadas.
5
6 En este capítulo, aprenderá cómo escribir tus propios módulos de
extensión, lo que te permitirá aprovechar las aplicaciones existentes
de núcleo o comunidad. Como un ejemplo relevante, aprenderás a agregar
las funciones sociales y de mensajería de Odoo a sus propios módulos.
7
8 ## Añadiendo capacidades de uso compartido a la aplicación To - Do
9
10 Nuestra aplicación de To-Do ahora permite a los usuarios gestionar de
forma privada sus propias tareas pendientes. ¿No será genial llevar la
aplicación a otro nivel agregando funcionalidades de colaboración y
redes sociales? Podremos compartir tareas y discutirlas con otras
personas.
11
12 Lo haremos con un nuevo módulo para ampliar la aplicación To-Do
previamente creada y agregar estas nuevas características utilizando
los mecanismos de herencia. Esto es lo que esperamos lograr al final de
este capítulo:
13
14 ![Mytodo](file:img/3-01.jpg)
15
16 Este será nuestro plan de trabajo para las extensiones de
características que se implementarán:
17
18 + Amplía el modelo de tareas, como el usuario responsable de la tarea
19 + Modifica la lógica de negocio para que sólo funcione en las tareas
del usuario actual, en lugar de todas las tareas que el usuario pueda ver
20 + Amplía las vistas para agregar los campos necesarios a las vistas
21 + Añade funciones de redes sociales: una pared de mensajes y los
seguidores
22
23 Comenzaremos a crear el esqueleto básico de un nuevo módulo `todo_user`
junto al módulo `todo_app`. Siguiendo el ejemplo de instalación del
Capítulo 1, *Iniciando con desarrollo Odoo*, estamos hospedando
nuestros módulos en `~ / odoo-dev / custom-addons /`. Deberíamos añadir
allí un nuevo directorio `todo_user` para el módulo, que contenga un
archivo vacío `__init__.py`.
24
25 Ahora crea `todo_user / __ manifest__.py`, que contenga este código:
26
27 ```
28 { 'name': 'Multiuser To-Do',
29 'description': 'Extend the To-Do app to multiuser.',
30 'author': 'Daniel Reis',
31 'depends': ['todo_app'], }
32 ```
33
34
35 No lo hemos hecho aquí, pero incluir las claves de `summary` y claves
`category` puede ser importante al publicar módulos en la tienda de
aplicaciones en línea de Odoo.
Page 1
capitulo-3.md 07/02/2018

36 Observa que hemos añadido la dependencia explícita al módulo


`todo_app`. Esto es necesario e importante para que el mecanismo de
herencia funcione correctamente. Y de ahora en adelante, cuando el
módulo `todo_app` se actualice, todos los módulos dependiendo de él,
como el módulo `todo_user`, también serán actualizados.
37
38 A continuación, instálalo. Debería ser suficiente actualizar la lista
de módulos utilizando la opción de menú **Update Apps List** debajo de
**Apps**; Busca el nuevo módulo en la lista de **Apps** y haz clic en
el botón **Install**. Ten en cuenta que esta vez tendrás que quitar el
filtro de aplicaciones predeterminado para ver el nuevo módulo en la
lista, ya que no se marca como una aplicación. Para obtener
instrucciones más detalladas sobre cómo descubrir e instalar un módulo,
consulta el Capítulo 1, *Iniciando con el desarrollo Odoo*.
39
40 Ahora, vamos a empezar a añadir nuevas características a la misma.
41
42 ## Extediendo modelos
43
44 Los nuevos modelos se definen a través de las clases de Python.
Extenderlos también se hace a través de clases de Python, pero con la
ayuda de un mecanismo de herencia específico de Odoo.
45
46 Para extender un modelo existente, usamos una clase Python con un
atributo `_inherit`. Esto identifica el modelo a ser extendido. La
nueva clase hereda todas las características del modelo Odoo padre, y
solo necesitamos declarar las modificaciones que queremos introducir.
47
48 De hecho, los modelos Odoo existen fuera de nuestro módulo particular
de Python, en un registro central. Este registro, se puede acceder
desde los métodos del modelo utilizando `self.env [<model name>]`. Por
ejemplo, para obtener una referencia al objeto que representa el modelo
`res.partner`, escribiríamos `self.env ['res.partner']`.
49
50 Para modificar un modelo Odoo, obtenemos una referencia a su clase de
registro y luego realizamos cambios en el sitio en él. Esto significa
que estas modificaciones también estarán disponibles en todas partes
donde se utilice este nuevo modelo.
51
52 Durante el arranque del servidor Odoo, el módulo que carga la secuencia
es relevante: las modificaciones realizadas por un módulo
complementario sólo serán visibles para los módulos complementarios
cargados posteriormente. Por lo tanto, es importante que las
dependencias del módulo se establezcan correctamente, asegurando que
los módulos que proporcionan los modelos que usamos estén incluidos en
nuestro árbol de dependencias.
53
54 ### Agregando campos a un modelo
55
56 Vamos a ampliar el modelo `todo.task` para añadir un par de campos a
ella: el usuario responsable de la tarea y una fecha límite.
57
58 Las pautas de estilo de codificación recomendaron tener un
subdirectorio `models /` con un archivo por modelo Odoo. Así que
debemos comenzar creando el subdirectorio modelo, haciéndolo
Python-importable.
59
60 Edita el archivo `todo_user / __ init__.py` para tener este contenido:
61 ```
62 from .import models
63 ```
Page 2
capitulo-3.md 07/02/2018

64
65
66 Crea `todo_user / models / __ init__.py` con el siguiente código:
67 ```
68 from . import todo_task
69 ```
70
71
72 La línea anterior le indica a Python que busque un archivo llamado
odoo_task.py en el mismo directorio y lo importe. Por lo general,
tendrías una línea `from` para cada archivo Python en el directorio:
73 ahora crea el archivo `todo_user/models/todo_task.py` para extender el
modelo original:
74 ```
75 # -*- coding: utf-8 -*-
76 from odoo import models, fields, api
77 class TodoTask(models.Model):
78 _inherit = 'todo.task'
79 user_id = fields.Many2one('res.users', 'Responsible')
80 date_deadline = fields.Date('Deadline')
81 ```
82 El nombre de clase `TodoTask` es local para este archivo de Python y,
en general, es irrelevante para otros módulos. El atributo de clase
`_inherit` es la clave aquí: le dice a Odoo que esta clase está
heredando y modificando así el modelo `todo.task`.
83
84 #### Nota
85 Observa que el atributo `_name` está ausente. No es necesario porque ya
está heredado del modelo padre.
86
87 Las dos líneas siguientes son declaraciones de campo regulares. El
campo `user_id` representa un usuario del modelo de usuarios
`res.users`. Es un campo `Many2one`, que es equivalente a una clave
extranjera en la jerga de la base de datos. El `date_deadline` es un
simple campo de fecha. En el capítulo 5, *Modelos - Estructurando de
los datos de aplicación*, explicaremos los tipos de campos disponibles
en Odoo con más detalle.
88
89 Para que los nuevos campos se agreguen a la tabla de base de datos de
soporte del modelo, necesitamos realizar una actualización de módulo.
Si todo sale como se esperaba, deberías ver los nuevos campos al
inspeccionar el modelo `todo.task` en las opciones de menú **Technical
| Database Estructure | Models**.
90
91 ### Modificando campos existentes
92
93 Como puedes ver, agregar nuevos campos a un modelo existente es
bastante sencillo. Desde Odoo 8, también es posible modificar los
atributos en los campos heredados existentes. Se hace agregando un
campo con el mismo nombre y estableciendo valores sólo para los
atributos que se van a cambiar.
94
95 Por ejemplo, para agregar una herramienta de ayuda al campo de nombre,
agregamos esta línea a `todo_ task.py`, descrito anteriormente:
96
97 ```
98 name = fields.Char(help="What needs to be done?")
99 ```
100
101 Esto modifica el campo con los atributos especificados, dejando sin
modificar todos los otros atributos que no se utilizan explícitamente
Page 3
capitulo-3.md 07/02/2018

aquí. Si actualizamos el módulo, ve a un formulario de tareas


pendientes y haz una pausa en el cursor sobre el campo **Description**;
Se mostrará el texto de la herramienta de información.
102
103 ### Modificando los métodos del modelo
104
105 La herencia también funciona en el nivel de lógica empresarial. Agregar
nuevos métodos es simple: solo declara sus funciones dentro de la clase
de herencia.
106
107 Para extender o cambiar la lógica existente, el método correspondiente
puede anularse declarando un método con el mismo nombre. El nuevo
método reemplazará al anterior, y también puede extender el código de
la clase heredada, utilizando el método `super ()` de Python para
llamar al método padre. Puede entonces agregar nueva lógica alrededor
de la lógica original antes y después de que el método `super ()` sea
llamado.
108
109 #### Tip
110
111 Es mejor evitar cambiar la firma de la función del método (es decir,
mantener los mismos argumentos) para asegurarse de que las llamadas
existentes en él seguirán funcionando correctamente. En caso de que
necesites agregar parámetros adicionales, haz de ellos argumentos de
palabras clave opcionales (con un valor predeterminado).
112
113 La acción original **Clear All Done** no es apropiada para nuestro
módulo de intercambio de tareas ya que borra todas las tareas,
independientemente de su usuario. Tenemos que modificarlo para que
borre sólo las tareas del usuario actual.
114
115 Para ello, anularemos (o reemplazaremos) el método original por una
nueva versión que primero encuentre la lista de tareas completadas para
el usuario actual y luego las inactive:
116 ```
117 @api.multi
118 def do_clear_done(self):
119 domain = [('is_done', '=', True),
120 '|', ('user_id', '=', self.env.uid),
121 ('user_id', '=', False)]
122 dones = self.search(domain)
123 dones.write({'active': False})
124 return True
125 ```
126
127
128
129 Para mayor claridad, primero construimos la expresión de filtro que se
utilizará para encontrar los registros que se van a borrar.
130
131 Esta expresión de filtro sigue una sintaxis específica de Odoo
denominada `domain`: que es una lista de condiciones, donde cada
condición es una tupla.
132
133 Estas condiciones se unen implícitamente con el operador AND (&). Para
la operación OR, una tubería, | , se utiliza en el lugar de una tupla,
y se une a las dos condiciones siguientes. Vamos a entrar en más
detalles acerca de los dominios en el Capítulo 6, *Vistas - Diseñando
de la interfaz de usuario*.
134
135 El dominio utilizado aquí filtra todas las tareas hechas `('is_done',
Page 4
capitulo-3.md 07/02/2018

'=', True)` que tienen el usuario actual como responsable ('user_id',


'=', self.env.uid) o no tienen un set de usuario actual `('user_id',
'=', False)`.
136
137 A continuación, utilizamos el método de búsqueda para obtener un
conjunto de registros con los registros hechos para actuar y,
finalmente, hacer una escritura masiva en ellos estableciendo el campo
activo a `False`. El valor Python `False` aquí representa el valor
`NULL` de la base de datos.
138
139 En este caso, hemos sobrescrito completamente el método padre,
reemplazándolo con una nueva implementación, pero eso no es lo que
normalmente queremos hacer. En su lugar, debemos ampliar la lógica
existente con algunas operaciones adicionales. De lo contrario,
podríamos romper las características ya existentes.
140
141 Para que el método principal mantenga la lógica ya existente, usamos la
construcción `super ()` de Python para llamar a la versión del método
padre. Veamos un ejemplo de esto.
142
143 Podemos mejorar el método `do_toggle_done ()` para que sólo realice su
acción en las tareas asignadas al usuario actual. Este es el código
para lograrlo:
144 ```
145 from odoo.exceptions import ValidationError
146 # ...
147 # class TodoTask(models.Model):
148 # ...
149 @api.multi
150 def do_toggle_done(self):
151 for task in self:
152 if task.user_id != self.env.user:
153 raise ValidationError(
154 'Only the responsible can do this!')
155 return super(TodoTask, self).do_toggle_done()
156
157 ```
158
159 El método en la clase heredada comienza con un bucle `for` para
comprobar que ninguna de las tareas a activar pertenece a otro usuario.
Si estos chequeos pasan, entonces continúa llamando al método de clase
padre, usando `super ()`. Si no se plantea un error, y debemos utilizar
para ello las excepciones incorporadas de Odoo. Los más relevantes son
`ValidationError`, utilizado aquí y `UserError`.
160
161 Estas son las técnicas básicas para reemplazar y extender la lógica
empresarial definida en las clases de modelo. A continuación, veremos
cómo ampliar las vistas de la interfaz de usuario.
162
163 ## Extendiendo vistas
164 Las formas, listas y vistas de búsqueda se definen utilizando las
estructuras de `arch XML`. Para ampliar vistas, necesitamos una forma
de modificar este XML. Esto significa localizar elementos XML y luego
introducir modificaciones en esos puntos.
165
166 Las vistas heredadas permiten justamente eso. Una declaración de vista
heredada tiene este aspecto:
167 ```
168 <record id="view_form_todo_task_inherited"
169 model="ir.ui.view">
170 <field name="name">Todo Task form - User
Page 5
capitulo-3.md 07/02/2018

171 extension</field>
172 <field name="model">todo.task</field>
173 <field name="inherit_id"
174 ref="todo_app.view_form_todo_task"/>
175 <field name="arch" type="xml">
176 <!-- ...match and extend elements here! ... -->
177 </field
178 </record>
179
180 ```
181 El campo `inherit_id` identifica la vista que debe extenderse
consultando su identificador externo usando el atributo especial `ref`.
Los identificadores externos se tratarán con más detalle en el Capítulo
4, *Datos del módulo*.
182
183 Siendo XML, la mejor manera de localizar elementos en XML es usar
expresiones XPath. Por ejemplo, tomando la vista de formulario definida
en el capítulo anterior, una expresión XPath para localizar el elemento
`<field name = "is_done">` es `// field [@name] = 'is_done'`. Esta
expresión encuentra cualquier elemento `field` con un atributo `name`
igual a `is_done`. Puedes encontrar más información sobre XPath en
https://docs.python.org/2/library/xml.etree.elementtree.html#xpath-suppor
t.
184
185 Si una expresión XPath coincide con varios elementos, sólo se
modificará la primera. Por lo tanto, deben ser hechos lo más específico
posible, utilizando atributos únicos. El uso del atributo `name` es la
forma más fácil de asegurar que encontramos los elementos exactos que
queremos utilizar un punto de extensión. Por lo tanto, es importante
configurarlos en nuestros elementos de vista XML.
186
187 Una vez localizado el punto de extensión, puedes modificarlo o tener
elementos XML añadidos cerca de él. Como un ejemplo práctico, para
agregar el campo `date_deadline` antes del campo `is_done`,
escribiremos lo siguiente en `arch`:
188 ```
189 <xpath expr="//field[@name]='is_done'" position="before">
190 <field name="date_deadline" />
191 </xpath>
192 ```
193 Afortunadamente, Odoo proporciona la notación de acceso directo para
esto, así que la mayoría de las veces podemos evitar la sintaxis XPath
por completo. En lugar del elemento XPath anterior, podemos utilizar
sólo la información relacionada con el tipo de tipo de elemento para
localizar y sus atributos distintivos, y en lugar de la anterior XPath,
escribimos esto:
194 ```
195 <field name="is_done" position="before">
196 <field name="date_deadline" />
197 </field>
198 ```
199 Sólo ten en cuenta que si el campo aparece más de una vez en la misma
vista, siempre debe utilizar la sintaxis XPath. Esto es porque Odoo se
detendrá en la primera aparición del campo y puede aplicar sus cambios
al campo incorrecto.
200
201 A menudo, queremos agregar nuevos campos junto a los existentes, por lo
que la etiqueta `<field>` se utilizará como localizador con frecuencia.
Pero cualquier otra etiqueta se puede utilizar: `<sheet>, <group>,
<div>`, etc. El atributo `name` suele ser la mejor opción para los
elementos coincidentes, pero a veces, es posible que necesitemos
Page 6
capitulo-3.md 07/02/2018

utilizar otra cosa: el elemento de `CSS class`, por ejemplo. Odoo


encontrará el primer elemento que tiene al menos todos los atributos
especificados.
202
203 #### Nota
204
205 Antes de la versión 9.0, el atributo `string` (para la etiqueta de
texto que se muestra) también podría utilizarse como localizador de
extensión. Desde 9.0, esto ya no está permitido. Esta limitación está
relacionada con el mecanismo de traducción del lenguaje que opera en
esas cadenas.
206
207 El atributo `position` utilizado con el elemento localizador es
opcional y puede tener los siguientes valores:
208
209 + `after` agrega el contenido al elemento padre, después del nodo
coincidente.
210 + `before` añade el contenido, antes del nodo coincidente.
211 + `inside` (valor predeterminado) agrega el contenido dentro del nodo
compatible.
212 + `replace` reemplaza el nodo coincidente. Si se utiliza con contenido
vacío, elimina un elemento. Puesto que Odoo 10 también permite que se
envuelva un elemento con otro marcado, usando $ 0 en el contenido para
representar el elemento que se está reemplazando.
213 + `attributes` modifica los atributos XML del elemento emparejado. Esto
se hace utilizando en los elementos del contenido `<attribute name =
"attr-name">` con los nuevos valores de atributo que se deben establecer.
214
215 Por ejemplo, en el formulario Tarea, tenemos el campo activo, pero
tenerlo visible no es tan útil. Podríamos ocultarlo del usuario. Esto
puede hacerse estableciendo su atributo `invisible`:
216 ```
217 <field name="active" position="attributes">
218 <attribute name="invisible">1</attribute>
219 </field>
220 ```
221 Establecer el atributo `invisible` para ocultar un elemento es una
buena alternativa para usar el localizador `replace` para eliminar
nodos. Se debe evitar la eliminación de nodos, ya que puede romper
módulos dependientes que pueden depender del nodo eliminado como
marcador de posición para agregar otros elementos.
222
223 #### Ampliando de la vista de formulario
224
225 Juntando todos los elementos de formulario anteriores, podemos agregar
los nuevos campos y ocultar el campo `active`. La vista de herencia
completa para ampliar el formulario de tareas pendientes es la siguiente:
226 ```
227 <record id="view_form_todo_task_inherited"
228 model="ir.ui.view">
229 <field name="name">Todo Task form - User
230 extension</field>
231 <field name="model">todo.task</field>
232 <field name="inherit_id"
233 ref="todo_app.view_form_todo_task"/>
234
235 <field name="arch" type="xml">
236 <field name="name" position="after">
237 <field name="user_id">
238 </field>
239 <field name="is_done" position="before">
Page 7
capitulo-3.md 07/02/2018

240 <field name="date_deadline" />


241 </field>
242 <field name="active" position="attributes">
243 <attribute name="invisible">1</attribute>
244 </field>
245 </field>
246
247
248
249
250 </record>
251 ```
252 Esto debe agregarse a un archivo `views / todo_task.xml` en nuestro
módulo, dentro del elemento `<odoo>`, como se muestra en el capítulo
anterior.
253
254 #### Nota
255
256 Las vistas heredadas también se pueden heredar, pero como esto crea
dependencias más intrincadas, debe evitarse. Debes preferir heredar de
la vista original siempre que sea posible.
257
258 Además, no debemos olvidar añadir el atributo `data` al archivo
descriptor `__manifest__.py`:
259 ```
260 'data': ['views/todo_task.xml'],
261 ```
262 ### Ampliandola vista de árbol y búsqueda
263
264 Las extensiones de vista de árbol y búsqueda también se definen
utilizando la estructura XML de `arch`, y pueden extenderse de la misma
forma que las vistas de formulario. Continuaremos nuestro ejemplo
ampliando las vistas de lista y de búsqueda.
265
266 Para la vista de lista, queremos añadirle el campo de usuario:
267 ```
268 <record id="view_tree_todo_task_inherited"
269 model="ir.ui.view">
270 <field name="name">Todo Task tree - User
271 extension</field>
272 <field name="model">todo.task</field>
273
274 <field name="inherit_id"
275
276
277
278
279
280 ref="todo_app.view_tree_todo_task"/>
281
282
283
284
285 <field name="arch" type="xml">
286
287 <field name="name" position="after">
288 <field name="user_id" />
289 </field>
290
291
292
Page 8
capitulo-3.md 07/02/2018

293
294 </field
295 </record>
296 ```
297 Para la vista de búsqueda, agregamos la búsqueda por el usuario y
filtros predefinidos para las propias tareas del usuario y las tareas
no asignadas a nadie:
298 ```
299 <record id="view_filter_todo_task_inherited"
300 model="ir.ui.view">
301 <field name="name">Todo Task tree - User
302 extension</field>
303 <field name="model">todo.task</field>
304
305 <field name="inherit_id"
306
307
308
309
310
311 ref="todo_app.view_filter_todo_task"/>
312
313
314
315
316 <field name="arch" type="xml">
317
318 <field name="name" position="after">
319 <field name="user_id" />
320 <filter name="filter_my_tasks" string="My Tasks"
321 domain="[('user_id','in',[uid,False])]" />
322 <filter name="filter_not_assigned" string="Not
323
324
325
326
327
328 Assigned" domain="[('user_id','=',False)]" />
329 </field>
330
331
332
333
334 </field
335 </record>
336
337 ```
338
339 No se preocupe demasiado por la sintaxis específica de estas vistas.
Los cubriremos con más detalle en el Capítulo 6, *Vistas - Diseñando la
interfaz de usuario*.
340
341 ## Más modelos de mecanismos de herencia
342
343 Hemos visto la extensión básica de los modelos, llamada *herencia de
clase* en la documentación oficial. Este es el uso más frecuente de la
herencia, y es más fácil pensar en ello como una *extensión in situ*.
Tu tomas un modelo y lo extiendes. A medida que agregas nuevas
funciones, se agregan al modelo existente. No se crea un nuevo modelo.
También podemos heredar de varios modelos padre, estableciendo una
lista de valores para el atributo `_inherit`. Con esto, podemos hacer
Page 9
capitulo-3.md 07/02/2018

uso de *clases de mixin*. Las clases de Mixin son modelos que


implementan características genéricas que podemos agregar a otros
modelos. No se espera que se usen directamente, y son como un
contenedor de características listas para ser agregadas a otros modelos.
344
345 Si también usamos el atributo `_name` con un valor diferente del modelo
padre, obtenemos un nuevo modelo que reutiliza las características del
heredado pero con su propia tabla de base de datos y datos. La
documentación oficial llama a este *prototipo de herencia*. Aquí tu
tomas un modelo y creas uno nuevo que es una copia de la vieja. A
medida que agregas nuevas funciones, se agregan al nuevo modelo. El
modelo existente no se modifica.
346
347 También existe el método de *delegación de herencia*, utilizando el
atributo `_inherits`. Este permite que un modelo contenga otros modelos
de manera transparente para el observador mientras que, detrás de
escenas, cada modelo maneja sus propios datos. Tu tomas un modelo y lo
extiendes. A medida que agrega nuevas funciones, se agregan al nuevo
modelo. El módulo existente no se cambia. Los registros del nuevo
modelo tienen un enlace a un registro en el modelo original y los
campos del modelo original están expuestos y se pueden usar
directamente en el nuevo modelo.
348
349 Exploremos estas posibilidades con más detalle.
350
351 ### Copiando características con herencia de prototipo
352
353 El método que usamos antes para extender un modelo utiliza sólo el
atributo `_inherit`. Definimos una clase heredando el modelo
`todo.task` y le agregamos algunas características. El atributo de
clase `_name` no se estableció explícitamente; Implícitamente, era
`todo.task`.
354
355 Sin embargo, el uso del atributo `_name` nos permite crear un nuevo
modelo copiando las características de las heredadas. Aquí hay un
ejemplo:
356 ```
357 from odoo import models
358 class TodoTask(models.Model):
359 _name = 'todo.task'
360 _inherit = 'mail.thread'
361 ```
362
363 Esto amplía el modelo `todo.task` copiando en él las características
del modelo `mail.thread`. El modelo `mail.thread` implementa las
características de mensajes y seguidores de Odoo y es reutilizable para
que sea fácil agregar esas características a cualquier modelo.
364
365 Copiar significa que los métodos y campos heredados también estarán
disponibles en el modelo de herencia. Para los campos, esto significa
que también se crearán y almacenarán en las tablas de la base de datos
del modelo de destino. Los registros de datos de los modelos originales
(heredados) y los nuevos (hereditarios) se mantienen sin relación. Sólo
se comparten las definiciones.
366
367 En un momento, vamos a discutir en detalle cómo usar esto para agregar
`mail.thread` y sus características de redes sociales a nuestro módulo.
En la práctica, al usar mixins, rara vez heredamos de modelos regulares
porque esto causa duplicación de las mismas estructuras de datos.
368
369 Odoo también proporciona el mecanismo de herencia de delegación que
Page 10
capitulo-3.md 07/02/2018

evita la duplicación de estructura de datos, por lo que suele ser


preferido cuando se hereda de modelos regulares. Miremos esto con más
detalle.
370
371 ### Incorporando modelos mediante la herencia de delegación
372
373 La herencia de delegación se utiliza menos frecuentemente, pero puede
proporcionar soluciones muy convenientes. Se utiliza a través del
atributo `_inherits` (nota la `s` adicional) con mapeo de diccionario
modelos heredados con campos que se enlazan a ellos.
374
375 Un buen ejemplo de esto es el modelo de usuario estándar, `res.users`;
Tiene un modelo de Socio incrustado en él:
376 ```
377 from odoo import models, fields
378 class User(models.Model):
379 _name = 'res.users'
380 _inherits = {'res.partner': 'partner_id'}
381 partner_id = fields.Many2one('res.partner')
382 ```
383 Con la herencia de delegación, el modelo `res.users` incrusta el modelo
heredado `res.partner` de forma que cuando se crea una nueva clase
`User`, también se crea un socio y se mantiene una referencia en el
campo `partner_id` de la clase `User`. Tiene algunas similitudes con el
concepto de polimorfismo en la programación orientada a objetos.
384
385 A través del mecanismo de delegación, todos los campos del modelo
heredado y del Socio están disponibles como si fueran campos de `user`.
Por ejemplo, los campos Nombre y dirección del asociado se exponen como
campos de usuario, pero de hecho, se almacenan en el modelo Socio
asociado y no se produce duplicación de datos.
386
387 La ventaja de esto, en comparación con la herencia de prototipo, es que
no hay necesidad de repetir estructuras de datos, como direcciones, a
través de varias tablas. Cualquier modelo nuevo que necesite incluir
una dirección puede delegarla a un modelo de socio incrustado. Y si las
modificaciones se introducen en los campos de la dirección del socio,
¡éstas están inmediatamente disponibles para todos los modelos que lo
incrustan!
388
389 #### Nota
390
391 Tenga en cuenta que con la herencia de delegación, los campos se
heredan, pero los métodos no.
392
393 ### Agregando las funciones de redes sociales
394
395 El módulo de red social (nombre técnico `mail`) proporciona el panel de
mensajes que se encuentra en la parte inferior de muchos formularios y
la función **Seguidores**, así como la lógica de los mensajes y las
notificaciones. Esto es algo que a menudo queremos añadir a nuestros
modelos, así que vamos a aprender cómo hacerlo.
396
397 Las características de mensajería de redes sociales son proporcionadas
por el modelo `mail.thread` del módulo de `mail`. Para agregarlo a un
modelo personalizado, debemos hacer lo siguiente:
398
399 + Tener un módulo que dependa de `mail`
400 + Tener la clase heredada de `mail.thread`
401 + Tener añadidos los seguidores y widgets de subproceso a la vista de
formulario
Page 11
capitulo-3.md 07/02/2018

402 + Opcionalmente, necesitamos establecer reglas de registro para los


seguidores.
403
404 Sigamos la siguiente lista de verificación.
405
406 En cuanto al primer punto, nuestro módulo de extensión necesitará la
dependencia adicional de `mail` en el módulo `__manifest__.py manifest
file`:
407 ```
408 'depends': ['todo_app', 'mail'],
409 ```
410 Con respecto al segundo punto, la herencia en `mail.thread` se hace
usando el atributo `_inherit` que utilizamos antes. Pero nuestra
extensión de tareas pendientes ya está usando el atributo `_inherit`.
Afortunadamente, puede aceptar una lista de modelos de los que heredar,
por lo que podemos usar esto para que también incluya la herencia en
`mail.thread`:
411 ```
412 _name = 'todo.task'
413 _inherit = ['todo.task', 'mail.thread']
414 ```
415 `mail.thread` es un modelo abstracto. Los **modelos abstractos** son
como modelos regulares, excepto que no tienen una representación de
base de datos; no se crean tablas reales para ellos. Los modelos
abstractos no están destinados a ser utilizados directamente. En su
lugar, se espera que se utilizan como clases mixin, como acabamos de
hacer. Podemos pensar en ellos como plantillas con características
listas para usar. Para crear una clase abstracta, solo la necesitamos
para usar models.AbstractModel en lugar de models.Model para la clase
que los define.
416
417 Para el tercer punto, queremos agregar los widgets de red social en la
parte inferior del formulario. Esto se hace extendiendo la definición
de vista de formulario. Podemos reutilizar la vista heredada que ya
hemos creado, `view_form_todo_task_inherited`, y añadirla a sus datos
`arch`:
418 ```
419 <sheet position="after">
420 <div class="oe_chatter">
421 <field name="message_follower_ids"
422 widget="mail_followers" />
423 <field name="message_ids" widget="mail_thread" />
424 </div>
425 </sheet>
426 ```
427
428 Los dos campos agregados aquí no han sido declarados explícitamente por
nosotros, pero son proporcionados por el modelo `mail.thread`.
429
430 El paso final, que es el paso cuatro, consiste en establecer reglas de
registro para seguidores: control de acceso a nivel de fila. Esto sólo
es necesario si nuestro modelo es requerido para limitar el acceso de
otros usuarios a los registros. En este caso, queremos que cada
registro de tarea también sea visible para cualquiera de sus seguidores.
431
432 Ya tenemos un Registro de Reglas definido en el modelo de tareas
pendientes, por lo que debemos modificarlo para agregar este nuevo
requisito. Esa es una de las cosas que haremos en la próxima sección.
433 ##Modificando los datos
434 A diferencia de las vistas, los registros de datos regulares no tienen
una estructura `XML arch` y no se pueden ampliar utilizando expresiones
Page 12
capitulo-3.md 07/02/2018

XPath. Pero todavía se pueden modificar modificando los valores en sus


campos.
435
436 Los elementos de carga de datos `<record id = "x" model = "y">`
realizan realmente una operación `insert` o `update` en el modelo `y`:
si el modelo `x` no existe, se crea; De lo contrario, se actualiza /
sobre escribe.
437
438 Dado que se puede acceder a los registros de otros módulos con un
identificador global `<model>. <Identifier>`, es posible que nuestro
módulo sobrescriba algo que fue escrito antes por otro módulo.
439
440 #### Nota
441
442 Ten en cuenta que dado que el punto está reservado para separar el
nombre del módulo del identificador de objeto, no se puede utilizar en
los nombres de identificador. En su lugar, utiliza la opción subrayado.
443
444 ### Modificando el menú y acciones de registro
445 `
446 Como ejemplo, cambiemos la opción de menú creada por el módulo
`todo_app` a `My To-Do`. Para ello, podemos agregar lo siguiente al
archivo `todo_user / views / todo_task.xml`:
447 ```
448 <!-- Modify menu item -->
449 <record id="todo_app.menu_todo_task" model="ir.ui.menu">
450 <field name="name">My To-Do</field>
451 </record>
452 ```
453 También podemos modificar la acción utilizada en el ítem menú. Las
acciones tienen un atributo contextual opcional. Puede proporcionar
valores predeterminados para campos de vista y filtros. Lo utilizaremos
para activar de forma predeterminada el filtro **Mis tareas**, definido
anteriormente en este capítulo:
454 ```
455 <!-- Action to open To-Do Task list -->
456 <record model="ir.actions.act_window"
457 id="todo_app.action_todo_task">
458 <field name="context">
459 {'search_default_filter_my_tasks': True}
460 </field>
461 </record>
462 ```
463
464 ### Modificando las reglas de registro de seguridad
465
466 La aplicación Tareas incluye una regla de registro para garantizar que
cada tarea sólo fuera visible para el usuario que la creó. Pero ahora,
con la adición de características sociales, necesitamos que los
seguidores de las tareas también tengan acceso a ellas. El módulo de
red social no maneja esto por sí mismo.
467
468 Además, ahora las tareas pueden tener usuarios asignados a ellos, por
lo que tiene más sentido tener las reglas de acceso para trabajar en el
usuario responsable en lugar del usuario que creó la tarea.
469
470 El plan sería el mismo que lo hicimos para el elemento del ítem menú:
sobreescribir `todo_app.todo_task_user_rule` para modificar el campo
`domain_force` a un nuevo valor.
471
472
Page 13
capitulo-3.md 07/02/2018

473
474
475 La idea es mantener los archivos de seguriad relacionados en un
subdirectorio `segurity`, por lo que crearemos un archivo `security /
todo_access_rules.xml` con el siguiente contenido:
476 ```
477 <?xml version="1.0" encoding="utf-8"?>
478 <odoo>
479 <data noupdate="1">
480 <record id="todo_app.todo_task_per_user_rule"
481 model="ir.rule">
482 <field name="name">ToDo Tasks for owner and
483 followers</field>
484 <field name="model_id" ref="model_todo_task"/>
485 <field name="groups" eval="[(4,
486 ref('base.group_user'))]"/>
487 <field name="domain_force">
488 ['|',('user_id','in', [user.id,False]),
489 ('message_follower_ids','in',
490 [user.partner_id.id])]
491 </field>
492 </record>
493 </data>
494 </odoo>
495 ```
496
497 Esto sobrescribe la regla de registro `todo_task_per_user_rule` del
módulo `todo_app`. El nuevo filtro de dominio ahora hace que una tarea
sea visible para el usuario responsable, `user_id` o para todos si el
usuario responsable no está establecido (equivale a `False`); Es
visible para todos los seguidores de la tarea también.
498
499 La regla de registro se ejecuta en un contexto en el que una variable
`user` está disponible y representa el registro para el usuario de
sesión actual. Dado que los seguidores son socios, no `users`, en lugar
de `user.id`, necesitamos usar `user.partner_id`.
500
501 El campo de grupos es una relación de muchos. La edición de datos en
estos campos utiliza una notación especial. El código `4` utilizado
aquí es para añadir a la lista de registros relacionados. También se
utiliza con frecuencia el código `6`, para reemplazar completamente los
registros relacionados con una nueva lista. Podremos analizar esta
notación con mayor detalle en el Capítulo 4, *Datos del Módulo*.
502
503 El atributo `noupdate = "1"` del elemento de registro significa que
estos datos de registro sólo se escribirán en las acciones de
instalación y se ignorarán en las actualizaciones del módulo. Esto
permite su personalización, sin asumir los riesgos de sobrescribir
personalizaciones y perderlas al hacer una actualización de módulo en
algún momento en el futuro.
504 ####Tip
505 Trabajar en archivos de datos con `<data noupdate ="1">` en el momento
del desarrollo puede ser complicado porque las ediciones posteriores de
la definición XML serán ignoradas en las actualizaciones de módulos.
Para evitar esto, puedes volver a instalar el módulo. Esto es más fácil
hecho a través de la línea de comandos usando el `-i`
506
507 Como de costumbre, no debemos olvidar agregar el nuevo archivo al
atributo de datos en el `__manifest__.py`:
508 ```
509 'data': ['views/todo_task.xml', 'security/todo_access_rules.xml'],
Page 14
capitulo-3.md 07/02/2018

510 ```
511
512 # Resumen
513
514 Ahora deberías ser capaz de crear tus propios módulos extendiendo los
existentes.
515
516 Para demostrar cómo hacerlo, ampliamos el módulo Tareas que creamos en
el capítulo anterior, añadiendo nuevas características a las varias
capas que componen una aplicación.
517
518 Hemos ampliado un modelo Odoo para agregar nuevos campos y ampliar sus
métodos de lógica de negocio. A continuación, modificamos las vistas
para poner a su disposición los nuevos campos. Finalmente, aprendimos a
heredar características de otros modelos y usarlas para agregar las
funciones de red social a la aplicación de tareas pendientes.
519
520 Los tres primeros capítulos nos dieron una visión general de las
actividades comunes involucradas en el desarrollo Odoo, desde la
instalación y configuración de Odoo hasta la creación y extensión de
módulos.
521
522 Los próximos capítulos se centrarán en un área específica del
desarrollo Odoo, la mayoría de los cuales visitamos brevemente en estos
primeros capítulos. En el capítulo siguiente, abordaremos la
serialización de datos y el uso de archivos de datos XML y CSV con más
detalle.
523
524
525
526

Page 15
capitulo-4.md 07/02/2018

1 # Capítulo 4. Datos del módulo


2
3 La mayoría de las definiciones de módulos Odoo, como las interfaces de
usuario y las reglas de seguridad, son en realidad registros de datos
almacenados en tablas de bases de datos específicas. Los archivos XML y
CSV que se encuentran en los módulos no son utilizados por las
aplicaciones Odoo en tiempo de ejecución. En su lugar, son un medio
para cargar esas definiciones en las tablas de la base de datos.
4
5 Debido a esto, una parte importante de los módulos de Odoo consiste en
representar (serializando) esos datos utilizando archivos de modo que
puedan cargarse posteriormente en una base de datos.
6
7 Los módulos también pueden tener datos predeterminados y de
demostración. La representación de datos permite añadir eso a nuestros
módulos. Además, la comprensión de los formatos de representación de
datos de Odoo es importante para exportar e importar datos
empresariales en el contexto de la implementación de un proyecto.
8
9 Antes de entrar en casos prácticos, primero exploraremos el concepto de
identificador externo, que es la clave para la representación de datos
Odoo.
10
11 ## Comprendiendo los identificadores externos
12
13 Un **identificador externo** (también llamado XML ID) es un
identificador de cadena legible por humanos que identifica de forma
única un registro particular en Odoo. Son importantes al cargar datos
en Odoo.
14
15 Una de las razones es que al actualizar un módulo, sus archivos de
datos se cargarán nuevamente en la base de datos y debemos detectar los
registros ya existentes para actualizarlos en lugar de crear nuevos
registros duplicados.
16
17 Otra razón que apoya los datos interrelacionados: los registros de
datos deben ser capaces de hacer referencia a otros registros de datos.
El identificador de base de datos real es un número secuencial asignado
automáticamente por la base de datos, durante la instalación del
módulo. Los identificadores externos proporcionan una forma de hacer
referencia a un registro relacionado sin necesidad de saber de antemano
qué ID de base de datos se asignará, lo que nos permite definir las
relaciones de datos en los archivos de datos Odoo.
18
19 Odoo se encarga de traducir los nombres de identificadores externos en
los IDs de base de datos reales asignados a ellos. El mecanismo detrás
de esto es bastante simple: Odoo mantiene una tabla con el mapeo entre
los identificadores externos nombrados y sus IDs de base de datos
numéricos correspondientes: el modelo `ir.model.data`.
20
21 Para inspeccionar los mapeos existentes, ve a la sección **Technical**
del menú superior **Settings** y selecciona el elemento de menú
**External Identifiers** bajo **Sequences & Identifiers**.
22
23 Por ejemplo, si visitamos la lista de **External identifiers** y lo
filtramos por el módulo `todo_app`, veremos los identificadores
externos generados por el módulo creado anteriormente:
24
25 ![Externalidentifiers](file:img/4-01.jpg)
26
27 Podemos ver que los identificadores externos tienen una etiqueta **ID
Page 1
capitulo-4.md 07/02/2018

Completa**. Observa cómo se compone del nombre del módulo y el nombre


del identificador unido por un punto, por ejemplo,
`todo_app.action_todo_task`.
28
29 Los identificadores externos deben ser únicos dentro de un módulo Odoo,
por lo que no hay riesgo de que dos módulos entren en conflicto porque
accidentalmente eligieron el mismo nombre de identificador. Para
construir un identificador único global Odoo une el nombre de los
módulos con el nombre del identificador externo real. Esto es lo que
puede ver en el campo **ID Completo**.
30
31 Cuando se utiliza un identificador externo en un archivo de datos,
puedes elegir utilizar el identificador completo o simplemente el
nombre del identificador externo. Por lo general, es más sencillo usar
el nombre de identificador externo, pero el identificador completo nos
permite referenciar registros de datos de otros módulos. Al hacerlo,
asegúrate de que esos módulos están incluidos en las dependencias del
módulo, para asegurarse de que esos registros estén cargados antes de
los nuestros.
32
33 En la parte superior de la lista, tenemos el identificador completo
`todo_app.action_todo_task`. Esta es la acción de menú que creamos para
el módulo, al que también se hace referencia en el ítem de menú
correspondiente. Al hacer clic en él, vamos a la vista de formulario
con sus detalles; El identificador externo `action_todo_task` en el
módulo `todo_app` mapea un ID de registro específico en el modelo
`ir.actions.act_window`, `72` en este caso:
34
35 ![Todo](file:img/4-02.jpg)
36
37 Además de proporcionar una forma para que los registros hagan
referencia fácilmente a otros registros, los identificadores externos
también te permiten evitar la duplicación de datos en las importaciones
repetidas. Si el identificador externo ya está presente, se actualizará
el registro existente; No es necesario crear un nuevo registro. Es por
esto que en las actualizaciones de módulos posteriores, los registros
cargados previamente se actualizan en vez de duplicarse.
38
39 ### Hallando identificadores externos
40
41 Al preparar archivos de definición y demostración de datos para los
módulos, con frecuencia necesitamos buscar identificadores externos
existentes que sean necesarios para las referencias.
42
43 Podemos usar el menú de **External Identifiers** mostrado
anteriormente, pero el menú **Developer** puede proporcionar un método
más conveniente para esto. Como puedes recordar del Capítulo 1,
*Iniciando con desarrollo de Odoo*, el menú **Developer** se activa en
el panel de **Settings**, en una opción en la parte inferior derecha.
44
45 Para encontrar el identificador externo de un registro de datos, en la
vista de formulario correspondiente, seleccione la opción **View
Metadata** en el menú **Developer**. Esto mostrará un diálogo con el ID
de base de datos del registro y el identificador externo (también
conocido como XML ID).
46
47 Como ejemplo, para buscar el ID de `demo user`, podemos navegar a la
vista de formulario, en **Settings | Users** y seleccione la opción
**View Metadata** y esto se mostrará:
48
49 ![Metadata](file:img/4-03.jpg)
Page 2
capitulo-4.md 07/02/2018

50
51 Para hallar el identificador externo para elementos de vista, como
formulario, árbol, búsqueda o acción, el menú **Developer** también es
una buena fuente de ayuda. Para ello, podemos utilizar la opción
**Manage Views** o abrir la información para la vista deseada mediante
la opción **Edit <view type>**. A continuación, selecciona la opción
**View metadata**.
52
53 ## Exportando e importando data
54
55 Comenzaremos a explorar cómo exportar e importar datos desde la
interfaz de usuario de Odoo, ya partir de ahí, pasaremos a los detalles
más técnicos sobre cómo utilizar los archivos de datos en nuestros
módulos de complemento.
56
57 ### Exportando data
58
59 La exportación de datos es una característica estándar disponible en
cualquier vista de lista. Para usarlo, primero debemos seleccionar las
filas para exportar seleccionando las casillas de verificación
correspondientes en el extremo izquierdo y luego seleccionar la opción
**Export** del botón **More**.
60
61 He aquí un ejemplo, usando las tareas de tareas pendientes creadas
recientemente:
62
63 ![Todocreate]((file:img/4-04.jpg)
64
65 También podemos marcar la casilla de verificación en el encabezado de
la columna. Comprobará todos los registros a la vez y exportará todos
los registros que coincidan con los criterios de búsqueda actuales.
66
67 ### Nota
68
69 En versiones anteriores de Odoo, sólo los registros vistos en la
pantalla (la página actual) se exportarían realmente. Desde Odoo 9,
esta casilla de verificación cambió y marcar el checkbox en el
encabezado exportará todos los registros que coincidan con el filtro
actual, no sólo los que se muestran actualmente. Esto es muy útil para
exportar grandes conjuntos de registros que no encajan en la pantalla.
70
71 La opción **Export** nos lleva a un formulario de diálogo, donde
podemos elegir qué exportar. La opción **Import-Compatible Export**
(Importar exportación compatible) asegura que el archivo exportado se
puede importar de nuevo a Odoo. Necesitaremos usar esto.
72
73 El formato de exportación puede ser CSV o Excel. Preferiremos un
archivo CSV para obtener una mejor comprensión del formato de
exportación. A continuación, seleccionamos las columnas que queremos
exportar y hacemos clic en el botón **Export to File**. Esto iniciará
la descarga de un archivo con los datos exportados:
74
75 ![Minion](file:img/4-05.jpg)
76
77 Si seguimos estas instrucciones y seleccionamos los campos mostrados en
la captura de pantalla anterior, deberíamos terminar con un archivo de
texto CSV similar a este:
78
79 ```
80 "id","name","user_id/id","date_deadline","is_done"
81 "todo_user.todo_task_a","Install
Page 3
capitulo-4.md 07/02/2018

Odoo","base.user_root","2015-01-30","False"
82 "__export__.todo_task_9","Create my first
module","base.user_root","","False"
83 ```
84
85
86 #### Nota
87
88 Observa que Odoo exportó automáticamente una columna de id adicional.
Este es un identificador externo asignado a cada registro. Si no se ha
asignado ninguna todavía, se genera automáticamente una nueva
utilizando `__export__` en lugar de un nombre de módulo real. Los
nuevos identificadores se asignan solamente a los registros que no
tienen ya uno, y de allí en adelante, se mantienen ligados al mismo
expediente. Esto significa que las exportaciones posteriores
preservarán los mismos identificadores externos.
89
90 ### Importando datos
91
92 Primero tenemos que asegurarnos de que la función de importación esté
habilitada. Ya que Odoo 9 está habilitado de forma predeterminada. Si
no, la opción está disponible en el menú principal **Settings**, opción
**General Settings**. Bajo la sección **Import | Export** hay una
casilla de verificación **Allow users to import data from
CSV/XLS/XLSX/ODS files** que debe ser habilitada.
93
94 #### Nota
95
96 Esta característica es proporcionada por el complemento **Initial Setup
Tools** (`base_setup` es el nombre técnico). El efecto real de la
casilla **Allow users to import**... es instalar o desinstalar
`base_setup`.
97
98 Con esta opción habilitada las vistas de lista muestran una opción
**Import** junto al botón **Create** en la parte superior de la lista.
99
100 Vamos a realizar una edición masiva en nuestros datos de tareas
pendientes primero. Abre el archivo CSV que acabamos de descargar en
una hoja de cálculo o un editor de texto y cambia algunos valores.
Además, agregua algunas nuevas filas, dejando la columna de ID en blanco.
101
102 Como se mencionó anteriormente, la primera columna, id, proporciona un
identificador único para cada fila. Esto permite actualizar los
registros ya existentes en vez de duplicarlos cuando importamos los
datos de nuevo a Odoo. Para las nuevas filas añadidas al archivo CSV,
podemos elegir entre proporcionar un identificador externo de nuestra
elección, o dejar en blanco la columna id, se creará un nuevo registro
para ellos.
103
104 Después de guardar los cambios en el archivo CSV, haz clic en la opción
**Import** (junto al botón **Create**) y se nos presentará el asistente
de importación.
105
106 Allí, debemos seleccionar la ubicación del archivo CSV en el disco y
hacer clic en **Validate** para comprobar su exactitud. Ya que que el
archivo a importar se basa en una exportación Odoo, lo más probable es
que sea válido:
107
108 ![Minion]((file:img/4-06.jpg)
109
110 Ahora podemos hacer clic en **Import**, y ahí lo tienes; Nuestras
Page 4
capitulo-4.md 07/02/2018

modificaciones y nuevos registros deberían haber sido cargados en Odoo.


111
112 ### Archivos relacionados en archivos de datos CSV
113
114 En el ejemplo anterior, el usuario responsable de cada tarea es un
registro relacionado en el modelo de usuario, con una relación de
varios a uno (o una clave externa). El nombre de columna utilizado para
ello fue `id_usuario / id` y los valores de campo eran identificadores
externos para los registros relacionados, tales como `base.user_root`
para el usuario administrador.
115
116 #### Tip
117 El uso de IDs de base de datos sólo se recomienda si estás exportando e
importando desde / hasta la misma base de datos. Por lo general,
preferirás utilizar identificadores externos.
118
119 Las columnas de la relación deben tener `/ id` añadido a su nombre si
utilizan identificadores externos o `/.id` si utilizan IDs (numéricos)
de la base de datos. Alternativamente, se puede usar un colon (:) en
lugar de una barra para el mismo efecto.
120
121 Del mismo modo, también se apoyan las relaciones de muchos a muchos. Un
ejemplo de una relación muchos-a-muchos es el que existe entre usuarios
y grupos: cada usuario puede estar en muchos grupos y cada grupo puede
tener muchos usuarios. El nombre de columna para este tipo de campo
debe tener `/ id` añadido. Los valores de campo aceptan una lista
separada por comas de identificadores externos, rodeada de comillas
dobles.
122
123 Por ejemplo, los seguidores de tareas pendientes tienen una relación
muchos-a-muchos entre las tareas por realizar y los socios. Su nombre
de columna debe ser `follower_ids / id`, y un valor de campo con dos
seguidores podría tener este aspecto:
124 ```
125 "__export__.res_partner_1,__export__.res_partner_2"
126 ```
127 Finalmente, las relaciones uno-a-muchos también se pueden importar a
través de un archivo CSV. El ejemplo típico para este tipo de relación
es un documento *cabecera* de documento con varias *líneas*. Observa
que una relación de uno a muchos es siempre la inversa de una relación
de muchos a uno. Cada *cabecera* del documento puede tener muchas
*líneas* e inversamente cada línea tiene una *cabecera*.
128
129 Podemos ver un ejemplo de tal relación en el modelo de empresa (la
vista de formulario está disponible en el menú **Settings**): cada
empresa puede tener varias cuentas bancarias, cada una con sus propios
detalles; Inversamente, cada registro de cuenta bancaria pertenece y
tiene una relación de muchos a uno con una sola compañía.
130
131 Podemos importar empresas junto con sus cuentas bancarias en un solo
archivo. Aquí hay un ejemplo donde cargamos una empresa con tres bancos:
132 ```
133 id,name,bank_ids/id,bank_ids/acc_number,bank_ids/state
134 base.main_company,YourCompany,__export__.res_partner_bank_4,123456789,ban
k
135 ,,__export__.res_partner_bank_5,135792468,bank
136 ,,__export__.res_partner_bank_6,1122334455,bank
137
138 ```
139
140 Podemos ver que las dos primeras columnas, `id` y `name`, tienen
Page 5
capitulo-4.md 07/02/2018

valores en la primera línea y están vacíos en las dos líneas


siguientes. Tienen datos para el registro de la cabeza, que es la
empresa.
141
142 Las otras tres columnas están todas prefijadas con `bank_ids /` y
tienen valores en todas las tres líneas. Tienen datos para las tres
líneas relacionadas para las cuentas bancarias de la compañía. La
primera línea tiene datos de la empresa y el primer banco, y las dos
líneas siguientes tienen datos sólo para la compañía adicional y los
bancos.
143
144 Estos son aspectos esenciales mientras se trabaja con la exportación e
importación de la GUI. Es útil configurar datos en nuevas instancias de
Odoo o preparar archivos de datos que se incluirán en módulos Odoo. A
continuación, aprenderemos más sobre el uso de archivos de datos en
módulos.
145
146 ## Datos de modulo
147
148 Los módulos utilizan archivos de datos para cargar sus configuraciones
en la base de datos, datos predeterminados y datos de demostración.
Esto se puede hacer utilizando archivos CSV y XML. Para completar,
también se puede usar el formato de archivo YAML, pero es muy raro que
se use para cargar datos; Por lo tanto, no lo estaremos discutiendo.
149
150 Los archivos CSV utilizados por los módulos son exactamente los mismos
que los que hemos visto y utilizado para la función de importación.
Cuando se usan en módulos, una restricción adicional es que el nombre
de archivo debe coincidir con el nombre del modelo al que se cargarán
los datos, de modo que el sistema pueda inferir el modelo en el que
deben importarse los datos.
151
152 Un uso común de los archivos CSV de datos es para acceder a
definiciones de seguridad, cargadas en el modelo `ir.model.access`.
Usualmente usan archivos CSV que se llaman `ir.model.access.csv`.
153
154 ### Datos demostrativos
155
156 Los módulos addon de Odoo pueden instalar datos de demostración, y se
considera una buena práctica hacerlo. Esto es útil para proporcionar
ejemplos de uso para un módulo y conjuntos de datos que se utilizarán
en las pruebas. Los datos de demostración de un módulo se declaran
utilizando el atributo de demostración del archivo de manifiesto
`__manifest__.py`. Al igual que el atributo de datos, es una lista de
nombres de archivo con las correspondientes rutas relativas dentro del
módulo.
157
158 Es hora de agregar algunos datos de demostración a nuestro módulo
`todo_user`. Podemos comenzar exportando algunos datos de las tareas
pendientes, como se explicó en la sección anterior. La convención es
colocar los archivos de datos en un subdirectorio `data/`. Así que
debemos guardar estos archivos de datos en el módulo addon `todo_user`
como `data / todo.task.csv`. Dado que estos datos serán propiedad de
nuestro módulo, deberíamos editar los valores de ID para eliminar el
prefijo `__export__` de los identificadores.
159
160 Por ejemplo, nuestro archivo de datos `todo.task.csv` podría tener este
aspecto:
161 ```
162 id,name,user_id/id,date_deadline
163 todo_task_a,"Install Odoo","base.user_root","2015-01-30"
Page 6
capitulo-4.md 07/02/2018

164 todo_task_b","Create dev database","base.user_root",""


165 ```
166 no debemos olvidar añadir este archivo de datos al atributo `demo`
`__manifest__.py`:
167 ```
168 'demo': ['data/todo.task.csv'],
169 ```
170 La próxima vez que actualizemos el módulo, siempre y cuando se instale
con los datos de demostración habilitados, se importará el contenido
del archivo. Ten en cuenta que estos datos se reimportarán siempre que
se realice una actualización de módulo.
171
172 Los archivos XML también se utilizan para cargar los datos del módulo.
Vamos a aprender más acerca de lo que los archivos de datos XML pueden
hacer que los archivos CSV no pueden.
173
174 ## Archivo de datos XML
175 Mientras que los archivos CSV proporcionan un formato simple y compacto
para representar datos, los archivos XML son más potentes y dan más
control sobre el proceso de carga. Sus nombres de archivo no son
necesarios para coincidir con el modelo que se va a cargar. Esto se
debe a que el formato XML es mucho más rico y esa información es
proporcionada por los elementos XML dentro del archivo.
176
177 Ya hemos utilizado archivos de datos XML en los capítulos anteriores.
Los componentes de la interfaz de usuario, tales como vistas y
elementos de menú, son de hecho registros almacenados en modelos de
sistema. Los archivos XML en los módulos son los medios utilizados para
cargar estos registros en el servidor.
178
179 Para mostrar esto, agregaremos un segundo archivo de datos al módulo
`todo_user`, `data / todo_data.xml`, con el siguiente contenido:
180 ```
181 <?xml version="1.0"?>
182 <odoo>
183 <!-- Data to load -->
184 <record model="todo.task" id="todo_task_c">
185 <field name="name">Reinstall Odoo</field>
186 <field name="user_id" ref="base.user_root" />
187 <field name="date_deadline">2015-01-30</field>
188 <field name="is_done" eval="False" />
189 </record>
190 </odoo>
191 ```
192 Este XML es equivalente al archivo de datos CSV que acabamos de ver en
la sección anterior.
193
194 Los archivos de datos XML tienen un elemento superior `<odoo>`, dentro
del cual podemos tener varios elementos `<record>` que corresponden a
las filas de datos CSV.
195
196 #### Nota
197
198 El elemento superior `<odoo>` en archivos de datos se introdujo en la
versión 9.0 y reemplaza a la etiqueta anterior `<openerp>`. La sección
`<data>` dentro del elemento superior todavía es compatible, pero ahora
es opcional. De hecho, ahora `<odoo>` y `<data>` son equivalentes, por
lo que podríamos usar uno como elementos principales para nuestros
archivos de datos XML.
199
200 Un elemento `<record>` tiene dos atributos obligatorios a saber,
Page 7
capitulo-4.md 07/02/2018

`modelo` e `id` (el identificador externo para el registro), y contiene


una etiqueta `<field>` para cada campo en el que escribir.
201
202 Ten en cuenta que la notación de barras en los nombres de campo no está
disponible aquí; No podemos usar `<field name = "user_id / id">`. En su
lugar, el atributo `ref` special se utiliza para hacer referencia a
identificadores externos. Discutiremos los valores de los campos
relacionados-a-muchos en un momento.
203
204 ### El atributo data noupdate
205
206 Cuando se repite la carga de datos, los registros cargados de la
ejecución anterior se reescriben. Es importante tener en cuenta que
esto significa que la actualización de un módulo sobrescribirá los
cambios manuales que se pudieron haber realizado dentro de la base de
datos. En particular, si las vistas se modificaron con las
personalizaciones, estos cambios se perderán con la próxima
actualización del módulo. El procedimiento correcto consiste en crear
vistas heredadas para los cambios que necesitamos, como se describe en
el Capítulo 3, *Herencia - Extendiendo las aplicaciones existentes*.
207
208 Este comportamiento de re-importación es el predeterminado, pero se
puede cambiar, de modo que cuando se actualiza un módulo, algunos
registros de archivos de datos quedan intactos. Esto se hace mediante
el atributo `noupdate = "1"` del elemento `<odoo>` o `<data>`. Estos
registros se crearán cuando se instale el módulo addon, pero en las
actualizaciones de módulos posteriores no se les hará nada.
209
210 Esto te permite asegurar que las personalizaciones hechas manualmente
se mantengan a salvo de las actualizaciones de módulos. A menudo se
utiliza con reglas de acceso a registros, lo que les permite adaptarse
a las necesidades específicas de la implementación.
211
212 Es posible tener más de una sección `<data>` en el mismo archivo XML.
Podemos aprovechar esto para separar los datos para importar sólo uno,
con `noupdate = "1"`, y los datos para ser reimportados en cada
actualización, con `noupdate = "0"`.
213
214 El indicador `noupdate` se almacena en la información de **External
Identifier** para cada registro. Es posible editarlo manualmente
directamente usando el formulario al Identifier** disponible en el menú
**Technical**, usando la casilla **Non Updatable**.
215 ####Tip
216
217 El atributo `noupdate` puede ser complicado al desarrollar módulos, ya
que los cambios realizados en los datos posteriormente serán ignorados.
Una solución es, en lugar de actualizar el módulo con la opción `-u`,
vuelve a instalarlo con la opción `-i`. Al reinstalar desde la línea de
comandos utilizando la opción `-i`, se ignoran los indicadores de
`noupdate` en los registros de datos.
218
219 ### Definiendo registros en XML
220
221 Cada elemento `<record>` tiene dos atributos básicos, `id` y `model`, y
contiene elementos `<field>` que asignan valores a cada columna. Como
se mencionó anteriormente, el atributo `id` corresponde al
identificador externo del registro y al atributo `model` al modelo de
destino en el que se escribirá el registro. Los elementos <field>
tienen diferentes formas de asignar valores. Echemos un vistazo a ellos
en detalle.
222
Page 8
capitulo-4.md 07/02/2018

223 #### Configurando de valores de campo


224
225 El elemento `<record>` define un registro de datos y contiene elementos
`<field>` para establecer valores en cada campo.
226
227 El atributo `name` del elemento de campo identifica el campo que se va
a escribir.
228
229 El valor a escribir es el contenido del elemento: el texto entre la
etiqueta de apertura y cierre del campo. Para dates y datetimes, las
cadenas con `"YYYY-mm-dd"` y `"AAAA-mm-dd HH: MM: SS"` se convertirán
correctamente. Sin embargo, para los campos Booleanos cualquier valor
no vacío se convertirá en `True` y los valores `"0"` y `"False"` se
convertirán en `False`.
230
231 #### Tip
232
233 La forma en que los valores Booleanos `false` se leen de los archivos
de datos se mejora en Odoo 10. En las versiones anteriores, los valores
no vacíos, incluidos `"0"` y `"Falso"`, se convirtieron en `True`.
Serecomendó para los booleanos que usaban el atributo `eval` descrito a
continuación.
234
235 #### Configurando valores mediante expresiones
236
237 Una alternativa más elaborada para definir un valor de campo es el
atributo `eval`. Evalúa una expresión de Python y asigna el valor
resultante al campo.
238
239 La expresión se evalúa en un contexto que, además de Python
incorporado, también tiene algunos identificadores adicionales
disponibles. Echemos un vistazo a ellos.
240
241 Para manejar fechas, están disponibles los siguientes módulos: `time,
datetime, timedelta y relativedelta`. Ellos permiten calcular valores
de fecha, algo que se utiliza con frecuencia en los datos de
demostración y prueba, de modo que las fechas utilizadas están cerca de
la fecha de instalación del módulo. Por ejemplo, para establecer un
valor para ayer, utilizaremos esto:
242
243 ```
244 <field name="date_deadline"
245 eval="(datetime.now() + timedelta(-1)).strftime('%Y-%m-%d')" />
246 ```
247
248
249 También está disponible en el contexto de evaluación la función `ref
()`, que se utiliza para traducir un identificador externo en el ID de
base de datos correspondiente. Esto se puede usar para establecer
valores para campos relacionales. Como ejemplo, lo hemos utilizado
antes para establecer el valor de `user_id`:
250
251 ```
252 <field name="user_id" eval="ref('base.group_user')" />
253 ```
254
255 #### Valores de ajuste para campos de relación
256
257 Acabamos de ver cómo establecer un valor en un campo de relación
many-to-one, como `user_id`, usando el atributo `eval` con una función
`ref ()`. Pero hay una manera más simple.
Page 9
capitulo-4.md 07/02/2018

258
259 El elemento `<field>` también tiene un atributo `ref` para establecer
el valor de un campo many-to-one, utilizando un identificador externo.
Con esto, podemos fijar el valor para `user_id` usando apenas esto:
260 ```
261
262 <Field name = "user_id" ref = "base.user_demo" />
263 ```
264
265 Para los campos one-to-many y many-to-many, se espera una lista de IDs
relacionados, por lo que se necesita una sintaxis diferente; Odoo
proporciona una sintaxis especial para escribir en este tipo de campos.
266
267 El siguiente ejemplo, tomado de la aplicación oficial Fleet, reemplaza
la lista de registros relacionados de un campo `tag_ids`:
268 ```
269 <field name="tag_ids"
270 eval="[(6,0,
271 [ref('vehicle_tag_leasing'),
272 ref('fleet.vehicle_tag_compact'),
273 ref('fleet.vehicle_tag_senior')]
274 )]" />
275
276 ```
277
278 Para escribir en un campo to-many, usamos una lista de triples. Cada
triple es un comando de escritura que hace cosas diferentes según el
código usado:
279
280 + (0,_ ,{'field': value}) crea un nuevo regristro y lo enlaza con este
281 + (1,id,{'field': value}) Actualiza los valores en un registro
previamente enlazado
282 + (2,id,_) Desenlaza y elimina un registro relacionado
283 + (3,id,_) Desenlaza pero no elimina un registro relacionado
284 + (4,id,_) Enlaza un registro ya existente
285 + (5,_,_) Desenlaza pero no elimina todos los registros relacionados
286 + (6,_,[ids]) reemplaza la lista de registros enlazados con la lista
proporcionada
287
288 El símbolo de subrayado utilizado en la lista anterior representa
valores irrelevantes, normalmente se rellenan con `0` o `False`.
289
290 ### Atajos para los modelos de uso frecuente
291
292 Si volvemos al Capítulo 2, *Construyendo tu Primera Aplicación Odoo*,
encontraremos elementos distintos de `<record>`, como `<act_window>` y
`<menuitem>`, en los archivos XML.
293
294 Estos son accesos directos convenientes para los modelos de uso
frecuente que también se pueden cargar utilizando elementos `<record>`
normales. Ellos cargan datos en modelos de base que soportan la
interfaz de usuario y se explorarán con más detalle más adelante,
específicamente en el Capítulo 6, *Vistas - Diseñando la interfaz de
usuario*.
295
296 Como referencia, los siguientes elementos de acceso directo están
disponibles con los modelos correspondientes en los que cargan datos:
297
298 + `<Act_window>` es el modelo de acción de la ventana,
`ir.actions.act_window`
299 + `<Menuitem>` es el modelo de elementos de menú, `ir.ui.menu`
Page 10
capitulo-4.md 07/02/2018

300 + `<Report>` es el modelo de acción del informe, `ir.actions.report.xml`


301 + `<Plantilla>` es para las plantillas QWeb almacenadas en el modelo
`ir.ui.view`
302 + `<Url>` es el modelo de acción de URL, `ir.actions.act_url`
303
304 ### Otras acciones en archivos de datos XML
305
306 Hasta ahora, hemos visto cómo agregar o actualizar datos usando
archivos XML. Pero los archivos XML también le permiten realizar otros
tipos de acciones que a veces son necesarias para configurar los datos.
En particular, pueden eliminar datos, ejecutar métodos de modelo
arbitrarios y activar eventos de flujo de trabajo.
307
308 #### Eliminando registros
309
310 Para eliminar un registro de datos, usamos el elemento `<delete>`,
proporcionándolo con un ID o un dominio de búsqueda para encontrar el
registro de destino. Por ejemplo, el uso de un dominio de búsqueda para
encontrar el registro que desea eliminar se ve así:
311
312 ```
313 <delete
314 model="ir.rule"
315 search="
316 [('id','=',ref('todo_app.todo_task_user_rule'))]"
317 />
318 ```
319
320 Puesto que en este caso conocemos el ID específico a eliminar,
podríamos haberlo utilizado directamente para el mismo efecto:
321
322 ```
323 <delete model="ir.rule" id="todo_app.todo_task_user_rule" />
324 ```
325
326 #### Funciones desencadenantes y flujos de trabajo
327
328 Un archivo XML también puede ejecutar métodos durante su proceso de
carga a través del elemento `<function>`. Esto se puede utilizar para
configurar datos de demostración y de prueba. Por ejemplo, la
aplicación CRM lo utiliza para configurar datos de demostración:
329
330 ```
331 <function
332 model="crm.lead"
333 name="action_set_lost"
334 eval="[ref('crm_case_7'), ref('crm_case_9')
335 , ref('crm_case_11'), ref('crm_case_12')]
336 , {'install_mode': True}" />
337
338 ```
339
340 Esto llama al método `action_set_lost` del modelo `crm.lead`, pasando
dos argumentos a través del atributo `eval`. El primero es la lista de
IDs para trabajar, y el siguiente es el contexto a utilizar.
341
342 Otra forma en que los archivos de datos XML pueden realizar acciones es
desencadenando flujos de trabajo Odoo a través del elemento
`<workflow>`. Los flujos de trabajo pueden, por ejemplo, cambiar el
estado de una orden de ventas o convertirlo en una factura. La
aplicación `sale` ya no utiliza flujos de trabajo, pero este ejemplo
Page 11
capitulo-4.md 07/02/2018

todavía se puede encontrar en los datos de demostración:


343 ```
344 <workflow model="sale.order"
345 ref="sale_order_4"
346 action="order_confirm" />
347 ```
348
349
350 El atributo `model` es autoexplicativo por ahora, y `ref` identifica la
instancia de flujo de trabajo sobre la que estamos actuando. `action`
es la señal de flujo de trabajo enviada a esta instancia de flujo de
trabajo.
351
352 ## Resumen
353
354 Haz aprendido todos los aspectos esenciales sobre la serialización de
datos y adquirido una mejor comprensión de los aspectos XML que vimos
en los capítulos anteriores. También pasamos un tiempo entendiendo los
identificadores externos, un concepto central de manejo de datos en
general y configuraciones de módulos en particular. Los archivos de
datos XML se explicaron en detalle. Aprendiste acerca de las varias
opciones disponibles para establecer valores en campos y también para
realizar acciones, como eliminar registros y métodos de modelo de
llamada. También se explican los archivos CSV y las funciones de
importación / exportación de datos. Estas son herramientas valiosas
para la configuración inicial de Odoo o para la edición masiva de datos.
355
356 En el próximo capítulo, exploraremos cómo construir modelos Odoo en
detalle y aprenderemos más acerca de cómo construir sus interfaces de
usuario.

Page 12
capitulo-5.md 07/02/2018

1 # Capítulo 5. Modelos - Estructurando los Datos de la Aplicación.


2
3 En los capítulos anteriores, tuvimos un resumen de principio a fin de
la creación de nuevos módulos para Odoo. En el Capítulo 2,
*Construyendo tu Primera Aplicación Odoo*, construimos una aplicación
completamente nueva, y en el Capítulo 3, *Extendiendo Aplicaciones
Existentes de Herencia*, exploramos la herencia y cómo usarla para
crear un módulo de extensión para nuestra aplicación. En el Capítulo 4,
*Datos de Módulos*, discutimos cómo agregar datos iniciales y de
demostración a nuestros módulos.
4
5 En estas reseñas, hemos abordado todas las capas involucradas en la
construcción de una aplicación backend para Odoo. Ahora, en los
capítulos siguientes, es el momento de explicar estas varias capas que
componen una aplicación con más detalle: modelos, vistas y lógica de
negocio.
6
7 En este capítulo, aprenderás cómo diseñar las estructuras de datos que
soportan una aplicación y cómo representar las relaciones entre ellas.
8
9 ## Organizando las características de la aplicación en módulos
10
11 Como antes, usaremos un ejemplo para ayudarnos a explicar los conceptos.
12
13 Las características de herencia de Odoo proporcionan un mecanismo de
extensibilidad efectivo. Esto te permite ampliar aplicaciones de
terceros existentes sin cambiarlas directamente. Esta composición
también permite un patrón de desarrollo orientado a módulos, en el que
las aplicaciones grandes se pueden dividir en características más
pequeñas, lo suficientemente ricas como para mantenerse por sí solas.
14
15 Esto puede ser útil para limitar la complejidad, tanto en el nivel
técnico como en el nivel de experiencia del usuario. Desde una
perspectiva técnica, dividir un gran problema en partes más pequeñas
facilita la solución y es más amigable para el desarrollo incremental
de características. Desde la perspectiva de la experiencia del usuario,
podemos optar por activar sólo las características que realmente son
necesarias para ellos, para una interfaz de usuario más sencilla. Por
lo tanto, vamos a mejorar nuestra aplicación To-Do a través de módulos
addon adicionales para finalmente formar una aplicación completa.
16
17 ### Presentando el módulo todo_ui
18
19 En el capítulo anterior, primero creamos una aplicación para tareas
personales y luego la ampliamos para que la tarea se pudiera compartir
con otras personas.
20
21 Ahora queremos llevar nuestra aplicación al siguiente nivel mejorando
su interfaz de usuario, incluyendo un tablero kanban. El tablero kanban
es una herramienta de flujo de trabajo simple que organiza los
elementos en columnas, donde estos elementos fluyen de la columna
izquierda a la derecha, hasta que se completan. Organizaremos nuestras
Tareas en columnas, de acuerdo con sus Etapas, como **Esperar**,
**Listo**, **Iniciado** o **Hecho**.
22
23 Comenzaremos agregando las estructuras de datos para permitir esta
visión. Tenemos que añadir etapas, y será bueno añadir soporte para las
etiquetas también, permitiendo que las tareas se clasifiquen por tema.
En este capítulo, nos centraremos únicamente en los modelos de datos.
La interfaz de usuario para estas características se describirá en el
Capítulo 6, *Vistas - Diseñando la interfaz de usuario* y las vistas de
Page 1
capitulo-5.md 07/02/2018

kanban en el Capítulo 9, *Vistas QWeb y Kanban*.


24
25 Lo primero que debemos averiguar es cómo serán estructurados nuestros
datos para que podamos diseñar los modelos de apoyo. Ya tenemos la
entidad central: la Tarea pendiente. Cada tarea estará en una etapa a
la vez y las tareas también pueden tener una o más etiquetas en ellas.
Necesitaremos agregar estos dos modelos adicionales, y ellos tendrán
estas relaciones:
26
27 + Cada tarea tiene una etapa, y puede haber muchas tareas en cada etapa
28 + Cada tarea puede tener muchas etiquetas, y cada etiqueta se puede
adjuntar a muchas tareas
29
30 Esto significa que las Tareas tienen una relación de muchos a uno con
las Fases, y las relaciones de muchos a muchos con las Etiquetas. Por
otro lado, las relaciones inversas son: Las etapas tienen una relación
de uno a muchos con las Tareas y las Etiquetas tienen una relación de
muchos a muchos con Tareas.
31
32 Comenzaremos por crear el nuevo módulo `todo_ui` y añadiremos los
modelos de Tareas pendientes y de Tareas.
33
34 Hemos estado usando el directorio `~ / odoo-dev / custom-addons /` para
alojar nuestros módulos. Debemos crear un nuevo directorio `todo_ui`
dentro de él para los nuevos addons. Desde el shell, podríamos usar los
siguientes comandos:
35
36 ```
37
38 $ cd ~/odoo-dev/custom-addons
39
40
41
42
43
44 $ mkdir todo_ui
45
46
47
48
49
50 $ cd todo_ui
51 ```
52
53 Comenzamos añadiendo el archivo manifiesto `__manifest__.py`, con este
contenido:
54
55 ```
56 {
57 'name': 'User interface improvements to the To-Do app',
58 'description': 'User friendly features.',
59 'author': 'Daniel Reis',
60 'depends': ['todo_user'] }
61 ```
62 También debemos añadir un archivo` __init__.py`. Está perfectamente
bien que esté vacío por ahora.
63
64 Ahora podemos instalar el módulo en nuestra base de datos Odoo y
comenzar con los modelos.
65
66 ## Creando modelos
Page 2
capitulo-5.md 07/02/2018

67
68 Para que las tareas pendientes tengan un tablero kanban, necesitamos
Etapas. Las etapas son columnas del tablero, y cada tarea cabrá en una
de estas columnas:
69
70
71 1. Edita `todo_ui/__init__.py` para importar el submodulo `models`:
72
73 ```
74 from . import models
75 ```
76
77 1. Crea el directorio `todo_ui/models` y añádelo al archivo `an
__init__.py` con esto:
78 ```
79 from . import todo_model
80 ```
81 1. Ahora, añadamoslo al archivo de código Pyton
`todo_ui/models/todo_model.py`:
82 ```
83 # -*- coding: utf-8 -*-
84 from odoo import models, fields, api
85
86 class Tag(models.Model):
87 _name = 'todo.task.tag'
88 _description = 'To-do Tag'
89 name = fields.Char('Name', 40, translate=True)
90 class Stage(models.Model):
91 _name = 'todo.task.stage'
92 _description = 'To-do Stage'
93 _order = 'sequence,name'
94
95 name = fields.Char('Name', 40, translate=True)
96 sequence = fields.Integer('Sequence')
97 ```
98
99 Aquí hemos creado los dos nuevos modelos que serán referenciados en las
tareas pendientes.
100
101 Centrándonos en las etapas de la tarea, tenemos una clase Python,
Etapa, basada en la clase `models.Model`, que define un nuevo modelo
Odoo llamado `todo.task.stage`. También tenemos dos campos: `nombre y
secuencia`. Podemos ver algunos atributos de modelo (prefijados con un
subrayado) que son nuevos para nosotros. Echemos un vistazo a ellos.
102
103 ## Atributos del modelo
104
105 Las clases de modelo pueden utilizar atributos adicionales que
controlan algunos de sus comportamientos. Estos son los atributos más
utilizados:
106
107 + `_name` es el identificador interno del modelo Odoo que estamos
creando. Obligatorio cuando se crea un nuevo modelo.
108 + `_description` es un título fácil de usar para los registros del
modelo, que se muestra cuando se ve el modelo en la interfaz de
usuario. Opcional pero recomendado.
109 + `_order` establece el orden predeterminado para utilizar cuando se
exploran los registros del modelo o se muestran en una vista de lista.
Es una cadena de texto que se usará como cláusula `SQL order by`, por
lo que puede ser cualquier cosa que puedas utilizar allí, aunque tiene
un comportamiento inteligente y admite nombres de campo traducibles y
Page 3
capitulo-5.md 07/02/2018

muchos a uno.
110
111 Para completar, hay un par de más atributos que se pueden utilizar en
casos avanzados:
112
113 + `_rec_name` indica el campo a utilizar como la descripción del
registro cuando se hace referencia desde campos relacionados, tales
como una relación de varios a uno. De forma predeterminada, utiliza el
campo de `nombre`, que es un campo común en los modelos. Pero este
atributo nos permite usar cualquier otro campo para ese propósito.
114 + `_table` es el nombre de la tabla de la base de datos que soporta el
modelo. Por lo general, se deja que se calcule automáticamente, y es el
nombre del modelo con los puntos reemplazados por subrayados. Pero es
posible establecer para indicar un nombre de tabla específico.
115
116 También podemos tener los atributos `_inherit` y `_inherits`, como se
explicó en el capítulo 3, *Herencia - Extendiendo las aplicaciones
existentes.
117
118 ### Modelos y clases Python
119 Los modelos Odoo están representados por clases Python. En el código
anterior, tenemos una `Etapa` clase Python, basada en la clase
`models.Model`, que define un nuevo modelo Odoo llamado
`todo.task.stage`.
120
121 Los modelos Odoo se mantienen en un registro central, también conocido
como piscina en las versiones más antiguas de Odoo. Es un diccionario
que mantiene referencias a todas las clases de modelo disponibles en la
instancia, y puede ser referenciado por un nombre de modelo.
Específicamente, el código de un método de modelo puede usar `self.env
['x']` para obtener una referencia a una clase que representa el modelo
`x`.
122
123 Puedes ver que los nombres de los modelos son importantes ya que son
las claves utilizadas para acceder al registro. La convención para los
nombres de modelo es usar una lista de palabras en minúsculas unidas
con puntos, como `todo.task.stage`. Otros ejemplos de los módulos
principales son `project.project`, `project.task` o
`project.task.type`. Deberíamos utilizar el modelo singular `todo.task`
en lugar de `todo.task`. Por razones históricas, es posible encontrar
algunos modelos básicos que no siguen esto, como `res.users`, pero no
es la regla.
124
125 Los nombres de los modelos deben ser globalmente únicos. Debido a esto,
la primera palabra debe corresponder a la aplicación principal que
módulo se refiere. En nuestro ejemplo, es `todo`. Otros ejemplos de los
módulos principales son `project`, `crm` o `sale`.
126
127 Las clases de Python, por otro lado, son locales al archivo Python
donde se declaran. El identificador utilizado para ellos es sólo
significativo para el código en ese archivo. Debido a esto, no se
requiere que los identificadores de clase sean prefijados por la
aplicación principal con la que se relacionan. Por ejemplo, no hay
ningún problema para nombrar nuestra clase `Stage` para el modelo
`todo.task.stage`. No hay riesgo de colisión con las posibles clases
con el mismo nombre en otros módulos.
128
129 Se pueden utilizar dos convenciones diferentes para los identificadores
de clase: `snake_case` o `CamelCase`. Históricamente, el código de Odoo
usó el caso snake, y todavía es posible encontrar las clases que
utilizan esta convención. Pero la tendencia es utilizar el caso case,
Page 4
capitulo-5.md 07/02/2018

puesto que es el estándar de Python definido por las convenciones de


codificación del PEP8. Puedes haber notado que estamos utilizando la
última forma.
130
131 ### Modelos transitorios y abstractos
132
133 En el código anterior y en la gran mayoría de los modelos de Odoo, las
clases se basan en la clase `models.Model`. Estos tipos de modelos
tienen persistencia permanente de la base de datos: se crean tablas de
base de datos para ellos y sus registros se almacenan hasta que se
borran explícitamente.
134
135 Pero Odoo también proporciona otros dos tipos de modelos que se
utilizarán: modelos transitorios y abstractos.
136
137 + **Los modelos transitorios** se basan en la clase
`models.TransientModel` y se utilizan para la interacción del usuario
estilo asistente. Sus datos aún se almacenan en la base de datos, pero
se espera que sea temporal. Un trabajo periódico de vacío limpia los
datos antiguos de estas tablas. Por ejemplo, la ventana de diálogo
`Load a Lenguage`, que se encuentra en el menú `Setings |
Translations`, utiliza un modelo Transient para almacenar selecciones
de usuarios e implementar la lógica del asistente.
138 + **Los modelos abstractos** se basan en la clase models.AbstractModel
y no tienen ningún almacenamiento de datos adjunto a ellos. Actúan como
conjuntos de funciones reutilizables que se mezclan con otros modelos,
utilizando las capacidades de herencia de Odoo. Por ejemplo,
`mail.thread` es un modelo abstracto, proporcionado por el addon
`Discuss`, utilizado para agregar funciones de mensaje y seguidores a
otros modelos.
139
140 ### Inspeccionando los modelos existentes
141
142 Los modelos y campos creados a través de las clases Python tienen su
metadata disponíble a través de la interface de usuario. En el menú
superior **Settings** navega al ítem de menú ** Technical | Database
Structure | Models**.
143 Aquí, hallarás la lista de todos los modelos disponíbles en la base de
datos. Haciendo click en un modelo en la lista, abrirá una forma con
estos detalles:
144
145 ![Modelo_de_lista](file:img/5-01.jpg)
146
147 esta es una buena herramienta para inspeccionar la estructura de un
modelo, ya que en un lugar, puedes ver los resultados de la
personaliación de diferentes modulos. En este caso, tal como lo puedes
ver en en la esquina superior derecha en el campo **In Apps**, las
definiciones `todo.task`para este modelo provienen de ambos modulos
`todo_app` y `todo_user`.
148
149 En el área inferior, tenemos algunas pestañas de información
disponíbles: una referencia rápida para los modelos **Campos**, los
**Derechos de Acceso** garantizados en grupos de seguridad y también
enlista las **Vistas** disponíbles para este modelo.
150
151 Podemos hallar el **Identificador Externo** del modelo utilizando,
desde el menú **Desarrollador**, la opción **Metadata View**. Los
identificadores de modelo externo, o IDs XML, son generadas
automáticamente por el ORM pero justamente predecible: para el modelo
`todo.task`, el identificador externo es `model_todo_task`.
152
Page 5
capitulo-5.md 07/02/2018

153 ### Tip


154 ¡El formulario **Modelos** es editable!. Es posible crear y modificar
modelos, campos, y vistas desde aqui. Puedes utilizar este para
construír prototipos antes de persistir en modulos.
155
156 ## Creando campos
157 Luego de crear un nuevo modelo, el próximo paso es añadir campos a
éste. Odoo soporta todos los tipos de datos básicos que se esperan,
tales como cadenas de texto, la base de datos, enteros, números de
punto flotante, Booleanos, fechas,
158
159 Algunos nombres de campo son especiales, se marchitan porque están
reservados por el ORM para propósitos especiales, o porque algunas
características incorporadas usan por defecto algunos nombres de campo
predeterminados.
160
161 Vamos a explorar los diversos tipos de campos disponibles en Odoo.
162
163 ### Tipos de campos básicos
164
165 Ahora tenemos un modelo de `Etapa` y lo ampliaremos para agregar
algunos campos adicionales. Debemos editar el archivo `todo_ui / models
/ todo_model.py` y añadir definiciones de campo adicionales para que se
vea así:
166
167 ```
168 class Stage(models.Model):
169 _name = 'todo.task.stage'
170 _description = 'To-do Stage'
171 _order = 'sequence,name'
172 # String fields:
173 name = fields.Char('Name', 40)
174 desc = fields.Text('Description')
175 state = fields.Selection(
176 [('draft','New'), ('open','Started'),
177 ('done','Closed')],'State')
178 docs = fields.Html('Documentation')
179 # Numeric fields:
180 sequence = fields.Integer('Sequence')
181 perc_complete = fields.Float('% Complete', (3, 2))
182 # Date fields:
183 date_effective = fields.Date('Effective Date')
184 date_changed = fields.Datetime('Last Changed')
185 # Other fields:
186 fold = fields.Boolean('Folded?')
187 image = fields.Binary('Image')
188 ```
189 Aquí, tenemos una muestra de los tipos de campo no relacional
disponibles en Odoo con los argumentos posicionales esperados por cada
uno.
190
191 En la mayoría de los casos, el primer argumento es el título del campo,
que corresponde al argumento del campo de la `string`; Se utiliza como
texto predeterminado para las etiquetas de la interfaz de usuario. Es
opcional, y si no se proporciona, un título se generará automáticamente
a partir del nombre del campo.
192
193 Para los nombres del campo de fecha, hay una convención para usar la
fecha como un prefijo. Por ejemplo, deberíamos usar el campo
`date_effective` en lugar de `effective_date`. Convenciones similares
también se aplican a otros campos, como `amount_`, `price_` o `qty_`.
Page 6
capitulo-5.md 07/02/2018

194
195 Estos son los argumentos de posición estándar esperados por cada uno de
los tipos de campo:
196
197 + `Char` espera un segundo tamaño de argumento opcional para el tamaño
máximo de texto. Se recomienda no usarlo a menos que exista un
requisito de negocio que lo requiera, como un número de seguro social
con una longitud fija.
198 + `Text`o difiere de `Char`, ya que puede albergar contenido de texto
multilínea, pero espera los mismos argumentos.
199 + `Selection` es una lista de selección desplegable. El primer
argumento es la lista de opciones seleccionables y el segundo es el
título de la cadena. El elemento de selección es una lista de tuplas
(`'value'`, `'Títle'`), para el valor almacenado en la base de datos y
la correspondiente descripción de interfaz de usuario. Cuando se
extiende a través de la herencia, el argumento `selection_add` está
disponible para añadir nuevos elementos a una lista de selección
existente.
200 + `Html` se almacena como un campo de texto, pero tiene un manejo
específico en la interfaz de usuario, para la presentación de contenido
HTML. Por razones de seguridad, se desinfectan de forma predeterminada,
pero este comportamiento se puede sobreescribir.
201 + `Integer` sólo espera un argumento de cadena para el título del campo.
202 + `Float` tiene un segundo argumento opcional, una tupla (`x,y`) con la
precisión del campo: `x` es el número total de dígitos; De éstos, `y`
son dígitos decimales.
203 + Los campos `Date` y `Datetime` sólo esperan la cadena de texto como
un argumento de posicional. Por razones históricas, el ORM maneja sus
valores en un formato de cadena. Las funciones auxiliares se deben
utilizar para convertirlas en objetos de fecha real. También los
valores de fecha y hora se almacenan en la base de datos en tiempo UTC
pero presentadas en hora local, utilizando las preferencias de zona
horaria del usuario. Esto se discute con más detalle en el Capítulo 6,
*Vistas - Diseñando la interfaz de usuario*.
204 + `Boolean` tiene valores `True` o `False`, como puedes esperar, y sólo
tiene un argumento de posición para la cadena de texto.
205 + `Binary` almacena datos binarios de tipo archivo y también espera
sólo el argumento de cadena. Pueden ser manejados por código Python
usando cadenas codificadas en `base64`.
206
207 Aparte de estos, también tenemos los campos relacionales, que serán
presentados más adelante en este capítulo. Pero ahora, todavía hay más
para aprender acerca de estos tipos de campo y sus atributos.
208
209 ### Atributos de campo comunes
210
211 Los campos tienen atributos que se pueden establecer al definirlos.
Dependiendo del tipo de campo, algunos atributos pueden ser pasados en
posición, sin una palabra clave de argumento, como se muestra en la
sección anterior.
212
213 Por ejemplo, `name=fields.Char('Name', 40)` podría hacer uso de
argumentos posicionales. Usando los argumentos de la palabra clave, lo
mismo se podría escribir como `name=fields.Char (size=40,
string='Name')`. Puedes encontrar más información sobre argumentos de
palabras clave en la documentación oficial de Python en
https://docs.python.org/2/tutorial/controlflow.html#keyword-arguments.
214
215 Todos los atributos disponibles se pueden pasar como un argumento de
palabra clave. Estos son los atributos generalmente disponibles y las
palabras clave de argumento correspondientes:
Page 7
capitulo-5.md 07/02/2018

216
217 + `string` es la etiqueta por defecto del campo, que se utilizará en la
interfaz de usuario. Excepto para los campos de selección y
relacionales, es el primer argumento posicional, por lo que la mayoría
de las veces no se utiliza como argumento de palabra clave.
218 + `default` establece un valor predeterminado para el campo. Puede ser
un valor estático, como una cadena o una referencia callable, ya sea
una función con nombre o una función anónima (una expresión lambda).
219 + `size` sólo se aplica a los campos `Char` y puede establecer un
tamaño máximo permitido. La mejor práctica actual es no usarla a menos
que sea realmente necesaria.
220 + `translate` se aplica sólo a los campos `Char`, `Text` y `Html`, y
hace que el contenido del campo se pueda traducir, manteniendo valores
diferentes para diferentes idiomas.
221 + `help` proporciona el texto para las sugerencias que se muestran a
los usuarios.
222 + `readonly=True` hace que el campo por defecto no sea editable por la
interfaz de usuario. Esto no se aplica a nivel API; Es sólo una
configuración de interfaz de usuario.
223 + `required=True` hace obligatorio el campo por defecto en la interfaz
de usuario. Esto se aplica en el nivel de base de datos mediante la
adición de una restricción `NOT NULL` en la columna.
224 + `index=True` creará un índice de base de datos en el campo.
225 + `copy=False` tiene el campo ignorado cuando se utiliza la función de
registro duplicado, método ORM `copy ()`. Los campos no relacionales
son `copyable` de forma predeterminada.
226 + `groups` permite limitar el acceso y la visibilidad del campo a sólo
algunos grupos. Espera una lista separada por comas de IDs XML para
grupos de seguridad, como `groups='base.group_user, base.group_system'`.
227
228 + `states` espera un diccionario que asigna valores para los atributos
UI que dependen de los valores del campo `satate`. Por ejemplo:
`states = {'done': [('readonly', True)]}`. Los atributos que se pueden
utilizar son `readonly`, `required` e `invisible`.
229
230 #### Nota
231
232 Ten en cuenta que el campo`satates` es equivalente al atributo `attrs`
en las vistas. Nota que las vistas admiten un atributo `states`, pero
tiene un uso diferente: acepta una lista de estados separados por comas
para controlar cuando el elemento debe ser visible.
233
234 Para completar, a veces se utilizan otros dos atributos cuando se
actualiza entre versiones Odoo principales:
235
236 + `deprecated=True` registra una advertencia cada vez que se utiliza el
campo.
237 + `oldname='field'` se utiliza cuando un campo se renombra en una
versión más reciente, permitiendo que los datos en el campo antiguo se
copien automáticamente en el nuevo campo.
238
239 ### Nombres de campos especiales
240
241 Algunos nombres de campo están reservados para ser utilizados por el ORM.
242
243 El campo `id` es un número automático que identifica de manera única
cada registro y se utiliza como la clave principal de la base de datos.
Se agrega automáticamente a cada modelo.
244
245 Los siguientes campos se crean automáticamente en los nuevos modelos, a
menos que se establezca el atributo `_log_access=False`:
Page 8
capitulo-5.md 07/02/2018

246
247 + `create_uid` es para el usuario que creó el registro
248 + `create_date` es la fecha y la hora en que se crea el registro
249 + `write_uid` es para que el último usuario modifique el registro
250 + `write_date` es la última fecha y hora en que se modificó el registro
251
252 Esta información está disponible desde el cliente web, navegando hasta
el menú **Developer Mode** y seleccionando la opción **View Metadata**.
253
254 Algunas características API incorporadas por defecto esperan nombres de
campos específicos. Debemos evitar el uso de estos nombres de campo
para propósitos diferentes a los que se pretenden. Algunos de ellos son
incluso reservado y no se puede utilizar para otros fines en absoluto:
255
256 + `name` se utiliza de forma predeterminada como el nombre para mostrar
para el registro. Normalmente es un campo de tipo `Char`, pero también
puede ser un `Text` o un `Many2one`. Todavía podemos establecer otro
campo para ser utilizado para el nombre de visualización, utilizando el
atributo del modelo `_rec_name`.
257 + `Active`, de tipo `Boolean`, permite inactivar registros. Los
registros con `active==False` se excluirán automáticamente de las
consultas. Para acceder a ellos debe añadirse una condición `('active',
'=', False)` al dominio de búsqueda, o `'active_test': False` Se debe
agregar al contexto actual.
258 + `Sequence`, de tipo `Integer`, si está presente en una vista de
lista, permite definir manualmente el orden de los registros. Para que
funcione correctamente, no debes olvidar usarlo con el atributo del
modelo `_order`.
259 + `State`, de tipo `Selection`, representa los estados básicos del
ciclo de vida del registro y puede ser utilizado por el atributo de
campo del estado para modificar dinámicamente la vista: algunos campos
de formulario se pueden `readonly` o `invisible` en estados de registro
específicos.
260 + `parent_id`, `parent_left` y `parent_right`, de tipo `Integer`,
tienen un significado especial para las relaciones jerárquicas
padre/hijo. Lo analizaremos en detalle en la siguiente sección.
261
262 Hasta ahora, hemos discutido campos no relacionales. Pero una buena
parte de una estructura de aplicación de datos es acerca de describir
las relaciones entre entidades. Ahora vamos a mirar esto.
263
264
265 ## Relaciones entre modelos
266 Mirando de nuevo el diseño de nuestro módulo, tenemos estas relaciones:
267
268 + Cada tarea tiene una etapa. Esa es una relación de muchos a uno,
también conocida como clave extranjera. La inversa es una relación
uno-a-muchos, lo que significa que cada etapa puede tener muchas tareas.
269 + Cada tarea puede tener muchas etiquetas. Esa es una relación de
muchos a muchos. La relación inversa, por supuesto, es también un mucho
a muchos, ya que cada etiqueta puede estar en muchas tareas.
270
271 El siguiente diagrama de relación de entidad puede ayudar a visualizar
las relaciones que estamos a punto de crear en el modelo. Las líneas
que terminan con un triángulo representan muchos lados de las relaciones:
272
273 ![EntityRelationshipDiagram](file:img/5-02.jpg)
274
275 Añadamos los correspondientes campos de relación a las tareas
pendientes en nuestro archivo `todo_model.py`:
276
Page 9
capitulo-5.md 07/02/2018

277 ```
278 class TodoTask(models.Model):
279 _inherit = 'todo.task'
280 stage_id = fields.Many2one('todo.task.stage', 'Stage')
281 tag_ids = fields.Many2many('todo.task.tag', string='Tags')
282 ```
283
284 El código anterior muestra la sintaxis básica de estos campos,
estableciendo el modelo relacionado y el título del campo `string`. La
convención para los nombres de campos relacionales es añadir `_id` o
`_ids` a los nombres de campo, para a-una y a-muchas relaciones,
respectivamente.
285
286 Como ejercicio, puedes intentar también agregar las relaciones inversas
correspondientes a los modelos relacionados:
287
288 + La inversa de la relación `Many2one` es un campo `One2many` en las
etapas, ya que cada etapa puede tener muchas tareas. Deberíamos añadir
este campo a la clase Etapas.
289 + La inversa de la relación `Many2many` es también un campo `Many2many`
en Etiquetas, ya que cada etiqueta también se puede usar en muchas
Tareas.
290
291 Echemos un vistazo más de cerca a las definiciones de campos
relacionales.
292
293 ### Relaciones de muchos a uno (Many-to-one)
294
295 La relación `Many2one` acepta dos argumentos posicionales: el modelo
relacionado (correspondiente al argumento de la palabra clave
`comodel`) y el título `string`. Crea un campo en la tabla de base de
datos con una clave externa a la tabla relacionada.
296
297 Algunos argumentos con nombre adicionales también están disponibles
para utilizar con este tipo de campo:
298
299 + `ondelete` define lo que ocurre cuando se elimina el registro
relacionado. Su valor predeterminado es `set null`, lo que significa
que se establece un valor vacío cuando se elimina el registro
relacionado. Otros valores posibles son `restrict`, generando un error
que impida la eliminación y `cascade` también eliminar este registro.
300 + `context` es un diccionario de datos, significativo para las vistas
del cliente web, para llevar información al navegar a través de la
relación. Por ejemplo, para establecer vales predeterminados. Se
explicará mejor en el Capítulo 6, *Vistas - Diseñando la interfaz de
usuario*.
301 + `domain` es una expresión de dominio, una lista de tuplas, filtra los
registros disponibles para el campo de relación.
302 + `auto_join=True` permite al ORM utilizar combinaciones de SQL cuando
se realizan búsquedas utilizando esta relación. Si se usan, las reglas
de seguridad de acceso serán anuladas y el usuario podría tener acceso
a registros relacionados que las reglas de seguridad no permitirían,
pero las consultas SQL serán más eficientes y se ejecutarán más rápido.
303
304 ### Relaciones de muchos a muchos (Many-to-many)
305 La firma mínima `Many2many` acepta un argumento para el modelo
relacionado, y se recomienda proporcionar también el argumento
`strings` con el título del campo.
306
307 En el nivel de base de datos, no se agrega ninguna columna a las tablas
existentes. En su lugar, crea automáticamente una nueva tabla de
Page 10
capitulo-5.md 07/02/2018

relación que tiene sólo dos campos de ID con las claves externas para
las tablas relacionadas. El nombre de la tabla de relación y los
nombres del campo se generan automáticamente. El nombre de tabla de
relación es el nombre de ambas tablas unidos con un subrayado con
`_rel` añadido a él.
308
309 En algunas ocasiones podemos necesitar anular estos valores
predeterminados automáticos.
310
311 Uno de estos casos es cuando los modelos relacionados tienen nombres
largos, y el nombre de la tabla de relaciones generado automáticamente
es demasiado largo, superando el límite de 63 caracteres de PostgreSQL.
En estos casos, debemos elegir manualmente un nombre para la tabla de
relaciones, para que se ajuste al límite de tamaño de nombre de tabla.
312
313 Otro caso es cuando necesitamos una segunda relación muchos-a-muchos
entre los mismos modelos. En estos casos, necesitamos proporcionar
manualmente un nombre para la tabla de relaciones, para que no
colisione con el nombre de tabla que ya se está utilizando para la
primera relación.
314
315 Hay dos alternativas para anular manualmente estos valores: ya sea
utilizando argumentos posicionales o argumentos de palabra clave.
316
317 Utilizando argumentos posicionales para la definición de campo tenemos:
318
319 ```
320 # Task <-> Tag relation (positional args):
321 tag_ids = fields.Many2many(
322 'todo.task.tag', # related model
323 'todo_task_tag_rel', # relation table name
324 'task_id', # field for "this" record
325 'tag_id', # field for "other" record
326 string='Tags')
327 ```
328
329 #### Nota
330
331 Ten en cuenta que los argumentos adicionales son opcionales. Podríamos
simplemente establecer el nombre de la tabla de relaciones y dejar que
los nombres de campo usen los valores predeterminados automáticos.
332
333 En su lugar, podemos utilizar argumentos de palabras clave, que algunas
personas prefieren para la legibilidad:
334
335 ```
336 # Task <-> Tag relation (keyword args):
337 tag_ids = fields.Many2many(
338 comodel_name='todo.task.tag', # related model
339 relation='todo_task_tag_rel',# relation table name
340 column1='task_id', # field for "this" record
341 column2='tag_id', # field for "other" record
342 string='Tags')
343 ```
344
345 Al igual que los campos muchos a uno, los campos muchos-a-muchos
también admiten los atributos de palabras clave `domain` y `context`.
346
347 #### Nota
348
349 Actualmente hay una limitación en el diseño de ORM, con respecto a los
Page 11
capitulo-5.md 07/02/2018

modelos abstractos, que cuando se obliga a los nombres de la tabla de


relación y las columnas, ya no se pueden heredar. Así que esto no
debería hacerse en modelos abstractos.
350
351 El inverso de la relación `Many2many` es también un campo `Many2many`.
Si también añadimos un campo `Many2many` al modelo `Tags`, Odoo infiere
que esta relación de muchos-a-muchos es la inversa de la del modelo
`Task`.
352
353 La relación inversa entre Tareas y Etiquetas se puede implementar de la
siguiente manera:
354
355 ```
356 class Tag(models.Model):
357 _name = 'todo.task.tag'
358
359 # Tag class relationship to Tasks:
360
361
362
363
364
365 task_ids = fields.Many2many(
366
367
368
369
370
371 'todo.task', # related model
372
373
374
375
376
377 string='Tasks')
378
379
380 ```
381
382 ### Relaciones inversas Uno a muchos (One-to-many)
383
384 Un inverso de un `Many2one` se puede agregar al otro extremo de la
relación. Esto no tiene ningún impacto en la estructura de la base de
datos real, pero nos permite navegar fácilmente desde **un** lado de
los **muchos** registros relacionados. Un caso de uso típico es la
relación entre un encabezado de documento y sus líneas.
385
386 En nuestro ejemplo, una relación inversa `One2many` en Etapas nos
permite listar fácilmente todas las Tareas en esa Etapa. El código para
agregar esta relación inversa a Etapas es:
387
388 ```
389 class Stage(models.Model):
390 _name = 'todo.task.stage'
391
392 # Stage class relationship with Tasks:
393
394
395
396
397
Page 12
capitulo-5.md 07/02/2018

398
399 tasks = fields.One2many(
400
401
402
403
404
405
406 'todo.task', # related model
407
408
409
410
411
412
413 'stage_id', # field for "this" on related model
414
415
416
417
418
419
420 'Tasks in this stage')
421
422
423 ```
424 El `One2many` acepta tres argumentos posicionales: el modelo
relacionado, el nombre del campo en ese modelo que hace referencia a
este registro y la cadena de título. Los dos primeros argumentos
posicionales corresponden a los argumentos de palabra clave
`comodel_name` y `inverse_name`.
425
426 Los parámetros de palabras clave adicionales disponibles son los mismos
que para `Many2one : context`, `domain`, `ondelete` (aquí actúa en el
lado **muchos** de la relación) y `auto_join`.
427
428 ### Relaciones jerárquicas
429
430 Las relaciones de árbol padre-hijo se representan usando una relación
`Many2one` con el mismo modelo, de modo que cada registro hace
referencia a su padre. Y la inversa `One2many` hace que sea fácil para
un padre mantener el seguimiento de sus hijos.
431
432 Odoo proporciona un soporte mejorado para estas estructuras de datos
jerárquicas, para una navegación más rápida a través de hermanos de
árbol y para una búsqueda más fácil usando el operador adicional de
expresiones de dominio `child_of`.
433
434 Para habilitar estas características necesitamos establecer la bandera
de atributo `_parent_store` y añadir al modelo los campos auxiliares:
`parent_left` y `parent_right`. Ten en cuenta que esta operación
adicional se produce en tiempo de almacenamiento y penalidades de
tiempo de ejecución, por lo que es mejor utilizarla cuando se espera
leer con más frecuencia que escribir, como en el caso de un árbol de
categorías.
435
436 Revisitando el modelo `Tags`, definido en el archivo `todo_model.py`,
debemos editarlo para que parezca lo siguiente:
437
438 ```
439 class Tags(models.Model):
Page 13
capitulo-5.md 07/02/2018

440 _name = 'todo.task.tag'


441 _description = 'To-do Tag'
442
443 _parent_store = True
444
445
446
447
448 # _parent_name = 'parent_id'
449 name = fields.Char('Name')
450
451 parent_id = fields.Many2one(
452
453
454
455
456
457
458 'todo.task.tag', 'Parent Tag', ondelete='restrict')
459
460
461
462
463
464
465 parent_left = fields.Integer('Parent Left', index=True)
466
467
468
469
470
471
472 parent_right = fields.Integer('Parent Right', index=True)
473
474 ```
475 Aquí, tenemos un modelo básico, con un campo `parent_id` para
referenciar el registro principal y el atributo `additional`
`_parent_store` para agregar soporte de búsqueda jerárquica. Al hacer
esto, los campos `parent_left` y `parent_right` también se deben agregar.
476
477 El campo que se refiere al padre se espera que se nombre `parent_id`,
pero cualquier otro nombre de campo se puede utilizar siempre y cuando
lo declaremos en el atributo `_parent_name`.
478
479 Además, a menudo es conveniente agregar un campo con los hijos directos
del registro:
480
481 ```
482 child_ids = fields.One2many(
483 'todo.task.tag', 'parent_id', 'Child Tags')
484 ```
485
486 ## Campos de referencia que utilizan relaciones dinámicas
487
488 Los campos relacionales regulares hacen referencia a un comodelo fijo.
El tipo de campo de referencia no tiene esta limitación y admite
relaciones dinámicas, de modo que el mismo cam po puede referirse a más
de un modelo.
489
490 Por ejemplo, podemos usarlo para añadir un campo `Refers to` a las
tareas pendientes, que puede referirse a un `User` o a un `Partner`:
Page 14
capitulo-5.md 07/02/2018

491 ```
492 # class TodoTask(models.Model):
493 refers_to = fields.Reference(
494 [('res.user', 'User'), ('res.partner', 'Partner')],
495 'Refers to')
496 ```
497
498 Como puedes ver, la definición de campo es similar a un campo de
selección, pero aquí la lista de selección contiene los modelos que se
pueden utilizar. En la interfaz de usuario, el usuario primero
seleccionará un modelo de la lista disponible av, y luego escogerá un
registro de ese modelo.
499
500 Esto puede llevarse a otro nivel de flexibilidad: existe una tabla de
configuración de **Modelos de Referenciables** que puede ser utilizada
en los campos de **Referencia**. Está disponible en el menú
**Settings|Technical|database Structure**. Al crear tal campo podemos
configurarlo para usar cualquier modelo registrado allí, con la ayuda
de la función `referenceable_models (` en el módulo
`odoo.addons.res.res_request`.
501
502 Utilizando la configuración de **Modelos Referenciables**, una versión
mejorada del campo `Refers to` luciría así:
503
504
505 ```
506
507 from odoo.addons.base.res.res_request import referenceable_models
508
509
510
511
512 # class TodoTask(models.Model):
513
514 refers_to = fields.Reference(
515
516
517
518
519
520
521 referenceable_models, 'Refers to')
522
523
524
525
526
527 ```
528
529 Ten en cuenta que en Odoo 9.0 esta función utiliza una ortografía
ligeramente diferente y todavía estaba utilizando la API antigua. Así
que en la versión 9.0, antes de usar el código mostrado antes, tenemos
que añadir algún código en la parte superior de nuestro archivo Python
para envolverlo para que utilice la nueva API:
530
531 ```
532 from openerp.addons.base.res import res_request
533 def referenceable_models(self):
534 return res_request.referencable_models(
535 self, self.env.cr, self.env.uid, context=self.env.context)
536 ```
Page 15
capitulo-5.md 07/02/2018

537
538 ## Campos computados
539
540 Los campos pueden tener valores calculados por una función, en lugar de
simplemente leer un valor almacenado en una base de datos. Un campo
computado se declara igual que un campo regular, pero tiene el
argumento de adicional `compute` que define la función utilizada para
calcularlo.
541
542 En la mayoría de los casos, los campos computados implican escribir
alguna lógica de negocio, por lo que desarrollaremos este tema más en
el Capítulo 7, *Aplicación Lógica de ORM - Soportando procesos de
negocios`. Seguiremos explicándolos aquí, pero mantendremos la lógica
de negocios lo más simple posible.
543
544 Vamos a trabajar en un ejemplo: Las etapas tienen un campo de plegado
`fold`. Vamos a añadir a las Tareas pendientes un campo computado con
la etiqueta **Folded?** para la Etapa correspondiente.
545
546 Debemos editar el modelo `TodoTask` en el archivo `todo_model.py` para
agregar lo siguiente:
547
548 ```
549 # class TodoTask(models.Model):
550 stage_fold = fields.Boolean(
551 'Stage Folded?',
552 compute='_compute_stage_fold')
553
554 @api.depends('stage_id.fold')
555 def _compute_stage_fold(self):
556 for task in self:
557 task.stage_fold = task.stage_id.fold
558 ```
559 El código anterior agrega un nuevo campo `stage_fold` y el método
`_compute_stage_fold` utilizado para computarlo. El nombre de la
función se pasó como una cadena, pero también se le permite pasar como
una referencia llamable (el identificador de función sin comillas). En
este caso, debemos asegurarnos de que la función esté definida en el
archivo Python antes de que lo sea el campo.
560
561 El decorador `@api.depends` es necesario cuando la computación depende
de otros campos, como suele ocurrir. Permite al servidor saber cuándo
volver a calcular los valores almacenados o en datos caché. Uno o más
nombres de campo se aceptan como argumentos y la notación de puntos se
puede utilizar para seguir relaciones de campo.
562
563 Se espera que la función de computación asigne un valor al campo o a
los campos a computar. Si no lo hace, se producirá un error. Dado que
`self` es un objeto de registro, nuestro cálculo aquí es simplemente
para obtener el campo **Folded?** utilizando `stage_id.fold`. El
resultado se logra asignando ese valor (escribiéndolo) al campo
computado, `stage_fold`.
564
565 No vamos a estar trabajando todavía en las vistas de este módulo, pero
puede hacer ahora una edición rápida en el formulario de tarea para
confirmar si el campo computado está funcionando como se esperaba:
usando el **Developer Mode** selecciona la opción **Edit View** y
agregua el archivo directamente en el formato XML. No te preocupes:
será reemplazado por la vista de módulo limpio en la próxima
actualización.
566
Page 16
capitulo-5.md 07/02/2018

567 ### Búscando y escribiendo en campos computados


568
569 El campo computado que acabamos de crear se puede leer, pero no puede
ser buscado o escrito. Para habilitar estas operaciones, primero
necesitamos implementar funciones especializadas para ellos. Junto con
la función `compute`, también podemos configurar una función `search`,
implementando la lógica de búsqueda, y la función `inverse`,
implementando la lógica de escritura.
570
571 Utilizando estos, nuestra declaración de campo computado se convierte
así:
572
573 ```
574 # class TodoTask(models.Model):
575 stage_fold = fields.Boolean(
576 string='Stage Folded?',
577 compute='_compute_stage_fold',
578 # store=False, # the default
579
580 search='_search_stage_fold',
581
582
583
584
585
586
587 inverse='_write_stage_fold'
588
589
590
591 )
592 ```
593
594 Y las funciones de soporte son:
595
596 ```
597 def _search_stage_fold(self, operator, value):
598 return [('stage_id.fold', operator, value)]
599
600 def _write_stage_fold(self):
601 self.stage_id.fold = self.stage_fold
602 ```
603
604 La función `search` se llama siempre que una condición (`field,
operator, value`) en este campo se encuentra en una expresión de
dominio de búsqueda. Recibe al `operator` y el `value` para la búsqueda
y se espera que traduzca el elemento de búsqueda original en una
expresión de búsqueda de dominio alternativa.
605
606 La función `inverse` realiza la lógica inversa del cálculo, para
encontrar el valor a escribir en los campos fuente de la computación.
En nuestro ejemplo, esto significa escribir de nuevo en el campo
`stage_id.fold`.
607
608 ### Almacenando campos computados
609
610 Los valores del campo computado también se pueden almacenar en la base
de datos, estableciendo `store = True` en su definición. Serán
recomputados cuando cambien cualquiera de sus dependencias. Dado que
los valores están ahora almacenados, se pueden buscar como campos
regulares y no se necesita una función de búsqueda.
Page 17
capitulo-5.md 07/02/2018

611 ### Campos relacionados


612
613 El campo computado que implementamos en la sección anterior sólo copia
un valor de un registro relacionado en el propio campo del modelo. Sin
embargo, este es un uso común que puede ser manejado automáticamente
por Odoo.
614
615 El mismo efecto se puede lograr utilizando campos relacionados. Ponen a
disposición, directamente en un modelo, los campos que pertenecen a un
modelo relacionado, accesible mediante una cadena de punto-notación.
Esto los hace utilizables en situaciones donde la notación de punto no
se puede usar, como las vistas de formulario de UI.
616
617 Para crear un campo relacionado, declaramos un campo del tipo
necesario, al igual que con campos computados regulares, pero en lugar
de calcular usamos el atributo relacionado con la cadena de campo de
notación de puntos para alcanzar el campo deseado.
618
619 Las Tareas pendientes se organizan en etapas personalizables y éstas se
convierten en estados básicos. Haremos que el valor de estado esté
disponible directamente en el modelo de Tarea, de modo que pueda ser
usado para alguna lógica del lado del cliente en el próximo capítulo.
620
621 De forma similar a `stage_fold`, agregaremos un campo computado en el
modelo de tarea, pero esta vez usando el campo relacionado más simple:
622
623 ```
624 # class TodoTask(models.Model):
625 stage_state = fields.Selection(
626 related='stage_id.state',
627 string='Stage State')
628
629 ```
630
631 Detrás del escenario, los campos relacionados son sólo campos
computados que implementan convenientemente métodos `search` y
`inverse`. Esto significa que podemos buscar y escribir en ellos fuera
de la caja, sin necesidad de escribir un código adicional.
632
633 ## Restricciones del modelo
634
635 Para reforzar la integridad de los datos, los modelos también admiten
dos tipos de restricciones: SQL y Python
636
637 Las restricciones de SQL se añaden a la definición de la tabla de la
base de datos y son aplicadas directamente por PostgreSQL. Se definen
mediante el atributo de clase `_sql_constraints`. Es una lista de
tuplas con: el nombre del identificador de restricción; El SQL para la
restricción; Y el mensaje de error a utilizar.
638
639 Un caso de uso común es agregar restricciones únicas a los modelos.
Supongamos que no queremos permitir dos tareas activas con el mismo
título:
640
641 ```
642 # class TodoTask(models.Model):
643 _sql_constraints = [
644 ('todo_task_name_uniq',
645 'UNIQUE (name, active)',
646 'Task title must be unique!')]
647 ```
Page 18
capitulo-5.md 07/02/2018

648 Las restricciones de Python pueden usar una pieza de código arbitrario
para comprobar las condiciones. La función de verificación debe estar
decorada con `@api.constraints`, indicando la lista de campos
implicados en el chequeo. La validación se activa cuando cualquiera de
ellos se modifica y generará una excepción si la condición falla.
649
650 Por ejemplo, para validar que un nombre de tarea tiene al menos cinco
caracteres, podríamos agregar la siguiente restricción:
651
652 ```
653 rom odoo.exceptions import ValidationError
654 # class TodoTask(models.Model):
655 @api.constrains('name')
656 def _check_name_size(self):
657 for todo in self:
658 if len(todo.name) < 5:
659 raise ValidationError('Must have 5
660 chars!')
661 ```
662
663 ## Resumen
664 Pasamos por una explicación detallada de los modelos y los campos,
utilizandolos para ampliar la aplicación de tareas pendientes con
etiquetas y etapas en las tareas. Aprendiste cómo definir relaciones
entre modelos, incluyendo relaciones jerárquicas entre padres e hijos.
Finalmente, vimos ejemplos simples de campos computados y restricciones
usando código Python.
665
666 En el siguiente capítulo, trabajaremos en la interfaz de usuario para
estas características del modelo de backend, haciéndolas disponibles en
las vistas utilizadas para interactuar con la aplicación.

Page 19
capitulo-6.md 07/02/2018

1 # Capítulo 6. Vistas - Diseñando la interfaz de usuario.


2
3 Este capítulo te ayudará a aprender cómo crear la interfaz gráfica para
que los usuarios interactúen con la aplicación de tareas pendientes.
Descubrirá los distintos tipos de vistas y widgets disponibles,
comprenderás qué son contexto y dominio y cómo utilizarlos para
proporcionar una buena experiencia de usuario.
4
5 Continuaremos trabajando con el módulo `todo_ui`. Ya tiene la capa de
modelo lista, y ahora necesita la capa de vista para la interfaz de
usuario.
6 ##Definiendo la interfaz de usuario con archivos XML
7
8 Cada componente de la interfaz de usuario se almacena en un registro de
base de datos, al igual que los registros de negocios. Los módulos
añaden elementos de interfaz de usuario a la base de datos cargando los
datos correspondientes de archivos XML.
9
10 Esto significa que un nuevo archivo de datos XML para nuestra interfaz
de usuario debe agregarse al módulo `todo_ui`. Podemos comenzar
editando el archivo `__manifest__.py` para declarar estos nuevos
archivos de datos:
11
12 ```
13 {
14 'name': 'User interface improvements to the To-Do app',
15 'description': 'User friendly features.',
16 'author': 'Daniel Reis',
17 'depends': ['todo_user'],
18
19 'data': [
20 'security/ir.model.access.csv',
21 'views/todo_view.xml',
22 'views/todo_menu.xml',
23 ]
24
25
26
27 }
28 ```
29
30 #### Nota
31
32 Recuerda que los archivos de datos se cargan en el orden especificado.
Esto es importante porque sólo puede referenciar IDs XML que se
definieron antes de que ser utilizadas.
33
34
35 También podemos crear el subdirectorio y los archivos
`views/todo_view.xml` y `views/todo_menu.xml` con una estructura mínima:
36
37 ```
38 <?xml version="1.0"?>
39 <odoo>
40 <!-- Content will go here... -->
41 </odoo>
42 ```
43
44 En el capítulo 3, *Herencia - Ampliando las aplicaciones existentes*,
se dio un menú básico a nuestra aplicación, pero ahora queremos
mejorarlo. Por lo tanto, vamos a añadir nuevos elementos de menú y las
Page 1
capitulo-6.md 07/02/2018

acciones de la ventana correspondiente, que se activarán cuando se


seleccionan.
45
46 ### Elementos de menú
47
48 Los elementos de menú se almacenan en el modelo `ir.ui.menu` y se
pueden consultar a través del menú **Settings** en **Technical|User
Interface|Menu Items.
49
50 El complemento `todo_app` creó un menú de nivel superior para abrir las
tareas de la aplicación Tareas pendientes. Ahora queremos modificarlo a
un menú de segundo nivel y tener otras opciones de menú junto a él.
51
52 Para ello, agregaremos un nuevo menú de nivel superior para la
aplicación y modificaremos la opción de menú de tareas pendientes
existente. Para `views / todo_menu.xml`, añada:
53
54 ```
55 <!-- Menu items -->
56 <!-- Modify top menu item -->
57 <menuitem id="todo_app.menu_todo_task" name="To-Do" />
58 <!-- App menu items -->
59 <menuitem id="menu_todo_task_view"
60 name="Tasks"
61 parent="todo_app.menu_todo_task"
62 sequence="10"
63 action="todo_app.action_todo_task" />
64 <menuitem id="menu_todo_config"
65 name="Configuration"
66 parent="todo_app.menu_todo_task"
67 sequence="100"
68 groups="base.group_system" />
69 <menuitem id="menu_todo_task_stage"
70 name="Stages"
71 parent="menu_todo_config"
72 sequence="10"
73 action="action_todo_stage" />
74 ```
75 En lugar de usar elementos `<record model = "ir.ui.menu">`, podemos
usar el elemento de acceso directo más conveniente `<menuitem>`, que
proporciona una forma abreviada de definir el registro que se va a
cargar.
76
77 Nuestro primer elemento de menú es para la entrada del menú superior de
la aplicación de tareas pendientes, con sólo el atributo `name` y se
usará como padre para las dos opciones siguientes.
78
79 Observa que utiliza la ID XML existente `todo_app.menu_todo_task`,
reescribiendo así el elemento de menú, definido en el módulo
`todo_app`, sin ninguna acción adjunta a él. Esto se debe a que vamos a
agregar elementos de menú hijo, y la acción para abrir las vistas de
tarea ahora se llamará desde uno de ellos.
80
81 Los siguientes elementos de menú se colocan bajo el elemento de nivel
superior, a través del atributo `parent="todo_app.menu_todo_task"`.
82
83 El segundo menú es el que abre las vistas Tarea, a través del atributo
`action="todo_app.action_todo_task"`. Como puedes ver en el ID XML
utilizado, está reutilizando una acción creada por el módulo `todo_app`.
84
85 El tercer elemento de menú añade la sección **Configuration** de
Page 2
capitulo-6.md 07/02/2018

nuestra aplicación. Queremos que esté disponible sólo para super


usuarios, por lo que también utilizamos el atributo grupos para que sea
visible sólo para la **Administration|Settings** del grupo de seguridad.
86
87 Por último, en el menú **Configuration** agregamos la opción para la
tarea Etapas. Lo usaremos para mantener las etapas que usarán por la
función kanban que agregaremos a las Tareas pendientes.
88
89 En este punto, si intentamos actualizar el complemento debemos obtener
errores porque no hemos definido las IDs XML utilizados en los
atributos de `action`. Los agregaremos en la siguiente sección.
90
91 ### Acciones de ventana
92
93 Una **acción de ventana** da instrucciones al cliente de GUI, y
usualmente se usa en elementos de menú o botones en vistas. Le dice a
la GUI en qué modelo trabajar, y qué vistas para poner a disposición.
Estas acciones pueden forzar que sólo un subconjunto de los registros
sea visible, utilizando un filtro `domain`. También pueden establecer
valores predeterminados y filtros a través del atributo `context`.
94
95 Agregaremos acciones de ventana al archivo de datos
`views/todo_menu.xml`, que será utilizado por los elementos de menú
creados en la sección anterior. Edita el archivo y asegúrate de que se
agreguen antes de los elementos del menú:
96
97 ```
98 <!-- Actions for the menu items -->
99 <act_window id="action_todo_stage"
100 name="To-Do Task Stages"
101 res_model="todo.task.stage"
102 view_mode="tree,form"
103 target="current"
104 context="{'default_state': 'open'}"
105 domain="[]"
106 limit="80"
107 />
108 <act_window id="todo_app.action_todo_task"
109 name="To-Do Tasks"
110 res_model="todo.task"
111 view_mode="tree,form,calendar,graph,pivot"
112 target="current"
113 context="{'search_default_filter_my_tasks': True}"
114 />
115 <!-- Add option to the "More" button -->
116 <act_window id="action_todo_task_stage"
117 name="To-Do Task Stages"
118 res_model="todo.task.stage"
119 src_model="todo.task"
120 multi="False"
121 />
122
123 ```
124
125 Las acciones de la ventana se almacenan en el modelo
`ir.actions.act_window` y se pueden definir en archivos XML utilizando
el método abreviado `<act_window>` utilizado en el código anterior.
126
127 La primera acción abrirá el modelo Etapas de tareas e incluirá los
atributos más relevantes para las acciones de ventana:
128
Page 3
capitulo-6.md 07/02/2018

129 + `name` es el título que se mostrará en las vistas abiertas a través


de esta acción.
130 + `res_model` es el identificador del modelo de destino.
131 + `view_mode` es el tipo de vista disponible y su orden. La primera es
la abierta por defecto.
132 + `target`, si se establece en `new`, abrirá la vista en una ventana de
diálogo emergente. Por defecto es `current`, abriendo la vista en
línea, en el área de contenido principal.
133 + `context` establece información de contexto en las vistas de destino,
que puede establecer valores predeterminados o activar filtros, entre
otras cosas. Lo veremos con más detalles en un momento.
134 + `domain` es una expresión de dominio que obliga a un filtro para los
registros que serán explorables en las vistas abiertas.
135 + `límit` es el número de registros para cada página, en la vista de
lista.
136
137 La segunda acción definida en el XML reemplaza la acción original de
Tareas pendientes del complemento `todo_app` para que muestre los otros
tipos de vistas que exploraremos más adelante en este capítulo:
calendario y gráfico. Una vez instalados estos cambios, verás botones
adicionales en la esquina superior derecha, después de los botones de
lista y forma; Sin embargo, no funcionarán hasta que creemos las vistas
correspondientes.
138
139 También añadimos una tercera acción, no utilizada en ninguno de los
elementos del menú. Nos muestra cómo agregar una opción al menú
**More**s, disponible en la parte superior derecha de la lista y vistas
de formulario. Para ello, utiliza dos atributos específicos:
140 + `src_model` indica en qué modelo esta acción debe estar disponible.
141 + `multi`, cuando se establece en `True`, lo hace disponible en la
vista de lista para que pueda aplicarse a una selección múltiple de
registros. El valor predeterminado es `False`, como en nuestro ejemplo,
hará que la opción sólo esté disponible en la vista de formulario, por
lo que sólo puede aplicarse a un registro a la vez.
142
143 ## Contexto y dominio
144
145 Nos hemos topado con contexto y dominio varias veces. Hemos visto que
las acciones de ventana son capaces de establecerlos y los campos
relacionales en los modelos también pueden tenerlos como atributos.
146
147 ### Datos de contexto
148
149 El **contexto** es un diccionario que lleva datos de sesión que se
pueden utilizar tanto del lado del cliente en la interfaz de usuario
como del lado del servidor de la ORM y la lógica de negocio.
150
151 En el lado del cliente puede transportar información de una vista a la
siguiente, como la ID del registro activo en la vista anterior, después
de seguir un enlace o un botón o proporcionar valores predeterminados
para ser utilizados en la vista siguiente.
152 You can see the lang key with the user language, tz with the time zone
information, and uid with the current user ID.
153
154 When opening a form from a link or a button in a previous view, an
active_id key is added to the context, with the ID of record we were
positioned at, in the origin form. In the particular case of list
views, we have an active_ids context key containing a list of the
record IDs selected in the previous list.
155
156 On the client side, the context can be used to set default values or
Page 4
capitulo-6.md 07/02/2018

activate default filters on the target view, using keys with the
default_ or default_search_ prefixes. Here are some examples:
157
158 To set the current user as a default value of the user_id field, we
will use the following:
159 En el lado del servidor, algunos valores de campo del conjunto de
registros pueden depender de la configuración local proporcionada por
el contexto. En particular, la clave `lang` afecta al valor de los
campos traducibles. El contexto también puede proporcionar señales para
el código del lado del servidor. Por ejemplo, la clave `active_test`
cuando se establece en `False` cambia el comportamiento del método de
ORM `search ()` para que no filtre los registros inactivos.
160
161 Un contexto inicial del cliente web se ve así:
162
163 ```
164 {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 1}
165
166 ```
167
168 Puedes ver la clave `lang` con el idioma del usuario, `tz` con la
información de zona horaria y `uid` con el ID de usuario actual.
169
170 Al abrir un formulario desde un enlace o un botón en una vista
anterior, se agrega una clave `active_id` al contexto, con el ID de
registro en el que estábamos ubicados, en el formulario de origen. En
el caso particular de las vistas de lista, tenemos una clave de
contexto `active_ids` que contiene una lista de los identificadores de
registro seleccionados en la lista anterior.
171
172 En el lado del cliente, el contexto se puede utilizar para establecer
valores predeterminados o activar filtros predeterminados en la vista
de destino, utilizando claves con los prefijos `default_` o
`default_search_`. Aquí hay unos ejemplos:
173
174 Para configurar el usuario actual como un valor predeterminado del
campo `user_id`, utilizaremos lo siguiente:
175
176 ```
177 {'
178 default
179
180
181
182 _user_id': uid}
183
184 To have a filter_my_tasks filter activated by default on the target
view, we will use this:
185
186 {'
187 default_search
188
189
190
191 _filter_my_tasks': 1}
192
193 ```
194
195 ### Expresiones de dominio
196
197 El **dominio** se utiliza para filtrar registros de datos. Utilizan una
Page 5
capitulo-6.md 07/02/2018

sintaxis específica que la ORD Odoo analiza para producir las


expresiones SQL WHERE que consultarán la base de datos.
198
199 Una expresión de dominio es una lista de condiciones. Cada condición es
una tupla `('field_name', 'operator', value ')`. Por ejemplo, esta es
una expresión de dominio válida, con una sola condición: `[('is_done',
'=', False)]`.
200
201 La siguiente es una explicación de cada uno de estos elementos:
202
203 El **nombre del campo** es el campo que se está filtrando y puede usar
la notación de puntos para los campos de los modelos relacionados.
204
205 El **valor** se evalúa como una expresión de Python. Puede usar valores
literales, como números, Booleanos, cadenas o listas, y puede usar
campos e identificadores disponibles en el contexto de la evaluación.
En realidad hay dos posibles contextos de evaluación para los dominios:
206
207 + Cuando se utilizan en el lado del cliente, como en las acciones de
ventana o los atributos de campo, los valores de campo sin procesar
utilizados para procesar la vista actual están disponibles, pero no
podemos usar la notación de puntos en ellos.
208 + Cuando se utiliza en el lado del servidor, como en las reglas de
registro de seguridad y en el código Python del servidor, la notación
de puntos se puede utilizar en los campos, ya que el registro actual es
un objeto.
209
210 El **operador** puede ser:
211
212 + Los operadores de comparación habituales son `<,>, <=,> =, =,! =`.
213 + `'= like'` coincide con un patrón, donde el símbolo de subrayado,
`_`, coincide con cualquier carácter individual, y el símbolo de
porcentaje,`%`, coincide con cualquier secuencia de caracteres.
214 + `'like'` coincide con un patrón `'%value%'`. El `'ilike'` es similar,
pero no es sensible a las mayúsculas. Los operadores `"not like"` y
"not ilike"` también están disponibles.
215 + `'child of'` encuentra los valores de los hijos en una relación
jerárquica, para los modelos configurados para soportarlos.
216 + `'in'` y `'not in'` se utilizan para verificar la inclusión en una
lista dada, por lo que el valor debe ser una lista de valores. Cuando
se utiliza en un campo de relación "a-muchos", el operador `in` se
comporta como un operador `contains`.
217
218 Una expresión de dominio es una lista de elementos y puede contener
varias tuplas de condición. Por defecto, estas condiciones se
combinarán implícitamente con el ADN operador lógico. Esto significa
que sólo devolverá registros que cumplan todas estas condiciones.
219
220 También se pueden utilizar operadores de lógica explícita: el símbolo
ampersand, `"&"`, para operaciones AND (el predeterminado) y el símbolo
de tubo, `'|'` , Para las operaciones de OR. Éstos funcionarán en los
dos elementos siguientes, trabajando de una manera recursiva. Veremos
esto con más detalle en un momento.
221
222 El signo de exclamación, `'!'` , Representa al operador NOT, también
está disponible y opera en el siguiente elemento. Por lo tanto, debe
colocarse antes de que el elemento sea negado. Por ejemplo, la
expresión `['!', ('Is_done', '=', True)]` filtraría todos los registros
no hechos.
223
224 El "elemento siguiente" también puede ser un elemento del operador que
Page 6
capitulo-6.md 07/02/2018

actúa sobre sus elementos siguientes, definiendo condiciones anidadas.


Un ejemplo puede ayudarnos a entender mejor esto.
225
226 En las reglas del lado del servidor de registro, podemos encontrar
expresiones de dominio similares a esta:
227
228 ```
229 ['|', ('message_follower_ids', 'in',
230 [user.partner_id.id]),
231 '|', ('user_id', '=', user.id),
232 ('user_id', '=', False)
233 ]
234 ```
235
236 Este dominio filtra todos los registros donde el usuario actual está en
la lista de seguidores, es el usuario responsable o no tiene un grupo
de usuarios responsable.
237
238 El primer operador '|' (OR) actúa sobre la condición del seguidor más
el resultado de la condición siguiente. La condición siguiente es de
nuevo la unión de otras dos condiciones: registros donde el ID de
usuario es el usuario de sesión actual o no está establecido.
239
240 El siguiente diagrama ilustra esta resolución de los operadores anidados:
241
242 ![nestedoperationsresolutions](file:img/6-01.jpg)
243
244 ## Las vistas de formulario
245
246 Como hemos visto en capítulos anteriores, las vistas de formulario
pueden seguir un diseño simple o un diseño de documento comercial,
similar a un documento en papel.
247
248 Ahora veremos cómo diseñar estas vistas de documentos empresariales y
cómo usar los elementos y widgets disponibles. Normalmente haríamos
esto heredando y extendiendo las vistas de `todo_app`. Pero en aras de
la claridad, vamos a crear vistas completamente nuevas para anular las
originales.
249
250 ### Tratando con varias vistas del mismo tipo
251
252 El mismo modelo puede tener más de una vista del mismo tipo. Esto puede
ser útil ya que una acción de ventana puede indicar la vista específica
que debe utilizarse, a través de su ID XML. Así que tenemos la
flexibilidad de tener dos elementos de menú diferentes para abrir el
mismo modelo utilizando diferentes vistas. Esto se hace añadiendo un
atributo `view_id` a la acción de la ventana, con el ID XML de la vista
a utilizar. Por ejemplo, podríamos haber usado esto en la acción
`todo_app.action_todo_task`, con algo similar a: `view_id =
"view_form_todo_task_ui"`.
253
254 Pero, ¿qué sucede si no se define una vista específica? En ese caso, la
que se utilice será la primera que se devuelva al consultar las vistas.
Esta será la de menor prioridad. Si añadimos una nueva vista y la
configuramos con una prioridad inferior a las existentes, será la que
se use. El efecto final es que parece que esta nueva vista está
sobreponiendo a la original.
255
256 Dado que el valor predeterminado para la prioridad de vista es 16,
cualquier valor inferior bastaría, por lo que una prioridad 15
funcionará.
Page 7
capitulo-6.md 07/02/2018

257
258 No es la ruta más utilizada, para ayudar a mantener nuestros ejemplos
tan legibles como sea posible, usaremos el enfoque de prioridad en
nuestros próximos ejemplos.
259
260 ### Vistas del documento de negocios
261
262 Las aplicaciones empresariales son a menudo sistemas de registro - para
productos en un almacén, facturas en un departamento de contabilidad, y
muchos más. La mayoría de los datos grabados se pueden representar como
un documento en papel. Para una mejor experiencia de usuario, las
vistas de formulario pueden imitar estos documentos en papel. Por
ejemplo, en nuestra aplicación, podríamos pensar en una Tarea pendiente
como algo que tiene un simple formulario de papel que rellenar.
Proporcionaremos una vista de formulario que siga este diseño.
263
264 Para agregar una vista XML con el esqueleto básico de una vista de
documento de negocios, debemos editar el archivo `views/
todo_views.xml` y añadirlo a la parte superior:
265
266 ```
267 <record id="view_form_todo_task_ui"
268 model="ir.ui.view">
269 <field name="model">todo.task</field>
270 <field name="priority">15</field>
271 <field name="arch" type="xml">
272 <form>
273
274 <header>
275
276
277
278
279 <!-- To add buttons and status widget -->
280
281 </header>
282
283
284
285
286
287 <sheet>
288
289
290
291
292 <!-- To add form content -->
293
294 </sheet>
295
296
297
298
299 <!-- Discuss widgets for history and
300 communication: -->
301
302 <div class="oe_chatter">
303
304
305
306
Page 8
capitulo-6.md 07/02/2018

307 <field name="message_follower_ids"


308 widget="mail_followers" />
309 <field name="message_ids" widget="mail_thread" />
310
311 </div>
312
313
314
315
316 </form>
317 </field>
318 </record>
319
320 ```
321 El nombre de la vista es opcional y generado automáticamente si falta.
Para simplificar, aprovechamos esto y omitimos el elemento `<field
name="name">` del registro de vista.
322
323 Podemos ver que las vistas de documentos empresariales suelen utilizar
tres áreas principales: la barra de estado del encabezado **header**,
la hoja **sheet** del contenido principal y una sección inferior
**history and communication** de historial y comunicación, también
conocida como **chatter** o charla.
324
325 La sección de historial y comunicación, en la parte inferior, utiliza
los widgets de redes sociales proporcionados por el módulo addon de
correo. Para poder usarlos, nuestro modelo debe heredar el modelo
`mixin mail.thread`, como vimos en el Capítulo 3, *Herencia -
Extendiendo las Aplicaciones Existentes*.
326
327 #### El encabezado
328
329 El encabezado en la parte superior por lo general cuenta con el ciclo
de vida o los pasos que el documento seguirá a través y los botones de
acción.
330
331 Estos botones de acción son botones de forma regular, y los pasos
siguientes más importantes se pueden resaltar, usando
`class="oe_highlight"`.
332
333 El ciclo de vida del documento utiliza el widget de barra de
estado`statusbar` en un campo que representa el punto en el ciclo de
vida en el que se encuentra actualmente el documento. Normalmente se
trata de un campo de selección de estado **State** o de una etapa
**Stage** de varios a uno. Estos dos campos se pueden encontrar a
través de varios módulos de núcleo de Odoo.
334
335 la etapa es un campo de muchos a uno que utiliza un modelo de apoyo
para configurar los pasos del proceso. Debido a esto puede ser
configurado dinámicamente por los usuarios finales para adaptarse a su
proceso de negocio específico, y es perfecto para apoyar las juntas
kanban.
336
337 El estado es una lista de selección que contiene algunos pasos,
bastante estables, en un proceso, como `New`, `In progress`, y `Done`.
No es configurable por los usuarios finales, pero, ya que es estático,
es mucho más fácil de utilizar en la lógica de negocio. Los campos de
vista tienen incluso un soporte especial para ello: el atributo de
estado permite que un campo esté disponible para el usuario o no,
dependiendo del estado del documento.
338
Page 9
capitulo-6.md 07/02/2018

339 Históricamente, las etapas se introdujeron más tarde que los estados.
Ambos han coexistido, pero la tendencia en el núcleo de Odoo es para
las etapas de reemplazar a los estados. Pero como se ve en la
explicación anterior, los estados todavía proporcionan algunas
características que las etapas no lo hacen.
340
341 Todavía es posible beneficiarse de lo mejor de ambos mundos, mapeando
las etapas en estados. Esto fue lo que hicimos en el capítulo anterior,
añadiendo un campo de estado en el modelo de etapas de tareas y
haciéndolo disponible también en los documentos de Tarea pendiente a
través de un campo computado, permitiendo el uso del atributo de campo
de estado.
342
343 En el archivo `views/todo_view.xml` ahora podemos expandir el
encabezado básico para agregar una barra de estado:
344
345 ```
346 <header>
347 <field name="state" invisible="True" />
348 <button name="do_toggle_done" type="object"
349 attrs="{'invisible':[('state','in',['draft'])]}"
350 string="Toggle Done"
351 class="oe_highlight" />
352 <field name="stage_id"
353 widget="statusbar"
354 clickable="True"
355 options="{'fold_field': 'fold'}" />
356 </header>
357 ```
358
359 Aquí añadimos `state` como un campo oculto. Necesitamos esto para
obligar al cliente a incluir también ese campo en las solicitudes de
datos enviadas al servidor. De lo contrario, no estará disponible para
su uso en expresiones.
360
361 #### Tip
362
363 Es importante recordar que cualquier campo que desees utilizar, en un
dominio o expresión `attrs`, debe cargarse en la vista, por lo que los
campos quedarán invisibles en cualquier momento que los necesite, pero
no es necesario que los usuarios los vean.
364
365 A continuación, se agrega un botón a la barra de estado, para permitir
al usuario cambiar el indicador de tarea realizada **Done**.
366
367 Los botones que aparecen en la barra de estado deben cambiarse en
función del lugar del ciclo de vida del documento actual.
368
369 Utilizamos el atributo `attrs` para ocultar el botón cuando el
documento está en estado `draft`. La condición para hacer esto utiliza
el campo `state`, no mostrado en el formulario, por lo que tuvimos que
añadirlo como un campo oculto.
370
371 Si tenemos un campo de selección `state`, podemos usar el atributo
`states`. En este caso lo hacemos, y el mismo efecto podría lograrse
usando `states="open, done"`. Aunque no es tan flexible como el
atributo `attrs`, es más conciso.
372
373 Estas características de visibilidad también se pueden utilizar en
otros elementos de vista, como campos. Los exploraremos con más detalle
más adelante en este capítulo.
Page 10
capitulo-6.md 07/02/2018

374
375 El atributo `clickable` permite al usuario cambiar la etapa del
documento haciendo clic en la barra de estado. Normalmente queremos
habilitar esto, pero también hay casos en los que no lo hacemos, como
cuando necesitamos más control sobre el flujo de trabajo, y requerimos
que los usuarios progresen a través de las etapas usando sólo los
botones de acción disponibles, para que éstos puedan realizar
validaciones antes de moverse entre etapas.
376
377 Cuando se utiliza un widget de barra de estado con etapas, podemos
tener ocultas las etapas raramente usadas en un grupo de Más etapas.
Para esto, el modelo de etapas debe tener una bandera para configurar
las que ocultar, usualmente llamado `fold`. Y el widget de `statusbar`
debe utilizar un atributo `options`, como se muestra en el código
anterior, para proporcionar ese nombre de campo a la opción `fold_field`.
378
379 Cuando se utiliza el widget de barra de estado con un campo de estado,
se puede lograr un efecto similar con el atributo `statusbar_visible`,
utilizado para enumerar estados que deben estar siempre visibles y
ocultar estados de excepción necesarios para casos menos comunes. Por
ejemplo:
380
381 ```
382 <field name="stage_id" widget="statusbar"
383 clickable="True"
384 statusbar_visible="draft,open" />
385 ```
386
387 #### La hoja
388
389 El lienzo de hoja es el área principal del formulario donde se colocan
los elementos de datos reales. Está diseñado para que parezca un
documento en papel real, y es común ver que los registros en Odoo se
conocen como **documents**.
390
391 Normalmente, una estructura de hoja de documento tendrá estas áreas:
392
393 + Un título de documento y un subtítulo en la parte superior.
394 + Un cuadro de botones en la esquina superior derecha.
395 + Otros campos de encabezado del documento.
396 + Un cuaderno para campos adicionales organizados en pestañas o
páginas. Las líneas de documento también irían aquí, generalmente en la
primera página del cuaderno.
397
398 Vamos a pasar por cada una de estas áreas.
399
400 #### Título y subtítulos
401
402 Los campos fuera de un elemento `<group>` no tienen automáticamente
etiquetas renderizadas para ellos. Este será el caso de los elementos
de título, por lo que el elemento `<label for "..." />` debe ser
utilizado para procesarla. A expensas de un trabajo extra, esto tiene
la ventaja de dar más control sobre la pantalla de etiquetas.
403
404 El HTML regular, incluyendo elementos del estilo CSS, también se puede
utilizar para hacer brillar el título. Para obtener mejores resultados,
el título debe estar dentro de un `<div>` con la clase `oe_title`.
405
406 Aquí está el elemento `<sheet>` expandido para incluir el título además
de algunos campos adicionales como subtítulos:
407
Page 11
capitulo-6.md 07/02/2018

408 ```
409 <sheet>
410 <div class="oe_title">
411 <label for="name" class="oe_edit_only"/>
412 <h1><field name="name"/></h1>
413 <h3>
414 <span class="oe_read_only>By</span>
415 <label for="user_id" class="oe_edit_only"/>
416 <field name="user_id" class="oe_inline" />
417 </h3>
418 </div>
419 <!-- More elements will be added from here... -->
420 </sheet>
421 ```
422 Aquí podemos ver que usamos elementos HTML normales, como `div`,
`span`, `h1` y `h2`. El elemento `<label>` nos permite controlar cuándo
y dónde se mostrará. El atributo `for` identifica el campo del que
deberíamos obtener el texto de la etiqueta. Otra posibilidad es
utilizar el atributo de `string` para proporcionar un texto específico
para utilizar en la etiqueta. Nuestro ejemplo también utiliza el
atributo `class="oe_edit_only"` para que sea visible sólo en modo de
edición.
423
424 En algunos casos, como Socios o Productos, se muestra una imagen
representativa en la esquina superior izquierda. Suponiendo que
teníamos un campo binario `my_image`, podríamos añadir antes de la
línea `<div class="oe_title">`, usando:
425
426 ```
427 <field name="my_image" widget="image" class="oe_avatar"/>
428 ```
429
430 #### Área de botones inteligentes
431
432 El área superior derecha puede tener una caja invisible donde se pueden
colocar los botones. La versión 8.0 introdujo botones inteligentes,
mostrados como rectángulos con un indicador estadístico que se puede
seguir cuando se hace clic.
433
434 Podemos agregar la caja de botones justo después del final del DIV
`oe_title`, con lo siguiente:
435 ```
436 <div name="buttons" class="oe_right oe_button_box">
437 <!-- Smart buttons here … -->
438 </div>
439 ```
440
441 El contenedor de los botones es un `div` con la clase `oe_button_box` y
también `oe_right`, para alinearlo al lado derecho del formulario.
Estaremos discutiendo los botones con más detalle en una sección
posterior, así que esperaremos hasta entonces para agregar botones
reales en esta caja.
442
443 #### Agrupando contenido en un formulario
444
445 El contenido principal del formulario debe organizarse mediante
etiquetas `<group>`. La etiqueta `group` inserta dos columnas en el
lienzo y, en su interior, por defecto, los campos se mostrarán con
etiquetas.
446
447 Un valor de campo y una etiqueta de campo toman dos columnas, por lo
Page 12
capitulo-6.md 07/02/2018

que agregar campos dentro de un grupo los tendrá apilados


verticalmente. Si anidamos dos elementos `<group>` dentro de un grupo
superior, podremos obtener dos columnas de campos con etiquetas, lado a
lado.
448
449 Continuando con nuestra vista de formulario, ahora podemos agregar el
contenido principal después del cuadro de botones inteligentes:
450
451 ```
452
453 <group name="group_top">
454 <group name="group_left">
455
456
457
458
459 <field name="date_deadline" />
460
461 <separator string="Reference" />
462
463
464
465
466 <field name="refers_to" />
467
468 </group>
469 <group name="group_right">
470
471
472
473
474 <field name="tag_ids" widget="many2many_tags"/>
475
476 </group>
477
478
479
480
481
482 </group>
483
484 ```
485 Es una buena práctica asignar un nombre o `name` a las etiquetas de
grupo para que sea más fácil hacer referencia a ellas más tarde para
ampliar la vista (ya sea para tí u otro desarrollador). El atributo
`string`también está permitido, y si se establece, se utiliza para
mostrar el título de la sección.
486
487 Dentro de un grupo, un elemento `<newline>` forzará una nueva línea y
el siguiente elemento se renderizará en la primera columna del grupo.
Se pueden añadir títulos de sección adicionales dentro de un grupo
utilizando el elemento `<separator>`.
488
489 #### Tip
490
491 Prueba la opción Alternar formato de esquema de formulario **Toggle
Form Layout Outline** del menú Desarrollador **Developer**: dibuja
líneas alrededor de cada sección de grupo, lo que permite una mejor
comprensión del diseño de formulario actual.
492
493 Podemos tener un mayor control sobre el diseño de un elemento de grupo
Page 13
capitulo-6.md 07/02/2018

usando los atributos `col` y `colspan`.


494
495 El atributo `col` se puede utilizar en elementos `<group>` para
personalizar el número de columnas que contendrá. El valor
predeterminado es `2`, pero se puede cambiar a cualquier otro número.
Los números pares funcionan mejor ya que por defecto cada campo añadido
toma dos columnas, para la etiqueta más el valor del campo.
496
497 Los elementos colocados dentro del grupo, incluidos los elementos
`<field>`, pueden utilizar un atributo `colspan` para establecer un
número específico de columnas que deben tomar. De forma predeterminada,
se toma una columna.
498
499 #### Libretas de notas con pestañas
500
501 Otra forma de organizar el contenido es utilizar el elemento
`notebook`, que contiene varias secciones con pestañas, llamadas
páginas. Estos pueden ser utilizados para mantener menos datos usados
fuera de la vista hasta que sea necesario, o para organizar un gran
número de campos por tema.
502
503 No necesitaremos agregar esto a nuestro formulario Tarea pendiente,
pero aquí hay un ejemplo que podría agregarse a un formulario de Etapas
de tareas:
504
505 ```
506 <notebook>
507 <page string="Whiteboard" name="whiteboard">
508 <field name="docs" />
509 </page>
510 <page>
511 <!-- Second page content -->
512 </page>
513 </notebook>
514 ```
515
516 ## Ver componentes semánticos
517
518 Hemos visto cómo organizar el contenido en un formulario, utilizando
componentes estructurales como encabezado, grupo y block de notas.
Ahora podemos ver más de cerca los componentes semánticos, los campos y
los botones, y lo que podemos hacer con ellos.
519
520 ### Campos
521
522 Los campos de vista tienen algunos atributos disponibles para ellos. La
mayoría de ellos tienen valores tomados de su definición en el modelo,
pero estos pueden ser anulados en la vista.
523
524 Los atributos que son genéricos y no dependen del tipo de campo son:
525
526 + `name` identifica el nombre de la base de datos de campo.
527 + `string` es el texto de la etiqueta, que se utilizará si queremos
anular el texto de la etiqueta proporcionado por la definición del
modelo.
528 + `help` es una herramienta de texto que se muestra al colocar el
puntero sobre el campo y permite anular el texto de ayuda proporcionado
por la definición del modelo.
529 + `placeholder` de posición es un texto de sugerencia para mostrar
dentro del campo.
530 + `widget` nos permite anular el widget predeterminado utilizado para
Page 14
capitulo-6.md 07/02/2018

el campo. Exploraremos los widgets disponibles en un momento.


531 + `options` es una estructura de datos JSON con opciones adicionales
para el widget y depende de lo que cada widget admite.
532 + `class` son las clases CSS que se utilizarán para la representación
HTML de campo.
533 + `nolabel="True"` evita que se presente la etiqueta de campo
automática. Sólo tiene sentido para los campos dentro de un elemento
`<group>` y se utiliza a menudo junto con un elemento `<label
for="...">`.
534 + `invisible="True"` hace que el campo no sea visible, pero los datos
se obtienen del servidor y están disponibles en el formulario.
535 + `readonly="True"` hace que el campo no sea editable en el formulario.
536 + `required="True"` hace que el campo sea obligatorio en el formulario.
537
538 Los atributos específicos de algunos tipos de campos son:
539
540 + `password="True"` se utiliza para los campos de texto. Se muestra
como un campo de contraseña, enmascarando los caracteres introducidos.
541 + `filename` se utiliza para los campos binarios y es el nombre del
campo del modelo que se usará para almacenar el nombre del archivo
subido.
542 + `mode` se utiliza para campos uno-a-muchos. Especifica el tipo de
vista a utilizar para mostrar los registros. De forma predeterminada,
es árbol o `tree`, pero también puede ser `form`, `kanban` o `graph`.
543
544 #### Etiquetas para campos
545
546 El elemento `<label>` se puede utilizar para controlar mejor la
presentación de una etiqueta de campo. Un caso en el que se utiliza es
presentar la etiqueta sólo cuando el formulario está en modo de edición:
547 ```
548 <Label for="name" class="oe_edit_only" />
549 ```
550 Al hacer esto, si el campo está dentro de un elemento `<group>`, por lo
general queremos también establecer `nolabel="True"` en él.
551
552 #### Campos relacionales
553
554 En los campos relacionales, podemos tener algún control adicional sobre
lo que el usuario puede hacer. De forma predeterminada, el usuario
puede crear nuevos registros desde estos campos (también conocido como
"creación rápida") y abrir el formulario de registro relacionado. Esto
se puede desactivar usando el atributo de campo de `options`:
555 ```
556 Options={'no_open': True, 'no_create': True}
557 ```
558 El contexto y el dominio también son particularmente útiles en los
campos relacionales. El contexto puede definir los valores
predeterminados para los registros relacionados y el dominio puede
limitar los registros seleccionables. Un ejemplo común es top, tiene la
lista de registros seleccionable en un campo para depender del valor
actual para otro campo del registro actual. El dominio se puede definir
en el modelo, pero también se puede sobreescribir en la vista.
559
560 #### Widgets de campo
561
562 Cada tipo de campo se muestra en el formulario con el widget
predeterminado correspondiente. Pero los widgets alternativos
adicionales están disponibles para ser usados.
563
564 Para los campos de texto, tenemos los siguientes widgets:
Page 15
capitulo-6.md 07/02/2018

565
566 + `email` se utiliza para hacer que el texto del correo electrónico sea
una dirección de correo electrónico que se pueda ejecutar.
567 + `url` se utiliza para formatear el texto como una URL a la que se
puede hacer clic.
568 + `html` se utiliza para representar el texto como contenido HTML; en
el modo de edición, cuenta con un editor WYSIWYG para permitir el
formato del contenido sin necesidad de utilizar la sintaxis HTML.
569
570 Para los campos numéricos, tenemos los siguientes widgets:
571
572 + `handle` está diseñado específicamente para campos de secuencia en
vistas de lista y muestra un identificador que le permite arrastrar
líneas a una orden personalizada.
573 + `float_time` formatea un campo flotante con cantidades de tiempo como
horas y minutos.
574 + `monetary`muestra un campo flotante como el monto de la moneda.
Espera un campo asociado `currency_id`, pero otro nombre de campo puede
ser proporcionado con `options="{'currency_field': 'currency_id'}"`.
575 + `progressbar` presenta un flotante como un porcentaje de progreso y
puede ser útil para los campos que representan una tasa de finalización.
576
577 Para campos relacionales y de selección, tenemos estos widgets
adicionales:
578
579 + `many2many_tags` muestra los valores como una lista de etiquetas tipo
botón.
580 + `seleccion` utiliza el widget de campo `selection` para un campo de
varios a uno.
581 + `radio` muestra las opciones del campo `selection` utilizando los
botones de opción.
582 + `kanban_state_selection` muestra una luz de semáforo para la lista de
selección de estado kanban. El estado normal se representa en gris,
hecho se representa en verde, y cualquier otro estado se representa en
rojo.
583 + `priority` representa el campo de selección como una lista de
estrellas seleccionables. Las opciones de selección suelen ser un
dígito numérico.
584
585 ### Botones
586
587 Los botones admiten estos atributos:
588
589 + `icon` es para la imagen del icono a utilizar en el botón para
mostrar; A diferencia de los botones inteligentes, los iconos
disponibles para los botones normales están limitados a los disponibles
en `addons/web/static/src/img/icons`.
590 + `string` es la etiqueta del texto del botón o el texto HTML `alt`
cuando se utiliza un icono.
591 + `type` es el error tipográfico de la acción a realizar. Los valores
posibles son:
592 - `workflow` se utiliza para activar una señal de motor de flujo de
trabajo;
593 - `object` se utiliza para llamar a un método Python;
594 - `action` se utiliza para ejecutar una acción de ventana.
595 + `name` identifica la acción específica que se debe realizar, según el
tipo elegido: un nombre de señal de flujo de trabajo, un nombre de
método de modelo o el ID de base de datos de la acción de ventana que
se va a ejecutar. La fórmula% (xmlid) d se puede utilizar para traducir
el ID XML en el ID de base de datos requerido.
596 + `args` gs se utiliza cuando el tipo es objeto, para pasar parámetros
Page 16
capitulo-6.md 07/02/2018

adicionales al método.
597 + `context` añade valores al contexto, que pueden tener efectos después
de ejecutar la acción de windows o en los métodos de código Python
llamados.
598 + `confirm` muestra un cuadro de mensaje de confirmación, con el texto
asignado a este atributo.
599 + `pecial="cancel"` se utiliza en los asistentes, para cancelar y
cerrar el formulario del asistente.
600
601 ### Botones inteligentes
602
603 Al diseñar la estructura del formulario, incluimos un área superior
derecha para contener botones inteligentes. Ahora vamos a añadir un
botón dentro de él.
604
605 Para nuestra aplicación, tendremos un botón que muestra el número total
de tareas pendientes para el propietario de la tarea actual, y al hacer
clic en ella, navegaremos hasta la lista de esos elementos.
606
607 Primero debemos añadir el correspondiente campo computado a
`models/todo_model.py`. Agregue a la clase `TodoTask` con lo siguiente:
608
609 ```
610 def compute_user_todo_count(self):
611 for task in self:
612 task.user_todo_count = task.search_count(
613 [('user_id', '=', task.user_id.id)])
614
615 user_todo_count = fields.Integer(
616 'User To-Do Count',
617 compute='compute_user_todo_count')
618
619 ```
620 A continuación, agregamos la caja del botón y el botón dentro de ella.
Justo después de terminar el DIV `oe_title`, reemplaza el marcador de
posición de cuadro de botones que agregamos antes, con lo siguiente:
621
622 ```
623 <div name="buttons" class="oe_right oe_button_box">
624 <button class="oe_stat_button"
625 type="action" icon="fa-tasks"
626 name="%(action_todo_task_button)d"
627 context="{'default_user_id': user_id}"
628 help="All to-dos for this user" >
629 <field string="To-Dos" name="user_todo_count"
630 widget="statinfo"/>
631 </button>
632 </div>
633 ```
634
635 Este botón muestra el número total de Tareas pendientes para la persona
responsable de esta tarea, computada por el campo `user_todo_count`.
636
637 Estos son los atributos que podemos usar al agregar botones inteligentes:
638
639 + `class="oe_stat_button"` representa un rectángulo en lugar de un
botón regular.
640 + `icon` establece el icono a utilizar, elegido en el conjunto de
fuentes Awesome. Los iconos disponibles se pueden consultar en
http://fontawesome.io.
641 + `type` y `name` son el tipo de botón y el nombre de la acción a
Page 17
capitulo-6.md 07/02/2018

activar. Para los botones inteligentes, el tipo suele ser `action`,


para una acción de ventana, y `name` será el ID de la acción a
ejecutar. Espera un ID de base de datos real, por lo que tenemos que
usar una fórmula para convertir un ID XML en un ID de base de datos:
`"%(action-external-id) d"`. Esta acción debe abrir una vista con los
registros relacionados.
642 + `string` añade texto de etiqueta al botón. No lo hemos utilizado aquí
porque el campo contenido ya proporciona un texto para ello.
643 + `context` debe utilizarse para establecer valores predeterminados en
la vista de destino, que se utilizará en los nuevos registros creados
en la vista después de hacer clic en el botón.
644 + `help` agrega una herramienta de ayuda que se muestra cuando el
puntero del ratón está sobre el botón.
645
646 El elemento `button` en sí es un contenedor, con campos que muestran
estadísticas. Estos son campos regulares que utilizan el widget
`statinfo`. El campo debe ser un campo computado definido en el modelo
subyacente. Aparte de campos, dentro de un botón también podemos usar
texto estático, como:
647
648 ```
649 <div>User's To-dos</div>
650 ```
651
652 Al hacer clic en el botón, queremos ver una lista con sólo las Tareas
para el usuario actualresponsable. Esto se hará mediante la acción
action_`todo_task_button`, aún no implementada. Pero necesita saber el
usuario responsable actual, para poder realizar el filtro. Para eso
usamos el atributo del botón `context` para almacenar ese valor.
653
654 La Acción utilizada debe definirse antes del Formulario, por lo que
debemos añadirla en la parte superior del archivo XML:
655
656 ```
657 <act_window id="action_todo_task_button"
658 name="To-Do Tasks"
659 res_model="todo.task"
660 view_mode="tree,form,calendar,graph,pivot"
661 domain="[('user_id','=',default_user_id)]" />
662 ```
663
664 Observe cómo usamos la clave de contexto default_user_id para el filtro
de dominio. Esta clave en particular también establecerá el valor
predeterminado en el campo user_id al crear nuevas Tareas después de
seguir el enlace del botón.
665
666 ## Vistas dinámicas
667 Los elementos de vista también admiten algunos atributos dinámicos que
permiten a las vistas cambiar dinámicamente su apariencia o
comportamiento dependiendo de los valores del campo. Podemos tener en
eventos de cambio, la capacidad de cambiar valores en otros campos
mientras edita datos en un formulario, o tener campos obligatorios o
visibles sólo cuando se cumplen ciertas condiciones.
668
669 ### En los eventos de cambio
670
671 El mecanismo de cambio **on change** nos permite cambiar valores en
otros campos de formulario cuando se cambia un campo particular. Por
ejemplo, el cambio en un campo Producto puede establecer el campo
Precio con un valor predeterminado siempre que se cambie el producto.
672
Page 18
capitulo-6.md 07/02/2018

673 En las versiones anteriores, los eventos de cambio se definieron en el


nivel de vista, pero desde la versión 8.0 se definen directamente en la
capa Modelo, sin necesidad de ninguna marcación específica en las
vistas. Esto se hace creando métodos para realizar los cálculos, y
usando `@api.onchange('field1', 'field2')` para enlazarlo a los campos.
Estos métodos de cambio se analizan con más detalle en el Capítulo 7,
*Lógica de Aplicación de ORM - Procesos de Apoyo al Negocio.
674
675 ### Atributos dinámicos
676
677 El mecanismo de cambio también se encarga de la recomputación
automática de los campos computados, para reaccionar inmediatamente a
la entrada del usuario. Utilizando el mismo ejemplo anterior, si cambia
el campo Precio cuando cambiamos el Producto, también se actualizará
automáticamente un campo computado del valor total utilizando la nueva
información de precios. Un par de atributos proporcionan una manera
fácil de controlar la visibilidad de un elemento de interfaz de usuario
particular:
678
679 + `groups` pueden hacer visible un elemento en función de los grupos de
seguridad a los que pertenece el usuario actual. Sólo los miembros de
los grupos especificados lo verán. Espera una lista separada por comas
de la ID de XML de grupo.
680 + `states` pueden hacer que un elemento sea visible dependiendo del
campo Estado del registro. Espera una lista separada por comas de los
valores del Estado
681
682 Aparte de estos, también tenemos un método flexible disponible para
establecer una visibilidad del elemento dependiendo de una expresión
evaluada dinámicamente del lado del cliente. Este es el atributo
especial `attrs`, esperando un diccionario de valores que asigna el
valor del atributo `invisible` al resultado de una expresión.
683
684 Por ejemplo, para que el campo `refers_to` sea visible en todos los
estados, excepto el borrador, utilice el código siguiente:
685
686 ```
687 <field name="refers_to" attrs="{'invisible':
688 state=='draft'}" />
689 ```
690
691 El atributo `invisble` está disponible en cualquier elemento, no sólo
campos. Por ejemplo, podemos usarlo en páginas de block de notas y en
elementos de grupo.
692
693 Los `attrs` también pueden establecer valores para otros dos atributos:
`readonly` y `required`. Estos sólo tienen sentido para los campos de
datos, para que no sean editables o obligatorios. Esto nos permite
implementar alguna lógica básica del lado del cliente, como hacer un
campo obligatorio dependiendo de otros valores de registro, como el
Estado.
694
695 ## Vistas de lista
696
697 En este punto, las vistas de lista deben necesitar poca introducción,
pero todavía vamos a discutir los atributos que se pueden utilizar con
ellos. A continuación se muestra un ejemplo de una vista de lista para
nuestras Tareas pendientes:
698
699 ```
700 <record id="todo_app.view_tree_todo_task"
Page 19
capitulo-6.md 07/02/2018

701 model="ir.ui.view">
702 <field name="model">todo.task</field>
703 <field name="arch" type="xml">
704 <tree decoration-muted="is_done"
705 decoration-bf="state=='open'"
706 delete="false">
707 <field name="name"/>
708 <field name="user_id"/>
709 <field name="is_done"/>
710 <field name="state" invisible="1"/>
711 </tree>
712 </field>
713 </record>
714 ```
715
716 El color y la fuente del texto de fila pueden cambiar dinámicamente
dependiendo de los resultados de una evaluación de expresión de Python.
Esto se hace a través de los atributos `decoration-NAME`, con la
expresión para evaluar basada en atributos de campo. La parte `NAME`
puede ser `bf` o `it`, para fuentes en negrita y cursiva, o cualquier
color contextual de texto Bootstrap: `peligro, información, silenciado,
primario, éxito` o `advertencia`. La documentación Bootstrap contiene
ejemplos sobre cómo se presentan:
http://getbootstrap.com/css/#helper-classes-colors.
717
718 #### Tip
719
720 Los atributos `colors` y `fonts`, disponibles en la versión 8.0,
estaban obsoletos en la versión 9.0. Los nuevos atributos de decoración
deben ser utilizados en su lugar.
721
722 Recuerde que los campos utilizados en las expresiones deben declararse
en un elemento `<field>`, de modo que el cliente web sepa que esa
columna debe recuperarse del servidor. Si no queremos que se muestre al
usuario, debemos usar el atributo `invisible="1"` en él.
723
724 Otros atributos relevantes del elemento árbol son:
725
726 + `default_order` permite anular el orden de clasificación
predeterminado del modelo y su valor sigue el mismo formato que en el
atributo de orden utilizado en las definiciones de modelo.
727 + `create, delete` y `edit`, si se establece en `false` (en minúsculas)
deshabilita la acción correspondiente en la vista de lista.
728 + `editable` hace que los registros sean editables directamente en la
vista de lista. Los valores posibles son `top` y low`, la ubicación
donde se añadirán los nuevos registros.
729
730 Una vista de lista puede contener campos y botones, y la mayoría de sus
atributos para formularios también son válidos aquí.
731
732 En las vistas de lista, los campos numéricos pueden mostrar valores de
resumen para su columna. Para ello, agregue al campo uno de los
atributos de agregación disponibles, `sum`, `avg`, `min` o `max` y
asigne el texto de etiqueta para el valor de resumen. Por ejemplo:
733
734 ```
735 <field name="amount" sum="Total Amount" />
736 ```
737
738 ## Vistas de Búsqueda
739
Page 20
capitulo-6.md 07/02/2018

740 Las opciones de búsqueda disponibles se definen mediante el tipo de


vista `<búsqueda>` . Podemos elegir los campos se pueden buscar
automáticamente al escribir en el cuadro de búsqueda. También podemos
proporcionar filtros predefinidos, activados con un clic y opciones de
agrupación predefinidas que se utilizarán en vistas de lista.
741
742 Esta es una posible vista de búsqueda para las Tareas pendientes:
743
744 ```
745 <record id="todo_app.view_filter_todo_task"
746 model="ir.ui.view">
747 <field name="model">todo.task</field>
748 <field name="arch" type="xml">
749 <search>
750 <field name="name"/>
751 <field name="user_id"/>
752 <filter name="filter_not_done" string="Not Done"
753 domain="[('is_done','=',False)]"/>
754 <filter name="filter_done" string="Done"
755 domain="[('is_done','!=',False)]"/>
756 <separator/>
757 <filter name="group_user" string="By User"
758 context="{'group_by': 'user_id'}"/>
759 </search>
760 </field>
761 </record>
762 ```
763 Podemos ver dos campos a buscar: `–name` y `user_id`. Cuando el usuario
comience a escribir en el cuadro de búsqueda, un menú desplegable le
sugerirá buscar en cualquiera de estos campos. Si el usuario escribe
`ENTER`, la búsqueda se realizará en el primero de los campos de filtro.
764
765 Entonces tenemos dos filtros predefinidos, filtrando tareas hechas y no
hechas. Estos filtros se pueden activar independientemente, y se unirán
con un operador OR. Los bloques de filtros separados con un elemento
`<separator/>` se unirán con un operador AND.
766
767 El tercer filtro sólo establece un grupo por contexto. Esto le indica a
la vista agrupar los registros por ese campo, `user_id` en este caso.
768
769 Los elementos de campo pueden utilizar los siguientes atributos:
770
771 + `name` identifica el campo a utilizar.
772 + `string` es un texto de etiqueta que se utiliza en lugar del valor
predeterminado.
773 + `operador` se utiliza para cambiar el operador de la predeterminada
(`=` para los campos numéricos y `ilike` para los otros tipos de campo).
774 + `filter_domain` establece una expresión de dominio específica que se
utilizará para la búsqueda, proporcionando una alternativa flexible al
atributo operator. La cadena de texto buscada se refiere en la
expresión como self. Un ejemplo trivial es: `filter_domain="[('name',
'ilike', self)]"`.
775 + `grupos` hace que la búsqueda en el campo esté disponible sólo para
usuarios pertenecientes a algunos grupos de seguridad. Espera una lista
separada por comas de IDs XML.
776
777 Para los elementos de filtro, estos son los atributos disponibles:
778
779 + `name` es un identificador para utilizar por herencia o para
habilitarlo a través de acciones de ventana. No es obligatorio, pero es
una buena práctica siempre proporcionarlo.
Page 21
capitulo-6.md 07/02/2018

780 + `string` es el texto de la etiqueta que se mostrará para el filtro.


Necesario.
781 + `domain` es la expresión de dominio que se agregará al dominio actual.
782 + `context` es un diccionario de contexto que se agrega al contexto
actual. Generalmente establece una clave `group_id` con el nombre del
campo para agrupar registros.
783 + `groups` hace que la búsqueda en el campo sólo esté disponible para
una lista de grupos de seguridad (IDs XML).
784
785 ## Vistas de calendario
786
787 Como su nombre indica, este tipo de vista presenta los registros de un
calendario que se pueden ver durante mes, semana o días. Una vista de
calendario para las Tareas pendientes podría tener este aspecto:
788
789 ```
790 <record id="view_calendar_todo_task" model="ir.ui.view">
791 <field name="model">todo.task</field>
792 <field name="arch" type="xml">
793 <calendar date_start="date_deadline" color="user_id"
794 display="[name], Stage [stage_id]" >
795 <!-- Fields used for the display text -->
796 <field name="name" />
797 <field name="stage_id" />
798 </calendar>
799 </field>
800 </record>
801
802 ```
803
804 Los atributos del calendario son:
805
806 + `date_start` es el campo para la fecha de inicio. Obligatorio.
807 + `date_end` es el campo para la fecha de finalización. Opcional.
808 + `date_delay` es el campo con la duración en días, que se puede
utilizar en lugar de `date_end`.
809 + `all_day` proporciona el nombre de un campo booleano que se utilizará
para señalar eventos de día completo. En estos eventos, se ignora la
duración.
810 + `color` es el campo utilizado para agrupar el color de las entradas
del calendario. A cada valor distinto en este campo se le asignará un
color, y todas sus entradas tendrán el mismo color.
811 + `display` es el texto de visualización para cada entrada de
calendario. Puede registrar valores de usuario utilizando los nombres
de campo entre corchetes, como `[name]`. Estos campos deben declararse
como hijos del elemento de calendario, y en el ejemplo anterior.
812 + `mode` es el modo de visualización predeterminado para el calendario,
ya sea `day`, `week` o `month`.
813
814 ## Vistas de gráficos y pivotes
815
816 Las vistas de gráfico proporcionan una vista gráfica de los datos, en
forma de gráfico. Los campos actuales disponibles en las Tareas
pendientes no son buenos candidatos para un gráfico, por lo que
agregamos uno para utilizarlo en dicha vista.
817
818 En la clase `TodoTask`, en el archivo `todo_ui/models/todo_model.py`,
agrega:
819
820 ```
821 effort_estimate = fields.Integer('Effort Estimate')
Page 22
capitulo-6.md 07/02/2018

822 ```
823
824 También debe añadirse al formulario Tarea pendiente, de modo que
podamos añadir valores a los registros existentes y poder comprobar
esta nueva vista.
825
826 Ahora vamos a añadir la vista gráfica de las Tareas pendientes:
827
828 ```
829 <record id="view_graph_todo_task" model="ir.ui.view">
830 <field name="model">todo.task</field>
831 <field name="arch" type="xml">
832 <graph type="bar">
833 <field name="stage_id" />
834 <field name="effort_estimate" type="measure" />
835 </graph>
836 </field>
837 </record>
838 ```
839
840 El elemento de vista `graph` puede tener un atributo `type` que puede
establecerse en `bar` (el valor predeterminado),` pie o `line`. En el
caso de `bar`, el adicional `stacked="True"` se puede utilizar para
hacer un gráfico de barras apiladas.
841
842 Los datos también se pueden ver en una tabla dinámica, una matriz de
análisis dinámico. Para ello, tenemos la vista pivote, introducida en
la versión 9.0. Las tablas pivote ya estaban disponibles en la versión
8.0, pero en 9.0, se movieron a su propio tipo de vista. Junto con
esto, mejoró las características de interfaz de usuario de tablas de
pivote, y optimizó la recuperación de datos de tabla dinámica en gran
medida.
843
844 Para agregar también una tabla dinámica a las Tareas pendientes,
utilice este código:
845
846 ```
847 <record id="view_pivot_todo_task" model="ir.ui.view">
848 <field name="arch" type="xml">
849 <pivot>
850 <field name="stage_id" type="col" />
851 <field name="user_id" />
852 <field name="date_deadline" interval="week" />
853 <field name="effort_estimate" type="measure" />
854 </pivot>
855 </field>
856 </record>
857 ```
858 Las vistas de gráfico y pivote deben contener elementos de campo que
describen el eje y las medidas a utilizar. La mayoría de los atributos
disponibles son comunes a ambos tipos de vista:
859
860 + `name` identifica el campo que se va a utilizar en el gráfico, al
igual que en otras vistas
861 + `type` es cómo se usará el campo, como un grupo `row`
(predeterminado), un `measure` o como `col` (sólo para tablas
dinámicas, úsalo para grupos de columnas)
862 + `interval` es significativo para los campos de fecha y es el
intervalo de tiempo utilizado para agrupar datos de tiempo por `day`,
`week`, `month`, `quarter` o `año`.
863
Page 23
capitulo-6.md 07/02/2018

864 Por defecto, la agregación utilizada es la suma de los valores. Esto se


puede cambiar estableciendo el atributo `group_operator` en la
definición de campo Python. Los valores que se pueden usar incluyen
`avg`, `max` y `min`.
865
866 ## Otros tipos de vista
867
868 Vale la pena señalar que no cubrimos otros tres tipos de vista que
también están disponibles: kanban, gantt y diagrama.
869
870 Las vistas Kanban se tratarán en detalle en el Capítulo 9, *Vistas QWeb
y Kanban*.
871
872 La vista de gantt estaba disponible hasta la versión 8.0, pero se
eliminó en la versión 9.0 de la edición de la comunidad debido a las
incompatibilidades de las licencias.
873
874 Por último, las vistas de diagrama se utilizan para casos muy
específicos, y un módulo addon los necesitará raramente. Por si acaso,
le gustaría saber que el material de referencia para los dos tipos de
vista se puede encontrar en la documentación oficial,
https://www.odoo.com/documentation/10.0/reference/views.html.
875
876 ## Resumen
877
878 En este capítulo, aprendimos más sobre las vistas de Odoo para
construir la interfaz de usuario, cubriendo los tipos de vista más
importantes. En el próximo capítulo, aprenderemos más sobre cómo
agregar lógica de negocios a nuestras aplicaciones.

Page 24
capitulo-7.md 07/02/2018

1 # Capítulo 7. Lógica de aplicación de ORM - Procesos de soporte


empresariales.
2 Con la programación API de Odoo, podemos escribir lógica compleja y
asistentes para proporcionar una interacción de usuario rica para
nuestras aplicaciones. En este capítulo, veremos cómo escribir código
para apoyar la lógica de negocio en nuestros modelos, y también
aprenderemos cómo activarlo en eventos y acciones de usuario.
3
4 Podemos realizar cálculos y validaciones en eventos, como crear o
escribir en un registro, o realizar alguna lógica cuando se hace clic
en un botón. Por ejemplo, hemos implementado acciones de botón para las
Tareas pendientes, para alternar el indicador **Listo "Is Done"** y
desactivar todas las tareas realizadas desactivándolas.
5
6 Además, también podemos utilizar asistentes para implementar
interacciones más complejas con el usuario, lo que permite solicitar
entradas y proporcionar retroalimentación durante la interacción.
7
8 Comenzaremos por crear un asistente para nuestra aplicación de tareas
pendientes.
9
10 ## Creando un asistente
11
12 Supongamos que nuestros usuarios de la aplicación To-Do regularmente
necesitan establecer los plazos y la persona responsable de un gran
número de tareas. Podrían usar un asistente para ayudar con esto.
Debería permitirles seleccionar las tareas a actualizar y, a
continuación, elegir la fecha límite y / o el usuario responsable para
establecerlas.
13
14 Los asistentes son formularios utilizados para obtener información de
entrada de los usuarios y, a continuación, utilizarla para
procesamiento posterior. Pueden utilizarse para tareas sencillas, como
solicitar unos pocos parámetros y ejecutar un informe, o para
manipulaciones de datos complejas, como el caso de uso descrito
anteriormente.
15
16 Así será nuestro asistente:
17
18 ![ToDoTaskCreatingWizard](file:img/7-01.jpg)
19
20 Podemos empezar creando un nuevo módulo addon para la función
`todo_wizard`.
21
22 Nuestro módulo tendrá un archivo Python y un archivo XML, por lo que la
descripción de `todo_wizard/__ manifest__.py` será como se muestra en
el siguiente código:
23
24 ```
25 { 'name': 'To-do Tasks Management Assistant',
26 'description': 'Mass edit your To-Do backlog.',
27 'author': 'Daniel Reis',
28 'depends': ['todo_user'],
29 'data': ['views/todo_wizard_view.xml'], }
30 ```
31
32 Como en los complementos anteriores, el archivo `todo_wizard/__
init__.py` es sólo una línea:
33
34 ```
35 from . import models
Page 1
capitulo-7.md 07/02/2018

36 ```
37
38 A continuación, necesitamos describir el modelo de datos que soporta
nuestro asistente.
39
40 ### El modelo del asistente
41
42 Un asistente muestra una vista de formulario para el usuario,
normalmente como una ventana de diálogo, con algunos campos que se
rellenan. Estos serán utilizados por la lógica del asistente.
43
44 Esto se implementa utilizando la misma arquitectura de modelo/vista que
para vistas regulares, pero el modelo de soporte se basa en
`models.TransientModel` en lugar de `models.Model`.
45
46 Este tipo de modelo también tiene una representación de base de datos y
almacena el estado allí, pero se espera que estos datos sean útiles
sólo hasta que el asistente termine su trabajo. Un trabajo programado
limpia regularmente los datos antiguos de las tablas de la base de
datos del asistente.
47
48 El archivo `models/todo_wizard_model.py` definirá los campos que
necesitamos para interactuar con el usuario: la lista de tareas a
actualizar, el usuario responsable y la fecha límite para establecerlas.
49
50 Primero agregua el archivo`models/__init__.py` con la siguiente línea
de código:
51
52 ```
53 from . import todo_wizard_model
54
55 ```
56
57 Luego, crea el archivo actual `models/todo_wizard_model.py`:
58
59 ```
60 # -*- coding: utf-8 -*-
61 from odoo import models, fields, api
62
63 class TodoWizard(models.TransientModel):
64 _name = 'todo.wizard'
65 _description = 'To-do Mass Assignment'
66 task_ids = fields.Many2many('todo.task',
67 string='Tasks')
68 new_deadline = fields.Date('Deadline to Set')
69 new_user_id = fields.Many2one(
70 'res.users',string='Responsible to Set')
71
72 ```
73
74 Vale la pena señalar que las relaciones uno-a-muchos con los modelos
regulares no deben usarse en modelos transitorios. La razón de esto es
que requeriría que el modelo regular tuviera la relación inversa de
muchos a uno con el modelo transitorio, pero esto no está permitido, ya
que podría haber la necesidad de recolectar basura a los registros de
modelos regulares junto con el modelo Registros transitorios.
75
76 ### El formulario de asistente
77
78 Las vistas del formulario del asistente son las mismas que para los
modelos regulares, excepto en dos elementos específicos:
Page 2
capitulo-7.md 07/02/2018

79
80 + Se puede utilizar una sección `<footer>` para colocar los botones de
acción
81 + Un botón especial `type= "cancel"` disponible para interrumpir el
asistente sin realizar ninguna acción
82
83 Este es el contenido de nuestro archivo `views/todo_wizard_view.xml`:
84
85 ```
86 <odoo>
87 <record id="To-do Task Wizard" model="ir.ui.view">
88 <field name="name">To-do Task Wizard</field>
89 <field name="model">todo.wizard</field>
90 <field name="arch" type="xml">
91
92 <form>
93 <div class="oe_right">
94 <button type="object" name="do_count_tasks"
95 string="Count" />
96 <button type="object" name="do_populate_tasks"
97 string="Get All" />
98 </div>
99
100 <field name="task_ids">
101 <tree>
102 <field name="name" />
103 <field name="user_id" />
104 <field name="date_deadline" />
105 </tree>
106 </field>
107
108 <group>
109 <group> <field name="new_user_id" /> </group>
110 <group> <field name="new_deadline" /> </group>
111 </group>
112
113 <footer>
114 <button type="object" name="do_mass_update"
115 string="Mass Update" class="oe_highlight"
116 attrs="{'invisible':
117 [('new_deadline','=',False),
118 ('new_user_id', '=',False)]
119 }" />
120 <button special="cancel" string="Cancel"/>
121 </footer>
122 </form>
123 </field>
124 </record>
125
126 <!-- More button Action -->
127 <act_window id="todo_app.action_todo_wizard"
128 name="To-Do Tasks Wizard"
129 src_model="todo.task" res_model="todo.wizard"
130 view_mode="form" target="new" multi="True" />
131 </odoo>
132
133 ```
134 La acción de ventana `<act_window>` que vemos en el XML añade una
opción al botón **Más "More"** del formulario Tarea pendiente mediante
el atributo `src_model`. El atributo `target="new"` lo hace abrir como
una ventana de diálogo.
Page 3
capitulo-7.md 07/02/2018

135
136 También puedes haber notado que `attrs` se utiliza en el botón de
**actualización masiva "Mass Update"**, para añadir el toque agradable
de hacerlo invisible hasta que se seleccione un nuevo plazo o un
usuario responsable.
137
138 ### La lógica empresarial del asistente
139
140 A continuación, tenemos que implementar las acciones a realizar en los
botones de formulario. Excluyendo el botón **Cancelar "Cancel"**,
tenemos tres botones de acción para implementar, pero ahora nos
enfocaremos en el botón de **actualización masiva "Mass Update"**.
141
142 El método llamado por el botón es `do_mass_update` y debe definirse en
el archivo `models/todo_wizard_model.py`, como se muestra en el código
siguiente:
143
144 ```
145 from odoo import exceptions
146 import logging
147 _logger = logging.getLogger(__name__)
148
149 # ...
150 # class TodoWizard(models.TransientModel):
151 # ...
152
153 @api.multi
154 def do_mass_update(self):
155 self.ensure_one()
156 if not (self.new_deadline or self.new_user_id):
157 raise exceptions.ValidationError('No data to update!')
158 _logger.debug('Mass update on Todo Tasks %s',
159 self.task_ids.ids)
160 vals = {}
161 if self.new_deadline:
162 vals['date_deadline'] = self.new_deadline
163 if self.new_user_id:
164 vals['user_id'] = self.new_user_id
165 # Mass write values on all selected tasks
166 if vals:
167 self.task_ids.write(vals)
168 return True
169
170 ```
171
172 Nuestro código debe manejar una instancia de asistente a la vez, por lo
que usamos `self.ensure_one ()` para que quede claro. Aquí `self`
representa el registro de exploración para los datos en el formulario
del asistente.
173
174 El método comienza validando si se ha dado una nueva fecha límite o un
usuario responsable, y si no se produce un error. A continuación,
tenemos un ejemplo de cómo escribir un mensaje de depuración en el
registro del servidor.
175
176 A continuación, el diccionario `vals` se construye con los valores a
establecer con la actualización masiva: la nueva fecha, nuevo
responsable o ambas. A continuación, el método de escritura `write` se
utiliza en un conjunto de registros para realizar la actualización
masiva. Esto es más eficiente que un bucle que realiza escrituras
individuales en cada registro.
Page 4
capitulo-7.md 07/02/2018

177
178 Es una buena práctica para los métodos de siempre devolver algo. Es por
eso que devuelve el valor `True` al final. La única razón de esto es
que el protocolo XML-RPC no admite valores `None`, por lo que esos
métodos no se podrán utilizar mediante ese protocolo. En la práctica,
es posible que no conozcas el problema porque el cliente web utiliza
JSON-RPC, no XML-RPC, pero sigue siendo una buena práctica.
179
180 A continuación, vamos a tener una mirada más cercana al registro, y
luego trabajaremos en la lógica detrás de los dos botones en la parte
superior: Contar y obtener todo.
181
182 ### Logging
183
184 Estas actualizaciones masivas podrían ser mal utilizadas, por lo que
podría ser una buena idea registrar alguna información cuando se
utiliza. El código anterior inicializa el `_logger` en las dos líneas
antes de la clase `TodoWizard`, utilizando la biblioteca estándar de
registro de Python. La variable interna Python `__name__` es para
identificar los mensajes como procedentes de este módulo.
185
186 Para escribir mensajes de registro en el código del método podemos usar:
187
188 ```
189 _logger.debug('A DEBUG message')
190 _logger.info('An INFO message')
191 _logger.warning('A WARNING message')
192 _logger.error('An ERROR message')
193 ```
194
195 Al pasar valores para usar en el mensaje de registro, en lugar de usar
interpolación de cadena, deberíamos proporcionarlos como parámetros
adicionales. Por ejemplo, en lugar de `_logger.info('Hello% s' %
'World')` deberíamos usar `_logger.info('Hello% s', 'World')`. Puedes
notar que lo hicimos en el método `do_mass_update ()`.
196
197 #### Nota
198
199 Una cosa interesante a notar sobre el registro, es que las entradas del
registro imprimen siempre la marca de tiempo en UTC. Esto puede ser una
sorpresa para los nuevos administradores, pero se debe al hecho de que
el servidor maneja internamente todas las fechas en UTC.
200
201
202 ### Aumentando las excepciones
203
204 Cuando algo no está bien, querremos interrumpir el programa con un
mensaje de error. Esto se hace levantando una excepción. Odoo ofrece
algunas clases de excepción adicionales a las disponibles en Python.
Estos son ejemplos para los más útiles:
205
206 ```
207 from odoo import exceptions
208 raise exceptions.Warning('Warning message')
209 raise exceptions.ValidationError('Not valid message')
210 ```
211
212 El mensaje de advertencia `Warning` también interrumpe la ejecución,
pero puede sonar menos grave que un mensaje `ValidationError`. Aunque
no es la mejor interfaz de usuario, aprovechamos eso en el botón de
conteo **Count** para mostrar un mensaje al usuario:
Page 5
capitulo-7.md 07/02/2018

213
214 ```
215 @api.multi
216 def do_count_tasks(self):
217 Task = self.env['todo.task']
218 count = Task.search_count([('is done', '=', False)])
219 raise exceptions.Warning(
220 'There are %d active tasks.' %count)
221 ```
222
223 Como una nota de lado, parece que podríamos haber utilizado el
decorador `@api.model`, ya que este método no funciona en el conjunto
de registros `self`. Pero en este caso no podemos porque el método
tiene que ser llamado desde un botón.
224
225 ### Acciones de ayuda en asistentes
226
227 Ahora supongamos que queremos un botón para recoger automáticamente
todas las tareas pendientes para evitar que el usuario los escoge uno
por uno. Ese es el punto de tener el botón **Obtener Todo "Get All"**
en el formulario. El código detrás de este botón obtendrá un conjunto
de registros con todas las tareas activas y lo asignará a las tareas
del campo muchos-a-muchos.
228
229 Pero hay una trampa aquí. En las ventanas de diálogo, cuando se pulsa
un botón, la ventana del asistente se cierra automáticamente. No
enfrentamos este problema con el botón **Count** porque utiliza una
excepción para mostrar su mensaje; Por lo que la acción no tiene éxito
y la ventana no está cerrada.
230
231 Afortunadamente, podemos evitar este comportamiento pidiendo al cliente
que vuelva a abrir el mismo asistente. Los métodos de modelo pueden
devolver una acción de ventana que debe realizar el cliente web, en
forma de un objeto de diccionario. Este diccionario utiliza los mismos
atributos que se utilizan para definir acciones de ventana en archivos
XML.
232
233 Vamos a definir una función de ayuda para el diccionario de acción de
la ventana para reabrir la ventana del asistente, para que pueda
reutilizarse fácilmente en varios botones:
234
235 ```
236 @api.multi
237 def _reopen_form(self):
238 self.ensure_one()
239 return {
240 'type': 'ir.actions.act_window',
241 'res_model': self._name, # this model
242 'res_id': self.id, # the current wizard record
243 'view_type': 'form',
244 'view_mode': 'form',
245 'target': 'new'}
246 ```
247
248 Vale la pena señalar que la acción de la ventana podría ser algo más,
como saltar a un formulario de asistente diferente para solicitar la
entrada de usuario adicional y que se puede utilizar para implementar
asistentes de varias páginas.
249
250 Ahora, el botón **Get All** puede hacer su trabajo y mantener al
usuario trabajando en el mismo asistente:
Page 6
capitulo-7.md 07/02/2018

251
252 ```
253 @api.multi
254 def do_populate_tasks(self):
255 self.ensure_one()
256 Task = self.env['todo.task']
257 open_tasks = Task.search([('is_done', '=', False)])
258 # Fill the wizard Task list with all tasks
259 self.task_ids = all_tasks
260 # reopen wizard form on same wizard record
261 return self._reopen_form()
262 ```
263
264 Aquí podemos ver cómo trabajar con cualquier otro modelo disponible:
primero usamos `self.env[]` para obtener una referencia al modelo,
`todo.task` en este caso, y luego podemos realizar acciones en él, como
`search()` para recuperar registros que cumplan algunos criterios de
búsqueda.
265
266 El modelo transitorio almacena los valores en los campos de formulario
del asistente y puede leerse o escribirse como cualquier otro modelo.
La variable `all_tasks` se asigna al campo uno-a-muchos `task_ids`.
Como puede ver, esto se hace como lo haríamos para cualquier otro tipo
de campo.
267
268 ## Trabajando con la API ORM
269
270 De la sección anterior, ya tenemos una idea de cómo es usar la API de
ORM. A continuación veremos qué más podemos hacer con él.
271
272 ### Decoradores de métodos
273
274 Durante nuestro viaje, los varios métodos que encontramos utilizaron
decoradores API como `@api.multi`. Estos son importantes para el
servidor para saber cómo manejar el método. Vamos a recapitular los
disponibles y cuando deben ser utilizados.
275
276 El decorador `@api.multi` se utiliza para manejar conjuntos de
registros con la nueva API y es el más utilizado. Aquí `self` es un
conjunto de registros, y el método normalmente incluirá un bucle `for`
para iterarlo.
277
278 En algunos casos, el método se escribe para esperar un singleton: un
conjunto de registros que no contenga más de un registro. El decorador
de `@api.one` fue descontinuado desde 9.0 y debe ser evitado. En su
lugar debemos seguir utilizando `@api.multi` y añadir al código del
método una línea con `self.ensure_one()`, para asegurar que es un
singleton.
279
280 Como se mencionó, el decorador `@api.one` es obsoleto, pero todavía es
compatible. Para completar, puede ser que valga la pena saber que
envuelve el método decorado, alimentándolo de un registro a la vez,
haciendo la iteración del conjunto de registros. En nuestro método
`self` está garantizado para ser un singleton. Los valores de retorno
de cada llamada de método individual se agregan como una lista y se
devuelven.
281
282 El `@api.model` decora un método estático de nivel de clase y no
utiliza datos de conjunto de registros. Para la consistencia,
`self`sigue siendo un conjunto de registros, pero su contenido es
irrelevante. Ten en cuenta que este tipo de método no se puede utilizar
Page 7
capitulo-7.md 07/02/2018

desde los botones de la interfaz de usuario.


283
284 Algunos otros decoradores tienen propósitos más específicos y se van a
utilizar junto con los decoradores descritos anteriormente:
285
286 + `@api.depends(fld1,...)` se utiliza para que las funciones de campo
computadas identifiquen en qué cambios debe ser activado el (re) cálculo
287 + `@api.constrains(fld1,...)` se utiliza para las funciones de
validación para identificar en qué cambios debe activarse la
comprobación de validación
288 + `@api.onchange(fld1,...)` se utiliza para las funciones de cambio
para identificar los campos en el formulario que desencadenará la acción
289
290 En particular, los métodos `onchange` pueden enviar un mensaje de
advertencia a la interfaz de usuario. Por ejemplo, esto podría advertir
al usuario que la cantidad de producto recién ingresada no está
disponible en stock, sin impedir que el usuario continúe. Esto se hace
al hacer que el método devuelva un diccionario que describe el mensaje
de advertencia:
291
292 ```
293 return {
294 'warning': {
295 'title': 'Warning!',
296 'message': 'You have been warned'}
297 }
298 ```
299
300 ### Anulando los métodos predeterminados de ORM
301
302 Hemos aprendido sobre los métodos estándar proporcionados por la API,
¡pero sus usos no terminan ahí! También podemos ampliarlos para agregar
un comportamiento personalizado a nuestros modelos.
303
304 El caso más común es extender los métodos `create()` y `write()`. Esto
se puede utilizar para agregar la lógica a ser activada cada vez que se
ejecutan estas acciones. Colocando nuestra lógica en la sección
apropiada del método personalizado, podemos hacer que el código se
ejecute antes o después de que se ejecuten las operaciones principales.
305
306 Utilizando el modelo `TodoTask` como ejemplo, podemos hacer un
`create()` personalizado, que se vería así:
307
308 ```
309 @api.model
310 def create(self, vals):
311 # Code before create: can use the `vals` dict
312 new_record = super(TodoTask, self).create(vals)
313 # Code after create: can use the `new_record` created
314 return new_record
315
316 ```
317
318 Un `write()` personalizado debería seguir esta estructura:
319
320 ```
321 @api.multi
322 def write(self, vals):
323 # Code before write: can use `self`, with the old values
324 super(TodoTask, self).write(vals)
325 # Code after write: can use `self`, with the updated values
Page 8
capitulo-7.md 07/02/2018

326 return True


327
328 ```
329 Estos son ejemplos comunes de extensión, pero por supuesto cualquier
método estándar disponible para un modelo puede ser heredado de una
manera similar para agregar nuestra lógica personalizada a ella.
330
331 Estas técnicas abren muchas posibilidades, pero recuerda que también
hay otras herramientas disponibles que pueden ser más adecuadas para
tareas específicas comunes:
332
333 + Para tener un valor de campo calculado basado en otro, debemos usar
campos calculados. Un ejemplo de esto es calcular un total de cabecera
cuando se cambian los valores de las líneas.
334 + Para que los valores por defecto de campo se calculen dinámicamente,
podemos usar un campo predeterminado enlazado a una función en lugar de
un valor fijo.
335 + Para que los valores se establezcan en otros campos cuando se cambia
un campo, podemos usarlo en las funciones de cambio. Un ejemplo de esto
es cuando se selecciona un cliente, estableciendo su moneda como la
moneda del documento, que posteriormente puede ser modificada
manualmente por el usuario. Tenga en cuenta que el cambio sólo funciona
en la interacción de vista de formulario y no en las llamadas de
escritura directa.
336 + Para las validaciones, debemos utilizar funciones de restricción
decoradas con `@api.constraints(fld1, fld2, ...)`. Estos son como
campos calculados pero, en lugar de calcular valores, se espera que
generen errores.
337
338 ### Métodos para llamadas a clientes web y RPC
339
340 Hemos visto los métodos de modelo más importantes usados para generar
conjuntos de registros y cómo escribir en ellos. Pero hay algunos
métodos de modelo más disponibles para acciones más específicas, como
se muestra aquí:
341
342 + `read([fields])` es similar al método `browse`, pero en lugar de un
conjunto de registros, devuelve una lista de filas de datos con los
campos dados como argumento. Cada fila es un diccionario. Proporciona
una representación serializada de los datos que se pueden enviar a
través de protocolos RPC y está destinado a ser utilizado por los
programas cliente y no en la lógica del servidor.
343 + `search_read([domain], [fields], offset=0, limit=None, order=None)`
realiza una operación de búsqueda seguida de una lectura en la lista de
registros resultante. Se detina para ser utilizado por los clientes de
RPC y les ahorra el viaje redondo adicional necesario al hacer una
búsqueda `search` seguida de una lectura `read` en los resultados.
344 + `load([fields], [data])` se utiliza para importar datos adquiridos de
un archivo CSV. El primer argumento es la lista de campos a importar y
se mapea directamente a una fila superior de CSV. El segundo argumento
es una lista de registros, donde cada registro es una lista de valores
de cadena para analizar e importar, y se asigna directamente a las
filas y columnas de datos CSV. Implementa las características de
importación de datos CSV descritas en el Capítulo 4, *Datos de Módulo*,
como el soporte de identificadores externos. Se utiliza por la
característica de **importación "Import"** de cliente web. Sustituye el
método import_data obsoleto.
345 + `export_data([fields], raw_data=False)` es utilizado por la función
de ** exportación "Expor"**del cliente web. Devuelve un diccionario con
una clave de datos que contiene los datos; Una lista de filas. Los
nombres de campo pueden utilizar los sufijos `.id` y `/id` utilizados
Page 9
capitulo-7.md 07/02/2018

en los archivos CSV y los datos están en un formato compatible con un


archivo CSV importable. El argumento `raw_data` opcional permite que
los valores de datos se exporten con sus tipos Python, en lugar de la
representación de cadena utilizada en CSV.
346
347 Los siguiente métodos son utilizados principalmente por el cliente web
para renderizar la interfaz de ususario y realizar interacción básica:
348
349 + `name_get()`: Devuelve una lista de tuplas (`ID`, `name`) con el
texto que representa cada registro. Se utiliza por defecto para
calcular el valor `display_name`, proporcionando la representación de
texto de campos de relación. Se puede extender para implementar
representaciones de visualización personalizadas, como mostrar el
código de registro y el nombre en lugar de sólo el nombre.
350 + `name_search(name='', args=None, operator='ilike', limit=100)`
devuelve una lista de tuplas (`ID, name`) donde el nombre de
visualización coincide con el texto del argumento `name`. Se utiliza en
la interfaz de usuario al escribir en un campo de relación para
producir la lista con los registros sugeridos que coinciden con el
texto escrito. Por ejemplo, se utiliza para implementar la búsqueda de
productos tanto por nombre como por referencia, al escribir en un campo
para seleccionar un producto.
351 + `name_create(name)` crea un nuevo registro con sólo el nombre del
título que se va a utilizar para ello. Se utiliza en la interfaz de
usuario para la función de "creación rápida", donde puedes crear
rápidamente un registro relacionado proporcionando su nombre. Puede
extenderse para proporcionar valores predeterminados específicos para
los nuevos registros creados a través de esta función.
352 + `default_get([fields])` devuelve un diccionario con los valores por
defecto para crear un nuevo registro. Los valores predeterminados
pueden depender de variables como el usuario actual o el contexto de la
sesión.
353 + `fields_get()` se utiliza para describir las definiciones de campo
del modelo, como se ve en la opción **Ver Campos "Viem Fields"** del
menú del desarrollador.
354 + `fields_view_get()` es utilizado por el cliente web para recuperar la
estructura de la vista de interfaz de usuario a procesar. Se le puede
dar el ID de la vista como un argumento o el tipo de vista que queremos
usando `view_type='form'`. Por ejemplo, puedes probar esto:
`rset.fields_view_get(view_type='tree')`.
355
356 ### El comando shell
357
358 Python tiene una interfaz de línea de comandos que es una gran manera
de explorar su sintaxis. Del mismo modo, Odoo también tiene una
característica equivalente, donde podemos interactivamente probar
comandos para ver cómo funcionan. Ese es el comando `shell`.
359
360 Para usarlo, ejecuta Odoo con el comando `shell` y la base de datos
para usar, como se muestra aquí:
361
362
363 ```
364 $ ./odoo-bin shell -d todo
365
366 ```
367
368
369 Deberías ver la habitual secuencia de inicio del servidor en el
terminal hasta que pare en un prompt Python `>>>` esperando tu entrada.
Aquí, `self` representará el registro para el usuario `Administratoṛ`,
Page 10
capitulo-7.md 07/02/2018

el que puedes confirmar escribiendo lo siguiente:


370
371
372 ```
373
374 >>> self
375
376
377
378
379
380 res.users(1,)
381
382
383
384
385
386 >>> self._name
387
388
389
390
391
392 'res.users'
393
394
395
396
397
398 >>> self.name
399
400
401
402
403
404 u'Administrator'
405
406 ```
407
408 En la sesión anterior, hacemos una cierta inspección sobre nuestro
medio ambiente. El `self` representa un conjunto de registros
`res.users` que contiene sólo el registro con el ID `1`. También
podemos confirmar el nombre de modelo del conjunto de registros que
inspecciona `self._name` y obtener el valor del campo de registro
`name`, confirmando que es el usuario `Administrator`.
409
410 Al igual que con Python, puedes salir del prompt utilizando _**Ctrl +
D**_. Esto también cerrará el proceso del servidor y regresará al
prompt del sistema shell.
411
412 #### Tip
413
414 La característica de shell se agregó en la versión 9.0. Para la versión
8.0 hay un módulo back-portted de la comunidad para agregarlo. Una vez
descargado y incluido en la ruta addons, no es necesario realizar
ninguna instalación adicional. Se puede descargar desde
https://www.odoo.com/apps/modules/8.0/shell/.
415
416 ### El entorno del servidor
417
418 El servidor shell proporciona una referencia `self` idéntica a la que
Page 11
capitulo-7.md 07/02/2018

encontrarías dentro de un método del modelo Users, `res.users`.


419
420 Como hemos visto, `self`es un conjunto de registros. Los **Conjuntos de
Registros "Recordsets"** llevan consigo una información de entorno,
incluido el usuario que navega por los datos y la información de
contexto adicional, como el idioma y la zona horaria. Esta información
es importante y el indicador o zona horaria.
421
422 Podemos comenzar a inspeccionar nuestro entorno actual con:
423
424
425 ```
426
427 >>> self.env
428
429
430
431
432
433 <openerp.api.Environment object at 0xb3f4f52c>
434 ```
435
436 El entorno de ejecución en `self.env` tiene los siguientes atributos
disponibles:
437
438 + `env.cr` es el cursor de la base de datos que se está utilizando
439 + `env.uid` es el ID para el usuario de sesión
440 + `env.user` es el registro para el usuario actual
441 + `env.context` es un diccionario inmutable con un contexto de sesión
442
443 El entorno también proporciona acceso al registro donde están
disponibles todos los modelos instalados. Por ejemplo,
`self.env['res.partner']` devuelve una referencia al modelo Socios.
Podemos usar `search()` o `browse()` para recuperar los conjuntos de
registros:
444
445 ```
446
447 >>> self.env['res.partner'].search([('name', 'like', 'Ag')])
448
449
450
451
452
453 res.partner(7, 51)
454
455
456 ```
457
458 En este ejemplo, un conjunto de registros para el modelo `res.partner`
contiene dos registros, con IDs `7` y `51`.
459
460
461 ### Modificando del entorno de ejecución
462
463 El ambiente es inmutable, y por lo tanto no se puede modificar. Pero
podemos crear un entorno modificado y luego ejecutar acciones con él.
464
465 Estos métodos se pueden utilizar para eso:
466
467 + `env.sudo(user)` se proporciona con un registro de usuario, y
Page 12
capitulo-7.md 07/02/2018

devuelve un entorno con ese usuario. Si no se proporciona ningún


usuario, se utilizará el superusuario `Administrator`, que permite
ejecutar consultas específicas sin pasar por las reglas de seguridad.
468 + `env.with_context(diccionary)` sustituye el contexto por uno nuevo.
469 + `env.with_context(key=value,...)` modifica los valores actuales de
configuración de contexto para algunas de sus claves.
470
471 Además, tenemos la función `env.ref()`, tomando una cadena con un
identificador externo y devuelve un registro para ella, como se muestra
aquí:
472
473 ```
474
475 >>> self.env.ref('base.user_root')
476
477
478
479
480
481 res.users(1,)
482
483
484
485 ```
486
487 ### Transacciones y SQL de bajo nivel
488
489 Las operaciones de escritura de base de datos se ejecutan en el
contexto de una transacción de base de datos. Por lo general, no
tenemos que preocuparnos de esto ya que el servidor se encarga de que
mientras se ejecuta métodos de modelo.
490
491 Pero en algunos casos, podemos necesitar un control más fino sobre la
transacción. Esto se puede hacer a través del cursor de la base de
datos `self.env.cr`, como se muestra aquí:
492
493 + `self.env.cr.commit()` compromete las operaciones de escritura en
búfer de la transacción
494 + `self.env.savepoint()` establece un punto de almacenamiento de
transacciones para retroceder a
495 + `self.env.rollback()` cancela las operaciones de escritura de la
transacción desde el último punto de salvación, o todas si no se creó
ningún punto de salvación
496
497 #### Tip
498
499 En una sesión shell, la manipulación de datos no se hará efectiva en la
base de datos hasta que utilices `self.env.cr.commit()`.
500
501 Con el método cursor `execute()`, podemos ejecutar SQL directamente en
la base de datos. Se necesita una cadena con la instrucción SQL para
ejecutar y un segundo argumento opcional con una tupla o lista de
valores para usar como parámetros para el SQL. Estos valores se
utilizarán donde se encuentren los marcadores de posición `%s`.
502
503 #### Nota
504
505 #### ¡Precaución!
506
507 Con `cr.execute()` debemos resistir a añadir directamente los valores
de los parámetros a la cadena de consulta. Este es un riesgo de
Page 13
capitulo-7.md 07/02/2018

seguridad bien conocido que puede ser explotado a través de ataques de


inyección de SQL. Siempre usa marcadores de posición `%s`y el segundo
parámetro para pasar valores.
508
509 Si está utilizando una consulta `SELECT`, los registros se deben
buscar. La función `fetchall()` recupera todas las filas como una lista
de tuplas, y `dictfetchall()` las recupera como una lista de
diccionarios, como se muestra en el siguiente ejemplo:
510
511
512 ```
513
514 >>> self.env.cr.execute("SELECT id, login FROM res_users WHERE
515
516
517
518
519
520 login=%s OR id=%s", ('demo', 1))
521
522
523
524
525
526 >>> self.env.cr.fetchall()
527
528
529
530
531
532 [(4, u'demo'), (1, u'admin')]
533
534
535
536 También es posible ejecutar instrucciones de **Data Manipulation
Language (DML)** como `UPDATE` e `INSERT`. Dado que el servidor
mantiene cachés de datos, pueden volverse inconsistentes con los datos
reales de la base de datos. Debido a esto, mientras se utiliza DML sin
procesar, las caches se deben borrar después usando
`self.env.invalidate_all()`.
537
538 #### Nota
539
540 #### ¡Precaución!
541
542 Ejecutar SQL directamente en la base de datos puede dar lugar a datos
inconsistentes. Debes usarlo sólo si está seguro de lo que está haciendo.
543
544 ## Trabajando con conjuntos de registros
545
546 Ahora exploraremos cómo funciona el ORM y nos enteramos de las
operaciones más comunes que se realizan con ella. Usaremos el prompt
proporcionado por el comando `shell` para explorar interactivamente
cómo funcionan los recordsets.
547
548
549 ### Consultando modelos
550
551 Con `self`, sólo podemos acceder al conjunto de registros del método.
Pero la referencia de entorno `self.env` nos permite acceder a
cualquier otro modelo. Por ejemplo, `self.env['res.partner']` devuelve
Page 14
capitulo-7.md 07/02/2018

una referencia al modelo Partners (que en realidad es un conjunto de


registros vacío). Podemos usar `search()` o `browse()` en él para
generar conjuntos de registros.
552
553 El método `search()` toma una expresión de dominio y devuelve un
conjunto de registros con los registros que coinciden con esas
condiciones. Un dominio vacío `[]` devolverá todos los registros. Para
obtener más detalles sobre las expresiones de dominio, consulta el
Capítulo 6, *Vistas - Diseñando la interfaz de usuario*. Si el modelo
tiene el campo especial `active`, por defecto sólo se considerarán los
registros con `active=True`.
554
555 Algunos argumentos opcionales de palabras clave están disponibles, como
se muestra aquí:
556
557 + `order` es una cadena que se utilizará como la cláusula `ORDER BY` en
la consulta de la base de datos. Esto suele ser una lista de nombres de
campos separados por comas.
558 + limit` establece un número máximo de registros para recuperar.
559 + `offset` ignora los primeros resultados `n`; Se puede utilizar con
`límit` para consultar bloques de registros a la vez.
560
561 A veces sólo necesitamos saber el número de registros que cumplen
ciertas condiciones. Para ello podemos usar `search_count()`, que
devuelve el recuento de registros en lugar de un conjunto de registros.
Se ahorra el costo de recuperar una lista de registros sólo para
contarlos, por lo que es mucho más eficiente cuando aún no tenemos un
conjunto de registros y sólo queremos contar el número de registros.
562
563 El método `browse()` toma una lista de IDs o una sola ID y devuelve un
conjunto de registros con esos registros. Esto puede ser conveniente
para los casos en los que ya conocemos los ID de los registros que
queremos.
564
565 Algunos ejemplos de uso de esto se muestran aquí:
566
567
568 ```
569
570 >>> self.env['res.partner'].search([('name', 'like', 'Ag')])
571
572
573
574
575
576 res.partner(7, 51)
577
578
579
580
581
582 >>> self.env['res.partner'].browse([7, 51])
583
584
585
586
587
588
589 res.partner(7, 51)
590
591
Page 15
capitulo-7.md 07/02/2018

592
593 ```
594
595 ### Singletons
596
597 El caso especial de un conjunto de registros con sólo un registro se
llama un conjunto de registros **singleton**. Los singletons siguen
siendo un conjunto de registros y se pueden utilizar donde se espera un
conjunto de registros.
598
599 Pero a diferencia de los conjuntos de registros de elementos múltiples,
los singletons pueden acceder a sus campos usando la notación de
puntos, como se muestra aquí:
600
601 ```
602 >>> print self.name
603 Administrator
604 ```
605
606
607 En el siguiente ejemplo, podemos ver que el mismo conjunto de registros
`self` singleton también se comporta como un conjunto de registros, y
podemos iterarlo. Tiene sólo un registro, por lo que solo se imprime un
nombre:
608
609 ```
610 >>> for rec in self:
611 print rec.name
612 Administrator
613 ```
614
615 Intentar acceder a valores de campo en conjuntos de registros con más
de un registro generará error, por lo que este puede ser un problema en
los casos en los que no estamos seguros si estamos trabajando con un
conjunto de registros singleton. En los métodos diseñados para trabajar
sólo con singleton, podemos comprobar esto usando `self.ensure_one()`
al principio. Se planteará un error si `self` no es singleton.
616
617
618 #### Tip
619
620 Ten en cuenta que un registro vacío también es un singleton.
621
622 ### Escribir en los registros
623
624 Los conjuntos de registros implementan el patrón de registro activo.
Esto significa que podemos asignar valores a ellos, y estos cambios se
harán persistentes en la base de datos. Esta es una manera intuitiva y
conveniente de manipular datos, como se muestra aquí:
625
626 ```
627
628 >>> admin = self.env['res.users'].browse(1)
629
630
631
632
633
634 >>> print admin.name
635
636
Page 16
capitulo-7.md 07/02/2018

637
638
639
640 Administrator
641
642
643
644
645
646 >>> admin.name = 'Superuser'
647
648
649
650
651
652 >>> print admin.name
653
654
655
656
657
658 Superuser
659
660
661 ```
662
663 Los conjuntos de registros también tienen tres métodos para actuar en
sus datos: `create()`, `write()` y `unlink()`.
664
665 El método `create()` toma un diccionario para asignar los campos a los
valores y devuelve el registro creado. Los valores predeterminados se
aplican automáticamente como se espera, lo que se muestra aquí:
666
667 ```
668
669 >>> Partner = self.env['res.partner']
670
671
672
673
674
675 >>> new = Partner.create({'name': 'ACME', 'is_company': True})
676
677
678
679
680
681 >>> print new
682
683
684
685
686
687 res.partner(72,)
688
689
690 ```
691
692 El método `unlink()` borra los registros en los conjuntos de registros,
como se muestra aquí:
693
Page 17
capitulo-7.md 07/02/2018

694 ```
695
696 >>> rec = Partner.search([('name', '=', 'ACME')])
697
698
699
700
701
702 >>> rec.unlink()
703
704
705
706
707
708 True
709
710 ```
711
712 El método `write()` toma un diccionario para asignar campos a valores.
Éstos se actualizan en todos los elementos del conjunto de registros y
no se devuelve nada, como se muestra aquí:
713
714 ```
715 >>> Partner.write({'comment': 'Hello!'})
716 ```
717
718 El uso del patrón de registro activo tiene algunas limitaciones;
Actualiza sólo un campo a la vez. Por otro lado, el método `write()`
puede actualizar varios campos de varios registros al mismo tiempo
utilizando una sola instrucción de base de datos. Estas diferencias
deben tenerse en cuenta para los casos en que el rendimiento puede ser
un problema.
719
720 También vale la pena mencionar `copy()` para duplicar un registro
existente; Toma eso como un argumento opcional y un diccionario con los
valores para escribir en el nuevo registro. Por ejemplo, para crear una
nueva copia de usuario desde el usuario de demostración:
721
722 ```
723
724 >>> demo = self.env.ref('base.user_demo')
725
726
727
728
729
730 >>> new = demo.copy({'name': 'Daniel', 'login': 'dr', 'email':''})
731
732 ```
733
734 #### Nota
735
736 Recuerda que los campos con el atributo `copy=False` no se copiarán.
737
738
739 ### Trabajando con el tiempo y las fechas
740
741 Por razones históricas, los conjuntos de registros ORM controlan los
valores `date` y `datetime` utilizando sus representaciones de cadenas,
en lugar de los objetos `Date` y `Datetime` reales de Python. En la
base de datos se almacenan en los campos de fecha, pero las fechas se
Page 18
capitulo-7.md 07/02/2018

almacenan en la hora UTC.


742
743 + `odoo.tools.DEFAULT_SERVER_DATE_FORMAT`
744 + `odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT`
745
746 Corresponden a `%Y-%m-%d and %Y-%m-%d %H:%M:%S` respectivamente.
747
748 Para ayudar a manejar fechas, `fields.Date` y `fields.Datetime`
proporcionan pocas funciones. Por ejemplo:
749
750 ```
751
752 >>> from odoo import fields
753
754
755
756
757
758 >>> fields.Datetime.now()
759
760
761
762
763
764 '2014-12-08 23:36:09'
765
766
767
768
769
770 >>> fields.Datetime.from_string('2014-12-08 23:36:09')
771
772
773
774
775
776 datetime.datetime(2014, 12, 8, 23, 36, 9)
777
778 ```
779
780 Las fechas y horas son manejadas y almacenadas por el servidor en un
formato UTC, que no es consciente de la zona horaria y puede ser
diferente de la zona horaria en la que el usuario está trabajando.
Debido a esto, podemos hacer uso de algunas otras funciones para
ayudarnos a lidiar con esto:
781
782 + `fields.Date.today()` devuelve una cadena con la fecha actual en el
formato esperado por el servidor y utilizando UTC como referencia. Esto
es adecuado para calcular los valores predeterminados.
783 + `fields.Datetime.now()` devuelve una cadena con el datetime actual en
el formato esperado por el servidor utilizando UTC como referencia.
Esto es adecuado para calcular los valores predeterminados.
784 + `fields.Date.context_today(record, timestamp=None)` devuelve una
cadena con la fecha actual en el contexto de la sesión. El valor de la
zona horaria se toma del contexto del registro, y el parámetro opcional
a utilizar es datetime en lugar de la hora actual.
785 + `fields.Datetime.context_timestamp(record, timestamp)` convierte un
datetime ingenuo (sin zona horaria) en un datetime de zona horaria. La
zona horaria se extrae del contexto del registro, de ahí el nombre de
la función.
786
Page 19
capitulo-7.md 07/02/2018

787 Para facilitar la conversión entre formatos, ambos objetos


`fields.Date` y `fields.Datetime` proporcionan estas funciones:
788
789 + `from_string(value)` convierte una cadena en un objeto date o datetime
790 + `to_string(value)` convierte un objeto date o datetime en una cadena
en el formato esperado por el servidor
791
792 ### Operaciones en conjuntos de registros
793
794 Los conjuntos de registros admiten operaciones adicionales en ellos.
Podemos comprobar si un registro está incluido o no en un conjunto de
registros. Si `x` es un conjunto de registros singleton y
`my_recordset` es un conjunto de registros que contiene muchos
registros, podemos utilizar:
795
796 + `X im my_recordset`
797 + `X not en my_recordset`
798
799 También están disponibles las siguientes operaciones:
800
801 + `recordset.ids` devuelve la lista con los ID de los elementos del
conjunto de registros
802 + `recordset.ensure_one()` comprueba si es un registro único
(singleton); Si no es así, se genera una excepción `ValueError`
803 + `recordset.filtered(func)` devuelve un conjunto de registros filtrado
804 + `recordset.mapped(func)` devuelve una lista de valores mapeados
805 + `recordset.sorted(func)` devuelve un conjunto de registros ordenado
806
807 Estos son algunos ejemplos de uso para estas funciones:
808
809 ```
810
811 >>> rs0 = self.env['res.partner'].search([])
812
813
814
815
816
817 >>> len(rs0) # how many records?
818
819
820
821
822
823 40
824
825
826
827
828
829 >>> starts_A = lambda r: r.name.startswith('A')
830
831
832
833
834
835 >>> rs1 = rs0.filtered(starts_A)
836
837
838
839
Page 20
capitulo-7.md 07/02/2018

840
841 >>> print rs1
842
843
844
845
846
847 res.partner(8, 7, 19, 30, 3)
848
849
850
851
852
853 >>> rs2 = rs1.filtered('is_company')
854
855
856
857
858
859 >>> print rs2
860
861
862
863
864
865 res.partner(8, 7)
866
867
868
869
870
871 >>> rs2.mapped('name')
872
873
874
875
876
877 [u'Agrolait', u'ASUSTeK']
878
879
880
881
882
883 >>> rs2.mapped(lambda r: (r.id, r.name))
884
885
886
887
888
889 [(8, u'Agrolait'), (7, u'ASUSTeK')]
890
891
892
893
894
895 >> rs2.sorted(key=lambda r: r.id, reverse=True)
896
897
898
899
900
Page 21
capitulo-7.md 07/02/2018

901 res.partner(8, 7)
902
903 ```
904
905 ### Manipulando conjuntos de registros
906
907 Seguramente queremos añadir, eliminar o reemplazar los elementos en
estos campos relacionados, y esto nos lleva a la pregunta: ¿cómo se
pueden manipular los conjuntos de registros?
908
909 Los conjuntos de registros son inmutables, lo que significa que sus
valores no pueden modificarse directamente. En su lugar, modificar un
conjunto de registros significa componer un nuevo conjunto de registros
basado en los existentes.
910
911 Una forma de hacerlo es utilizando las operaciones de conjunto
compatibles:
912
913 + `rs1 | rs2` es la operación de conjunto de **unión** y da como
resultado un conjunto de registros con todos los elementos de ambos
conjuntos de registros.
914 + `rs1 + rs2` es la operación de conjunto de **adiciones**, para
concatenar ambos conjuntos de registros en uno. Puede resultar en un
conjunto con registros duplicados.
915 + `rs1 & rs2` es la operación de conjunto de **intersección** y resulta
en un conjunto de registros con sólo los elementos presentes en ambos
conjuntos de registros.
916 + `rs1 - rs2` es la operación de conjunto de **diferencias** y resulta
en un conjunto de registros con los elementos rs1 no presentes en rs2
917
918 La notación de corte también se puede utilizar, como se muestra en
estos ejemplos:
919
920 + `rs[0]` y `rs[-1]` recuperan el primer elemento y el último elemento,
respectivamente.
921 + `rs[1:]` da como resultado una copia del conjunto de registros sin el
primer elemento. Esto produce los mismos registros que `rs - rs[0]`
pero conserva su orden.
922
923 #### Nota
924
925 En Odoo 10, la manipulación del conjunto de registros conserva el
orden. Esto es diferente a las versiones anteriores de Odoo, donde la
manipulación del conjunto de registros no estaba garantizada para
conservar el orden, aunque se sabe que la adición y el corte mantienen
el orden de los registros.
926
927 Podemos utilizar estas operaciones para cambiar un conjunto de
registros mediante la eliminación o la adición de elementos. Aquí hay
unos ejemplos:
928
929 + `self.task_ids |= task1` agrega el registro `task1`, si no en el
conjunto de registros
930 + `self.task_ids -= task1` elimina el registro específico `task1`, si
está presente en el conjunto de registros
931 + `self.task_ids = self.task_ids[:-1]` elimina el último registro
932
933 Los campos relacionales contienen valores de conjunto de registros.
Campos muchos-a-uno pueden contener un conjunto de registros singleton
y campos a-muchos contienen conjuntos de registros con cualquier número
de registros. Establecemos valores en ellos mediante una sentencia de
Page 22
capitulo-7.md 07/02/2018

asignación regular, o utilizando los métodos `create()` y `write()` con


un diccionario de valores. En este último caso, se utiliza una sintaxis
especial para modificar muchos campos. Es el mismo que se utiliza en
los registros XML para proporcionar valores para los campos
relacionales y se describe en el capítulo 4, *Datos del módulo*, en la
sección *Configurando valores para los campos de relación*.
934
935 Como ejemplo, la sintaxis `write()` equivalente a los tres ejemplos
anteriores de asignación es:
936
937 + `self.write([(4, task1.id, None)])` agrega el registro `task1`
938 + `self.write([(3, task1.id, None)])` elimina `task1` del conjunto de
registros
939 + `self.write([(3, self.task_ids [-1].id, False)])` elimina el último
registro
940
941 ### Usando campos relacionales
942
943 Como vimos anteriormente, los modelos pueden tener campos relacionales:
**muchos-a-uno**, **uno-a-muchos** y **muchos-a-muchos**. Estos tipos
de campo tienen conjuntos de registros como valores.
944
945 En el caso de muchos-a-uno, el valor puede ser un singleton o un
conjunto de registros vacío. En ambos casos, podemos acceder
directamente a sus valores de campo. Por ejemplo, las siguientes
instrucciones son correctas y seguras:
946
947 ```
948
949 >>> self.company_id
950
951
952
953
954
955 res.company(1,)
956
957
958
959
960
961 >>> self.company_id.name
962
963
964
965
966
967 u'YourCompany'
968
969
970
971
972
973 >>> self.company_id.currency_id
974
975
976
977
978
979 res.currency(1,)
980
Page 23
capitulo-7.md 07/02/2018

981
982
983
984
985
986 >>> self.company_id.currency_id.name
987
988
989
990
991
992 u'EUR'
993
994 ```
995
996 Convenientemente, un conjunto de registros vacío también se comporta
como singleton, y el acceso a sus campos no devuelve un error, pero
simplemente devuelve `False`. Debido a esto, podemos recorrer registros
usando la notación de puntos sin preocuparnos por errores de valores
vacíos, como se muestra aquí:
997
998 ```
999
1000 >>> self.company_id.country_id
1001
1002
1003
1004
1005
1006 res.country()
1007
1008
1009
1010
1011
1012 >>> self.company_id.country_id.name
1013
1014
1015
1016
1017
1018 False
1019
1020 ```
1021
1022 ### Trabajando con campos relacionales
1023
1024 Mientras se utiliza el patrón de registro activo, se pueden asignar
conjuntos de registros a los campos relacionales.
1025
1026 Para campos muchos-a-uno, el valor asignado debe ser un registro único
(un conjunto de registros singleton).
1027
1028 Para los campos a-muchos, su valor también se puede asignar con un
conjunto de registros, reemplazando la lista de registros vinculados,
si los hay, por uno nuevo. Aquí se permite un conjunto de registros con
cualquier tamaño.
1029
1030 Al utilizar los métodos `create()` o `write()`, donde los valores se
asignan mediante diccionarios, no se pueden asignar campos relacionales
a los valores del conjunto de registros. Debe utilizarse el ID o la
Page 24
capitulo-7.md 07/02/2018

lista de identificadores correspondientes.


1031
1032 Por ejemplo, en lugar de `self.write({'user_id': self.env. user })`,
deberíamos usar `self.write({'user_id': self.env. user.id })`.
1033
1034 ## Resumen
1035
1036 En los capítulos anteriores, vimos cómo construir modelos y diseñar
vistas. Aquí fuimos un poco más lejos, aprendiendo a implementar la
lógica de negocios y usar conjuntos de registros para manipular datos
de modelos.
1037
1038 También vimos cómo la lógica de negocio puede interactuar con la
interfaz de usuario y aprendido a crear asistentes que se comunican con
el usuario y servir como una plataforma para lanzar procesos avanzados.
1039
1040 En el próximo capítulo, aprenderemos sobre la adición de pruebas
automatizadas para nuestro módulo addon, y algunas técnicas de
depuración.
1041

Page 25
capitulo-8.md 07/02/2018

1 # Capítulo 8. Escribiendo pruebas y depurando tu código


2
3 Una buena parte del trabajo de un desarrollador es probar y depurar
código. Las pruebas automatizadas son una herramienta inestimable para
construir y mantener software robusto. En este capítulo, aprenderemos
cómo agregar pruebas automatizadas a nuestros módulos de complemento,
para hacerlos más robustos. También se presentan las técnicas de
depuración del servidor, que permiten al desarrollador inspeccionar y
comprender lo que está sucediendo en su código.
4
5 ## Pruebas de unidad
6
7 Las pruebas automatizadas se aceptan generalmente como una mejor
práctica en software. No solo nos ayuda a asegurar que nuestro código
se implemente correctamente. Más importante aún, proporciona una red de
seguridad para futuras mejoras de código o reescritura.
8
9 En el caso de lenguajes de programación dinámica, como Python, ya que
no hay ningún paso de compilación, los errores de sintaxis pueden pasar
desapercibidos. Esto hace aún más importante que las pruebas de unidad
pasen por tantas líneas de código como sea posible.
10
11 Los dos objetivos descritos pueden proporcionar una luz de guía al
escribir pruebas. El primer objetivo para sus pruebas debe ser
proporcionar una buena cobertura de prueba, diseñando casos de prueba
que pasen por todas las líneas de código. Esto solo por lo general hará
un buen progreso en el segundo objetivo - para mostrar la corrección
funcional del código.
12
13 Esto solo por lo general hará un buen progreso en el segundo objetivo -
para mostrar la corrección funcional del código, ya que después de esto
seguramente tendrá un gran punto de partida para construir casos de
prueba adicionales para casos de uso no obvio.
14
15 ## Añadiendo pruebas de unidad
16
17 Las pruebas de Python se agregan a módulos addon mediante un
subdirectorio `tests/`. El corredor de pruebas descubrirá
automáticamente las pruebas en los subdirectorios con ese nombre en
particular.
18
19 Las pruebas de nuestro complemento `todo_wizard` estarán en un archivo
`test/test_wizard.py`. Necesitaremos agregar el archivo
`tests/__init__.py`:
20
21 ```
22 from . import test_wizard
23 ```
24 Y este sería el esqueleto básico para el `tests/test_wizard.py`:
25
26 ```
27 # -*- coding: utf-8 -*-
28 from odoo.tests.common import TransactionCase
29
30 class TestWizard(TransactionCase):
31
32 def setUp(self, *args, **kwargs):
33 super(TestWizard, self).setUp(*args, **kwargs)
34 # Add test setup code here...
35
36 def test_populate_tasks(self):
Page 1
capitulo-8.md 07/02/2018

37 "Populate tasks buttons should add two tasks"


38 # Add test code
39 ```
40
41 Odoo ofrece algunas clases para usar en las pruebas. Las pruebas de
`TransactionCase` utilizan una transacción diferente para cada prueba,
que se deshace automáticamente al final. También podemos usar
`SingleTransactionCase`, que ejecuta todas las pruebas en una sola
transacción, que se deshace sólo al final de la última prueba. Esto
puede ser útil cuando deseas que el estado final de cada prueba sea el
estado inicial de la siguiente prueba.
42
43 El método `setUp()` es donde preparamos los datos y las variables que
se van a utilizar. Normalmente los almacenaremos como atributos de
clase, de modo que estén disponibles para ser utilizados en los métodos
de prueba.
44
45 Las pruebas se deben implementar como métodos de clase, como
`test_populate_tasks()`. Los nombres de los métodos de casos de prueba
deben comenzar con un prefijo `test_`. Se descubren automáticamente, y
este prefijo es lo que identifica los métodos que implementan casos de
prueba.
46
47 Los métodos se ejecutarán en orden de los nombres de las funciones de
prueba. Cuando se utiliza la clase `TransactionCase`, se realizará una
reversión al final de cada clase. La docstring del método se muestra
cuando se ejecutan las pruebas, y debe proporcionar una breve
descripción de la misma.
48
49 Estas clases de prueba son envolturas alrededor de testcases
`unitcases`. Esto forma parte de la biblioteca estándar de Python y
puedes consultar su documentación para obtener más detalles en
https://docs.python.org/2/library/unittest.html.
50
51 Para ser más preciso, Odoo utiliza una biblioteca de extensión
`unittest`, `unittest2`.
52
53 ## Configurando pruebas
54
55 Debemos comenzar preparando los datos que se utilizarán en las pruebas.
56
57 Es conveniente realizar las acciones de prueba bajo un usuario
específico, para probar también que el control de acceso está
configurado correctamente. Esto se logra utilizando el método de modelo
`sudo()`. Los conjuntos de registros llevan esa información con ellos,
por lo que después de haber sido creados durante el uso de `sudo()`,
las operaciones posteriores en el mismo conjunto de registros se
realizarán utilizando ese mismo contexto.
58
59 Éste es el código para el método `setUp` y algunas instrucciones de
importación adicionales que también son necesarias:
60
61 ```
62 from datetime import date
63 from odoo.tests.common import TransactionCase
64 from odoo import fields
65
66 class TestWizard(TransactionCase):
67
68 def setUp(self, *args, **kwargs):
69 super(TestWizard, self).setUp(*args, **kwargs)
Page 2
capitulo-8.md 07/02/2018

70 # Close any open Todo tasks


71 self.env['todo.task']\
72 .search([('is_done', '=', False)])\
73 .write({'is_done': True})
74 # Demo user will be used to run tests
75 demo_user = self.env.ref('base.user_demo')
76 # Create two Todo tasks to use in tests
77 t0 = date.today()
78 Todo = self.env['todo.task'].sudo(demo_user)
79 self.todo1 = Todo.create({
80 'name': 'Todo1',
81 'date_deadline': fields.Date.to_string(t0)})
82 self.todo2 = Todo.create({
83 'name': 'Todo2'})
84 # Create Wizard instance to use in tests
85 Wizard = self.env['todo.wizard'].sudo(demo_user)
86 self.wizard = Wizard.create({})
87
88 ```
89 Para probar nuestro asistente, queremos tener exactamente abiertas dos
tareas pendientes. Así que empezamos cerrando cualquier tarea pendiente
existente, para que no se interpongan en nuestras pruebas, y creamos
dos nuevas tareas pendientes para las pruebas, usando el usuario Demo.
Finalmente creamos una nueva instancia de nuestro asistente, utilizando
el usuario Demo, y asignandolo a `self.wizard`, de modo que esté
disponible para los métodos de prueba.
90
91 ## Probando excepciones
92
93 A veces necesitamos nuestras pruebas para verificar si se generó una
excepción. Un caso común es cuando se prueba si algunas validaciones se
están haciendo correctamente.
94
95 En nuestro ejemplo, el método `test_count()` utiliza una excepción de
advertencia `warning` como una forma de proporcionar información al
usuario. Para comprobar si se genera una excepción, colocamos el código
correspondiente dentro de un bloque `whit self.assertRaises()`.
96
97 Necesitamos importar la excepción `Warning` en la parte superior del
archivo:
98
99 ```
100 from odoo.exceptions import Warning
101
102 ```
103
104 Y agregua a la clase de prueba un método con otro caso de prueba:
105
106 ```
107 def test_count(self):
108 "Test count button"
109 with self.assertRaises(Warning) as e:
110 self.wizard.do_count_tasks()
111 self.assertIn(' 2 ', str(e.exception))
112 ```
113
114 Si el método `do_count_tasks()` no genera una excepción, la
comprobación fallará. Si levanta esa excepción, la comprobación tiene
éxito y la excepción generada se almacena en la variable `e`.
115
116 Lo usamos para inspeccionarlo. El mensaje de excepción contiene el
Page 3
capitulo-8.md 07/02/2018

número de tareas contadas, que esperamos que sean dos. En la sentencia


final usamos `assertIn` para comprobar que el texto de excepción
contiene la cadena `' 2 '`.
117
118 ## Ejecutando de pruebas
119
120 Las pruebas están escritas, es hora de ejecutarlas. Para eso solo
necesitamos agregar la opción `--test-enable` al comando de inicio del
servidor Odoo, mientras instalamos o actualizamos (`-i` o `-u`) el
módulo addon.
121
122 El comando se vería así:
123
124 ```
125 $ ./odoo-bin -d todo --test-enable -i todo_wizard --stop-after-init
126 --addons-path="..."
127 ```
128
129 Sólo se probarán los módulos instalados o actualizados. Si algunas
dependencias necesitan ser instaladas, sus pruebas también se
ejecutarán. Si desea evitar esto, puede instalar el módulo para probar
la forma usual y, a continuación, ejecutar las pruebas mientras realiza
una actualización (`-u`) del módulo para probar.
130
131 # Acerca de las pruebas de YAML
132
133 Odoo también soporta un segundo tipo de pruebas, descritas usando
archivos de datos YAML. Originalmente todas las pruebas usaban YAML,
hasta que más recientemente se introdujeron las pruebas basadas en
pruebas `unittest`. Si bien ambos son compatibles, y muchos
complementos principales aún incluyen pruebas YAML, la documentación
oficial actualmente no menciona las pruebas YAML. La última
documentación disponible está disponible en
https://doc.odoo.com/v6.0/contribute/15_guidelines/coding_guidelines_test
ing/.
134
135 Los desarrolladores con un fondo Python probablemente se sentirán más a
gusto con `unittest`, ya que es una característica estándar de Python,
mientras que las pruebas YAML están diseñadas con convenciones
específicas de Odoo. La tendencia es claramente preferir `unittest`
sobre YAML, y se espera que el soporte de YAML sea eliminado en futuras
versiones.
136
137 Por estas razones, no haremos una cobertura en profundidad de las
pruebas de YAML. Podría ser útil tener algún entendimiento básico sobre
cómo funcionan.
138
139 Las pruebas YAML son archivos de datos, similares a CSV y XML. De
hecho, el formato YAML estaba destinado a ser un formato de datos más
compacto que se puede utilizar en lugar de XML. A diferencia de las
pruebas Python, donde las pruebas deben estar en un subdirectorio
`test/`, los archivos de prueba YAML pueden estar en cualquier parte
dentro del módulo de complemento. Pero frecuentemente estarán dentro de
un subdirectorio `test/` o `tests/`. Y aunque las pruebas de Python se
descubren automáticamente, las pruebas de YAML deben declararse en el
archivo de manifiesto `__manifest__.py`. Esto se hace con la clave
`test`, similar a la clave `data` que ya conocemos.
140
141 En Odoo 10 pruebas YAML ya no se utilizan, pero aquí es un ejemplo,
desde el `02_order_to_invoice.yml` en el módulo addon `point_of_sale`:
142
Page 4
capitulo-8.md 07/02/2018

143 ```
144 -
145 I click on the "Make Payment" wizard to pay the PoS order
146 -
147 !record {model: pos.make.payment, id: pos_make_payment_2, context:
'{"active_id": ref("pos_order_pos1"), "active_ids":
[ref("pos_order_pos1")]}' }:
148 amount: !eval >
149 (450*2 + 300*3*1.05)*0.95
150 -
151 I click on the validate button to register the payment.
152 -
153 !python {model: pos.make.payment}: |
154 self.check(cr, uid, [ref('pos_make_payment_2')],
context={'active_id': ref('pos_order_pos1')} )
155 ```
156
157 Las líneas que comienzan con un `!` Son etiquetas YAML, equivalentes a
los elementos de la etiqueta que encontramos en los archivos XML. En el
código anterior podemos ver una etiqueta `¡ record`, equivalente a la
etiqueta XML `<record>`, y una etiqueta `!pyython`, que nos permite
ejecutar código Python en un modelo, `pos.make.payment` en el ejemplo.
158
159 Como puedes ver, las pruebas YAML utilizan una sintaxis específica de
Odoo que necesita aprendizaje. En comparación, las pruebas de Python
utilizan el framework `unittest` existente, solo añadiendo clases
envolventes específicas de Odoo como `TransactionCase`.
160
161 ## Herramientas de desarrollo
162
163 Hay algunas técnicas que el desarrollador debe aprender para ayudar en
su trabajo. En el Capítulo 1, *Iniciando con el desarrollo Odoo*, ya
hemos introducido la interfaz de usuario del modo de desarrollador
**Developer Mode**. También tenemos disponible una opción de servidor
que proporciona algunas características amigables para desarrolladores.
Lo describiremos más detalladamente a continuación. Después de eso
vamos a discutir otro tema relevante para los desarrolladores: cómo
depurar el código del lado del servidor.
164
165
166 ### Opciones de desarrollo del servidor
167
168 El servidor Odoo proporciona la opción `--dev` para permitir que
algunas funciones del desarrollador aceleren nuestro ciclo de
desarrollo, como por ejemplo:
169
170 + Introducir el depurador cuando se encuentre una excepción en un
módulo de complemento
171 + Recargar código Python automáticamente, una vez que se guarda un
archivo Python, evitando un reinicio manual del servidor
172 + Lee las definiciones de vista directamente desde los archivos XML,
evitando las actualizaciones manuales de módulos
173
174 La opción `--dev` acepta una lista de opciones separada por comas,
aunque la opción todo será adecuada la mayor parte del tiempo. También
podemos especificar el depurador que preferimos usar. De forma
predeterminada, se utiliza el depurador Python, `pdb`. Algunas personas
pueden preferir instalar y usar depuradores alternativos. Aquí también
se admiten `ipdb` y `pudb`.
175
176
Page 5
capitulo-8.md 07/02/2018

177 #### Nota


178
179 Antes de Odoo 10 teníamos en lugar de la opción `--debug`, permitiendo
abrir el depurador en una excepción de módulo de complemento.
180
181 Cuando se trabaja con código Python, el servidor debe reiniciarse cada
vez que se cambia el código, para que se vuelva a cargar. La opción de
línea de comandos `--dev` hace que la recarga: cuando el servidor
detecta que se cambia un archivo Python, repite automáticamente la
secuencia de carga del servidor, haciendo que el cambio de código sea
efectivo de inmediato.
182
183 Para usarlo simplemente agregue la opción `--dev=all` al comando del
servidor:
184
185
186 ```
187 $ ./odoo-bin -d todo --dev=al
188 ```
189
190 Para que esto funcione el paquete Python `watchdog` es necesario, y
debe instalarse como se muestra aquí:
191
192 ```
193 $ pip install watchdog
194
195 ```
196 #### Nota
197
198 Ten en cuenta que esto es útil sólo para los cambios de código Python y
arquitecturas de vista en archivos XML. Para otros cambios, como la
estructura de datos del modelo, se necesita una actualización del
módulo y la recarga no es suficiente.
199
200
201 ### Depuración
202
203 Todos sabemos que una buena parte del trabajo de un desarrollador es
depurar código. Para hacer esto a menudo hacemos uso de un editor de
código que puede establecer puntos de interrupción y ejecutar nuestro
programa paso a paso.
204
205 Si estás utilizando Microsoft Windows como tu estación de trabajo de
desarrollo, la configuración de un entorno capaz de ejecutar código
Odoo desde el origen es una tarea no trivial. También el hecho de que
Odoo es un servidor que espera las llamadas del cliente, y sólo
entonces actúa sobre ellos, lo hace muy diferente de depurar en
comparación con los programas del lado del cliente.
206
207 #### El depurador Python
208
209 Si bien puede parecer un poco intimidante para los recién llegados, el
enfoque más pragmático para depurar Odoo es utilizar el depurador
integrado de Python, `pdb`. También introduciremos extensiones que
proporcionarán una interfaz de usuario más rica, similar a lo que
normalmente proporcionan los IDEs sofisticados.
210
211 Para usar el depurador, el mejor enfoque es insertar un punto de
interrupción en el código que queremos inspeccionar, normalmente un
método de modelo. Esto se hace insertando la siguiente línea en el
lugar deseado:
Page 6
capitulo-8.md 07/02/2018

212
213 ```
214 import pdb; pdb.set_trace()
215
216 ```
217
218 Ahora reinicia el servidor para que se cargue el código modificado. Tan
pronto como la ejecución del programa llegue a esa línea, se mostrará
un prompt de Python (`pdb`) en la ventana de terminal donde se está
ejecutando el servidor, esperando por nuestra entrada.
219
220 #### Nota
221
222 La opción `--dev` no es necesaria para usar puntos de interrupción de
depurador Python manualmente configurados.
223
224 Este prompt funciona como un shell de Python, donde puede ejecutar
cualquier expresión o comando en el contexto de ejecución actual. Esto
significa que las variables actuales pueden ser inspeccionadas e
incluso modificadas. Estos son los comandos de acceso directo más
importantes disponibles:
225
226 + `h` (ayuda) muestra un resumen de los comandos de pdb disponibles
227 + `p` (imprimir) evalúa e imprime una expresión
228 + `pp` (impresión bonita) es útil para imprimir estructuras de datos
como diccionarios o listas
229 + `l` (lista) enumera el código alrededor de la instrucción siguiente a
ejecutar
230 + `n`(siguiente) pasos hacia la siguiente instrucción
231 + `s`(paso) pasos en la instrucción actual
232 + `c`(continuar) continúa la ejecución normalmente
233 + `u` (arriba) sube la pila de ejecución
234 + `d` (hacia abajo) se mueven hacia abajo en la pila de ejecución
235
236 El servidor Odoo también admite la opción `dev=all`. Si se activa,
cuando se produce una excepción, el servidor entra en un modo *post
mortem* en la línea correspondiente. Este es un prompt `pdb`, como el
descrito anteriormente, que nos permite inspeccionar el estado del
programa en el momento en que se encontró el error.
237
238 Mientras que `pdb` tiene la ventaja de estar disponible fuera de la
caja, puede ser bastante concisa, y existen algunas opciones más cómodas.
239
240 #### Un ejemplo de sesión de depuración
241
242 Veamos cómo se ve una simple sesión de depuración. Podemos comenzar
añadiendo punto de interrupción del depurador en la primera línea del
método del asistente `do_populate_tasks`:
243
244 ```
245 def do_populate_tasks(self):
246 import pdb; pdb.set_trace()
247 self.ensure_one()
248 # ...
249 ```
250
251 Ahora reinicia el servidor, abre un formulario **Asistente para tareas
pendientes "To-do Tasks Wizard"** y haz clic en el botón **Obtener todo
"Get All"**. Esto activará el método del asistente `do_populate_tasks`
en el servidor y el cliente web permanecerá en un estado de carga
**Loading...**, esperando la respuesta del servidor. Observando la
Page 7
capitulo-8.md 07/02/2018

ventana de terminal donde se ejecuta el servidor, verás algo similar a


esto:
252
253 ```
254 >
/home/daniel/odoo-dev/custom-addons/todo_wizard/models/todo_wizard_model.
py(54)do_populate_tasks()
255 -> self.ensure_one()
256 (Pdb)
257 ```
258
259 Este es el prompt del depurador de pdb y las dos primeras líneas te
proporcionan información sobre dónde se encuentra en la ejecución del
código Python. La primera línea informa el archivo, el número de línea
y el nombre de la función en la que te encuentras, y la segunda línea
es la siguiente línea de código que se va a ejecutar.
260
261 Durante una sesión de depuración, los mensajes del registro del
servidor pueden deslizarse. Estos no perjudicarán nuestra depuración,
pero pueden perturbarnos. Podemos evitarlo reduciendo la verbosidad de
los mensajes de registro. La mayoría de las veces estos mensajes de
registro serán del módulo `werkzeug`. Podemos silenciarlos usando la
opción `--log-handler=werkzeug:CRITICAL`. Si esto no es suficiente,
podemos bajar el nivel de registro general, usando `--log-level=warn`.
262
263 Si escribimos `h` ahora, veremos una referencia rápida de los comandos
disponibles. Al escribir `l` se muestra la línea de código actual y las
líneas de código circundantes.
264
265 Al escribir `n` se ejecutará la línea de código actual y se moverá a la
siguiente. Si simplemente pulsamos *Enter*, el comando anterior se
repetirá. Si haces eso tres veces deberíamos estar en la declaración de
retorno del método.
266
267 Podemos inspeccionar el contenido de cualquier variable, como las
`open_tasks` utilizadas en este método y escribiendo `p open_tasks` o
`print open_tasks` mostrará la representación de esa variable.
Cualquier expresión de Python está permitida, incluso las asignaciones
de variables. Por ejemplo, para mostrar una lista más amigable con los
nombres de tareas que podríamos usar:
268
269 ```
270 (pdb) p open_tasks.mapped('name')
271 ```
272
273 Al ejecutar la línea de retorno, usando `n` una vez más, se mostrarán
los valores retornados de la función. Algo como esto:
274
275 ```
276 --Return--
277 >
/home/daniel/odoo-dev/custom-addons/todo_wizard/models/todo_wizard_model.
py(59)do_populate_tasks()->{'res_id': 14, 'res_model': 'todo.wizard',
'target': 'new', 'type': 'ir.actions.act_window', ...}
278 -> return self._reopen_form()
279 ```
280
281 La sesión de depuración continuará en las líneas de código de la
persona que llama, pero podemos terminarla y continuar la ejecución
normal escribiendo `c`.
282
Page 8
capitulo-8.md 07/02/2018

283 #### Depuradores de Python alternativos


284
285 Mientras que `pdb` tiene la ventaja de estar disponible fuera de la
caja, puede ser bastante concisa, y algunas opciones más cómodas existen.
286
287 El depurador de hierro de Python, `ipdb`, es una opción popular que
utiliza los mismos comandos que `pdb`, pero añade mejoras como la
finalización de tabulaciones y el resaltado de sintaxis, para un uso
más cómodo. Se puede instalar con:
288
289 ```
290
291 $ sudo pip install ipdb
292
293
294
295
296
297
298 ```
299
300 Y se agrega un punto de interrupción con la línea:
301
302 ```
303 import ipdb; ipdb.set_trace()
304 ```
305
306 Otro depurador alternativo es `pudb`. También soporta los mismos
comandos que `pdb` y funciona en terminales sólo de texto, pero utiliza
una pantalla gráfica similar a lo que puedes encontrar en un depurador
IDE. La información útil, como las variables en el contexto actual y
sus valores, está fácilmente disponible en la pantalla en sus propias
ventanas:
307
308 ![debugger](file:img/9-01.jpg)
309
310 Se puede instalar a través del gestor de paquetes del sistema o a
través de `pip`, como se muestra aquí:
311
312 ```
313
314 $ sudo apt-get install python-pudb # using OS packages
315
316
317
318
319
320 $ sudo pip install pudb # using pip, possibly in a virtualenv
321
322 ```
323
324 La adición de un punto de interrupción `pudb` se hace justo de la
manera que esperarías:
325
326 ```
327 import pudb; pudb.set_trace()
328 ```
329
330 #### Imprmiendo mensajes y registro
331
332 A veces sólo necesitamos inspeccionar los valores de algunas variables
Page 9
capitulo-8.md 07/02/2018

o comprobar si se están ejecutando algunos bloques de código. Una


instrucción `print` de Python puede hacer el trabajo perfectamente sin
detener el flujo de ejecución. Al ejecutar el servidor en una ventana
de terminal, el texto impreso se mostrará en la salida estándar. Pero
no se almacenará en el registro del servidor si se está escribiendo en
un archivo.
333
334 Otra opción a tener en cuenta es establecer los mensajes de registro de
nivel de depuración en puntos sensibles de nuestro código si creemos
que podríamos necesitarlos para investigar los problemas en una
instancia implementada. Sólo sería necesario elevar ese nivel de
registro de servidor para depurar e inspeccionar los archivos de
registro.
335
336 ### Inspeccioando procesos en ejecución
337
338 También hay algunos trucos que nos permiten inspeccionar un proceso
Odoo en ejecución.
339
340 Para eso primero necesitamos encontrar el ID de proceso correspondiente
(PID). Para encontrar el PID, ejecuta otra ventana de terminal y escribe:
341
342 ```
343 $ ps ax | grep odoo-bin
344 ```
345 La primera columna en la salida es el PID para ese proceso. Toma una
nota en el PID para el proceso de inspección, ya que lo necesitaremos a
continuación.
346
347 Ahora queremos enviar una señal al proceso. El comando utilizado para
hacer eso es matar "kill". De forma predeterminada envía una señal para
finalizar un proceso, pero también puede enviar otras señales más
amigables.
348
349 Conociendo el PID para nuestro proceso de servidor Odoo en ejecución,
podemos imprimir las trazas del código que se está ejecutando
actualmente usando:
350
351 ```
352
353 $ kill -3 <PID>
354
355
356
357
358 ```
359
360 Si observamos la ventana de terminal o el archivo de registro donde se
está escribiendo la salida del servidor, veremos la información de los
varios subprocesos que se están ejecutando y los trazados detallados de
la pila en la línea de código que están ejecutando.
361
362 También podemos ver un volcado de las estadísticas de caché/memoria
usando:
363
364 ```
365
366 $ kill -USR1 <PID>
367
368
369
Page 10
capitulo-8.md 07/02/2018

370
371
372 ```
373
374 ## Resumen
375
376 Las pruebas automatizadas son una práctica valiosa, tanto para las
aplicaciones empresariales en general, como para garantizar la robustez
del código en un lenguaje de programación dinámico, como Python.
377
378 Aprendimos los principios básicos de cómo agregar y ejecutar pruebas
para un módulo addon. También discutimos algunas técnicas para
ayudarnos a depurar nuestro código.
379
380 En el próximo capítulo, profundizaremos en la capa de vistas, y
discutiremos las vistas kanban.

Page 11
capitulo-9.md 07/02/2018

1 # Capítulo 9. Vistas QWeb y Kanban


2
3 **QWeb** es un motor de plantilla utilizado por Odoo. Se basa en XML y
se utiliza para generar fragmentos y páginas HTML. QWeb fue introducido
por primera vez en la versión 7.0 para permitir vistas de kanban más
ricas y, desde la versión 8.0, también se utiliza para el diseño de
informes y páginas web de CMS.
4
5 Aquí aprenderás sobre la sintaxis de QWeb y cómo usarla para crear sus
propias vistas kanban e informes personalizados. Comencemos aprendiendo
más sobre los tableros kanban.
6
7 ## Acerca de tableros kanban
8
9 **Kanban** es una palabra japonesa utilizada para representar un método
de gestión de colas de trabajo. Se inspira en el Sistema de Producción
Toyota y en la Fabricación Lean. Se ha convertido en popular en la
industria del software con la adopción de metodologías Agile.
10
11 El tablero kanban **kanban board** es una herramienta para visualizar
la cola de trabajo. La junta está organizada en columnas que
representan las etapas **stages** del proceso de trabajo. Los objetos
de trabajo se representan mediante tarjetas **cards** colocadas en la
columna apropiada del tablero. Los nuevos elementos de trabajo empiezan
desde la columna más a la izquierda y viajan a través de la tabla hasta
llegar a la columna más a la derecha, representando el trabajo terminado.
12
13 La simplicidad y el impacto visual de los tableros kanban los hacen
excelentes para soportar procesos empresariales simples. Un ejemplo
básico de un tablero kanban puede tener tres columnas, como se muestra
en la siguiente imagen: **Por Hacer "To Do"**, **Haciendo "Doing"**,
and **Hecho "Done"**.
14
15 Por supuesto, puede ampliarse a cualquier proceso específico que
podamos necesitar:
16
17 ![kanban](file:img/9-01.jpg)
18
19 Créditos de la foto: "A Scrum board suggesting to use kanban" por
Jeff.lasovski. Cortesía de Wikipedia.
20
21 ### Vistas Kanban
22
23 Para muchos casos de uso empresarial, un tablero kanban puede ser una
forma más eficaz de administrar el proceso correspondiente que el motor
de flujo de trabajo normalmente más pesado. Odoo admite las vistas de
tablas kanban, junto con la lista clásica y vistas de formulario. Esto
facilita la implementación de este tipo de vista. Vamos a aprender cómo
usarlos.
24
25 En las vistas de formulario, usamos elementos XML específicos, como
`<field>` y `<group>`, y pocos elementos HTML, como `<h1>` o `<div>`.
Con las vistas kanban, es todo lo contrario; Son plantillas basadas en
HTML y admiten sólo dos elementos específicos de Odoo,`<field>` y
`<button>`.
26
27 El HTML se genera dinámicamente con las plantillas de QWeb. El motor
QWeb procesa etiquetas y atributos XML especiales para producir el HTML
final que se presentará en el cliente web. Esto aporta mucho control
sobre cómo procesar el contenido, pero también hace que el diseño de la
vista sea más complejo.
Page 1
capitulo-9.md 07/02/2018

28
29 El diseño de la vista kanban es bastante flexible, por lo que haremos
todo lo posible para prescribir una manera sencilla de crear
rápidamente sus vistas kanban. Un buen enfoque es encontrar una vista
kanban existente similar a lo que usted necesita, e inspeccionarla para
ideas sobre cómo construir la suya.
30
31 Podemos ver dos formas diferentes de usar las vistas de kanban. Una es
una lista de tarjetas. Se utiliza en lugares como contactos, productos,
directorios de empleados o aplicaciones.
32
33 Así es como se ve la vista de kanban de **Contactos "Contacts"**:
34
35 ![kanban_contacts](file:img/9-02.jpg)
36
37 Pero esto no es un tablero kanban verdadero. Se espera que un tablero
kanban tenga las tarjetas organizadas en columnas y, por supuesto, la
vista kanban también apoya ese diseño. Podemos ver ejemplos en **Sales
| My Pipeline** o en **Tareas de Proyecto "Project Tak"**.
38
39 Aquí está como se ve ** Sales | My Pipeline**:
40
41 ![sales_my_pipeline](file:img/9-03.jpg)
42
43 La diferencia más llamativa entre los dos es la organización de las
tarjetas en columna del tablero de kanban. Esto se logra mediante la
función **Agrupar por "Group By"**, similar a lo que proporcionan las
vistas de lista. Por lo general, el agrupamiento se realiza en un campo
**Etapa "Stage"**. Una característica muy útil de las vistas de kanban
es que soporta arrastrar y soltar tarjetas entre columnas, asignando
automáticamente el valor correspondiente al campo que agrupa la vista.
44
45 Mirando las tarjetas en ambos ejemplos, podemos ver algunas
diferencias. De hecho, su diseño es bastante flexible, y no hay una
sola manera de diseñar una tarjeta kanban. Pero estos dos ejemplos
pueden proporcionar un punto de partida para sus diseños.
46
47 Las tarjetas de **Contacto "Contact"** básicamente tienen una imagen en
el lado izquierdo, y un título en negrita en el área principal, seguido
de una lista de valores. Las tarjetas **My Pipeline** tienen un poco
más de estructura. El área de la tarjeta principal también tiene un
título seguido por una lista de información relevante, así como un área
de pie de página, en este caso con un widget de prioridad en el lado
izquierdo, y el usuario responsable en el lado derecho. No es visible
en la imagen, pero las tarjetas también tienen un menú de opciones en
la parte superior derecha, que se muestra al pasar el puntero del ratón
sobre él. Este menú permite, por ejemplo, cambiar el color de fondo de
la tarjeta.
48
49 Vamos a utilizar esta estructura más elaborada como un modelo para las
tarjetas en nuestro tablero kanban de tareas pendientes.
50
51 ## Diseñando vistas kanban
52
53 Vamos a agregar la vista kanban a las tareas pendientes con un nuevo
módulo addon. Sería más sencillo añadirlo directamente al módulo
`todo_ui`. Sin embargo, para una explicación más clara, usaremos un
nuevo módulo y evitaremos muchos cambios, posiblemente confusos, en los
archivos ya creados.
54
55 Nombraremos este nuevo módulo addon como `todo_kanban` y crearemos los
Page 2
capitulo-9.md 07/02/2018

archivos iniciales habituales. Edite el archivo descriptor


`todo_kanban/__manifest__.py` como sigue:
56
57 ```
58 {'name': 'To-Do Kanban',
59 'description': 'Kanban board for to-do tasks.',
60 'author': 'Daniel Reis',
61 'depends': ['todo_ui'],
62 'data': ['views/todo_view.xml'] }
63
64 ```
65 También agregue un archivo vacío `todo_kanban/__ init__.py`, para que
el directorio Python pueda ser importado, como se requiere para los
módulos addon de Odoo.
66
67 A continuación, crea el archivo XML en el que vaya nuestra nueva vista
de kanban brillante y establece kanban como la vista predeterminada en
la acción de la ventana de la tarea pendiente. Esto debe ser en
`todo_kanban/views/todo_view.xml`, que contiene el siguiente código:
68
69 ```
70 <?xml version="1.0"?>
71 <odoo>
72 <!-- Add Kanban view mode to the menu Action: -->
73 <act_window id="todo_app.action_todo_task" name="To-Do
Tasks"
74 res_model="todo.task"
view_mode="kanban,tree,form,calendar,graph,pivot"
75 context="{'search_default_filter_my_tasks': True}" />
76 <!-- Add Kanban view -->
77 <record id="To-do Task Kanban" model="ir.ui.view">
78 <field name="model">todo.task</field>
79 <field name="arch" type="xml">
80 <kanban>
81
82 <!-- Empty for now, but the Kanban will go here! -->
83
84
85
86
87 </kanban>
88 </field>
89 </record>
90 </odoo>
91
92 ```
93 Ahora tenemos el esqueleto básico para nuestro módulo en su lugar.
94
95 Antes de comenzar con las vistas kanban, necesitamos agregar un par de
campos al modelo de tareas pendientes.
96
97 ### Prioridad, estado kanban y color
98
99 Aparte de las etapas, algunos campos más son útiles y se utilizan con
frecuencia en las juntas kanban.
100
101 + `priority` permite a los usuarios organizar sus elementos de trabajo,
señalando lo que debe abordarse en primer lugar.
102 + `kanban_state` indica si una tarea está lista para pasar a la
siguiente etapa o si está bloqueada por alguna razón. En la capa de
definición del modelo, ambos son campos de selección. En la capa de
Page 3
capitulo-9.md 07/02/2018

vista, tienen widgets específicos para ellos que se pueden utilizar en


forma y vistas kanban.
103 + `color` se utiliza para almacenar el color que la tarjeta kanban debe
mostrar, y se puede establecer mediante un menú selector de color
disponible en las vistas kanban.
104
105 Para agregar estos campos a nuestro modelo, agregamos un archivo
`models/todo_task_model.py`.
106
107 Pero primero, tendremos que hacerlo importable y editar el archivo
`todo_kanban/__init__.py` para importar el subdirectorio de modelos:
108
109 ```
110 from . import models
111 ```
112
113 A continuación, crea el archivo `models/__init__.py` con:
114
115 ```
116 from . import todo_task
117 ```
118
119 Ahora vamos a editar el archivo `models/todo_task.py`:
120
121 ```
122 from odoo import models, fields
123 class TodoTask(models.Model):
124 _inherit = 'todo.task'
125 color = fields.Integer('Color Index')
126 priority = fields.Selection(
127 [('0', 'Low'),
128 ('1', 'Normal'),
129 ('2', 'High')],
130 'Priority', default='1')
131 kanban_state = fields.Selection(
132 [('normal', 'In Progress'),
133 ('blocked', 'Blocked'),
134 ('done', 'Ready for next stage')],
135 'Kanban State', default='normal')
136 ```
137
138 Ahora podemos trabajar en la vista kanban.
139
140 ### Elementos de tarjeta Kanban
141
142 La arquitectura de vista kanban tiene un elemento superior `<kanban>` y
la siguiente estructura básica:
143
144 ```
145 <kanban default_group_by="stage_id" class="o_kanban_small_column" >
146
147 <!-- Fields to use in expressions... -->
148
149
150
151
152 <field name="stage_id" />
153 <field name="color" />
154 <field name="kanban_state" />
155 <field name="priority" />
156 <field name="is_done" />
Page 4
capitulo-9.md 07/02/2018

157 <field name="message_partner_ids" />


158
159 <!-- (...add other used fields). -->
160
161
162
163
164 <templates>
165 <t t-name="kanban-box">
166
167 <!-- HTML QWeb template... -->
168
169
170
171
172 </t>
173 </templates>
174 </kanban>
175
176
177 ```
178
179 Observa el atributo `default_group_by="stage_id"` utilizado en el
elemento `<kanban>`. Lo usamos para que, por defecto, las tarjetas
kanban se agrupen por etapas como deberían los tableros kanban. En los
kanbans de listas de tarjetas simples, como el de **Contactos
"Contacts"**, no necesitamos esto y en su lugar simplemente usaríamos
una sencilla etiqueta de apertura `<kanban>`.
180
181 El elemento superior `<kanban>` admite algunos atributos interesantes:
182
183 + `default_group_by` establece el campo a utilizar para los grupos de
columnas predeterminados.
184 + `default_order` establece un orden por defecto para usar para los
elementos kanban.
185 + `quick_create="false"` desactiva la opción de creación rápida (el
signo *más* grande), disponible en la parte superior de cada columna
para crear nuevos elementos proporcionando sólo una descripción de
título. El valor false es un literal de JavaScript, y debe estar en
minúsculas.
186 + `class` añade una clase CSS al elemento raíz de la vista renderizada
de kanban. Una clase relevante es `o_kanban_small_column`, lo que hace
que las columnas sean algo más compactas que las predeterminadas. Las
clases adicionales pueden estar disponibles por el módulo proporcionado
CSS personalizado.
187
188 A continuación, vemos una lista de campos utilizados en las plantillas.
Para ser exactos, sólo los campos utilizados exclusivamente en las
expresiones QWeb deben ser declarados aquí, para asegurarse de que sus
datos son obtenidos desde el servidor.
189
190 A continuación, tenemos un elemento `<templates>`, que contiene una o
más plantillas QWeb para generar los fragmentos HTML utilizados.
Debemos tener una plantilla llamada `kanban-box`, que renderizará las
tarjetas kanban. También se pueden agregar plantillas adicionales,
generalmente para definir fragmentos HTML que se reutilizarán en la
plantilla principal.
191
192 Estas plantillas utilizan HTML estándar y el lenguaje de plantillas
QWeb. QWeb proporciona directivas especiales, que se procesan para
generar dinámicamente el HTML final que se va a presentar.
Page 5
capitulo-9.md 07/02/2018

193
194 #### Tip
195
196 Odoo utiliza la biblioteca de estilo web de Twitter Bootstrap 3, por lo
que esas clases de estilo están generalmente disponibles dondequiera
que se pueda renderizar HTML. Puedes obtener más información sobre
Bootstrap en https://getbootstrap.com
197
198 Ahora vamos a ver más de cerca las plantillas de QWeb para usarlas en
las vistas kanban.
199
200
201 ### El diseño de la tarjeta kanban
202
203 El área de contenido principal de una tarjeta kanban se define dentro
de la plantilla `kanban-box`. Este área de contenido también puede
tener un sub-contenedor de pie de página.
204
205 Para un único pie de página, usaríamos un elemento `<div>` en la parte
inferior del cuadro kanban, con la clase `oe_kanban_footer` CSS. Esta
clase dividirá automáticamente sus elementos internos con espacios
flexibles, haciendo explícita la alineación izquierda y derecha dentro
de ella superflua.
206
207 Un botón que abre un menú de acción también puede aparecer en la
esquina superior derecha de la tarjeta. Como alternativa, el Bootstrap
proporciona las clases `pull-left` y `pull-right` para añadir elementos
alineados a la izquierda oa la derecha en cualquier parte de la
tarjeta, incluso en el pie `oe_kanban_footer`.
208
209 Aquí está nuestra primera iteración en la plantilla QWeb para nuestra
tarjeta kanban:
210
211 ```
212 <!-- Define the kanban-box template -->
213 <t t-name="kanban-box">
214 <!-- Set the Kanban Card color: -->
215 <div t-attf-class="#{kanban_color(record.color.raw_value)}
216 oe_kanban_global_click">
217 <div class="o_dropdown_kanban dropdown">
218
219 <!-- Top-right drop down menu here... -->
220
221
222
223
224 </div>
225 <div class="oe_kanban_content">
226 <div class="oe_kanban_footer">
227 <div>
228
229 <!-- Left hand footer... -->
230
231
232
233
234 </div>
235 <div>
236
237 <!-- Right hand footer... -->
238
Page 6
capitulo-9.md 07/02/2018

239
240
241
242 </div>
243 </div>
244 </div> <!-- oe_kanban_content -->
245 <div class="oe_clear"/>
246 </div> <!-- kanban color -->
247 </t>
248
249 ```
250
251 Esto establece la estructura general de la tarjeta kanban. Puedes notar
que el campo `color` se utiliza en el elemento superior `<div>` para
establecer dinámicamente el color de la tarjeta. Explicaremos más
detalladamente la directiva Qweb `t-attf` en una de las siguientes
secciones.
252
253 Ahora vamos a trabajar en el área de contenido principal, y elegir qué
colocar allí:
254
255 ```
256
257 <!-- Content elements and fields go here... -->
258
259
260
261
262 <div>
263 <field name="tag_ids" />
264 </div>
265
266 <div>
267 <strong>
268 <a type="open"><field name="name" /></a>
269 </strong>
270 </div>
271
272 <ul>
273 <li><field name="user_id" /></li>
274 <li><field name="date_deadline" /></li>
275 </ul>
276 ```
277
278 La mayor parte de esta plantilla es HTML normal, pero también vemos el
elemento `<field>` usado para renderizar los valores de los campos, y
el atributo `type` utilizado en los botones regulares de vista de
formulario, usados aquí en una etiqueta de anclaje `<a>`.
279
280 En el pie de página izquierdo, vamos a insertar el widget de prioridad:
281
282 ```
283 <div>
284
285 <!-- Left hand footer... -->
286
287
288
289
290 <field name="priority" widget="priority"/>
291 </div>
Page 7
capitulo-9.md 07/02/2018

292 ```
293 Aquí podemos ver el campo `priority` añadido, tal como lo haríamos en
una vista de formulario.
294
295 En el pie derecho colocaremos el widget de estado kanban y el avatar
para el propietario de la tarea:
296
297 ```
298 <div>
299
300 <!-- Right hand footer... -->
301
302
303
304
305 <field name="kanban_state" widget="kanban_state_selection"/>
306 <img t-att- t-att-src="kanban_image(
307 'res.users', 'image_small', record.user_id.raw_value)"
308 width="24" height="24" class="oe_kanban_avatar pull-right" />
309 </div>
310 ```
311 El estado kanban se agrega utilizando un elemento `<field`, al igual
que en las vistas de formulario normales. La imagen del avatar del
usuario se inserta mediante la etiqueta HTML `<img>`. El contenido de
la imagen se genera dinámicamente con la directiva QWeb `t-att-`, que
explicaremos en un momento.
312
313 A veces queremos tener una pequeña imagen representativa que se
mostrará en la tarjeta, como en el ejemplo de **Contactos "Contacts"**.
Para hacer referencia, esto se puede hacer agregando lo siguiente como
el primer elemento de contenido:
314
315 ```
316 <img t-att-src="kanban_image( 'res.partner', 'image_medium',
317 record.id.value)" class="o_kanban_image"/>
318 ```
319
320 ### Añadiendo un menú de opciones de tarjeta kanban
321
322 Las tarjetas Kanban pueden tener un menú de opciones, situado en la
parte superior derecha. Las acciones habituales son editar o eliminar
el registro, pero es posible tener cualquier acción que se puede llamar
desde un botón. También tenemos un widget para configurar el color de
la tarjeta.
323
324 El siguiente es un código HTML de línea de base para el menú de
opciones que se agregará en la parte superior del elemento
`oe_kanban_content`:
325
326 ```
327 <div class="o_dropdown_kanban dropdown">
328
329 <!-- Top-right drop down menu here... -->
330
331
332
333
334 <a class="dropdown-toggle btn" data-toggle="dropdown" href="#">
335 <span class="fa fa-bars fa-lg"/>
336 </a>
337 <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
Page 8
capitulo-9.md 07/02/2018

338
339 <!-- Edit and Delete actions, if available: -->
340
341
342
343
344 <t t-if="widget.editable">
345 <li><a type="edit">Edit</a></li>
346 </t>
347 <t t-if="widget.deletable">
348 <li><a type="delete">Delete</a></li>
349 </t>
350
351 <!-- Call a server-side Model method: -->
352
353
354
355
356 <t t-if="!record.is_done.value">
357 <li><a name="do_toggle_done" type="object">Set as Done</a>
358 </li>
359 </t>
360
361 <!-- Color picker option: -->
362
363
364
365
366 <li>
367 <ul class="oe_kanban_colorpicker" data-field="color"/>
368 </li>
369 </ul>
370 </div>
371 ```
372 Observa que lo anterior no funcionará a menos que tengamos `<field
name="is_done" />` en algún lugar de la vista, porque se usa en una de
las expresiones. Si no necesitamos usarla dentro de la plantilla,
podemos declararla antes del elemento `<templates>`, como lo hicimos al
definir la vista `<kanban>`.
373
374 El menú desplegable es básicamente una lista HTML de los elementos
`<a>`. Algunas opciones, como **Editar "Edit"** y **Eliminar
"Delete"**, sólo están disponibles si se cumplen ciertas condiciones.
Esto se hace con la directiva QWeb `t-if`. Más adelante en este
capítulo, explicamos estas y otras directivas QWeb con más detalle.
375
376 La variable global `widget` representa el objeto `KanbanRecord()`
JavaScript actual responsable de la representación de la tarjeta kanban
actual. Dos propiedades particularmente útiles son `widget.editable` y
`widget.deletable` para inspeccionar si las acciones están disponibles.
377
378 También podemos ver cómo mostrar u ocultar una opción dependiendo de
los valores del campo de registro. La opción **Establecer como Hecho
"Set as Done"** solo se mostrará si el campo `is_done` no está
establecido.
379
380 La última opción agrega el widget especial del selector de color usando
el campo de datos `color` para seleccionar y cambiar el color de fondo
de la tarjeta.
381
382 ### Acciones en vistas kanban
Page 9
capitulo-9.md 07/02/2018

383
384 En las plantillas de QWeb, la etiqueta `<a>` para los vínculos puede
tener un atributo `type`. Establece el tipo de acción que el enlace
realizará para que los enlaces puedan actuar igual que los botones en
formas regulares. Así, además de los elementos `<button>`, las
etiquetas `<a>` también se pueden usar para ejecutar acciones Odoo.
385
386 Al igual que en las vistas de formulario, el tipo de acción puede ser
`action` u `object`, y debe ir acompañado de un atributo `name`, que
identifica la acción específica a ejecutar. Además, también están
disponibles los siguientes tipos de acción:
387
388 + `open` abre la vista de formulario correspondiente
389 + `edit` abre la vista de formulario correspondiente directamente en
modo de edición
390 + `delete` elimina el registro y remueve el elemento de la vista kanban
391
392 ## El lenguaje de plantillas QWeb
393
394 El analizador QWeb busca directivas especiales en las plantillas y las
reemplaza con HTML generado dinámicamente. Estas directivas son
atributos de elementos XML y pueden utilizarse en cualquier etiqueta o
elemento válido, como `<div>`, `<span>` o `<campo>`.
395
396 A veces queremos usar una directiva QWeb pero no queremos colocarla en
ninguno de los elementos XML de nuestra plantilla. Para esos casos,
tenemos un elemento especial `<t>` que puede tener directivas QWeb,
como un `t-if` o un `t-foreach`, pero es silencioso y no tendrá ningún
resultado en el XML / HTML final producido.
397
398 Las directivas QWeb usan frecuentemente expresiones evaluadas para
producir resultados diferentes dependiendo de los valores de registro
actuales. Existen dos implementaciones QWeb diferentes: JavaScript en
el lado del cliente y Python en el lado del servidor.
399
400 Los informes y las páginas del sitio web utilizan la implementación
Python del lado del servidor. Por otro lado, las vistas kanban utilizan
la implementación JavaScript del lado del cliente. Esto significa que
la expresión QWeb utilizada en las vistas kanban debe escribirse
utilizando la sintaxis JavaScript, no Python.
401
402 Al mostrar una vista de kanban, los pasos internos son aproximadamente
como sigue:
403
404 1. Obten el XML de las plantillas para procesar.
405 1. Llama al método del servidor `read()` para obtener los datos de los
campos de las plantillas.
406 1. Busca la plantilla `kanban-box` y analizala utilizando QWeb para
generar los fragmentos HTML finales.
407 1. Inyecta el HTML en la pantalla del navegador (el DOM).
408
409 Esto no pretende ser técnicamente exacto. Es sólo un mapa mental que
puede ser útil para entender cómo funcionan las cosas en las vistas
kanban.
410
411 A continuación, aprenderemos acerca de la evaluación de expresiones de
QWeb y exploraremos las directivas QWeb disponibles, usando ejemplos
que mejoren nuestra tareas pendientes de la tarjeta kanban.
412
413 ### La evaluación del contexto Qweb Javascript
414
Page 10
capitulo-9.md 07/02/2018

415 Muchas de las directivas QWeb utilizan expresiones que se evalúan para
producir algún resultado. Cuando se utiliza desde el lado del cliente,
como es el caso de las vistas kanban, estas expresiones están escritas
en JavaScript. Se evalúan en un contexto que tiene algunas variables
útiles disponibles.
416
417 Un objeto `record` está disponible, representando el registro que se
procesa, con los campos solicitados desde el servidor. Se puede acceder
a los valores de campo utilizando los atributos `raw_value` o `value`:
418
419 + `raw_value` es el valor devuelto por el método de servidor `read()`,
por lo que es más adecuado para usar en expresiones de condición.
420 + `value` se formatea de acuerdo con la configuración del usuario, y
está destinado a ser utilizado para mostrar en la interfaz de usuario.
Esto es típicamente relevante para los campos date/datetime y
float/monetary.
421
422 La evaluación de contexto QWeb también tiene referencias disponibles
para la instancia de cliente web JavaScript. Para hacer uso de ellos,
se necesita una buena comprensión de la arquitectura de cliente web,
pero no vamos a ser capaces de entrar en eso en detalle. Para fines de
referencia, los siguientes identificadores están disponibles en la
evaluación de la expresión QWeb:
423
424 + `widget` es una referencia al objeto de widget `KanbanRecord()`
actual, responsable de la representación del registro actual en una
tarjeta kanban. Expone algunas funciones auxiliares útiles que podemos
usar.
425 + `record` es un atajo para `widget.records` y proporciona acceso a los
campos disponibles, usando la notación de puntos.
426 + `read_only_mode` indica si la vista actual está en modo de lectura (y
no en el modo de edición). Es un acceso directo para
`widget.view.options.read_only_mode`.
427 + `instance` es una referencia a la instancia completa del cliente web.
428
429 También es de destacar que algunos caracteres no están permitidos
dentro de las expresiones. El signo inferior a (`<`) es tal caso. Esto
es debido al estándar XML, donde dichos caracteres tienen un
significado especial y no deben usarse en el contenido XML. Un `>=`
negado es una alternativa válida, pero la práctica común es utilizar
los siguientes símbolos alternativos que están disponibles para las
operaciones de desigualdad:
430
431 + `lt` es por menos que
432 + `lte` es por menor o igual que
433 + `gt` es por mayor que
434 + `gte`es por mayor o igual que
435
436 ### Utilizando t-attf para la sustitución de atributos de cadena
437
438 Nuestra tarjeta kanban utiliza la directiva QWeb `t-attf` para
establecer dinámicamente una clase en el elemento superior `<div>` para
que la tarjeta se coloree dependiendo del valor del campo `color`. Para
ello, se utilizó la directiva QWeb `t-attf-`.
439
440 La directiva `t-attf-` genera dinámicamente atributos de etiqueta
mediante sustitución de cadena. Esto se permite para las partes de
cadenas más grandes generadas dinámicamente, como una dirección URL o
nombres de clase CSS.
441
442 La directiva busca bloques de expresión que serán evaluados y
Page 11
capitulo-9.md 07/02/2018

reemplazados por el resultado. Estos son delimitados por {{and}} o por


#{and}. El contenido de los bloques puede ser cualquier expresión
JavaScript válida y puede utilizar cualquiera de las variables
disponibles para expresiones QWeb, como `record` y `widget`.
443
444 En nuestro caso, también utilizamos la función JavaScript
`kanban_color()`, proporcionada especialmente para asignar números de
índice de color a los nombres de color de la clase CSS.
445
446 Como un ejemplo más elaborado, podemos usar esta directiva para cambiar
dinámicamente el color de la **Fecha Límite "Deadline Date"**, de modo
que las fechas vencidas se muestren en rojo.
447
448 Para ello, reemplaza `<field name="date_deadline"/>` en nuestra tarjeta
kanban con esto:
449
450 ```
451 <li t-attf-class="oe_kanban_text_{{
452 record.date_deadline.raw_value and
453 !(record.date_deadline.raw_value > (new Date()))
454 ? 'red' : 'black' }}">
455 <field name="date_deadline"/>
456 </li>
457 ```
458
459 Esto resulta en `class="oe_kanban_text_red"` o en
`class="oe_kanban_text_black", dependiendo de la fecha límite. Tenga en
cuenta que, aunque la clase CSS `oe_kanban_text_red` está disponible en
las vistas kanban, la clase CSS `oe_kanban_text_black` no existe y se
utilizó para explicar mejor el punto.
460
461 #### Tip
462
463 El signo más bajo que `<`, no está permitido en las expresiones, y
elegimos trabajar alrededor de esto usando una comparación negada mayor
que. Otra posibilidad sería utilizar la función `&lt`; (Menor que)
símbolo de escape en su lugar.
464
465
466 ### Utilizando t-att para atributos dinámicos
467
468 La directiva QWeb `t-att-` genera dinámicamente un valor de atributo
mediante la evaluación de una expresión. Nuestra tarjeta kanban lo usa
para establecer dinámicamente algunos atributos en la etiqueta `<img>`.
469
470 El elemento `títle` se procesa dinámicamente usando:
471
472 ```
473 -att-
474 ```
475
476 El campo `.value` devuelve su representación de valores como debería
mostrarse en la pantalla, para campos de muchos-a-uno, generalmente
este es el valor `name` del registro relacionado. Para los usuarios,
este es el nombre de usuario. Como resultado, al pasar el puntero del
ratón sobre la imagen, verá el nombre de usuario correspondiente.
477
478 La etiqueta `src` también se genera dinámicamente, para proporcionar la
imagen correspondiente al usuario responsable. Los datos de imagen son
proporcionados por la función JavaScript de ayuda, `kanban_image()`:
479
Page 12
capitulo-9.md 07/02/2018

480 ```
481 t-att-src="kanban_image('res.users', 'image_small',
482 record.user_id.raw_value)"
483 ```
484
485 Los parámetros de la función son: el modelo para leer la imagen, el
nombre del campo a leer y el ID del registro. Aquí usamos `.raw_value`,
para obtener el ID de la base de datos del usuario en lugar de su texto
de representación.
486
487 No se detiene allí, y `t-att-NAME` y `t-attf-NAME` se pueden hacer para
renderizar cualquier atributo, ya que el nombre del atributo generado
se toma del sufijo `NAME` utilizado.
488
489 ### Usando t-foreach para bucles
490
491 Un bloque de HTML se puede repetir iterando a través de un bucle.
Podemos usarlo para agregar los avatares de los seguidores de tareas a
la tarjeta kanban de la tarea.
492
493 Comencemos por representar sólo los ID de socio de la tarea, de la
siguiente manera:
494
495 ```
496 <t t-foreach="record.message_partner_ids.raw_value" t-as="rec">
497 <t t-esc="rec" />;
498 </t>
499 ```
500
501 La directiva `t-foreach` acepta una expresión JavaScript que evalúa una
colección para iterar. En la mayoría de los casos, este será sólo el
nombre de un campo de relación *a-muchos*. Se utiliza con una directiva
`t-as` para establecer el nombre a utilizar para referirse a cada
elemento de la iteración.
502
503 La directiva `t-esc` utilizada a continuación evalúa la expresión
proporcionada, sólo el nombre de la variable `rec` en este caso, y lo
hace como código HTML con seguridad.
504
505 En el ejemplo anterior, pasamos por los seguidores de tareas,
almacenados en el campo `message_parter_ids`. Puesto que hay espacio
limitado en la tarjeta kanban, podríamos haber usado la función
JavaScript `slice()` para limitar el número de seguidores a mostrar,
como se muestra en lo siguiente:
506
507 ```
508 t-foreach="record.message_partner_ids.raw_value.slice(0, 3)"
509 ```
510
511 La variable `rec`contine cada valor de iteración, una ID de Socio en
este caso. Con esto, podemos reescribir los loops de los seguidores
como se muestra a continuación:
512
513 ```
514 <t t-foreach="record.message_parter_ids.raw_value.slice(0, 3)"
515 t-as="rec">
516 <img t-att-src="kanban_image('res.partner', 'image_small', rec)"
517 class="oe_avatar" width="24" height="24" />
518 </t>
519 ```
520
Page 13
capitulo-9.md 07/02/2018

521 Por ejemplo, esto podría agregarse junto a la imagen del usuario
responsable, en el pie de página derecho.
522
523 Algunas variables auxiliares también están disponibles. Su nombre tiene
como prefijo el nombre de variable definido en `t-as`. En nuestro
ejemplo, utilizamos `rec`, por lo que las variables auxiliares
disponibles son las siguientes:
524
525 + `rec_index` es el índice de iteración, a partir de cero
526 + `rec_size` es el número de elementos de la colección
527 + `rec_first` es verdadero en el primer elemento de la iteración
528 + `rec_last` es verdadero en el último elemento de la iteración
529 + `rec_even` es verdadero en índices pares
530 + `rec_odd` es verdadero en índices impares
531 + `rec_parity` es impar `odd` o par `even`, dependiendo del índice actual
532 + `rec_all` representa el objeto sobre el que se está iterando
533 + `rec_value` al iterar a través de un diccionario, `{key: value}`,
contiene el valor (`rec` contiene el nombre de la clave)
534
535 Por ejemplo, podríamos hacer uso de lo siguiente para evitar una coma
de arrastre en nuestra lista de ID:
536
537 ```
538 <t t-foreach="record.message_parter_ids.raw_value.slice(0, 3)"
539 t-as="rec">
540 <t t-esc="rec" />
541 <t t-if="!rec_last">;</t>
542 </t>
543 ```
544
545 ### Usando t-if para renderizado condicional
546
547 Nuestra vista kanban utilizó la directiva `t-if` en el menú de opciones
de la tarjeta para hacer que algunas opciones estén disponibles
dependiendo de algunas condiciones. La directiva `t-if` espera que una
expresión se evalúe en JavaScript al representar vistas kanban en el
lado del cliente. La etiqueta y su contenido se renderizan sólo si la
condición se evalúa como verdadera.
548
549 Como otro ejemplo, para mostrar la estimación del esfuerzo de tarea en
la tarjeta kanban, sólo si tiene un valor, agregue lo siguiente después
del campo date_deadline:
550
551 ```
552 <t t-if="record.effort_estimate.raw_value gt 0">
553 <li>Estimate <field name="effort_estimate"/></li>
554 </t>
555 ```
556
557 Utilizamos un elemento `<t t-if="...">` para que si la condición es
falsa, el elemento no produce salida. Si es cierto, sólo el elemento
`<li>` contenido se renderiza a la salida. Observe que la expresión de
condición usó el símbolo `gt` en lugar de `>`, para representar el
operador *mayor que*.
558
559 ### Utilizando t-esc y t-raw para generar valores
560
561 Utilizamos el elemento `<field>` para representar el contenido del
campo. Pero los valores de campo también se pueden presentar
directamente sin una etiqueta `<field>`.
562
Page 14
capitulo-9.md 07/02/2018

563 La directiva `t-esc` evalúa una expresión y la procesa como un valor de


escape HTML, como se muestra a continuación:
564
565 ```
566 <t t-esc="record.message_parter_ids.raw_value" />
567 ```
568
569 En algunos casos, y si se garantiza que los datos de origen son
seguros, `t-raw` se puede utilizar para renderizar el valor crudo de
campo sin ningún escape, como se muestra en el ejemplo siguiente:
570
571 ```
572 <t t-raw="record.message_parter_ids.raw_value" />
573 ```
574
575 #### Tip
576
577 Por razones de seguridad, es importante evitar el uso de `t-raw` tanto
como sea posible. Su uso debe ser estrictamente reservado para la
salida de datos HTML que fue preparado específicamente sin datos de
usuario en él, o donde cualquier usuario de datos se escapó
explícitamente para HTML caracteres especiales.
578
579 ### Usando t-set para establecer valores en variables
580
581 Para una lógica más compleja, podemos almacenar el resultado de una
expresión en una variable para usarla más adelante en la plantilla.
Esto se debe hacer usando la directiva `t-set`, nombrando la variable a
establecer seguida por la directiva `t-value` con la expresión
calculando el valor a asignar.
582
583 Como ejemplo, el siguiente código hace que los plazos perdidos estén en
rojo, como en la sección anterior, pero utiliza una variable
`red_or_black` para usar la clase CSS, como se muestra a continuación:
584
585 ```
586 <t t-set="red_or_black" t-value=" record.date_deadline.raw_value and
587 record.date_deadline.raw_value lte (new Date())
588 ? 'oe_kanban_text_red' : ''" />
589 <li t-att-class="red_or_black">
590 <field name="date_deadline" />
591 </li>
592
593 ```
594 Las variables también se pueden asignar contenido HTML a una variable,
como en el ejemplo siguiente:
595
596 ```
597 <t t-set="calendar_sign">
598 <span class="oe_e">Using t-set to set values on variables
599
600 </span>
601 </t>
602 <t t-raw="calendar_sign" />
603 ```
604
605 La clase `oe_e` CSS utiliza la fuente de pictograma Entypo. La
representación HTML del signo de calendario se almacena en una variable
que puede usarse cuando sea necesario en la plantilla. El conjunto de
iconos de **Fuente Impresionante "Font Awesome"** también está
disponible fuera de la caja, y podría haber sido utilizado.
Page 15
capitulo-9.md 07/02/2018

606
607
608 ### Usando t-call para insertar otras plantillas
609
610 Las plantillas de QWeb pueden ser fragmentos de HTML reutilizables, que
se pueden insertar en otras plantillas. En lugar de repetir los mismos
bloques HTML una y otra vez, podemos diseñar bloques de construcción
para componer vistas de interfaz de usuario más complejas.
611
612 Las plantillas reutilizables se definen dentro de la etiqueta
`<templates>` y se identifican por un elemento superior con un `t-name`
distinto de `kanban-box`. Estas otras plantillas pueden ser incluidas
usando la directiva `t-call`. Esto es cierto para las plantillas
declaradas junto a la misma vista kanban, en otro lugar del mismo
módulo addon o en un addon diferente.
613
614 La lista de avatar del seguidor es algo que podría aislarse en un
fragmento reutilizable. Vamos a volver a trabajar para usar una
sub-plantilla. Debemos comenzar agregando otra plantilla a nuestro
archivo XML, dentro del elemento `<templates>`, después del nodo `<t
t-name="kanban-box">`, como se muestra a continuación:
615
616
617 ```
618 <t t-name="follower_avatars">
619 <div>
620 <t t-foreach="record.message_parter_ids.raw_value.slice(0, 3)"
621 t-as="rec">
622 <img t-att-src="kanban_image('res.partner', 'image_small', rec)"
623 class="oe_avatar" width="24" height="24" />
624 </t>
625 </div>
626 </t>
627 ```
628
629 Llamarlo desde la plantilla `kanban-box` principal es bastante
sencillo. En lugar del elemento `<div>` que contiene el parámetro para
la directiva `for each`, debemos utilizar lo siguiente:
630
631 ```
632 <t t-call="follower_avatars" />
633 ```
634
635 Para llamar a las plantillas definidas en otros módulos addon,
necesitamos usar el identificador completo `module.name`, como lo
hacemos con las otras vistas. Por ejemplo, este fragmento puede
referirse utilizando el identificador completo
`todo_kanban.follower_avatars`.
636
637 La plantilla llamada se ejecuta en el mismo contexto que la persona que
llama, por lo que cualquier nombre de variable disponible en la persona
que llama también está disponible al procesar la plantilla llamada.
638
639 Una alternativa más elegante es pasar argumentos a la plantilla
llamada. Esto se hace mediante el establecimiento de variables dentro
de la etiqueta `t-call`. Éstos se evaluarán y se pondrán a disposición
en el contexto de la sub-plantilla solamente, y no existirán en el
contexto de la persona que llama.
640
641 Podríamos utilizar esto para tener el número máximo de avatares de
seguidores establecidos por la persona que llama en lugar de ser
Page 16
capitulo-9.md 07/02/2018

codificados en la sub-plantilla. Primero, necesitamos reemplazar el


valor fijo, 3 con una variable, `arg_max` por ejemplo:
642
643 ```
644 <t t-name="follower_avatars">
645 <div>
646 <t t-foreach="record.message_parter_ids.raw_value.slice(0, arg_max)"
647 t-as="rec">
648 <img t-att-src="kanban_image('res.partner', 'image_small', rec)"
649 class="oe_avatar" width="24" height="24" />
650 </t>
651 </div>
652 </t>
653 ```
654
655 A continuación, define el valor de esa variable al realizar la llamada
de sub-plantilla de la siguiente manera:
656
657 ```
658 <t t-call="follower_avatars">
659 <t t-set="arg_max" t-value="3" />
660 </t>
661 ```
662
663 El contenido completo dentro del elemento `t-call` también está
disponible para la sub-plantilla a través de la variable mágica `0`. En
lugar de variables de argumento, podemos definir un fragmento de código
HTML que se puede usar en la sub-plantilla con `<t t-raw="0" /> `.
664
665
666 ### Más formas de utilizar t-attf
667
668 Hemos pasado por las directivas QWeb más importantes, pero hay algunas
más que deberíamos tener en cuenta. Vamos a hacer una breve explicación
de ellos.
669
670 Hemos visto los atributos de etiqueta dinámica del estilo `t-att-NAME`
y `t-attf-NAME`. Además, se puede utilizar la directiva `t-att` fija.
Acepta un mapeo de diccionario de clave-valor o un par (una lista de
dos elementos).
671
672 Utiliza la siguiente asignación:
673
674 ```
675 <p t-att="{'class': 'oe_bold', 'name': 'test1'}" />
676
677
678 ```
679
680 Esto da como resultado lo siguiente:
681
682 ```
683 <p class="oe_bold" name="test1" />
684 ```
685
686 Utiliza el siguiente par:
687
688 ```
689 <p t-att="['class', 'oe_bold']" />
690 ```
691
Page 17
capitulo-9.md 07/02/2018

692 Esto da como resultado lo siguiente:


693
694 ```
695 <p class="oe_bold" />
696 ```
697
698 ## Herencia en vistas kanban
699
700 Las plantillas utilizadas en las vistas e informes de kanban se amplían
utilizando las técnicas habituales utilizadas para otras vistas, por
ejemplo, mediante expresiones XPath. Consulta el Capítulo 3, *Herencia
- Ampliando las aplicaciones existentes*, para obtener más detalles.
701
702 Un caso común es usar los elementos `<field>` como selector, para luego
agregar otros elementos antes o después de ellos. En el caso de las
vistas kanban, el mismo campo se puede declarar más de una vez, por
ejemplo, una vez antes de las plantillas, y de nuevo dentro de las
plantillas. Aquí el selector coincidirá con el primer elemento de campo
y no agregará nuestra modificación dentro de la plantilla, como se
pretende.
703
704 Para evitar esto, necesitamos usar expresiones XPath para asegurarse de
que el campo dentro de la plantilla es el que coincide. Por ejemplo:
705
706 ```
707 <record id="res_partner_kanban_inherit" model="ir.ui.view">
708 <field name="name">Contact Kanban modification</field>
709 <field name="model">res.partner</field>
710 <field name="inherit_id" ref="base.res_partner_kanban_view" />
711 <field name="arch" type="xml">
712
713 <xpath expr="//t[@t-name='kanban- box']//field[@name='display_name']"
714
715
716
717
718 position="before">
719 <span>Name:</span>
720 </xpath>
721 </field>
722 </record>
723 ```
724
725 En el ejemplo anterior, XPath busca un elemento `<field
name="display_name">` dentro de un elemento `<t tname="kanban-box">`.
Esto elimina el mismo elemento de campo fuera de la sección
`<templates>`.
726
727 Para estas expresiones XPath más complejas, podemos explorar la
sintaxis correcta usando algunas herramientas de línea de comandos. La
utilidad de línea de comandos `xmllint` probablemente ya esté
disponible en su sistema Linux y tiene una opción `--xpath` para
realizar consultas en archivos XML.
728
729 Otra opción, que proporciona resultados más agradables, es el comando
`xpath` del paquete Debian/Ubuntu `libxml-xpath-perl`:
730
731 ```
732
733 $ sudo apt-get install libxml-xpath-perl
734
Page 18
capitulo-9.md 07/02/2018

735
736
737
738
739 $ xpath -e "//record[@id='res_partner_kanban_view']" -e
"//field[@name='display_name']]" /path/to/*.xml
740
741
742 ```
743
744 ## Activos personalizados CSS y JavaScript
745
746 Como hemos visto, las vistas kanban son en su mayoría HTML y hacen uso
intensivo de las clases CSS. Hemos introducido algunas clases de CSS
utilizadas con frecuencia por el producto estándar. Pero para obtener
mejores resultados, los módulos también pueden agregar su propio CSS.
747
748 No vamos a entrar en detalles aquí sobre cómo escribir código CSS, pero
es relevante para explicar cómo un módulo puede agregar sus propios
activos web CSS (y JavaScript). Los activos Odoo para el backend se
declaran en la plantilla `assets_backend`. Para agregar nuestros
activos de módulo, debemos ampliar esa plantilla. El archivo XML para
esto generalmente se coloca dentro de un subdirectorio `views/module`.
749
750 A continuación se muestra un archivo XML de ejemplo para agregar un
archivo CSS y JavaScript al módulo `todo_kanban`, y podría estar en
`todo_kanban/views/todo_kanban_assets.xml`:
751
752 ```
753 <?xml version="1.0" encoding="utf-8"?>
754 <odoo>
755 <template id="assets_backend" inherit_id="web.assets_backend"
756 name="Todo Kanban Assets" >
757 <xpath expr="." position="inside">
758 <link rel="stylesheet"
759 href="/todo_kanban/static/src/css/todo_kanban.css"/>
760 <script type="text/javascript"
761 src="/todo_kanban/static/src/js/todo_kanban.js">
762 </script>
763 </xpath>
764 </template>
765 </odoo>
766 ```
767 Como de costumbre, se debe hacer referencia en el archivo descriptor
`__manifest__.py`. Observa que los activos se encuentran dentro de un
subdirectorio `/static/src`. Aunque esto no es necesario, es una
convención generalmente usada.
768
769 ## Resúmen
770
771 Aprendiste acerca de los tableros kanban y cómo construir las vistas
kanban para implementarlas. También introdujimos el modelo QWeb y cómo
se puede utilizar para diseñar tarjetas kanban. QWeb es también el
motor de renderizado que potencia el sitio web CMS, por lo que está
creciendo en importancia en el conjunto de herramientas de Odoo. En el
próximo capítulo, seguiremos usando QWeb, pero en el lado del servidor,
para crear nuestros informes personalizados.
772
773

Page 19
capitulo-10.md 07/02/2018

1 # Capítulo 10. Creando reportes de QWeb


2
3 Los informes son una característica invaluable para las aplicaciones
empresariales. El motor incorporado de los informes de QWeb, disponible
desde la versión 8.0, es el motor de informe por defecto. Los informes
se diseñan utilizando plantillas QWeb para producir documentos HTML que
luego se pueden convertir en formato PDF.
4
5 Los motores de informe incorporados de Odoo han experimentado cambios
significativos. Antes de que los informes de la versión 7.0 estuvieran
basados en la biblioteca ReportLab y utilizaran una sintaxis de marcado
específico, RML. En la versión 7.0, el motor de informes Webkit se
incluyó en el núcleo, permitiendo que los informes se diseñen
utilizando HTML normal. Finalmente, en la versión 8.0 este concepto fue
tomado un poco más, y las plantillas de QWeb se convirtieron en el
concepto principal detrás del motor de informes incorporado.
6
7 Esto significa que podemos aprovechar convenientemente lo que hemos
aprendido sobre QWeb y aplicarlo para crear informes de negocios. En
este capítulo, estaremos agregando un informe a nuestra aplicación de
**Tareas Pendientes "To Do"** y revisaremos las técnicas más
importantes que se utilizarán con los informes de QWeb, incluidos los
cálculos de informes, como los totales, los formatos de traducción y de
impresión de papel.
8
9 Pero antes de empezar, debemos asegurarnos de que hemos instalado la
versión recomendada de la utilidad utilizada para convertir HTML en
documentos PDF.
10
11
12 ## Instalando wkhtmltopdf
13
14 Para generar correctamente los informes, es necesario instalar la
versión recomendada de la biblioteca `wkhtmltopdf. Su nombre significa
**Webkit HTML a PDF**. Odoo lo usa para convertir una página HTML
renderizada en un documento PDF.
15
16 Se sabe que las versiones anteriores de la biblioteca `wkhtmltopdf`
tienen problemas, como no imprimir encabezados y pie de página, por lo
que debemos ser exigentes con la versión que se va a usar. Para la
versión 9.0, en el momento de escribir la versión recomendada es
0.12.1. Desafortunadamente, las probabilidades son que la versión
empaquetada proporcionada para su sistema host, Debian/Ubuntu u otro,
no sea adecuada. Por lo tanto, debemos descargar e instalar el paquete
recomendado para nuestro SO y la arquitectura del CPU. Los enlaces de
descarga se pueden encontrar en http://wkhtmltopdf.org o
http://download.gna.org/wkhtmltopdf.
17
18 Primero debemos asegurarnos de que no tenemos una versión incorrecta ya
instalada en nuestro sistema:
19
20 ```
21 $ wkhtmltopdf --version
22
23 ```
24
25 Si lo anterior informa una versión diferente a la que queremos, debemos
desinstalarlo. En un sistema Debian/Ubuntu podemos usar:
26
27 ```
28 $ sudo apt-get remove --purge wkhtmltopdf
Page 1
capitulo-10.md 07/02/2018

29 ```
30
31 a continuación necesitamos descargar el paquete apropiado para nuestro
sistema e instalarlo. Comprueba el nombre de archivo correcto en
http://download.gna.org/wkhtmltopdf/0.12/0.12.1. Para Ubuntu 14.04 LTS
(Trusty) 64 bits, el comando de descarga sería así:
32
33 ```
34 $ wget
http://download.gna.org/wkhtmltopdf/0.12/0.12.1/wkhtmltox-0.12.1_linux-tr
usty-amd64.deb -O /tmp/wkhtml.deb
35 ```
36
37 Luego debemos instalarlo. La instalación de un archivo `deb` local no
instala automáticamente las dependencias, por lo que se necesitará un
segundo paso para hacerlo y completar la instalación:
38
39 ```
40
41 $ sudo dpkg -i wkhtml.deb
42
43
44
45
46
47 $ sudo apt-get -f install
48
49 ```
50
51 Ahora podemos comprobar si la biblioteca `wkhtmltopdf` está
correctamente instalada y confirmar que es el número de versión que
queremos:
52
53
54 ```
55
56 $ wkhtmltopdf --version
57
58
59
60
61
62 wkhtmltopdf 0.12.1 (with patched qt)
63
64 ```
65
66 Después de esto, la secuencia de inicio del servidor Odoo no mostrará
el mensaje de información **Necesitas un archivo Wkhtmltopdf para
imprimir una versión en pdf del reporte "You need Wkhtmltopdf to print
a pdf version of the report's"**.
67
68 ## Creando informes empresariales
69
70 Por lo general, implementaríamos el informe en nuestro módulo de
complemento de la aplicación de tareas pendientes. Pero para propósitos
de aprendizaje, crearemos un nuevo módulo addon solo para nuestro
informe.
71
72 Nuestro informe se verá así:
73
74 ![Report](file:img/10-01.jpg)
Page 2
capitulo-10.md 07/02/2018

75
76 Nombraremos este nuevo módulo addon `todo_report`. Lo primero que debe
hacer es crear un archivo `__init__.py` vacío y el archivo de
manifiesto `__manifest__.py`:
77
78 ```
79 {
80 'name': 'To-Do Report',
81 'description': 'Report for To-Do tasks.',
82 'author': 'Daniel Reis',
83 'depends': ['todo_kanban'],
84 'data': ['reports/todo_report.xml'] }
85
86 ```
87
88 El archivo `reports/todo_report.xml` puede comenzar declarando el nuevo
informe de la siguiente manera:
89
90 ```
91 <?xml version="1.0"?>
92 <odoo>
93 <report id="action_todo_task_report"
94 string="To-do Tasks"
95 model="todo.task"
96 report_type="qweb-pdf"
97 name="todo_report.report_todo_task_template"
98 />
99 </odoo>
100
101 ```
102
103 La etiqueta `<report>` es un acceso directo para escribir datos al
modelo `ir.actions.report.xml`, que es un tipo particular de acción del
cliente. Sus datos están disponibles en las opciones de menú
**Configuración | Técnico | Informes " Settings | Technical | Reports
"**.
104
105 #### Tip
106
107 Durante el diseño del informe, es posible que prefieras dejar
`report_type="qweb-html"`, y cambiarlo de nuevo al archivo `qweb-pdf`
una vez terminado. Esto hará que sea más rápido generar y más fácil
inspeccionar el resultado HTML de la plantilla de OWeb.
108
109 Después de instalarlo, la vista de formulario de tareas pendientes
mostrará un botón **Imprimir "Print"** en la parte superior, a la
izquierda del botón **Más "More"**, que contiene esta opción para
ejecutar el informe.
110
111 No funcionará en este momento, ya que todavía no hemos definido el
informe. Este será un informe QWeb, por lo que utilizará una plantilla
QWeb. El atributo `name` identifica la plantilla que se va a utilizar.
A diferencia de otras referencias de identificador, se requiere el
prefijo del módulo en el atributo `name`. Debemos usar la referencia
completa `<module_name>.<identifier_name>`.
112
113 ## Plantillas de informes QWeb
114
115 Los informes generalmente seguirán un esqueleto básico, como se muestra
a continuación. Esto se puede agregar al archivo
`reports/todo_report.xml`, justo después del elemento `<report>`.
Page 3
capitulo-10.md 07/02/2018

116
117 ```
118 <template id="report_todo_task_template">
119 <t t-call="report.html_container">
120 <t t-call="report.external_layout">
121 <div class="page">
122 <!-- Report page content -->
123 </div>
124 </t>
125 </t>
126 </template>
127 ```
128
129 Los elementos más importantes aquí son las directivas `t-call` que
utilizan estructuras de informe estándar. La plantilla
report.html_container realiza la configuración básica para admitir un
documento HTML. La plantilla `report.external_layout` maneja el
encabezado y pie de página del informe, utilizando la configuración
correspondiente de la empresa adecuada. Como alternativa, podemos usar
la plantilla `report.internal_layout`, que utiliza sólo un encabezado
básico.
130
131 Ahora tenemos en su lugar, el esqueleto básico para nuestro módulo y
vista de informe. Ten en cuenta que, dado que los informes son sólo
plantillas QWeb, se puede aplicar la herencia, al igual que en las
otras vistas. Las plantillas QWeb utilizadas en los informes se pueden
ampliar utilizando las vistas heredadas normales con expresiones
**XPATH**.
132
133 ## Presentando datos en informes
134
135 A diferencia de las vistas Kanban, las plantillas de QWeb en los
informes se representan en el lado del servidor y utilizan una
implementación Python QWeb. Podemos ver esto como dos implementaciones
de la misma especificación, y hay algunas diferencias que necesitamos
tener en cuenta.
136
137 Para empezar, las expresiones QWeb se evalúan utilizando la sintaxis de
Python, no JavaScript. Para las expresiones más simples, puede haber
poca o ninguna diferencia, pero las operaciones más complejas
probablemente serán diferentes.
138
139 La forma en que se evalúan las expresiones también es diferente. Para
los informes, tenemos disponibles las siguientes variables:
140
141 + `docs` es una colección iterable con los registros a imprimir
142 + `doc_ids es una lista de los IDs de los registros a imprimir
143 + `doc_model` identifica el modelo de los registros, `todo.task` por
ejemplo
144 + `time is` es una referencia a la biblioteca de tiempo de Python
145 + `user` es el registro para el usuario que ejecuta el informe
146 + `res_company` es el registro de la empresa del usuario actual
147
148 El contenido del informe está escrito en HTML, los valores de los
campos pueden referenciarse mediante el atributo `t-field` y puede
complementarse con el atributo `t-field-options` para utilizar un
widget específico para representar el contenido del campo.
149
150 Ahora podemos empezar a diseñar el contenido de la página de nuestro
informe:
151
Page 4
capitulo-10.md 07/02/2018

152 ```
153 <!-- Report page content
154 <div class="row bg-primary">
155 <div class="col-xs-3">
156 <span class="glyphicon glyphicon-pushpin" />
157 What
158 </div>
159 <div class="col-xs-2">Who</div>
160 <div class="col-xs-1">When</div>
161 <div class="col-xs-3">Where</div>
162 <div class="col-xs-3">Watchers</div>
163 </div>
164
165 <t t-foreach="docs" t-as="o">
166 <div class="row">
167
168 <!-- Data Row Content -->
169
170
171
172
173 </div>
174 </t>
175
176 ```
177
178 El diseño del contenido puede utilizar el sistema de cuadrícula HTML de
Bootstrap de Twitter. En pocas palabras, Bootstrap tiene un diseño de
cuadrícula con 12 columnas. Se puede añadir una nueva fila usando `<div
class="row">`. Dentro de una fila, tenemos celdas, cada una de ellas
abarcando un cierto número de columnas, que debe ocupar las 12
columnas. Cada celda se puede definir con la fila `<div
class="col-xs-N">`, donde N es el número de columnas que abarca.
179
180 #### Nota
181
182 Una referencia completa para Bootstrap, que describe estos y otros
elementos de estilo, se puede encontrar en http://getbootstrap.com.
183
184 Aquí estamos agregando una fila de encabezado con títulos, y luego
tenemos un bucle `t-foreach`, iterando a través de cada registro, y
representando una fila para cada uno.
185
186 Dado que la renderización se realiza en el lado del servidor, los
registros son objetos y podemos usar la notación de puntos para acceder
a los campos de los registros de datos relacionados. Esto hace que sea
fácil de seguir a través de campos relacionales para acceder a sus
datos. Ten en cuenta que esto no es posible en el Qweb renderado del
lado del cliente, vistas, como las vistas kanban del cliente web.
187
188 Este es el XML para el contenido de las filas de registros:
189
190 ```
191 <div class="col-xs-3">
192 <h4><span t-field="o.name" /></h4>
193 </div>
194 <div class="col-xs-2">
195 <span t-field="o.user_id" />
196 </div>
197 <div class="col-xs-1">
198 <span t-field="o.date_deadline" />
Page 5
capitulo-10.md 07/02/2018

199 <span t-field="o.amount_cost"


200 t-field-options='{
201 "widget": "monetary",
202 "display_currency": "o.currency_id"}'/>
203 </div>
204 <div class="col-xs-3">
205 <div t-field="res_company.partner_id"
206 t-field-options='{
207 "widget": "contact",
208 "fields": ["address", "name", "phone", "fax"],
209 "no_marker": true}' />
210 </div>
211 <div class="col-xs-3">
212 <!-- Render followers -->
213 </div>
214 ```
215
216 Como podemos ver, los campos se pueden utilizar con opciones
adicionales. Estos son muy similares al atributo `options` utilizado en
las vistas de formulario, como se muestra en el Capítulo 6, *Vistas -
Diseñando la interfaz de usuario*, utilizado con un `widget` adicional
para configurar el widget para que se utilice para representar el campo.
217
218 Un ejemplo es el widget monetario, usado arriba, al lado de la fecha
límite.
219
220 Un ejemplo más sofisticado es el widget `contact`, utilizado para dar
formato a las direcciones. Utilizamos la dirección de la empresa,
`res_company.partner_id`, ya que tiene algunos datos predeterminados y
podemos ver inmediatamente la dirección procesada. Pero tendría más
sentido usar la dirección del usuario asignado, `o.user_id.partner_id`.
De forma predeterminada, el widget `contact` muestra direcciones con
algunos pictogramas, como un icono de teléfono. La opción
`no_marker="true"` que usamos los deshabilita.
221
222 ## Renderizando imágenes
223
224 La última columna de nuestro informe contará con la lista de
seguidores, con los avatares. Usaremos el componente Bootstrap
`media-list` y un bucle a través de los seguidores para renderizar cada
uno de ellos:
225
226 ```
227 <!-- Render followers -->
228 <ul class="media-list">
229 <t t-foreach="o.message_follower_ids" t-as="f">
230 <li t-if="f.partner_id.image_small"
231 class="media-left">
232 <img class="media-object"
233 t-att-src="'data:image/png;base64,%s' %
234 f.partner_id.image_small"
235 style="max-height: 24px;" />
236 <span class="media-body"
237 t-field="f.partner_id.name" />
238 </li>
239 </t>
240 </ul>
241 ```
242 El contenido de los campos binarios se proporciona en una
representación `base64`. El elemento `<img>` puede aceptar directamente
este tipo de datos para el atributo `src`. Así podemos utilizar la
Page 6
capitulo-10.md 07/02/2018

directiva Qweb `t-att-src` para generar dinámicamente cada una de las


imágenes.
243
244 ## Total de resumen y totales corrientes
245
246 Una necesidad común en los informes es proporcionar totales. Esto se
puede hacer usando expresiones de Python para calcular esos totales.
247
248 Después de la etiqueta de cierre de `<t t-foreach>`, añadiremos una
fila final con los totales:
249
250 ```
251 <!-- Totals -->
252 <div class="row">
253 <div class="col-xs-3">
254 Count: <t t-esc="len(docs)" />
255 </div>
256 <div class="col-xs-2" />
257 <div class="col-xs-1">
258 Total:
259 <t t-esc="sum([o.amount_cost for o in docs])" />
260 </div>
261 <div class="col-xs-3" />
262 <div class="col-xs-3" />
263 </div>
264 ```
265
266 La instrucción Python `len()` se utiliza para contar el número de
elementos de una colección. Los totales se pueden calcular utilizando
el valor `sum()` sobre una lista de valores. En el ejemplo anterior,
utilizamos una comprensión de lista para producir una lista de valores
fuera del conjunto de registros `docs`. Puedes pensar en la lista de
comprensiones como un bucle `for` incorporado.
267
268 A veces queremos realizar algunos cálculos a medida que vamos junto con
el informe. Por ejemplo, un total de ejecución, con el total hasta el
registro actual. Esto se puede implementar con t-set para definir una
variable de acumulación y luego actualizarla en cada fila.
269
270 Para ilustrar esto, podemos calcular el número acumulado de seguidores.
Debemos empezar por inicializar la variable, justo antes del bucle
`t-foreach` en el conjunto de registros `docs`, usando:
271
272 ```
273 <t t-set="follower_count" t-value="0" />
274 ```
275 Y luego, dentro del bucle, añade el número de seguidores del registro a
la variable. Elegiremos hacerlo justo después de presentar la lista de
seguidores, e imprimiremos también el total actual en cada línea:
276
277 ```
278 <!-- Running total-->
279 <t t-set="follower_count"
280 t-value="follower_count + len(o.message_follower_ids)" />
281 Accumulated # <t t-esc="follower_count" />
282
283 ```
284
285 ## Definiendo formatos de papel
286 En este punto nuestro informe se ve bien en HTML, pero no se imprime
bien en una página PDF. Podríamos obtener mejores resultados usando una
Page 7
capitulo-10.md 07/02/2018

página horizontal. Así que tenemos que añadir este formato de papel.
287
288 En la parte superior del archivo XML, agregua este registro:
289
290 ```
291 <record id="paperformat_euro_landscape"
292 model="report.paperformat">
293 <field name="name">European A4 Landscape</field>
294 <field name="default" eval="True" />
295 <field name="format">A4</field>
296 <field name="page_height">0</field>
297 <field name="page_width">0</field>
298 <field name="orientation">Landscape</field>
299 <field name="margin_top">40</field>
300 <field name="margin_bottom">23</field>
301 <field name="margin_left">7</field>
302 <field name="margin_right">7</field>
303 <field name="header_line" eval="False" />
304 <field name="header_spacing">35</field>
305 <field name="dpi">90</field>
306 </record>
307 ```
308
309 Es una copia del formato A4 europeo, definido en el archivo
`report_paperformat.xml`, efinido en `addons/report/data`, pero
cambiando la orientación de Vertical a Horizontal. Los formatos de
papel definidos se pueden ver desde el cliente web a través del menú
**Configuración | Técnico | Informes | Formato del papel "Settings |
Technical | Reports | Paper Format"**.
310
311 Ahora podemos usarlo en nuestro informe. El formato de papel
predeterminado se define en la configuración de la empresa, pero
también podemos especificar el formato de papel que utilizará un
informe específico. Esto se hace utilizando un atributo `paperfomat` en
la acción del informe.
312
313 Vamos a editar la acción utilizada para abrir nuestro informe, para
agregar este atributo:
314
315 ```
316 <report id="action_todo_task_report"
317 string="To-do Tasks"
318 model="todo.task"
319 report_type="qweb-pdf"
320 name="todo_report.report_todo_task_template"
321 paperformat="paperformat_euro_landscape"
322 />
323 ```
324 #### Tip
325
326 El atributo `paperformat` en la etiqueta `<report>` se agregó en la
versión 9.0. Para 8.0 necesitamos usar un elemento `<record>` para
agregar una acción de informe con un valor `paperformat`.
327
328 ## Habilitando la traducción de idiomas en los informes
329
330 Para habilitar las traducciones de un informe, debe ser llamado desde
una plantilla, usando un elemento `<t t-call>` con un atributo `t-lang`.
331
332 El atributo `t-lang` debe evaluarse a un código de idioma, como `es` o
`en_US`. Necesita el nombre del campo donde se puede encontrar el
Page 8
capitulo-10.md 07/02/2018

idioma a utilizar. Este será con frecuencia el idioma del socio al que
se enviará el documento, almacenado en el campo `partner_id.lang`. En
nuestro caso, no tenemos un campo de socio, pero podemos utilizar el
usuario responsable, y la preferencia de idioma correspondiente está en
`user_id.lang`.
333
334 La función espera un nombre de plantilla, y lo procesará y traducirá.
Esto significa que necesitamos definir el contenido de la página de
nuestro informe en una plantilla separada, como se muestra a
continuación:
335
336 ```
337 <report id="action_todo_task_report_translated"
338 string="Translated To-do Tasks"
339 model="todo.task"
340 report_type="qweb-pdf"
341 name="todo_report.report_todo_task_translated"
342 paperformat="paperformat_euro_landscape"
343 />
344
345 <template id="report_todo_task_translated">
346 <t t-call="todo_report.report_todo_task_template"
347 t-lang="user.lang" >
348 <t t-set="docs"
349 t-value="docs" />
350 </t>
351 </t>
352 </template>
353
354 ```
355
356 ## Informes basados en SQL personalizado
357
358 El informe que construimos se basó en un conjunto de registros
regulares. Pero en algunos casos necesitamos transformar o agregar
datos de una manera que no sea fácil al procesar datos sobre la marcha,
como al procesar el informe.
359
360 Un enfoque para esto es escribir una consulta SQL para construir el
conjunto de datos que necesitamos, exponer esos resultados a través de
un modelo especial y tener nuestro trabajo de informe basado en un
conjunto de registros.
361
362 Para ello, crearemos un archivo `reports/todo_task_report.py` con este
código:
363
364 ```
365 # -*- coding: utf-8 -*-
366 from odoo import models, fields
367
368 class TodoReport(models.Model):
369 _name = 'todo.task.report'
370 _description = 'To-do Report'
371 _sql = """
372 CREATE OR REPLACE VIEW todo_task_report AS
373 SELECT *
374 FROM todo_task
375 WHERE active = True
376 """
377 name = fields.Char('Description')
378 is_done = fields.Boolean('Done?')
Page 9
capitulo-10.md 07/02/2018

379 active = fields.Boolean('Active?')


380 user_id = fields.Many2one('res.users', 'Responsible')
381 date_deadline = fields.Date('Deadline')
382
383
384
385 ```
386
387 Para que este archivo se cargue necesitamos agregar una línea `from .
import reports` a la parte superior del archivo `__init__.py` y `from .
import todo_task_report` al archivo `reports/__init__.py`.
388
389 El atributo `sql` se utiliza para anular la creación automática de la
tabla de la base de datos, proporcionando un SQL para eso. Queremos que
cree una vista de base de datos para proporcionar los datos necesarios
para el informe. Nuestra consulta SQL es bastante simple, pero el punto
es que podríamos usar cualquier consulta SQL válida para nuestra vista.
390
391 También mapeamos los campos que necesitamos con tipos de campo ORM,
para que estén disponibles en conjuntos de registros generados en este
modelo.
392
393 A continuación, podemos agregar un nuevo informe basado en este modelo,
`reports/todo_model_report.xml`:
394
395 ```
396 <odoo>
397
398 <report id="action_todo_model_report"
399 string="To-do Special Report"
400 model="todo.task"
401 report_type="qweb-html"
402 name="todo_report.report_todo_task_special"
403 />
404
405 <template id="report_todo_task_special">
406 <t t-call="report.html_container">
407 <t t-call="report.external_layout">
408 <div class="page">
409
410 <!-- Report page content -->
411 <table class="table table-striped">
412 <tr>
413 <th>Title</th>
414 <th>Owner</th>
415 <th>Deadline</th>
416 </tr>
417 <t t-foreach="docs" t-as="o">
418 <tr>
419 <td class="col-xs-6">
420 <span t-field="o.name" />
421 </td>
422 <td class="col-xs-3">
423 <span t-field="o.user_id" />
424 </td>
425 <td class="col-xs-3">
426 <span t-field="o.date_deadline" />
427 </td>
428 </tr>
429 </t>
430 </table>
Page 10
capitulo-10.md 07/02/2018

431
432 </div>
433 </t>
434 </t>
435 </template>
436
437 </odoo>
438
439 ```
440
441 Para casos aún más complejos, podemos utilizar una solución diferente:
un asistente. Para esto debemos crear un modelo transitorio con líneas
relacionadas, donde el encabezado incluya parámetros de informe,
introducidos por el usuario, y las líneas tendrán los datos generados
que usará el informe. Estas líneas se generan mediante un método modelo
que puede contener cualquier lógica que podamos necesitar. Se
recomienda encarecidamente inspirarse en un informe similar existente.
442
443 ## Resúmen
444
445 En el capítulo anterior aprendimos sobre QWeb y cómo usarlo para
diseñar una vista Kanban. En este capítulo aprendimos sobre el motor de
informes QWeb y sobre las técnicas más importantes para generar
informes con el lenguaje de plantillas QWeb.
446
447 En el próximo capítulo, seguiremos trabajando con QWeb, esta vez para
construir páginas web. También aprenderemos a escribir controladores
web, proporcionando funciones más completas a nuestras páginas web.

Page 11
capitulo-11.md 07/02/2018

1 # Capítulo 11. Creación de características Frontend de un website


2
3 Odoo comenzó como un sistema backend, pero la necesidad de una interfaz
de frontend pronto se sintió. Las primeras características del portal,
basadas en la misma interfaz que el backend, no eran muy flexibles ni
amigables con dispositivos móviles.
4
5 Para solucionar este vacío, la versión 8 introdujo nuevas
características de sitio web, agregando un Sistema de Gestión de
Contenidos (CMS) al producto. Esto nos permitiría construir frontends
hermosos y eficaces sin la necesidad de integrar un CMS de terceros.
6
7 Aquí aprenderemos cómo desarrollar nuestros propios módulos
complementarios orientados al usuario, aprovechando la función de sitio
web proporcionada por Odoo.
8
9 ## Mapa de ruta
10
11 Crearemos una página web en la que se enumerarán nuestras Tareas
pendientes, lo que nos permitirá navegar a una página detallada para
cada tarea existente. También queremos poder proponer nuevas Tareas
pendientes a través de un formulario web.
12
13 Con esto podremos cubrir las técnicas esenciales para el desarrollo de
sitios web: crear páginas dinámicas, pasar parámetros a otra página,
crear formularios y manejar su validación y lógica de cálculo.
14
15 Pero primero, introduciremos los conceptos básicos del sitio web con
una página web **Hola Mundo** muy sencilla.
16
17 ## Nuestra primera página web
18
19 Vamos a crear un módulo de complemento para las características de
nuestro sitio web. Podemos llamarlo `todo_website`. Para introducir los
fundamentos del desarrollo web de Odoo, implementaremos una sencilla
página web **Hola Mundo**. Imaginativo, ¿verdad?
20
21 Como de costumbre, comenzaremos a crear su archivo de manifiesto. Crea
el archivo `todo_website/__ manifest__.py` con:
22
23 ```
24 {
25 'name': 'To-Do Website',
26 'description': 'To-Do Tasks Website',
27 'author': 'Daniel Reis',
28 'depends': ['todo_kanban']}
29 ```
30 Estamos construyendo sobre el módulo todo_kanban, de modo que tengamos
todas las características disponibles añadidas al modelo de Tareas
pendientes a lo largo del libro.
31
32 Tenga en cuenta que ahora mismo no dependemos del módulo `website`.
Aunque `website` ofrece un marco útil para crear sitios web con todas
las funciones, las capacidades básicas de la Web se incorporan en el
núcleo del framework. Vamos a explorarlos.
33
34 ## ¡Hola Mundo!
35
36 Para proporcionar nuestra primera página web, agregaremos un objeto
controlador. Podemos empezar por tener su archivo importado con el
módulo:
Page 1
capitulo-11.md 07/02/2018

37
38 Primero agrega un archivo `todo_website/__ init__.py` con la siguiente
línea:
39
40 ```
41 from . import controllers
42 ```
43 A continuación, agregue un archivo `todo_website/controllers/__
init__.py` con la siguiente línea:
44
45 ```
46 from . import main
47 ```
48 Ahora agregue el archivo como tal para el controlador,
`todo_website/controllers/main.py`, con el siguiente código:
49
50 ```
51 # -*- coding: utf-8 -*-
52 from odoo import http
53
54 class Todo(http.Controller):
55
56 @http.route('/helloworld', auth='public')
57 def hello_world(self):
58 return('<h1>Hello World!</h1>')
59 ```
60
61
62 El módulo `odoo.http` proporciona las funciones web relacionadas con
Odoo. Nuestros controladores, responsables de la representación de
páginas, deben ser objetos heredados de la clase
`odoo.http.Controller`. El nombre real utilizado para la clase no es
importante; Aquí elegimos utilizar `Main`.
63
64 Dentro de la clase del controlador tenemos métodos, que coinciden
rutas, hace algún procesamiento, y luego devuelve un resultado; La
página que se mostrará al usuario.
65
66 El decorador `odoo.http.route` es lo que une un método a una ruta de
URL. Nuestro ejemplo utiliza la ruta `/hello`. Navegue hasta
http://localhost:8069/hello y recibirá un mensaje de `Hello World`. En
este ejemplo, el procesamiento realizado por el método es bastante
trivial: simplemente devuelve una cadena de texto con el marcado HTML
para el mensaje Hello World.
67
68 Probablemente haya notado que hemos añadido el argumento auth =
'public' a la ruta. Esto es necesario para que la página esté
disponible para usuarios no autenticados. Si lo eliminamos, sólo los
usuarios autenticados pueden ver la página. Si no hay sesión activa, en
su lugar se mostrará la pantalla de inicio de sesión.
69
70 ## Hola Mundo! Con una plantilla Qweb
71
72 El uso de cadenas de Python para construir HTML se tornará aburrido muy
rápido. Las plantillas QWeb hacen un trabajo mucho mejor en eso. Así
que vamos a mejorar nuestra página web de **Hola Mundo** para usar una
plantilla en su lugar.
73
74 Las plantillas de QWeb se agregan a través de archivos de datos XML, y
técnicamente son un tipo de vista, junto con vistas de formulario o
árbol. En realidad se almacenan en el mismo modelo, ir.ui.view.
Page 2
capitulo-11.md 07/02/2018

75
76 Como de costumbre, los archivos de datos que se cargan deben declararse
en el archivo de manifiesto, por lo que debes editar el archivo
`todo_website/__ manifest__.py` para agregar la clave:
77
78 ```
79 'data': ['views/todo_templates.xml'],
80 ```
81 A continuación, agrega el archivo de data, `views/todo_web.xml, con el
siguiente contenido:
82
83 ```
84 <odoo>
85 <template id="hello" name="Hello Template">
86 <h1>Hello World !</h1>
87 </template>
88 </odoo>
89 ```
90
91 ### Nota
92
93
94 > El elemento `<template>` es en realidad un acceso directo para
declarar un `<record>` para el modelo `ir.ui.view`, usando `type =
"qweb"`, y una plantilla `<t>` dentro de él.
95
96 Ahora necesitamos que nuestro método de controlador use esta plantilla:
97
98 ```
99 from odoo.http import request
100 # ...
101 @http.route('/hello', auth='public')
102 def hello(self, **kwargs):
103 return request.render('todo_website.hello')
104 ```
105 La representación de la plantilla se proporciona mediante `request`, a
través de su función `render()`.
106
107 ### Consejo
108
109 > Observa que agregamos `**kwargs` a los argumentos del método. Con
esto, si cualquier parámetro adicional proporcionado por la petición
HTTP, como una consulta de cadenas de texto o parámetros POST, puede
ser capturado por el diccionario kwargs. Esto hace que nuestro método
sea más robusto, ya que proporcionar parámetros inesperados no causará
errores.
110
111 ## Ampliación de funciones web
112
113 Extensibilidad es algo que esperamos en todas las características de
Odoo, y las características de la web no son una excepción. Y de
hecho podemos extender los controladores y plantillas existentes. Por
ejemplo, extenderemos nuestra página web de **Hola Mundo** para que
tome un parámetro con el nombre para saludar: usando la
`URL/hello?name=John` devolvería un saludo **Hello John!** .
114
115 La extensión se hace generalmente desde un módulo distinto, pero
funciona de igual manera dentro del mismo módulo. Para mantener las
cosas concisas y sencillas, lo haremos sin crear un nuevo módulo.
116
117 Vamos añadir un nuevo archivo `todo_website/controllers/extend.py` con
Page 3
capitulo-11.md 07/02/2018

el siguiente código:
118
119 ```
120 # -*- coding: utf-8 -*-
121 from odoo import http
122 from odoo.addons.todo_website.controllers.main import Todo
123
124 class TodoExtended(Todo):
125 @http.route()
126 def hello(self, name=None, **kwargs):
127 response = super(TodoExtended, self).hello()
128 response.qcontext['name'] = name
129 return response
130 ```
131 Aquí podemos ver lo que necesitamos hacer para extender un controlador.
132
133 Primero usamos una importación de Python para obtener una referencia a
la clase de controlador que queremos extender. Comparados con los
modelos, tienen un registro central, proporcionado por el objeto `env`,
donde se puede obtener una referencia a cualquier clase de modelo, sin
necesidad de conocer el módulo y el archivo que los implementa. Con los
controladores no tenemos eso, y necesitamos conocer el módulo y el
archivo que implementa el controlador que queremos ampliar.
134
135 A continuación tenemos que (re)definir el método desde el controlador
que se está extendiendo. Tiene que estar decorado con al menos el
simple `@http.route()` para que su ruta se mantenga activa.
Opcionalmente, podemos proporcionar parámetros a `route()`, y luego
estaremos reemplazando y redefiniendo sus rutas.
136
137 El método extendido `hello()` ahora tiene un parámetro de nombre. Los
parámetros pueden obtener sus valores de segmentos de la URL de la
ruta, de los parámetros de consulta de una cadena o de los parámetros
POST. En este caso, la ruta no tiene variable extraíble (lo
demostraremos en un momento), y como estamos manejando peticiones GET,
no POST, el valor del parámetro name será extraído de la consulta de
cadena en la URL. Una URL de prueba podría ser
`http://localhost:8069/hello?name=John`.
138
139 Dentro del método `hello()` ejecutamos el método heredado para obtener
su respuesta, y luego podemos modificarlo de acuerdo a nuestras
necesidades. El patrón común para los métodos de controlador es que
terminen con una instrucción para renderizar una plantilla. En nuestro
caso:
140
141 ```
142 return request.render('todo_website.hello')
143
144 ```
145
146 Esto genera un objeto `http.Response`, pero la representación real se
retrasa hasta el final del despacho.
147
148 Esto significa que el método de herencia todavía puede cambiar la
plantilla QWeb y el contexto a utilizar para la representación.
Podríamos cambiar la plantilla modificando `response.template`, pero no
lo necesitaremos. Preferimos modificar `response.qcontext` para agregar
la clave `name al contexto de renderizado.
149
150 No olvides agregar el nuevo archivo Python en
`todo_website/controllers/__init__.py:`
Page 4
capitulo-11.md 07/02/2018

151
152 ```
153 from . import main
154 from . import extend
155 ```
156
157 Ahora necesitamos modificar la plantilla `QWeb`, Para que haga uso de
esta información adicional. Agrega lo que sigue a
`todo/website/views/todo_extend.xml`:
158
159 ```
160 <odoo>
161 <template id="hello_extended"
162 name="Extended Hello World"
163
164 inherit_id="todo_website.hello">
165 <xpath expr="//h1" position="replace">
166 <h1>
167 Hello <t t-esc="name or 'Someone'" />!
168 </h1>
169 </xpath>
170 </template>
171 </odoo>
172 ```
173 Las plantillas de páginas web son documentos XML, al igual que los
otros tipos de vista Odoo, y podemos usar xpath para localizar
elementos y luego manipularlos, tal como podríamos con los otros tipos
de vista. La plantilla heredada se identifica en el elemento
`<template>` por el atributo `inherited_id`.
174
175 No debemos olvidar declarar este archivo de datos adicional en nuestro
manifiesto, `todo_website/__ manifest__.py`:
176
177 ```
178 'data': [
179 'Views/todo_web.xml',
180
181 'views/todo_extend.xml'
182 ],
183 ```
184 Después de esto, accediendo a `http://localhost:8069/hello?name=John
debe mostrarnos un mensaje **Hello John!**.
185
186 También podemos proporcionar parámetros a través de segmentos de URL.
Por ejemplo, podríamos obtener exactamente el mismo resultado de la URL
`http://localhost:8069/hello/John usando esta implementación alternativa:
187
188 ```
189 class TodoExtended(Todo):
190
191 @ Http.route (['/ hello', '/hello/<name>])
192 def hello(self, name=None, **kwargs):
193 response = super(TodoExtended, self).hello()
194 response.qcontext['name'] = name
195 return response
196 ```
197
198 Como puedes ver, las rutas pueden contener **placeholders**
correspondientes a los parámetros que se van a extraer y luego pasar al
método. Los **placeholders** también pueden especificar un convertidor
para implementar una asignación de tipo específica. Por ejemplo, `<int:
Page 5
capitulo-11.md 07/02/2018

user_id>` extraería el parámetro `user_id` como un valor entero.


199
200 Los convertidores son una característica proporcionada por la
biblioteca werkzeug, utilizada por Odoo, y la mayoría de los
disponibles se pueden encontrar en la documentación de la biblioteca
`werkzeug`, en
[http://werkzeug.pocoo.org/docs/routing/](http://werkzeug.pocoo.org/docs/
routing/).
201
202 Odoo añade un conversor específico y particularmente útil: extraer un
registro de modelo. Por ejemplo
`@http.route('/hello/<model("res.users"):user>)` extrae el parámetro
user como un objeto de registro para el modelo `res.users`.
203
204 ## ¡HolaCMS!
205
206 Vamos a hacer esto aún más interesante, y crear nuestro propio CMS
simple. Para esto podemos hacer que la ruta espere un nombre de
plantilla (una página) en la URL y luego simplemente renderizarla.
Podríamos entonces crear dinámicamente páginas web y hacerlas servir
por nuestro CMS.
207
208 Resulta que esto es bastante fácil de hacer:
209
210 ```
211 @http.route('/hellocms/<page>', auth='public')
212 def hello (auto, página, ** kwargs):
213 return http.request.render(page)
214 ```
215 Ahora, abre `http://localhost:8069/hellocms/todo_website.hello` en tu
navegador web y verás nuestra página de **Hello World!**
216
217 De hecho, el sitio web incorporado ofrece funciones de CMS, incluyendo
una implementación más robusta de lo anterior, en la ruta del endpoint
`/page`.
218
219 ### Nota
220
221 En la jerga werkzeug el endpoint es un alias de la ruta, y representado
por su parte estática (sin los placeholders). Para nuestro ejemplo
simple de CMS, el endpoint fue `/hellocms`.
222
223 La mayor parte del tiempo queremos que nuestras páginas se integren en
el sitio web de Odoo. Así que para el resto de este capítulo en
nuestros ejemplos vamos a trabajar con el módulo `website`.
224
225 ## Creando sitios web
226
227 Las páginas dadas por los ejemplos anteriores no están integradas con
el módulo `website`: no tenemos pie de página, menú, etc. El módulo
`website` de Odoo proporciona convenientemente todas estas
características para que no tengamos que preocuparnos por ellas.
228
229 Para usarlo, debemos comenzar instalando el módulo `website` en nuestra
instancia de trabajo y luego agregarlo como una dependencia a nuestro
módulo. La clave `depends` en __manifest__.py debe tener el siguiente
aspecto:
230
231 ```
232 'depends': ['todo_kanban',
233 'website'
Page 6
capitulo-11.md 07/02/2018

234 ],
235 ```
236
237 Para utilizar el sitio web, también debemos modificar el controlador y
la plantilla.
238
239 El controlador necesita un argumento adicional `website=True` en la ruta:
240
241 ```
242 @http.route('/hello', auth='public', website=True)
243 def hello(self, **kwargs):
244 return request.render('todo_website.hello')
245 ```
246 Y la plantilla debe insertarse dentro del diseño general del sitio web:
247
248 ```
249 <template id="hello" name="Hello World">
250
251 <t t-call="website.layout">
252 <h1>Hello World!</h1>
253 </t>
254 </template>
255 ```
256 Con esto, el ejemplo **Hola Mundo** que utilizamos antes debe ahora ser
mostrado dentro de una página del sitio web de Odoo.
257
258 ## Adición de elementos CSS y JavaScript
259
260 Nuestras páginas web pueden necesitar algunos recursos adicionales de
CSS o JavaScript. Este aspecto de las páginas web es gestionado por el
sitio web, por lo que necesitamos una forma de decirle que también
utilice nuestros archivos.
261
262 Vamos a añadir un poco de CSS para agregar un efecto de tachado simple
para las tareas realizadas. Para ello, crea el archivo
`todo_website/static/src/css/index.css` con este contenido:
263
264 ```
265 .todo-app-done {
266 text-decoration: line-through;
267 }
268 ```
269 Luego tenemos que incluirlo en las páginas del sitio web. Esto se hace
agregándolos en la plantilla `website.assets_frontend` responsable de
cargar los activos específicos del sitio web. Edite el archivo de datos
todo_website / views / todo_templates.xml, para ampliar esa plantilla:
270
271 ```
272 <Odoo>
273 <Template id = "assets_frontend"
274 Name = "todo_website_assets"
275
276 Inherit_id = "website.assets_frontend">
277 <Xpath expr = "." Position = "inside">
278 <Link rel = "stylesheet" type = "text / css"
279 Href = "/ todo_website / static / src / css / index.css" />
280 </ Xpath>
281
282
283
284
Page 7
capitulo-11.md 07/02/2018

285 </ Template>


286 </ Odoo>
287 ```
288
289 Pronto estaremos usando esta nueva clase de estilo `todo-app-done`. Por
supuesto, los activos de JavaScript también se pueden agregar usando un
enfoque similar.
290
291 ## El controlador de lista de tareas pendientes
292
293 Ahora que hemos pasado por lo básico, vamos a trabajar en nuestra lista
de tareas por hacer. Tendremos una URL `/todo` mostrándonos una página
web con una lista de Tareas por hacer.
294
295 Para ello, necesitamos un método controlador, la preparación de los
datos a presentar, y una plantilla QWeb para presentar esa lista al
usuario.
296
297 Edita el archivo `todo_website/controllers/main.py` para agregar este
método:
298
299 ```
300 #class Main(http.Controller):
301
302 @http.route('/todo', auth='user' , website=True)
303 def index(self, **kwargs):
304 TodoTask = request.env['todo.task']
305 tasks = TodoTask.search([])
306 return request.render(
307 'todo_website.index', {'tasks': tasks})
308
309 ```
310 El controlador recupera los datos que se van a utilizar y los pone a
disposición de la plantilla renderizada. En este caso, el controlador
requiere una sesión autenticada, ya que la ruta tiene el atributo
```auth='user'```. Incluso si ese es el valor predeterminado, es una
buena práctica declarar explícitamente que se requiere una sesión de
usuario.
311
312 Con esto, la declaración Todo Task `search()` se ejecutará con el
usuario de la sesión actual.
313
314 Los datos accesibles a los usuarios públicos son muy limitados, cuando
usamos ese tipo de ruta, a menudo necesitamos usar `sudo()` para elevar
el acceso y hacer disponibles los datos de la página que de otro modo
no serían accesibles.
315
316 Esto también puede ser un riesgo de seguridad, así que tenga cuidado en
la validación de los parámetros de entrada y en las acciones
realizadas. También mantenga el uso del conjunto de registros `sudo()`
limitado a las operaciones mínimas posibles.
317
318 El método `request.render()` espera que el identificador de la
plantilla de QWeb se procese y un diccionario con el contexto
disponible para la evaluación de la plantilla.
319
320 ## La plantilla de lista de tareas pendientes
321
322 La plantilla de QWeb debe ser agregada por un archivo de datos, y
podemos agregarla al archivo de datos
`todo_website/views/todo_templates.xml` existente:
Page 8
capitulo-11.md 07/02/2018

323
324 ```
325 <template id="index" name="Todo List">
326 <t t-call="website.layout">
327 <div id="wrap" class="container">
328 <h1>Todo Tasks</h1>
329
330 <!-- List of Tasks -->
331 <t t-foreach="tasks" t-as="task">
332 <div class="row">
333 <input type="checkbox" disabled="True"
334 t-att-checked=" 'checked' if task.is_done else {}" />
335 <a t-attf-href="/todo/{{slug(task)}}">
336 <span t-field="task.name"
337 t-att-class="'todo-app-done' if task.is_done
338 else ''" />
339 </a>
340 </div>
341 </t>
342
343 <!-- Add a new Task -->
344 <div class="row">
345 <a href="/todo/add" class="btn btn-primary btn-lg">
346 Add
347 </a>
348 </div>
349
350 </div>
351 </t>
352 </template>
353 ```
354
355 El código anterior utiliza la directiva `t-foreach` para mostrar una
lista de tareas. La directiva `t-att` usada en la casilla de
verificación de entrada nos permite agregar o no el atributo *checked*
dependiendo del valor `is_done`.
356
357 Tenemos una entrada de casilla de verificación, y queremos que se
compruebe si la tarea se realiza. En HTML, se comprueba una casilla de
verificación en función de que tenga o no el atributo *checked*. Para
ello usamos la directiva `t-att-NAME` para renderizar dinámicamente el
atributo checked dependiendo de una expresión. En este caso, la
expresión se evalúa como `None`, QWeb omitirá el atributo, lo cual es
conveniente para este caso.
358
359 Al procesar el nombre de la tarea, se utiliza la directiva `t-attf`
para crear dinámicamente la URL para abrir el formulario de detalle
para cada tarea específica. Utilizamos la función especial `slug()`
para generar una URL legible por humanos para cada registro. El enlace
no funcionará por ahora, ya que todavía estamos por crear el
controlador correspondiente.
360
361 En cada tarea también usamos la directiva `t-att` para establecer el
estilo `todo-app-done` solo para las tareas que se realizan.
362
363 Finalmente, tenemos un botón Añadir para abrir una página con un
formulario para crear una nueva Tarea. Lo usaremos para introducir el
manejo de formularios web a continuación.
364
365 ## La página de detalles de las tareas por hacer
366
Page 9
capitulo-11.md 07/02/2018

367 Cada elemento de la lista de tareas es un enlace a una página de


detalles. Debemos implementar un controlador para esos enlaces, y una
plantilla QWeb para su presentación. En este punto, este debería ser un
ejercicio sencillo.
368
369 En el archivo `todo_website/controllers/main.py` agregue el método:
370
371 ```
372 #class Main(http.Controller):
373
374 @http.route('/todo/<model("todo.task"):task>', website=True)
375 def index(self, task, **kwargs):
376 return http.request.render(
377 'todo_website.detail',
378 {'task': task})
379 ```
380
381
382 Observe que la ruta utiliza un placeholder con el convertidor
`model("todo.task")`, asignando a la variable de tarea. Captura un
identificador de tarea desde la URL, ya sea un número de ID simple o
una representación de slug, y lo convierte en el objeto de registro de
exploración correspondiente.
383
384 Y para la plantilla de QWeb añada el siguiente código al archivo de
datos `todo_website/views/todo_web.xml`:
385
386 ```
387 <template id="detail" name="Todo Task Detail">
388 <t t-call="website.layout">
389 <div id="wrap" class="container">
390 <h1 t-field="task.name" />
391 <p>Responsible: <span t-field="task.user_id" /></p>
392 <p>Deadline: <span t-field="task.date_deadline" /></p>
393 </div>
394 </t>
395 </template>
396 ```
397 Cabe destacar aquí el uso del elemento `<t t-field>`. Se encarga de la
representación adecuada del valor del campo, al igual que en el
backend. Presenta correctamente valores de fecha y valores Many-to-One,
por ejemplo.
398
399 ## Formularios Web
400
401 Los formularios son una característica común que se encuentran en los
sitios web. Ya tenemos todas las herramientas necesarias para
implementar una: una plantilla de QWeb puede proporcionar el HTML para
el formulario, la acción de envío correspondiente puede ser una URL,
procesada por un controlador que puede ejecutar toda la lógica de
validación y finalmente almacenar los datos en el Modelo adecuado.
402
403 Pero para formas no triviales esto puede ser una tarea exigente. No es
tan simple realizar todas las validaciones necesarias y proporcionar
retroalimentación al usuario sobre lo que está mal.
404
405 Puesto que esto es una necesidad común, el módulo `website_form` está
disponible para ayudarnos con esto. Vamos a ver cómo usarlo.
406
407 Mirando hacia atrás en el botón Agregar en la lista Tarea por hacer,
podemos ver que abre la URL `/todo/add`. Esto presentará un formulario
Page 10
capitulo-11.md 07/02/2018

para enviar una nueva tarea por hacer y los campos disponibles serán el
nombre de la tarea, una persona (usuario) responsable de la tarea y un
archivo adjunto.
408
409 Debemos comenzar agregando la dependencia `website_form` a nuestro
módulo. Podemos reemplazar el módulo `website`, ya que mantenerlo
explícitamente sería redundante. En el `todo_website/__ manifest__.py`
edite la clave `depends` a:
410
411 ```
412 'Depende': ['todo_kanban',
413 'Website_form'
414
415
416
417 ],
418 ```
419 Ahora vamos a añadir la página con el formulario.
420
421 ## La página formulario
422
423 Podemos comenzar implementando el método del controlador para soportar
la renderización del formulario, en el archivo
`todo_website/controllers/ main.py`:
424
425 ```
426 @http.route('/todo/add', website=True)
427 def add(self, **kwargs):
428 users = request.env['res.users'].search([])
429 return request.render(
430 'todo_website.add', {'users': users})
431 ```
432
433 Se trata de un controlador sencillo, que muestra la plantilla
`todo_website.add` y que le proporciona una lista de usuarios, de modo
que pueda utilizarse para crear un cuadro de selección.
434
435 Ahora para la plantilla QWeb correspondiente. Podemos añadirlo al
archivo de datos `todo_website/views/todo_web.xml`:
436
437 ```
438 <template id="add" name="Add Todo Task">
439 <t t-call="website.layout">
440 <t t-set="additional_title">Add Todo</t>
441 <div id="wrap" class="container">
442 <div class="row">
443 <section id="forms">
444 <form method="post"
445 class="s_website_form
446 container-fluid form-horizontal"
447
448 action="/website_form/"
449 data-model_name="todo.task"
450 data-success_page="/todo"
451 enctype="multipart/form-data" >
452 <!-- Form fields will go here! -->
453 <!-- Submit button -->
454 <div class="form-group">
455 <div class="col-md-offset-3 col-md-7
456 col-sm-offset-4 col-sm-8">
457 <a
Page 11
capitulo-11.md 07/02/2018

458 class="o_website_form_send
459 btn btn-primary btn-lg">
460 Save
461 </a>
462
463 <span id="o_website_form_result"></span>
464 </div>
465 </div>
466
467 </form>
468 </section>
469 </div> <!-- rows -->
470 </div> <!-- container -->
471 </t> <!-- website.layout -->
472 </template>
473 ```
474
475 Como es de esperar, podemos encontrar el elemento `<t
t-call="website.layout"` específico de Odoo, responsable de insertar la
plantilla dentro del diseño del sitio web, y `<t
t-set="additional_title">` que establece un titulo adicional, esperado
por el layout del sitio web.
476
477 Para el contenido, la mayoría de lo que podemos ver en esta plantilla
se puede encontrar en un típico formulario Booststrap CSS. Pero también
tenemos algunos atributos y clases de CSS que son específicos para los
formularios del sitio web. Los marcamos en negrita en el código, por lo
que es más fácil identificarlos.
478
479 Las clases CSS son necesarias para que el código JavaScript pueda
realizar correctamente su lógica de manejo de formularios. Y luego
tenemos algunos atributos específicos en el elemento `<form>`:
480
481 + `action` es un atributo de formulario estándar, pero debe tener el
valor "/website_form/". Se requiere la barra diagonal.
482 + `Data-model_name` identifica el modelo al que escribir y se pasará al
controlador `/website_form.
483 + `Data-success_page` es la URL a redireccionar después de una
presentación de formulario correcta. En este caso, se nos enviará de
nuevo a la lista de tareas.
484
485 No necesitaremos proporcionar nuestro propio método de controlador para
manejar el envío de formularios. La ruta `/website_form` lo hará por
nosotros. Toma toda la información que necesita del formulario,
incluyendo los atributos específicos que acabamos de describir, y luego
realiza validaciones esenciales en los datos de entrada, y crea un
nuevo registro en el modelo de destino.
486
487 Para casos de uso avanzado, podemos forzar que se use un método de
controlador personalizado. Para ello debemos añadir un atributo
`data-force_action` al elemento `<form>`, con la palabra clave para que
el controlador objetivo a utilizar. Por ejemplo,
`data-force_action="todo-custom"` tendría la solicitud para llamar a la
URL `/website_form/todo-custom`. Entonces deberíamos proporcionar un
método de controlador adjunto a esa ruta. Sin embargo, hacer esto
quedará fuera de nuestro alcance aquí.
488
489 Todavía tenemos que terminar nuestro formulario, agregando los campos
para obtener las entradas del usuario. Dentro del elemento `<form>`,
añade:
490
Page 12
capitulo-11.md 07/02/2018

491 ```
492 <!-- Description text field, required -->
493 <div class="form-group form-field">
494 <div class="col-md-3 col-sm-4 text-right">
495 <label class="control-label" for="name">To do*</label>
496 </div>
497 <div class="col-md-7 col-sm-8">
498 <input name="name" type="text" required="True"
499
500 class="o_website_from_input
501
502 form-control" />
503 </div>
504 </div>
505
506 <!-- Add an attachment field -->
507 <div class="form-group form-field">
508 <div class="col-md-3 col-sm-4 text-right">
509 <label class="control-label" for="file_upload">
510 Attach file
511 </label>
512 </div>
513 <div class="col-md-7 col-sm-8">
514 <input name="file_upload" type="file"
515
516 class="o_website_from_input
517
518
519
520 form-control" />
521 </div>
522 </div>
523 ```
524 Aquí estamos agregando dos campos, un campo de texto regular para la
descripción y un campo de archivo, para cargar un archivo adjunto. Todo
el marcado se puede encontrar en los formularios regulares Bootstrap, a
excepción de la clase o_website_from_input, necesarios para la lógica
de formulario de `website` para preparar los datos para enviar.
525
526 La lista de selección de usuario no es muy diferente excepto que
necesita usar una directiva t-foreach QWeb para representar la lista de
usuarios seleccionables. Podemos hacer esto porque el controlador
recupera ese conjunto de registros y lo pone a disposición de la
plantilla bajo el nombre users:
527
528 ```
529 <!-- Select User -->
530 <div class="form-group form-field">
531 <div class="col-md-3 col-sm-4 text-right">
532 <label class="control-label" for="user_id">
533 For Person
534 </label>
535 </div>
536 <div class="col-md-7 col-sm-8">
537 <select name="user_id"
538
539 class="o_website_from_input
540
541 form-control" >
542
543 <t t-foreach="users" t-as="user">
Page 13
capitulo-11.md 07/02/2018

544 <option t-att-value="user.id">


545 <t t-esc="user.name" />
546 </option>
547 </t>
548
549 </select>
550 </div>
551 </div>
552 ```
553 Sin embargo, nuestro formulario todavía no funcionará hasta que hagamos
alguna configuración de seguridad de acceso.
554
555 ## Seguridad de acceso y elemento de menú
556
557 Dado que este manejo genérico de formularios está bastante abierto y se
basa en datos no confiables enviados por el cliente, por razones de
seguridad necesita algún tipo de configuración del servidor en lo que
el cliente puede hacer. En particular, los campos de modelo que se
pueden escribir basados en datos de formulario deben estar en la lista
blanca.
558
559 Para añadir campos a esta lista blanca, se proporciona una función de
ayuda y podemos usarla desde un archivo de datos XML. Debemos crear el
archivo `todo_website/data/config_data.xml` con:
560
561
562 ```
563 <?xml version="1.0" encoding="utf-8"?>
564 <odoo>
565 <data>
566
567 <record id="todo_app.model_todo_task" model="ir.model">
568 <field name="website_form_access">True</field>
569 </record>
570
571 <function model="ir.model.fields"
572 name="formbuilder_whitelist">
573 <value>todo.task</value>
574 <value eval="['name', 'user_id', 'date_deadline']"/>
575 </function>
576
577 </data>
578 </odoo>
579 ```
580
581 Para que un modelo pueda ser utilizado por los formularios, debemos
hacer dos cosas: activar un indicador en el modelo, y poner en lista
blanca el campo que se pueda utilizar. Estas son las dos acciones que
se están realizando en el archivo de datos anterior.
582
583 No olvide que, para que nuestro módulo conozca este archivo de datos,
debe agregarse a la clave `data` en el manifiesto del módulo.
584
585 También sería bueno que nuestra página Todo esté disponible en el menú
del módulo `website`. Vamos a añadirla usando el mismo archivo de
datos. Agrega otro elemento `<data>` como este:
586
587 ```
588 <Data noupdate = "1">
589 <Record id = "menu_todo" model = "website.menu">
590 <Field name = "name"> Todo </ field>
Page 14
capitulo-11.md 07/02/2018

591 <Field name = "url"> / todo </ field>


592 <Field name = "parent_id" ref = "website.main_menu" />
593 <Field name = "sequence" type = "int"> 50 </ field>
594 </ Record>
595 </ Data>
596 ```
597
598 Como se puede ver, para agregar un elemento de menú del sitio web solo
necesitamos crear un registro en el modelo `web.menu`, con un nombre,
una URL y el identificador del elemento de menú principal. El nivel
superior de este menú tiene como padre el elemento `website.main_menu`.
599
600 ## Adición de lógica personalizada
601
602 Los formularios web nos permiten conectar nuestras propias validaciones
y cálculos al procesamiento de formularios. Esto se realiza mediante la
implementación de un método `website_form_input_filter()` con la lógica
del modelo de destino. Acepta un diccionario de valores, lo valida y
realiza cambios en él y, a continuación, devuelve el diccionario de
valores posiblemente modificado.
603
604 Lo utilizaremos para implementar dos funciones: eliminar cualquier
espacio inicial y final del título de la tarea e imponer que el título
de la tarea tenga al menos tres caracteres.
605
606 Agregue el archivo `todo_website/models/todo_task.py` que contiene el
siguiente código:
607
608 ```
609 # -*- coding: utf-8 -*-
610 from odoo import api, models
611 from odoo.exceptions import ValidationError
612
613 class TodoTask(models.Model):
614 _inherit = 'todo.task'
615
616 @api.model
617 def website_form_input_filter(self, request, values):
618 if 'name' in values:
619 values['name'] = values['name'].strip()
620 if len(values.['name']) < 3:
621 raise ValidationError(
622 'Text must be at least 3 characters long')
623 return values
624 ```
625
626
627 El método `website_form_input_filter` realmente espera dos parámetros:
el objeto `request` y el diccionario `values`. Los errores que impiden
el envío de formularios deben generar una excepción `ValidationError`.
628
629 La mayor parte del tiempo este punto de extensión para los formularios
debería permitirnos evitar manipuladores de presentación de formularios
personalizados.
630
631 Como de costumbre, debemos importar este nuevo achivo de Python,
añadiendo `from .import models` en el archivo `todo_website/__
init__.py`, y añadiendo el archivo `todo_website/models/__ init__.py
con un `from . Import todo_task line`.
632
633 ## Sumario
Page 15
capitulo-11.md 07/02/2018

634
635 Ahora debes tener un buen entendimiento sobre lo esencial de las
características del sitio web. Hemos visto cómo usar controladores web
y plantillas QWeb para renderizar páginas web dinámicas. A
continuación, aprendimos a utilizar el complemento de sitio web y crear
nuestras propias páginas para ello. Por último, hemos introducido el
complemento de formularios de sitios web que nos ayudó a crear un
formulario web. Estos deben proporcionarnos las habilidades básicas
necesarias para crear las características del sitio web.
636
637 A continuación, aprenderemos cómo tener aplicaciones externas
interactuar con nuestras aplicaciones Odoo.

Page 16
capitulo-12.md 07/02/2018

1 # Capitulo 12. API Externa - Integración con otros sistemas.


2
3
4 El servidor Odoo también proporciona una API externa, que es utilizada
por su cliente web y también está disponible para otras aplicaciones
cliente.
5
6 En este capítulo, aprenderemos cómo usar la API externa de Odoo desde
nuestros propios programas cliente. Se puede utilizar cualquier
lenguaje de programación, siempre y cuando tenga soporte para
protocolos XML-RPC o JSON-RPC. Como ejemplo, la documentación oficial
proporciona ejemplos de código para cuatro lenguajes de programación
populares: Python, PHP, Ruby y Java.
7
8 Para evitar la introducción de idiomas adicionales que el lector podría
no estar familiarizado con, aquí nos centraremos en los clientes basados
en Python, aunque las técnicas para manejar las llamadas RPC también se
aplican a otros lenguajes de programación.
9
10 Describiremos cómo usar las llamadas RPC de Odoo y luego usarlas para
crear una aplicación de escritorio de tareas pendientes sencilla con
Python.
11
12 Finalmente, presentaremos el cliente ERPPeek. Se trata de una
biblioteca cliente de Odoo, que se puede utilizar como una capa de
abstracción conveniente para las llamadas RPC de Odoo y también es un
cliente de línea de comandos para Odoo, que permite administrar de
forma remota las instancias de Odoo.
13
14 ## Configuración de un cliente Python
15
16 La API de Odoo se puede acceder externamente usando dos protocolos
diferentes: XML-RPC y JSON-RPC. Cualquier programa externo capaz de
implementar un cliente para uno de estos protocolos podrá interactuar
con un servidor Odoo. Para evitar la introducción de lenguajes de
programación adicionales, seguiremos usando Python para explorar la API
externa.
17
18 Hasta ahora, hemos estado ejecutando código Python sólo en el servidor.
Esta vez, vamos a utilizar Python en el lado del cliente, por lo que es
posible que tenga que hacer alguna configuración adicional en su
estación de trabajo.
19
20 Para seguir los ejemplos de este capítulo, necesitaras poder ejecutar
archivos Python en tu computadora de trabajo. El servidor Odoo requiere
Python 2, pero nuestro cliente RPC puede estar en cualquier idioma, por
lo que Python 3 estará bien. Sin embargo, dado que algunos lectores
pueden estar ejecutando el servidor en la misma máquina en la que están
trabajando (¡hola usuarios de Ubuntu!), Será más fácil para todos
seguir si nos atenemos a Python 2.
21
22 Si estas utilizando Ubuntu o una Mac, Python probablemente ya esté
instalado. Abra una consola de terminal, escriba `python`, y debería
recibir algo similar a lo siguiente:
23
24 ```
25 Python 2.7.12 (default, Jul 1 2016, 15:12:24)
26 [GCC 5.4.0 20160609] on linux2
27 Type "help", "copyright",", "credits" or "license" for more information.
28 >>>
29 ```
Page 1
capitulo-12.md 07/02/2018

30
31 > **nota**
32
33 > Los usuarios de Windows pueden encontrar un instalador para Python y
también ponerse rápidamente al día. Los paquetes oficiales de
instalación se pueden encontrar en https://www.python.org/downloads/.
34
35 > Si es un usuario de Windows y tiene Odoo instalado en su máquina, es
posible que se pregunte por qué no tiene un intérprete de Python, y se
necesita una instalación adicional. La respuesta corta es que la
instalación de Odoo tiene un intérprete de Python incrustado que no se
utiliza fácilmente fuera.
36
37
38
39 ## Llamadas a la API de Odoo utilizando XML-RPC
40
41 El método más sencillo para acceder al servidor es utilizar XML-RPC.
Podemos usar la biblioteca xmlrpclib de la biblioteca estándar de
Python para esto. Recuerde que estamos programando un cliente para
conectarnos a un servidor, por lo que necesitamos una instancia de
servidor Odoo en la que se ejecute para conectarse. En nuestros
ejemplos, asumiremos que una instancia de servidor Odoo se ejecuta en
la misma máquina (localhost), pero puede utilizar cualquier dirección
IP o nombre de servidor accesible si el servidor se ejecuta en una
máquina diferente.
42
43 ## Abriendo una conexión XML-RPC
44
45 Vamos a tener un primer contacto con la API externa de Odoo. Inicie una
consola de Python y escriba lo siguiente:
46
47 ```
48 >>> import xmlrpclib
49 >>> srv = 'http://localhost:8069'
50 >>> common = xmlrpclib.ServerProxy('%s/xmlrpc/2/common' % srv)
51 >>> common.version()
52 {'server_version_info': [10, 0, 0, 'final', 0, ''], 'server_serie':
'10.0', 'server_version': '10.0', 'protocol_version': 1}
53 ```
54 Aquí, importamos la biblioteca xmlrpclib y luego configuramos una
variable con la información para la dirección del servidor y el puerto
de escucha. Siéntete libre de adaptarlos a tu configuración específica.
55
56 A continuación, configuramos el acceso a los servicios públicos del
servidor (que no requieren un inicio de sesión), expuestos en el
`/xmlrpc/2/common` endpoint. Uno de los métodos que está disponible es
`version ()`, que inspecciona la versión del servidor. Lo usamos para
confirmar que podemos comunicarnos con el servidor.
57
58 Otro método público es `authenticate()`. De hecho, esto no crea una
sesión, como usted puede ser llevado a creer. Este método sólo confirma
que el nombre de usuario y la contraseña se aceptan y devuelve el ID de
usuario que se debe utilizar en las solicitudes en lugar del nombre de
usuario, como se muestra aquí:
59
60 ```
61 >>> db = 'todo'
62 >>> user, pwd = 'admin', 'admin'
63 >>> uid = common.authenticate(db, user, pwd, {})
64 >>> print uid
Page 2
capitulo-12.md 07/02/2018

65 ```
66
67 Si las credenciales de inicio de sesión no son correctas, se devuelve
un valor False, en lugar de un ID de usuario.
68
69 ## Lectura de datos desde el servidor
70
71 Con XML-RPC, no se mantiene ninguna sesión y las credenciales de
autenticación se envían con cada solicitud. Esto agrega algo de
sobrecarga al protocolo, pero lo hace más fácil de usar.
72
73 A continuación, establecemos el acceso a los métodos de servidor que
necesitan un inicio de sesión para acceder. Éstos se exponen en el
endpoint `/xmlrpc2/object`, como se muestra a continuación:
74
75 ```
76 >>> api = xmlrpclib.ServerProxy('%s/xmlrpc/2/object' % srv)
77 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search_count'[[]])
78 40
79 ```
80
81 Aquí, estamos haciendo nuestro primer acceso a la API del servidor,
realizando un conteo en los registros de **Partner**. Los métodos se
llaman usando el método `execute_kw()` que toma los siguientes
argumentos:
82
83 + El nombre de la base de datos para conectarse a
84 + El ID de usuario de la conexión
85 + La contraseña de usuario
86 + El nombre del identificador del modelo de destino
87 + El método para llamar
88 + Una lista de argumentos posicionales
89 + Un diccionario opcional con argumentos de palabra clave
90
91 El ejemplo anterior llama al método `search_count` del modelo
`res.partner` con un argumento de posición, [], y sin argumentos de
palabra clave. El argumento posicional es un dominio de búsqueda; Ya
que estamos proporcionando una lista vacía, cuenta todos los Partners.
92
93 Las acciones frecuentes son buscar y leer. Cuando se llama desde el
RPC, el método `search` devuelve una lista de identificadores que
coinciden con un dominio. El método `browse` no está disponible en el
RPC, y leer debe ser utilizado en su lugar para dar una lista de IDs de
registro y recuperar sus datos, como se muestra en el código siguiente:
94
95 ```
96 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search',
[[('country_id', '=', 'be'), ('parent_id', '!=', False)]])
97 [18, 33, 23, 22]
98 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[18]],
{'fields': ['id', 'name', 'parent_id']})
99 [{'parent_id': [8, 'Agrolait'], 'id': 18, 'name': 'Edward Foster'}]
100 ```
101
102 Ten en cuenta que para el método `read`, estamos utilizando un
argumento de posición para la lista de IDs, [18] y un argumento de
palabra clave, `fields`. También podemos observar que los campos
relacionales many-to-one se recuperan como un par, con el ID del
registro relacionado y el nombre para mostrar. Eso es algo que debe
tener en cuenta al procesar los datos en su código.
103
Page 3
capitulo-12.md 07/02/2018

104 La combinación de búsqueda y lectura es tan frecuente que se


proporciona un método `search_read` para realizar ambas operaciones en
un solo paso. El mismo resultado que los dos pasos previos se puede
obtener con lo siguiente:
105
106 ```
107 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search_read',
[[('country_id', '=', 'be'), ('parent_id', '!=', False)]], {'fields':
['id', 'name', 'parent_id']})
108 ```
109 El método `search_read` se comporta como `read`, pero espera un dominio
como un primer argumento posicional en lugar de una lista de IDs. Vale
la pena mencionar que el argumento `field` en `read` y `search_read` no
es obligatorio. Si no se proporciona, se recuperarán todos los campos.
Esto puede causar costosos cálculos de campos de función y una gran
cantidad de datos que se recuperarán, pero probablemente nunca se
utiliza, por lo que generalmente se recomienda proporcionar una lista
explícita de campos.
110
111 ## Llamando otros métodos
112
113 Todos los demás métodos de modelo se exponen a través de RPC, excepto
los prefijados con un subrayado, que se consideran privados. Esto
significa que podemos usar `create`, `write` y `unlink` para modificar
datos en el servidor de la siguiente manera:
114
115 ```
116 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'create', [{'name':
'Packt Pub'}])
117 45
118 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'write', [[45],
{'name': 'Packt Publishing'}])
119 True
120 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[45], ['id',
'name']])
121 [{'id': 45, 'name': 'Packt Publishing'}]
122 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'unlink', [[45]])
123 True
124 ```
125
126 Una limitación del protocolo XML-RPC es que no admite valores `None`.
La implicación es que los métodos que no devuelven nada no serán
utilizables a través de XML-RPC, ya que están implicitamente
devolviendo `None`. Esta es la razón por la cual los métodos deben
terminar siempre con al menos una declaración verdadera de la vuelta.
127
128 Vale la pena repetir que la API externa de Odoo puede ser utilizada por
la mayoría de los lenguajes de programación. En la Documentación
oficial podemos encontrar ejemplos prácticos para Ruby, PHP y Java.
Está disponible en
[https://www.odoo.com/documentation/10.0/api_integration.html](https://ww
w.odoo.com/documentation/10.0/api_integration.html).
129
130 ## Desarrollando una aplicación de escritorio de Notas
131
132 Hagamos algo interesante con la API de RPC. Odoo proporciona una
aplicación sencilla para notas. ¿Qué pasa si los usuarios pueden
administrar sus notas personales directamente desde el escritorio de su
computadora? Vamos a escribir una sencilla aplicación de Python para
hacer eso, como se muestra en la siguiente captura de pantalla:
133
Page 4
capitulo-12.md 07/02/2018

134 ![Notas](file:img/12-01.jpg)
135
136
137 Para mayor claridad, lo dividiremos en dos archivos: uno relacionado
con las interacciones con el servidor backend, `note_api.py` y otro con
la interfaz gráfica de usuario, `note_gui.py`.
138
139 ## Capa de comunicación con Odoo
140
141 Crearemos una clase para configurar la conexión y almacenar su
información. Debe exponer dos métodos: `get()` para recuperar datos de
tareas y `set()` para crear o actualizar tareas.
142
143 Seleccione un directorio para alojar los archivos de la aplicación y
cree el archivo `note_api.py`. Podemos comenzar agregando el
constructor de la clase, de la siguiente manera:
144
145 ```
146 import xmlrpclib
147 class NoteAPI():
148 def __init__(self, srv, db, user, pwd):
149 common = xmlrpclib.ServerProxy(
150 '%s/xmlrpc/2/common' % srv)
151 self.api = xmlrpclib.ServerProxy(
152 '%s/xmlrpc/2/object' % srv)
153 self.uid = common.authenticate(db, user, pwd, {})
154 self.pwd = pwd
155 self.db = db
156 self.model = 'note.note'
157
158 ```
159 Aquí almacenamos toda la información necesaria en el objeto creado para
ejecutar llamadas en un modelo: la referencia de la API, uid,
contraseña, nombre de la base de datos y el modelo a utilizar.
160
161 A continuación, definiremos un método auxiliar para ejecutar las
llamadas. Se aprovecha de los datos almacenados del objeto para
proporcionar una firma de función más pequeña, como se muestra a
continuación:
162
163 ```
164 def execute(self, method, arg_list, kwarg_dict=None):
165 return self.api.execute_kw(
166 self.db, self.uid, self.pwd, self.model,
167 method, arg_list, kwarg_dict or {})
168 ```
169
170 Ahora podemos usarlo para implementar los métodos `get()` y `set()` de
nivel superior.
171
172 El método `get()` aceptará una lista opcional de ID para recuperar. Si
no aparece ninguno, se devolverán todos los registros:
173
174 ```
175 def get(self, ids=None):
176 domain = [('id',' in', ids)] if ids else []
177 fields = ['id', 'name']
178 return self.execute('search_read', [domain, fields])
179 ```
180
181 El método `set()` tendrá el texto de la tarea a escribir y un ID
Page 5
capitulo-12.md 07/02/2018

opcional como argumentos. Si no se proporciona ID, se creará un nuevo


registro. Devuelve el ID del registro escrito o creado, como se muestra
aquí:
182
183 ```
184 def set(self, text, id=None):
185 if id:
186 self.execute('write', [[id], {'name': text}])
187 else:
188 vals = {'name': text, 'user_id': self.uid}
189 id = self.execute('create', [vals])
190 return id
191 ```
192
193
194 Terminemos el archivo con un pequeño fragmento de código de prueba que
se ejecutará si ejecutamos el archivo Python:
195
196 ```
197 if __name__ == '__main__':
198 srv, db = 'http://localhost:8069', 'todo'
199 user, pwd = 'admin', 'admin'
200 api = NoteAPI(srv, db, user, pwd)
201 from pprint import pprint
202 pprint(api.get())
203 ```
204
205
206 Si ejecutamos el script Python, deberíamos ver el contenido de nuestras
tareas pendientes impresas. Ahora que tenemos un envoltorio simple
alrededor de nuestro backend Odoo, vamos a tratar con la interfaz de
usuario de escritorio.
207
208 ## Creación de la GUI
209
210 Nuestro objetivo aquí era aprender a escribir la interfaz entre una
aplicación externa y el servidor Odoo, y esto se hizo en la sección
anterior. Pero sería una vergüenza no ir el paso adicional y realmente
ponerlo a disposición del usuario final.
211
212 Para mantener la configuración tan simple como sea posible, usaremos
Tkinter para implementar la interfaz gráfica de usuario. Dado que forma
parte de la biblioteca estándar, no requiere ninguna instalación
adicional. No es nuestro objetivo explicar cómo funciona Tkinter, por
lo que será breve la explicación de la misma.
213
214 Cada tarea debe tener una pequeña ventana amarilla en el escritorio.
Estas ventanas tendrán un solo widget de texto. Presionando ***Ctrl +
N*** se abrirá una nueva nota y presionando ***Ctrl + S*** se escribirá
el contenido de la nota actual en el servidor Odoo.
215
216 Ahora, junto con el archivo `note_api.py`, crea un nuevo archivo
`note_gui.py`. Primero importará los módulos de Tkinter y los widgets
que usaremos, y luego la clase NoteAPI, como se muestra a continuación:
217
218 ```
219 from Tkinter import Text, Tk
220 import tkMessageBox
221 from note_api import NoteAPI
222 ```
223
Page 6
capitulo-12.md 07/02/2018

224 Si aparecen errores de código mostrando `ImportError: No module named


_tkinter, please install the python-tk package`, significa que se
necesitan bibliotecas adicionales en su sistema. En Ubuntu necesitaría
ejecutar el siguiente comando:
225
226 ```
227 $ sudo apt-get install python-tk
228 ```
229 A continuación, creamos nuestro propio widget de texto derivado del de
Tkinter. Al crear una instancia, esperará una referencia de la API, que
se utilizará para la acción de salvar, así como el texto y la
identificación de la tarea, como se muestra a continuación:
230
231 ```
232 class NoteText(Text):
233 def __init__(self, api, text='', id=None):
234 self.master = Tk()
235 self.id = id
236 self.api = api
237 Text.__init__(self, self.master, bg='#f9f3a9',
238 wrap='word', undo=True)
239 self.bind('<Control-n>', self.create)
240 self.bind('<Control-s>', self.save)
241 if id:
242 self.master.title('#%d' % id)
243 self.delete('1.0', 'end')
244 self.insert('1.0', text)
245 self.master.geometry('220x235')
246 self.pack(fill='both', expand=1)
247 ```
248
249 El constructor `Tk()` crea una nueva ventana de interfaz de usuario y
el widget de texto se coloca dentro de él, por lo que la creación de
una nueva instancia de Nota de texto abre automáticamente una ventana
de escritorio.
250
251 A continuación, implementaremos las acciones de crear y guardar. La
acción `create` abre una nueva ventana vacía, pero se almacenará en el
servidor sólo cuando se realice una acción de salvar. Aquí está el
código correspondiente:
252 ```
253 def create(self, event=None):
254 NoteText(self.api, '')
255
256 def save(self, event=None):
257 text = self.get('1.0', 'end')
258 self.id = self.api.set(text, self.id)
259 tkMessageBox.showinfo('Info', 'Note %d Saved.' % self.id)
260 ```
261
262 La acción `save` se puede realizar ya sea en tareas existentes o
nuevas, pero no hay necesidad de preocuparse por esto aquí ya que estos
casos ya están manejados por el método `set()` de `NoteAPI`.
263
264 Finalmente, agregamos el código que recupera y crea todas las ventanas
de notas cuando se inicia el programa, como se muestra en el código
siguiente:
265 ```
266 if __name__ == '__main__':
267 srv, db = 'http://localhost:8069', 'todo'
268 user, pwd = 'admin', 'admin'
Page 7
capitulo-12.md 07/02/2018

269 api = NoteAPI(srv, db, user, pwd)


270 for note in api.get():
271 x = NoteText(api, note['name'], note['id'])
272 x.master.mainloop()
273 ```
274
275
276 El último comando ejecuta `mainloop()` en la última ventana de nota
creada, para comenzar a esperar eventos de ventanas.
277
278 Esta es una aplicación muy básica, pero el punto aquí es hacer un
ejemplo de formas interesantes de aprovechar la Odoo RPC API.
279
280 ## Presentando el cliente ERPpeek
281
282 ERPpeek es una herramienta versátil que se puede utilizar tanto como
interfaz interactiva de línea de comandos (CLI) como biblioteca de
Python, con una API más conveniente que la proporcionada por xmlrpclib.
Está disponible en el índice PyPi y puede instalarse con lo siguiente:
283
284 ```
285 $ Pip install -U erppeek
286 ```
287
288 En un sistema Unix, si lo está instalando en todo el sistema, es
posible que deba agregar sudo al comando.
289
290 ## La API de ERPpeek
291
292 La biblioteca ERPpeek proporciona una interfaz de programación, que
envuelve xmlrpclib, que es similar a la interfaz de programación que
tenemos para el código del servidor.
293
294 Nuestro punto aquí es dar una idea de lo que la biblioteca ERPpeek
tiene para ofrecer, y no proporcionar una explicación completa de todas
sus características.
295
296 Podemos comenzar reproduciendo nuestros primeros pasos con xmlrpclib
usando la erppeek como sigue:
297
298 ```
299 >>> import erppeek
300 >>> api = erppeek.Client('http://localhost:8069', 'todo','admin',
'admin')
301 >>> api.common.version()
302 >>> api.count('res.partner', [])
303 >>> api.search('res.partner', [('country_id', '=', 'be'),
('parent_id','!=', False)])
304 >>> api.read('res.partner', [44], ['id', 'name', 'parent_id'])
305 ```
306
307 Como puedes ver, las llamadas a la API utilizan menos argumentos y son
similares a las contrapartes del servidor.
308
309 Pero ERPpeek no se detiene aquí, y también proporciona una
representación para los modelos. Tenemos las siguientes dos formas
alternativas de obtener una instancia para un modelo, ya sea usando el
método `model()` o accediendo a él como un nombre de atributo camel case:
310
311 ```
312 >>> m = api.model('res.partner')
Page 8
capitulo-12.md 07/02/2018

313 >>> m = api.ResPartner


314 ```
315 Ahora podemos realizar acciones en ese modelo de la siguiente manera:
316
317 ```
318 >>> m.count([('name', 'like', 'Packt%')])
319 1
320 >>> m.search([('name', 'like', 'Packt%')])
321 [44]
322 ```
323
324 También proporciona la representación de objetos del lado del cliente
para los registros de la siguiente manera:
325
326 ```
327 >>> recs = m.browse([('name', 'like', 'Packt%')])
328 >>> recs
329 <RecordList 'res.partner,[44]'>
330 >>> recs.name
331 ['Packt Publishing']
332 ```
333
334 Como puedes ver, la biblioteca de erppeek esta aun largo camino de
distancia de xmlrpclib, y hace posible escribir código que puede ser
reutilizado en el lado del servidor con poca o ninguna modificación.
335
336 ## La CLI de ERPpeek
337
338 No sólo se puede utilizar la biblioteca erppeek como una biblioteca
Python, también es una CLI que se puede utilizar para realizar acciones
administrativas en el servidor. Cuando el comando odoo shell
proporciona una sesión interactiva local en el servidor host, la
biblioteca erppeek proporciona una sesión interactiva remota en un
cliente a través de la red.
339
340 Al abrir una línea de comandos, podemos echar un vistazo a las opciones
disponibles, como se muestra a continuación:
341
342 ```
343 $ erppeek --help
344 ```
345
346 Veamos una sesión de ejemplo como sigue:
347
348 ```
349 $ erppeek --server='http://localhost:8069' -d todo -u admin
350 Usage (some commands):
351 models(name) # List models matching pattern
352 model(name) # Return a Model instance
353 (...)
354 Password for 'admin':
355 Logged in as 'admin'
356 todo >>> model('res.users').count()
357 3
358 todo >>> rec = model('res.partner').browse(43)
359 todo >>> rec.name
360 'Packt Publishing'
361 ```
362 Como se puede ver, se realizó una conexión con el servidor y el
contexto de ejecución proporcionó una referencia al método `model()`
para obtener instancias de modelo y realizar acciones en ellas.
Page 9
capitulo-12.md 07/02/2018

363
364 La instancia `erppeek.Client` utilizada para la conexión también está
disponible a través de la variable cliente.
365
366 En particular, proporciona una alternativa al cliente web para
administrar los módulos complementarios instalados:
367
368 + `client.modules()`: lista los módulos disponibles o instalados
369 + `client.install()`: realiza la instalación del módulo
370 + `client.upgrade()`: realiza actualizaciones de módulos
371 + `client.uninstall()`: desinstala los módulos
372 Por lo tanto, erppeek también puede proporcionar un buen servicio como
una herramienta de administración remota para los servidores Odoo.
373
374
375 ## Sumario
376
377 Nuestro objetivo para este capítulo fue aprender cómo funciona la API
externa y de qué es capaz. Empezamos a explorarla usando un simple
cliente XML-RPC de Python, pero la API externa se puede usar desde
cualquier lenguaje de programación. De hecho, los documentos oficiales
proporcionan ejemplos de código para Java, PHP y Ruby.
378
379 Existen varias bibliotecas para manejar XML-RPC o JSON-RPC, algunas
genéricas y otras específicas para usar con Odoo. Tratamos de no
señalar ninguna bibliotecas en particular, a excepción de erppeek, ya
que no sólo es un envoltorio probado para el Odoo / OpenERP XML-RPC,
sino porque también es una herramienta invaluable para la gestión de
servidores remotos y la inspección.
380
381 Hasta ahora, utilizamos nuestras instancias de servidor Odoo para
desarrollo y pruebas. Pero para tener un servidor de grado de
producción, hay configuraciones adicionales de seguridad y optimización
que deben hacerse. En el próximo capítulo, nos centraremos en ellos.
382
383
384

Page 10
capitulo-13.md 07/02/2018

1 # Capitulo 13. Lista de Verificación de despliegue - Levantar el servidor


2
3 En este capítulo, aprenderás como preparar tu ervidor odoo para su uso
en un entorno de producción.
4
5 Existen muchas posibles estrategias y herramientas que pueden ser
usadas para desplegar y administrar un servidor odoo de producción. Te
guiaremos en uno de las formas de hacerlo.
6
7 Para la configuración del servidor seguiremos la siguiente lista de
verificación:
8
9 + Instalar las dependencias y crear el usuario dedicado dedicado para
correr el servidor.
10 + Instalar odoo desde el código Fuente.
11 + Preparar el archivo de configuración de odoo.
12 + Preparar los workers de multiprocesamiento.
13 + Preparar el proxy reverso con soporte SSL.
14
15 Vamos a empezar.
16
17 ## Paquetes pre-construidos disponibles
18
19 Odoo cuenta con un paquete Debian/Ubuntu disponible para su
instalación, instalandolo obtendrás un servidor odoo funcional que
inicia automaticamente al arranque del sistema. Este proceso de
instalación es sencillo y puedes encontrar todo lo que necesario en
[https://nightly.odoo.com]https://nightly.odoo.com). También puedes
encontrar las compilaciones rpm para CentOS y los instaladores .exe.
20
21
22 Aunque esta es una forma fácil y conveniente de instalar Odoo, la
mayoría de los integradores prefieren implementar y ejecutar código
fuente controlado por versiones. Esto proporciona un mejor control
sobre lo que se despliega y facilita la administración de cambios y
arreglos una vez en producción.
23
24 ## Instalando Dependencias
25
26 Cuando usas una distribución de Debian, por defecto tu inicio de sesión
es `root` con poderes de administrador, y el símbolo del sistema
muestra #. Al usar Ubuntu, el registro con la cuenta raíz está
deshabilitado, y el usuario inicial configurado durante el proceso de
instalación es un `sudoer`, lo que significa que se le permite usar el
comando sudo para ejecutar comandos con privilegios de root.
27
28 En primer lugar, debemos actualizar el índice del paquete y luego
realizar una actualización para asegurarnos de que todos los programas
instalados estén actualizados:
29
30 ```
31 $ sudo apt-get update
32
33 $ sudo apt-get upgrade -y
34 ```
35
36 A continuación, vamos a instalar la base de datos PostgreSQL, y hacer
que nuestro usuario un superusuario de base de datos:
37
38 ```
39 $ sudo apt-get install postgresql -y
Page 1
capitulo-13.md 07/02/2018

40
41 $ sudo su -c "createuser -s $(whoami)" postgres
42 ```
43
44 Ejecutaremos Odoo desde el código fuente, pero antes de eso necesitamos
instalar las dependencias requeridas. Estos son los paquetes Debian
necesarios:
45
46 ```
47 $ sudo apt-get install git python-pip python2.7-dev -y
48
49 $ sudo apt-get install libxml2-dev libxslt1-dev
50
51 libevent-dev \
52 libsasl2-dev libldap2-dev libpq-dev
53
54
55 libpng12-dev libjpeg-dev \
56 poppler-utils
57
58
59 node-less node-clean-css -y
60
61 ```
62 No debemos olvidarnos de instalar wkhtmltox, que es necesario para
imprimir informes:
63
64 ```
65 $ wget
http://nightly.odoo.com/extra/wkhtmltox-0.12.1.2_linux-jessie-amd64.deb
66
67 $ sudo dpkg -i wkhtmltox-0.12.1.2_linux-jessie-amd64.deb
68
69 $ sudo apt-get -fy install
70 ```
71
72 Las instrucciones de instalación informarán un error de dependencias
faltantes, pero el último comando obliga a la instalación de esas
dependencias y termina correctamente la instalación.
73
74 Ahora solo nos faltan los paquetes de Python requeridos por Odoo.
Muchos de ellos también tienen paquetes para Debian / Ubuntu. El
paquete oficial de instalación de Debian los usa, y puedes encontrar
los nombres de los paquetes en el código fuente Odoo, en el archivo
`debian/control`.
75
76 Sin embargo, estas dependencias de Python también se pueden instalar
directamente desde el **Python Package Index (PyPI)**. Esto es más
amigable para aquellos que prefieren instalar Odoo en `virtualenv`. La
lista de paquetes requerida se encuentra en el archivo
`requirements.txt` de Odoo, como es habitual en los proyectos basados
en Python. Podemos instalarlos con los siguientes comandos:
77 ```
78 $ sudo -H pip install --upgrade pip # Ensure pip latest version
79
80 $ wget https://raw.githubusercontent.com/odoo/odoo/10.0/requirements.txt
81
82 $ sudo -H pip install -r requirements.txt
83 ```
84
85 Ahora que tenemos todas las dependencias instaladas, servidor de bases
Page 2
capitulo-13.md 07/02/2018

de datos, paquetes de sistema y paquetes de Python, podemos instalar


Odoo.
86
87 ## Preparación de un usuario de sistema dedicado
88
89 Una buena práctica de seguridad es ejecutar Odoo con un usuario
dedicado, sin privilegios especiales en el sistema.
90
91 Necesitamos crear el sistema y los usuarios de la base de datos para
eso. Podemos nombrarlos odoo-prod, por ejemplo
92
93 ```
94 $ sudo adduser --disabled-password --gecos "Odoo" odoo
95
96 $ sudo su -c "createuser odoo" postgres
97
98 $ createdb --owner=odoo odoo-prod
99 ```
100
101 Aquí, odoo es el nombre de usuario y odoo-prod es el nombre de la base
de datos que soporta nuestra instancia de Odoo.
102
103 Ten en cuenta que estos son usuarios regulares sin privilegios de
administración. Se crea automáticamente un directorio para el nuevo
usuario del sistema. En este ejemplo, es `/home/odoo`, y el usuario
puede referirse a su propio directorio personal con el símbolo `~` de
acceso directo. Lo usaremos para las configuraciones y archivos
específicos de Odoo.
104
105 Podemos abrir una sesión como este usuario utilizando el siguiente
comando:
106
107 ```
108 $ sudo su odoo
109 ```
110 El comando `exit` termina esa sesión y regresa a nuestro usuario
original.
111
112 ## Instalación desde el código fuente
113
114 Tarde o temprano, su servidor necesitará actualizaciones y parches. Un
repositorio controlado por versiones puede ser de gran ayuda cuando
llegue el momento. Utilizamos git para obtener nuestro código de un
repositorio, al igual que lo hicimos para instalar el entorno de
desarrollo.
115
116 A continuación, suplantaremos al usuario odoo y descargamos el código
en su directorio personal:
117
118 ```
119 $ sudo su odoo
120
121 $ git clone https://github.com/odoo/odoo.git /home/odoo/odoo-10.0 -b
10.0
122 --depth=1
123 ```
124
125
126 La opción -b asegura que obtengamos la rama correcta, y la opción
--depth = 1 ignora el historial de cambios y recupera sólo la última
revisión de código, haciendo la descarga mucho más pequeña y más rápida.
Page 3
capitulo-13.md 07/02/2018

127
128 ## Consejo
129
130 Git seguramente será una herramienta invaluable para administrar las
versiones de tus implementaciones Odoo. Acabamos de rayar la superficie
de lo que se puede hacer para administrar las versiones de código. Si
no está familiarizado con Git, vale la pena aprender más sobre él. Un
buen punto de partida es
[http://git-scm.com/doc](http://git-scm.com/doc).
131
132 Por ahora deberíamos tener todo lo necesario para ejecutar Odoo desde
la fuente. Podemos comprobar que se inicia correctamente y luego salir
de la sesión del usuario dedicado:
133
134 ```
135 $ /home/odoo/odoo-10.0/odoo-bin --help
136
137 $ exit
138 ```
139
140
141 A continuación, configuraremos algunos archivos y directorios a nivel
de sistema para ser usado por el servicio del sistema.
142
143 ## Configuración del archivo de configuración
144
145 Si se agrega la opción `--save` al iniciar un servidor Odoo, se guarda
la configuración utilizada en el archivo `~/.odoorc`. Podemos utilizar
el archivo como punto de partida para nuestra configuración de
servidor, que se almacenará en `/etc/odoo`, como se muestra en el
siguiente código:
146
147 ```
148 $ sudo su -c "~/odoo-10.0/odoo-bin -d odoo-prod --save
--stop-after-init" odoo
149 ```
150
151 Esto tendrá los parámetros de configuración que usará nuestra instancia
del servidor.
152
153 ## Consejo
154
155 El archivo de configuración anterior .openerp_serverrc sigue siendo
compatible y se utiliza si se encuentra. Esto puede causar cierta
confusión al configurar Odoo 10 en una máquina que también se utilizó
para ejecutar versiones anteriores de Odoo. En este caso, la opción
`--save` podría estar actualizando el archivo .openerp_serverrc en vez
de .odoorc.
156
157 A continuación, tenemos que colocar el archivo de configuración en la
ubicación esperada:
158
159 ```
160 $ sudo mkdir /etc/odoo
161
162 $ sudo cp /home/odoo/.odoorc /etc/odoo/odoo.conf
163
164 $ sudo chown -R odoo /etc/odoo
165 ```
166
167 También debemos crear el directorio donde el servicio Odoo almacenará
Page 4
capitulo-13.md 07/02/2018

sus archivos de registro. Se espera que esto esté dentro de `/var/log`:


168
169 ```
170 $ sudo mkdir /var/log/odoo
171
172 $ sudo chown odoo /var/log/odoo
173
174 ```
175
176 Ahora debemos asegurarnos de configurar algunos parámetros importantes.
Estos son los valores sugeridos para los más importantes:
177
178 ```
179 [options]
180 addons_path = /home/odoo/odoo-10.0/odoo/addons,
/home/odoo/odoo-10.0/addons
181 admin_passwd = False
182 db_user = odoo-prod
183 dbfilter = ^odoo-prod$
184 logfile = /var/log/odoo/odoo-prod.log
185 proxy_mode = True
186 without_demo = True
187 workers = 3
188 xmlrpc_port = 8069
189 ```
190 Vamos a explicarles:
191
192 + `Addons_path` es una lista separada por comas de las rutas donde se
buscarán módulos complementarios. Se lee de izquierda a derecha, con
los directorios de la izquierda que tienen una prioridad más alta.
193 + `Admin_passwd` es la contraseña maestra para acceder a las funciones
de administración de la base de datos del cliente web. Es fundamental
establecer esto con una contraseña fuerte o, mejor aún, establecerla en
Falso para desactivar la función.
194 + `Db_user` la instancia de la base de datos a inicializar durante la
secuencia de inicio del servidor.
195 + `Dbfilter` es un filtro para que las bases de datos sean accesibles.
Es una expresión regex interpretada en Python. Para que el usuario no
se le pida que seleccione una base de datos y para que las URL no
autenticadas funcionen correctamente, debe establecerse con `^dbname$`,
por ejemplo, `dbfilter=^odoo-prod$`. Es compatible con los marcadores
de posición `%h` y `%d`, que se reemplazan por el nombre de host de
solicitud HTTP y el nombre de subdominio.
196 + `logfile` es donde se debe escribir el registro del servidor. Para
los servicios del sistema, la ubicación esperada está dentro de
`/var/log`. Si se deja vacío o se establece en False, la impresión de
registro se imprimirá en la salida estándar.
197 `Proxy_mode` debe establecerse en True cuando se accede a Odoo detrás
de un proxy inverso, como lo haremos.
198 + `Sin_demo` se debe establecer en True en entornos de producción para
que las nuevas bases de datos no tengan datos de demostración en ellos.
199 + `workers` con un valor de dos o más permite el modo de
multiprocesamiento. Discutiremos esto en más detalle en un momento.
200 + `xmlrpc_port` es el número de puerto en el que el servidor escuchará.
De forma predeterminada, se utiliza el puerto 8069.
201
202 Los siguientes parámetros también pueden ser útiles:
203
204 + `Data_dir` es la ruta donde se almacenan los datos de sesión y los
archivos adjuntos. Recuerde tener copias de seguridad de esto.
205
Page 5
capitulo-13.md 07/02/2018

206 + `xmlrpc-interface` establece las direcciones que serán escuchadas.


Por defecto, escucha todos los 0.0.0.0, pero cuando se utiliza un proxy
inverso, se puede establecer en 127.0.0.1 con el fin de responder sólo
a las solicitudes locales.
207
208 Podemos comprobar el efecto de los ajustes realizados ejecutando el
servidor con la opción `-c` o `--config` de la siguiente manera:
209
210 ```
211 $ Sudo su -c "~ / odoo-10.0 / odoo-bin -c /etc/odoo/odoo.conf" odoo
212 ```
213
214 Ejecutar Odoo con la configuración anterior no mostrará ninguna salida
a la consola, ya que se está escribiendo en el archivo de registro
definido en el archivo de configuración. Para seguir lo que está
sucediendo con el servidor necesitamos abrir otra ventana de terminal,
y ejecutar allí:
215 ```
216 $ Sudo tail -f /var/log/odoo/odoo-prod.log
217 ```
218
219 También es posible utilizar el archivo de configuración y aún así forzar
220 imprimir la salida en la consola, agregando la opción `--logfile =
False`, como esto:
221
222 ```
223 $ Sudo su -c "~ / odoo-10.0 / odoo-bin -c /etc/odoo/odoo.conf --logfile
= False" odoo
224 ```
225
226 ## Workers multiprocesamiento
227
228 Se espera que una instancia de producción maneje una carga de trabajo
significativa. De forma predeterminada, el servidor ejecuta un proceso
y sólo puede utilizar un núcleo de CPU para su procesamiento, debido al
lenguaje GIL de Python. Sin embargo, un modo multiproceso está
disponible para que las solicitudes concurrentes puedan ser manejadas.
La opción `workers = N` establece el número de procesos de trabajo a
utilizar. Como pauta, puede intentar establecerla en **1 + 2 * P**,
donde P es el número de procesadores. El mejor ajuste a utilizar debe
ser ajustado para cada caso, ya que depende de la carga del servidor y
qué otros servicios de carga intensiva se ejecutan en el servidor, como
PostgreSQL.
229
230 Es mejor establecer trabajadores demasiado altos para la carga que
demasiado bajos. El mínimo debe ser 6 debido a las conexiones paralelas
utilizadas por la mayoría de los navegadores, y el máximo se limita
generalmente por la cantidad de RAM en la máquina.
231
232 Hay unos pocos parámetros de configuración de `limit- *` para
sintonizar a los trabajadores. Los trabajadores son reciclados cuando
alcanzan estos límites, el proceso correspondiente se detiene y se
inicia uno nuevo. Esto protege al servidor de fugas de memoria y de
procesos particulares que sobrecargan los recursos del servidor.
233
234 La documentación oficial ya proporciona buenos consejos sobre la
afinación de los parámetros de los trabajadores, y puedes referirte a
ella Para obtener más detalles, en
[https://www.odoo.com/documentation/10.0/setup/deploy.html](https://www.o
doo.com/documentation/10.0/setup/deploy.html.
235
Page 6
capitulo-13.md 07/02/2018

236 ## Configuración como servicio del sistema


237
238 A continuación, queremos configurar Odoo como un servicio del sistema y
que se inicie automáticamente cuando arranque el sistema.
239
240 En Ubuntu / Debian, el sistema `init` es responsable, para iniciar los
servicios. Históricamente, Debian (y los sistemas operativos derivados)
han utilizado `sysvinit` y Ubuntu ha utilizado un sistema compatible
llamado `Upstart`. Recientemente, esto ha cambiado, y el sistema de
`init` utilizado en la última versión es ahora `systemd`.
241
242 Esto significa que hay dos maneras diferentes de instalar un servicio
del sistema, y necesitas escoger el correcto dependiendo de la versión
de tu sistema operativo.
243
244 En la última versión estable de Ubuntu, 16.04, deberíamos usar
`systemd`. Sin embargo, las versiones más antiguas, como 14.04, todavía
se utilizan en muchos proveedores de nube, por lo que existe una buena
probabilidad de que necesites usarlo.
245
246 Para comprobar si `systemd` se utiliza en su sistema, pruebe este
comando:
247 ```
248 $ man init
249 ```
250
251 Esto abre la documentación del sistema init actualmente utilizado, y
podrá comprobar qué se está utilizando.
252
253 ## Creación de un servicio systemd
254
255 Si el sistema operativo que está utilizando es reciente, como Debian 8
o Ubuntu 16.04, debería usar `systemd` para el `init` del sistema.
256
257 Para agregar un nuevo servicio al sistema, solo necesitamos crear un
archivo que lo describa. Crea un archivo
`/lib/systemd/system/odoo.service` con el siguiente contenido:
258 ```
259 [Unit]
260 Description=Odoo
261 After=postgresql.service
262
263 [Service]
264 Type=simple
265 Usuario=odoo
266 Group=odoo
267 ExecStart=/home/odoo/odoo-10.0/odoo-bin -c /etc/odoo/odoo.conf
268
269 [Install]
270 WantedBy=multi-user.target
271 ```
272 A continuación, debemos registrar el nuevo servicio:
273
274 ```
275 $ Sudo systemctl habilitar odoo.service
276 ```
277
278 Para iniciar el nuevo servicio ejecuta el siguiente comando:
279
280 ```
281 sudo systemctl odoo start
Page 7
capitulo-13.md 07/02/2018

282 ```
283
284 Y para verificar su status corre esto:
285
286 ```
287 $ sudo systemctl odoo status
288 ```
289 Finalmente si deses deterner el servicio puede hacerlo con este comenado:
290
291 ```
292 $ sudo systemctl odoo stop
293 ```
294
295 ## Creación de un servicio Upstart / sysvinit
296
297 Si está utilizando un sistema operativo más antiguo, como Debian 7,
Ubuntu 15.04 o incluso 14.04, es probable que su sistema sea `sysvinit`
en `Upstart`. Para este propósito, ambos deben comportarse de la misma
manera. Muchos servicios VPS en la nube todavía se basan en imágenes de
Ubuntu 14.04, por lo que este podría ser un escenario que puede
encontrar al implementar su servidor Odoo.
298
299 Muchos servicios VPS en la nube todavía se basan en imágenes de Ubuntu
14.04, por lo que este podría ser un escenario que puede encontrar al
implementar su servidor Odoo.
300
301 El código fuente de Odoo incluye un script de inicio utilizado para la
distribución empaquetada de Debian. Podemos utilizarlo como nuestro
script `init` de servicio con pequeñas modificaciones, de la siguiente
manera:
302
303 ```
304 $ sudo cp /home/odoo/odoo-10.0/debian/init /etc/init.d/odoo
305
306 $ sudo chmod +x /etc/init.d/odoo
307 ```
308
309 En este punto, puede que desees comprobar el contenido del script
`init`. Los parámetros clave se asignan a las variables en la parte
superior del archivo. Un ejemplo de esto:
310
311 ```
312 PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
313 DAEMON=/usr/bin/odoo
314 NAME=odoo
315 DESC=odoo
316 CONFIG=/etc/odoo/odoo.conf
317 LOGFILE=/var/log/odoo/odoo-server.log
318 PIDFILE=/var/run/${NAME}.pid
319 USER=odoo
320 ```
321
322 Estas variables deben ser adecuadas y prepararemos el resto de la
configuración con sus valores por defecto en mente. Pero, por supuesto,
puedes cambiarlos para que se adapten mejor a tus necesidades.
323
324 La variable `USER` es el usuario del sistema en el que se ejecutará el
servidor. Ya hemos creado el usuario odoo esperado.
325
326 La variable `DAEMON` es la ruta al ejecutable del servidor. Nuestro
ejecutable real para iniciar Odoo está en un lugar diferente, pero
Page 8
capitulo-13.md 07/02/2018

podemos crear un enlace simbólico a él:


327
328 ```
329 $ sudo ln -s /home/odoo/odoo-10.0/odoo-bin /usr/bin/odoo
330
331 $ sudo chown -h odoo /usr/bin/odoo
332 ```
333
334 La variable `CONFIG` es el archivo de configuración que se va a usar.
En una sección anterior, creamos un archivo de configuración en la
ubicación predeterminada: /etc/odoo/odoo.conf.
335
336 Finalmente, la variable `LOGFILE` es el directorio donde se deben
almacenar los archivos de registro. El directorio esperado es
`/var/log/odoo` que creamos cuando estábamos definiendo el archivo de
configuración.
337
338 Ahora debemos ser capaces de iniciar y detener nuestro servicio Odoo de
la siguiente manera:
339
340 ```
341 $ sudo /etc/init.d/odoo start
342
343 Starting odoo: ok
344
345 ```
346
347 La interrupción del servicio se realiza de manera similar, como se
muestra a continuación:
348
349 ```
350 $ Sudo /etc/init.d/odoo stop
351
352 Stopping odoo: ok
353
354 ```
355
356 En Ubuntu, también se puede usar el comando `service`:
357
358 ```
359 $ sudo service odoo start
360
361
362 $ sudo service odoo status
363
364
365 $ sudo service odoo config
366 ```
367
368
369 Ahora solo necesitamos hacer que este servicio se inicie
automáticamente en el arranque del sistema:
370
371 ```
372 $ Sudo update-rc.d odoo defaults
373 ```
374
375 Después de esto, cuando reiniciamos nuestro servidor, el servicio Odoo
debe iniciarse automáticamente y sin errores. Es un buen momento para
comprobar que todo está funcionando como se esperaba.
376
Page 9
capitulo-13.md 07/02/2018

377 ## Comprobación del servicio Odoo desde la línea de comandos


378
379 En este punto, podríamos confirmar si nuestra instancia de Odoo está
lista y responde a las solicitudes.
380
381 Si Odoo se está ejecutando correctamente, ahora deberíamos ser capaces
de obtener una respuesta de él y no ver ningún error en el archivo de
registro. Podemos comprobar dentro del servidor si Odoo está
respondiendo a las peticiones HTTP utilizando el siguiente comando:
382
383 ```
384 $ Curl http: // localhost: 8069
385
386 <Html> <head> <script> window.location = '/ web' + ubicación.hash; </
script> </ head> </ html>
387 ```
388
389 Y para ver lo que está en el archivo de registro podemos utilizar lo
siguiente:
390
391 ```
392 $ Sudo less /var/log/odoo/odoo-server.log
393
394 ```
395 En el caso de que estés empezando con Linux, te gustaría saber que
puedes seguir lo que está sucediendo en el archivo de registro usando
tail -f:
396
397 ```
398 $ Sudo tail -f /var/log/odoo/odoo-server.log
399 ```
400
401 ## Uso de un proxy inverso
402
403 Mientras que Odoo en si mismo puede servir páginas web, se recomienda
encarecidamente tener un proxy inverso delante de él. Un proxy inverso
actúa como un intermediario que gestiona el tráfico entre los clientes
que envían solicitudes y los servidores Odoo que responden a ellos. El
uso de un proxy inverso tiene varios beneficios.
404
405 En el lado de la seguridad, puede hacer lo siguiente:
406
407 + Manejar (y hacer cumplir) los protocolos HTTPS para cifrar el tráfico.
408 + Ocultar las características de la red interna.
409 + Actuar como un cortafuegos de aplicacion que limita las URL aceptadas
para el procesamiento
410
411 Y en el lado del rendimiento, puede proporcionar mejoras significativas:
412
413 + Guardar cache contenido estático, reduciendo así la carga en los
servidores Odoo.
414 + Comprimir contenido para acelerar los tiempos de carga.
415 + Actuar como un equilibrador de carga, la distribución de carga entre
varios servidores.
416
417 Apache es una opción popular para usar como un proxy inverso. Nginx es
una alternativa reciente con buenos argumentos técnicos. Aquí,
elegiremos usar Nginx como un proxy inverso y mostraremos cómo puede
usarse para realizar las funciones de seguridad y rendimiento
mencionadas aquí.
418
Page 10
capitulo-13.md 07/02/2018

419 ## Configuración de Nginx para proxy inverso


420
421 Primero, debemos instalar Nginx. Queremos que escuche en los puertos
HTTP predeterminados, por lo que debemos asegurarnos de que ya no los
hayan tomado otros servicios. La ejecución de este comando debe
producir un error, de la siguiente manera:
422
423 ```
424 $ Curl http: // localhost
425
426 Curl: (7) Error al conectar al puerto localhost 80: Conexión rechazada
427 ```
428 De lo contrario, debes desactivar o eliminar dicho servicio para
permitir que Nginx utilice dichos puertos. Por ejemplo, para detener un
servidor Apache existente, utiliza este comando:
429
430 ```
431 $ sudo service apache2 stop
432 ```
433
434 O mejor aún, debe considerar eliminarlo del sistema o reconfigurarlo
para escucharlo en otro puerto, por lo que los puertos HTTP y HTTPS (80
y 443) pueden ser utilizados por Nginx.
435
436 Ahora podemos instalar Nginx, que se hace de la manera esperada:
437
438 ```
439 $ sudo apt-get install nginx
440 ```
441
442 Para confirmar que está funcionando correctamente, debemos ver una
página Bienvenido a nginx cuando visite la dirección del servidor con
un navegador o usando `curl http://localhost` dentro de nuestro servidor.
443
444 Los archivos de configuración de Nginx siguen el mismo enfoque que
Apache: se almacenan en `/etc/nginx/available-sites/` y se activan
agregando un enlace simbólico en `/etc/nginx/enabled-sites/`. También
debemos deshabilitar la configuración predeterminada proporcionada por
la instalación de Nginx, de la siguiente manera:
445
446 ```
447 $ sudo rm/etc/nginx/sites-enabled/default
448
449 $ sudo touch /etc/nginx/sites-available/odoo
450
451 $ sudo ln -s /etc/nginx/sites-available/odoo
/etc/nginx/sites-enabled/odoo
452 ```
453
454
455 Utilizando un editor, como nano o vi, debemos editar nuestro archivo de
configuración de Nginx de la siguiente manera:
456
457 ```
458 $ sudo nano/etc/nginx/sites-available/odoo
459 ```
460
461 Primero, agregamos los `upstreams`, y los servidores `backend`, Nginx
redirigirá el tráfico al servidor Odoo en nuestro caso, que está
escuchando en el puerto 8069, de la siguiente manera:
462
Page 11
capitulo-13.md 07/02/2018

463 ```
464 Upstream backend-odoo {
465 Servidor 127.0.0.1:8069;
466 }
467 Servidor {
468 ubicación / {
469 Proxy_pass http: // backend-odoo;
470 }
471 }
472 ```
473
474 Para probar si la configuración editada es correcta, utilice el
siguiente comando:
475
476 ```
477 $ sudo nginx -t
478 ```
479
480 Si encuentra errores, confirme que el archivo de configuración está
correctamente escrito. Además, un problema común es que el HTTP
predeterminado sea tomado por otro servicio, como Apache o el sitio web
predeterminado de Nginx. Compruebe las instrucciones dadas antes para
asegurarse de que este no es el caso, luego reinicie Nginx. Después de
esto, podemos tener Nginx para recargar la nueva configuración de la
siguiente manera:
481
482 ```
483 $ sudo /etc/init.d/nginx reload
484
485 ```
486
487
488 Ahora podemos confirmar que Nginx está redirigiendo el tráfico al
servidor backend Odoo:
489
490 ```
491 $ curl http://localhost
492
493 <Html> <head> <script> window.location = '/ web' + ubicación.hash; </
script> </ head> </ html>
494 ```
495
496 ## Aplicación de HTTPS
497
498 A continuación, debemos instalar un certificado para poder usar SSL.
Para crear un certificado autofirmado, siga estos pasos:
499
500 ```
501 $ sudo mkdir /etc/nginx/ssl && cd/etc/nginx/ssl
502
503 $ sudo openssl req -x509 -nuevo rsa: 2048 -keyout key.pem -out cert.pem
-days 365 -nodos
504
505 $ sudo chmod a-wx * # hacer que los archivos sean de solo lectura
506
507
508 $ sudo chown www-data:root * # acceso sólo al grupo www-data
509 ```
510
511 Esto crea un directorio `ssl/` dentro del directorio `/etc/nginx/` y
crea un certificado SSL auto-firmado sin contraseña. Al ejecutar el
Page 12
capitulo-13.md 07/02/2018

comando openssl, se le pedirá información adicional y se generarán un


certificado y archivos clave. Finalmente, la propiedad de estos
archivos se da al usuario www-data utilizado para ejecutar el servidor
web.
512
513 **Nota**
514
515 El uso de un certificado autofirmado puede plantear algunos riesgos de
seguridad, como los ataques de hombre en el medio, e incluso puede no
ser permitido por algunos navegadores. Para obtener una solución
robusta, debe utilizar un certificado firmado por una autoridad de
certificación reconocida. Esto es particularmente importante si está
ejecutando un sitio web comercial o de comercio electrónico.
516
517 Ahora que tenemos un certificado SSL, estamos listos para configurar
Nginx para usarlo.
518
519 Para hacer cumplir HTTPS, redirigiremos todo el tráfico HTTP hacia él.
Reemplace la directiva de servidor que definimos anteriormente con lo
siguiente:
520
521 ```
522 Servidor {
523 listen 80;
524 Add_header Strict-Transport-Security max-age = 2592000;
525 rewrite ^/.* $ Https://$host$request_uri? permanent;
526 }
527 ```
528
529 Si volvemos a cargar la configuración de Nginx ahora y accedemos al
servidor con un navegador web, veremos que la dirección `http://` se
convertirá en una dirección `https://`.
530
531 Pero no devolverá ningún contenido antes de configurar correctamente el
servicio HTTPS añadiendo la siguiente configuración de servidor:
532
533 ```
534 server {
535 listen 443 default;
536 # ssl settings
537 ssl on;
538 ssl_certificate /etc/nginx/ssl/cert.pem;
539 ssl_certificate_key /etc/nginx/ssl/key.pem;
540 keepalive_timeout 60;
541 # proxy header and settings
542 Proxy_set_header Host $host;
543 Proxy_set_header X-Real-IP $remote_addr;
544 Proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for;
545 proxy_set_header X-Forwarded-Proto $scheme;
546 proxy_redirect off;
547
548 location / {
549 proxy_pass http://backend-odoo;
550 }
551 }
552 ```
553
554 Esto escuchará el puerto HTTPS y utilizará los archivos
`/etc/nginx/ssl/ certificate` para cifrar el tráfico. También agregamos
cierta información al encabezado de la solicitud para que el servicio
de back-end de Odoo sepa que está siendo procesado.
Page 13
capitulo-13.md 07/02/2018

555
556 Por razones de seguridad, es importante que Odoo se asegure de que el
parámetro proxy_mode esté establecido en True. La razón de esto es que,
cuando Nginx actúa como un proxy, toda la solicitud parecerá venir del
propio servidor en lugar de la dirección IP remota. Establecer el
encabezado X-Forwarded-For en el proxy y habilitar --proxy-mode
resuelve eso. Pero activar el modo -proxy sin forzar este encabezado en
el nivel de proxy permitiría a cualquiera falsificar su dirección remota.
557
558 Al final, la directiva de ubicación define que todas las solicitudes se
pasan al backend-odoo upstream.
559
560 Vuelve a cargar la configuración y deberíamos tener nuestro servicio
Odoo trabajando a través de HTTPS, como se muestra en los siguientes
comandos:
561
562 ```
563 $ sudo nginx -t
564
565 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
566
567 nginx: configuration file /etc/nginx/nginx.conf test is successful
568
569 $ sudo service nginx reload
570
571 * Reloading nginx configuration nginx
572
573
574 ...done.
575
576
577 $ curl -k https://localhost
578
579
580 <html><head><script>window.location = '/web' +
location.hash;</script></head></html>
581 ```
582
583 La última salida confirma que el cliente web Odoo está siendo
distribuido a través de HTTPS.
584
585 ## Consejo
586
587 Para el caso particular en el que se está usando un Odoo POSBox,
necesitamos agregar una excepción para que el /pos/URL pueda acceder a
él en modo HTTP. El POSBox se encuentra en la red local pero no tiene
SSL habilitado. Si la interfaz POS está cargada en HTTPS, no podrá
contactar al POSBox.
588
589 ## Optimizaciones de Nginx
590
591 Ahora, es tiempo para algunos ajuste fino de los ajustes de Nginx. Se
recomiendan para habilitar el búfer de respuesta y la compresión de
datos que deberían mejorar la velocidad del sitio web. También
establecemos una ubicación específica para los registros.
592
593 Las siguientes configuraciones deben agregarse dentro del servidor que
escucha en el puerto 443, por ejemplo, justo después de las
definiciones de proxy:
594
595 ```
Page 14
capitulo-13.md 07/02/2018

596 # Archivos de registro odoo


597 Access_log /var/log/nginx/odoo-access.log;
598 Error_log /var/log/nginx/odoo-error.log;
599 # Aumentar el tamaño del búfer proxy
600 Proxy_buffers 16 64k;
601 Proxy_buffer_size 128k;
602 # forzar Timeouts si el backend muere
603 Proxy_next_upstream error timeout invalid_header http_500 http_502
http_503;
604 # Activar la compresión de datos
605 Gzip on
606 Gzip_min_length 1100;
607 Gzip_buffers 4 32k;
608 Gzip_types text/plain text /xml text/css texto/less
application/x-javascript application/xml application/json
application/javascript;
609 Gzip_vary on;
610 ```
611 También podemos activar el almacenamiento en caché de contenido
estático para respuestas más rápidas a los tipos de solicitudes
mencionadas en el ejemplo de código anterior y para evitar su carga en
el servidor Odoo. Después de la sección `location`, agrega la
siguiente segunda sección de ubicación:
612
613 ```
614 location~ * /web/static/ {
615 # Cache de datos estáticos
616 Proxy_cache_valid 200 60m;
617 Proxy_buffering on;
618 Expira 864000;
619 Proxy_pass http: // backend-odoo;
620 }
621 ```
622 Con esto, los datos estáticos se almacenan en caché durante 60 minutos.
Las solicitudes adicionales sobre esas peticiones en ese intervalo
serán respondidas directamente por Nginx desde la caché.
623
624 ## Long Polling
625
626 El long polling se usa para soportar la aplicación de mensajería
instantánea y, cuando se utilizan trabajadores multiprocesamiento, se
maneja en un puerto independiente, que es 8072 por defecto.
627
628 Para nuestro proxy inverso, esto significa que las peticiones de long
polling se deben pasar a este puerto. Para apoyar esto, necesitamos
agregar un nuevo upstream a nuestra configuración de Nginx, como se
muestra en el siguiente código:
629
630 ```
631 Upstream backend-odoo-im {server 127.0.0.1:8072; }
632 ```
633 A continuación, debemos agregar otra ubicación al servidor que maneja
las solicitudes HTTPS, como se muestra en el código siguiente:
634
635 ```
636 location /longpolling {proxy_pass http:// backend-odoo-im;}
637 ```
638
639 Con estos ajustes, Nginx debe pasar estas solicitudes al puerto
adecuado del servidor Odoo.
640
Page 15
capitulo-13.md 07/02/2018

641 ## Actualizaciones de servidor y módulos


642
643 Una vez que el servidor Odoo esté listo y funcionando, llegará el
momento en que necesitará instalar actualizaciones en Odoo. Esto
implica dos pasos: primero, obtener las nuevas versiones del código
fuente (servidor o módulos), y segundo, instalarlas.
644
645 Si has seguido el método descrito en la sección Instalación desde el
código fuente, podemos buscar y probar las nuevas versiones en el
repositorio git. Se recomienda encarecidamente que haga una copia de la
base de datos de producción y pruebe la actualización en ella. Si
odoo-prod es su base de datos de producción, esto podría hacerse con
los siguientes comandos:
646
647 ```
648 $ dropdb odoo-stage; createdb odoo-stage
649
650
651 $ pg_dump odoo-prod | psql -d odoo-stage
652
653
654 $ sudo su odoo
655
656 $ cd ~/.local/share/Odoo/filestore/
657
658 $ cp -al odoo-prod odoo-stage
659
660 $ ~/Odoo-10.0/odoo-bin -d odoo-stage --xmlrpc-port = 8080 -c
/etc/odoo/odoo.conf
661
662 $ exit
663
664 ```
665
666
667 Si todo va bien, debería ser seguro realizar la actualización en el
servicio de producción. Recuerde hacer una nota de la versión actual de
la referencia Git para poder volver atrás revisando esta versión de
nuevo. Mantener una copia de seguridad de la base de datos antes de
realizar la actualización también es muy recomendable.
668
669 ## Consejo
670
671 La copia de la base de datos se puede hacer de una manera mucho más
rápida, usando el siguiente comando `createdb: $ createdb --template
odoo-prod odoo-stage`
672
673 La advertencia aquí es que para que se ejecute no puede haber ninguna
conexión abierta a la base de datos odoo-prod, por lo que el servidor
Odoo debe detenerse para realizar la copia.
674
675 Después de esto, podemos cargar las nuevas versiones al repositorio de
producción usando Git y completar la actualización, como se muestra aquí
676
677 ```
678 $ sudo su odoo
679
680 $ cd ~/odoo-10.0
681
682 $ git pull
683
Page 16
capitulo-13.md 07/02/2018

684 $ exit
685
686 $ sudo service odoo restart
687 ```
688
689 En cuanto a la política de lanzamiento de Odoo, no se publican
versiones menores. Se espera que las ramas de GitHub representen la
última versión estable. Las construcciones nocturnas se consideran el
último lanzamiento oficial estable.
690
691 En la frecuencia de actualización, no hay ningún motivo para actualizar
con demasiada frecuencia, pero tampoco esperar un año entre
actualizaciones. Realizar una actualización cada pocos meses debería
estar bien. Y por lo general, un reinicio del servidor será suficiente
para habilitar las actualizaciones de código, y las actualizaciones de
módulos no deberían ser necesarias.
692
693 Por supuesto, si necesita una corrección de errores específica,
probablemente debería realizarse una actualización anterior. También
recuerde tener cuidado con las divulgaciones de errores de seguridad en
los canales públicos-GitHub Issues o en la lista de correo de la
comunidad. Como parte del servicio, los clientes de odoo entreprice
pueden esperar notificaciones por correo electrónico tempranas de este
tipo de problemas.
694
695 # Sumario
696
697 En este capítulo, aprendiste sobre los pasos adicionales para
configurar y ejecutar Odoo en un servidor de producción basado en
Debian. Se visitaron las configuraciones más importantes del archivo de
configuración y se aprendió cómo aprovechar el modo de
multiprocesamiento.
698
699 Para mejorar la seguridad y la escalabilidad, también aprendiste a usar
Nginx como un proxy inverso frente a nuestros procesos de servidor Odoo.
700
701 Esto debería cubrir lo esencial de lo que se necesita para ejecutar un
servidor Odoo y proporcionar un servicio estable y seguro a sus usuarios.
702
703 Para obtener más información sobre Odoo, también debe consultar la
documentación oficial en [ https://www.odoo.com/documentation](
https://www.odoo.com/documentation). Algunos temas se tratan con más
detalle allí, y también encontrará temas no cubiertos en este libro.
704
705 También hay otros libros publicados sobre Odoo que también puede ser
útil. Pack Publishing tiene algunos en su catálogo, y en particular el
Odoo Development Cookbook proporciona material más avanzado, cubriendo
más temas no discutidos aquí.
706
707 Y finalmente, Odoo es un producto de código abierto con una comunidad
vibrante. Involucrarse, hacer preguntas y contribuir es una gran manera
no sólo para aprender, sino también para construir una red de negocios.
Sobre esto no podemos dejar de mencionar la Odoo Community Association
(OCA), promoviendo la colaboración y la calidad del código abierto.
Puede obtener más información al respecto en
[odoo-comunity.org](odoo-comunity.org).

Page 17

Você também pode gostar