You are on page 1of 72

odoo

Building a Module

 
Iniciar / Detener el servidor Odoo
Odoo utiliza un / Arquitectura cliente-servidor en el que los clientes son los navegadores web que acceden al servidor
Odoo a través de RPC.La lógica de negocio y la extensión se realiza generalmente en el lado del servidor, aunque las
características del cliente de apoyo (por ejemplo, la nueva representación de datos tales como mapas interactivos)
puede añadirse al cliente.Con el fin de iniciar el servidor, basta con invocar el comando ​
odoo.py en la consola, la
adición de la ruta completa al archivo si es necesario:

$ ./odoo.py

El servidor es detenido por golpear Ctrl-C en dos ocasiones desde el terminal, o matando el proceso operativo
correspondiente.

Construir un módulo Odoo


Ambas extensiones de servidor y cliente se empaquetan como ​
módulos que están opcionalmente cargados en una
base de datos​
.
Módulos Odoo pueden o bien añadir nueva lógica de negocio de la marca a un sistema Odoo, o alterar y extender la
lógica de negocio existente: un módulo puede ser creado para agregar reglas de contabilidad de su país para el apoyo
contable genérico de Odoo, mientras que el siguiente módulo añade soporte para la visualización en tiempo real de
una flota de autobuses.
Todo en Odoo por lo tanto comienza y termina con módulos.

Composición de un módulo

Un módulo de Odoo puede contener una serie de elementos:

Los objetos de negocio


declaradas como clases de Python, estos recursos se conservan automáticamente por Odoo basado en su
configuración
Los archivos de datos
Archivos XML o CSV declarando metadatos (visitas o flujos de trabajo), los datos de configuración (módulos de
parametrización), datos de demostración y más
Controladores Web
Manejar las solicitudes de los navegadores web
Datos de la web estática


Imágenes, CSS o archivos javascript usados ​
por la interfaz web o sitio web

Estructura del módulo

Cada módulo es un directorio dentro de un ​


directorio de módulos . Directorios del módulo se especifican utilizando
--addons-path​
.

Tip ​
La mayoría de las opciones de línea de comandos también se pueden configurar mediante ​
un archivo de
configuración

manifiest​
Un módulo Odoo es declarado por su ​ o​ documentación de manifiesto​
. Consulte la ​ la información al respecto.

Un módulo es también un ​
paquete de Python con un archivo __init__.py, que contiene instrucciones de importación
de varios archivos de Python en el módulo.

Por ejemplo, si el módulo tiene un solo mymodule.py archivo __init__.py podría contener:

from . import mymodule

Odoo proporciona un mecanismo para ayudar a establecer un nuevo módulo, ​ s​


odoo.py tiene un subcomando ​caffold
para crear un módulo vacío:

$ odoo.py scaffold <module name> <where to put it>

El comando crea un subdirectorio para su módulo, y crea automáticamente un montón de archivos estándar para un
módulo. La mayoría de ellos contienen simplemente código comentado o XML. El uso de la mayor parte de esos
archivos se explicará a lo largo de este tutorial.

Ejercicio​
Creación del módulo

Utilice la línea de comandos anterior para crear un módulo vacío openacademy, e instalarlo en Odoo.

1. Invocar los comandos odoo.py scaffold openacademy addons


2. Adaptar el archivo de manifiesto a su módulo.
3. No te preocupes por los demás archivos.

openacademy/__openerp__.py

# -*- coding: utf-8 -*-


{
'name': "Open Academy",


'summary': """Manage trainings""",

'description': """
Open Academy module for managing trainings:
- training courses
- training sessions
- attendees registration
""",

'author': "Your Company",


'website': "http://www.yourcompany.com",

# Categories can be used to filter modules in modules listing


# Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
# for the full list
'category': 'Test',
'version': '0.1',

# any module necessary for this one to work correctly


'depends': ['base'],

# always loaded
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo.xml',
],
}

openacademy/__init__.py

# -*- coding: utf-8 -*-


from . import controllers
from . import models


openacademy/controllers.py

# -*- coding: utf-8 -*-


from openerp import http

# class Openacademy(http.Controller):
# @http.route('/openacademy/openacademy/', auth='public')
# def index(self, **kw):
# return "Hello, world"

# @http.route('/openacademy/openacademy/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('openacademy.listing', {
# 'root': '/openacademy/openacademy',
# 'objects': http.request.env['openacademy.openacademy'].search([]),
# })

# @http.route('/openacademy/openacademy/objects/<model("openacademy.openacademy"):obj>/',
auth='public')
# def object(self, obj, **kw):
# return http.request.render('openacademy.object', {
# 'object': obj
# })

openacademy/demo.xml

<openerp>
<data>
<!-- -->
<!-- <record id="object0" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 0</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object1" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 1</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object2" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 2</field> -->


<!-- </record> -->
<!-- -->
<!-- <record id="object3" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 3</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object4" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 4</field> -->
<!-- </record> -->
<!-- -->
</data>
</openerp>

openacademy/models.py

# -*- coding: utf-8 -*-

from openerp import models, fields, api

# class openacademy(models.Model):
# _name = 'openacademy.openacademy'

# name = fields.Char()

openacademy/security/ir.model.access.csv

id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_openacademy_openacademy,openacademy.openacademy,model_openacademy_openacademy,,1,0,0,0

openacademy/templates.xml

<openerp>
<data>
<!-- <template id="listing"> -->
<!-- <ul> -->
<!-- <li t-foreach="objects" t-as="object"> -->
<!-- <a t-attf-href="{{ root }}/objects/{{ object.id }}"> -->
<!-- <t t-esc="object.display_name"/> -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </template> -->


<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->
<!-- <t t-foreach="object._fields" t-as="field"> -->
<!-- <dt><t t-esc="field"/></dt> -->
<!-- <dd><t t-esc="object[field]"/></dd> -->
<!-- </t> -->
<!-- </dl> -->
<!-- </template> -->
</data>
</openerp>

Mapeo objeto-relacional

Un componente clave de Odoo es el ORM capa. Esta capa evita tener que escribir la mayoría de SQL con la mano y
proporciona servicios de extensibilidad y seguridad ​
[2]​
.
Los objetos de negocio son declarados como clases de Python extendiendo el ​
Modelo que los integra en el sistema de
persistencia automatizado.
Los modelos pueden ser configurados mediante el establecimiento de una serie de atributos bajo su definición. El
atributo más importante es ​
_name que se requiere y define el nombre para el modelo en el sistema Odoo. He aquí
una definición mínimamente completa de un modelo:

from openerp import models


class MinimalModel(models.Model):
_name = 'test.model'

Campos del Modelo

Los campos que se utilizan para definir lo que el modelo puede almacenar y dónde. Los campos se definen como
atributos de la clase del modelo:

from openerp import models, fields

class LessMinimalModel(models.Model):
_name = 'test.model2'
name = fields.Char()

Atributos comunes
Al igual que el modelo en sí, sus campos se pueden configurar, mediante el paso de configuración de atributos como
parámetros:


name = field.Char(required=True)

Algunos atributos están disponibles en todos los campos, estos son los más comunes:

string ( Unicode , por defecto: el nombre del campo)


La etiqueta del campo en la interfaz de usuario (visible por los usuarios).
required ( bool , por defecto: falso )
Si Verdadero , el campo no puede estar vacío, o bien debe tener un valor por defecto o que siempre se le da un
valor al crear un registro.
help ( Unicode , por defecto: '' )
De larga duración, proporciona una información sobre herramientas de ayuda a los usuarios en la interfaz de
usuario.
index ( bool , por defecto: falso )
Pide crear un ​
índice de base de datos​
en la columna

Campos simples
Hay dos grandes categorías de campos: campos "simples", que son valores atómicos almacenados directamente en la
mesa del modelo y los campos "relacionales" que unen los registros (del mismo modelo o de diferentes modelos).

Ejemplo de campos simples son ​


Boolean​
,​
Date​
,​
Char​
.

Campos reservados
Odoo crea algunos campos en todos los modelos ​
[1] . Estos campos son gestionados por el sistema y no se deben
escribir. Se pueden leer si es útil o necesario:

id (Id)
el identificador único para el registro de su modelo
create_date (​
Datetime​
)
fecha de creación del registro
create_uid (​
Many2one​
)
usuario que creó el registro
write_date (​
Datetime​
)
última fecha de modificación del registro
write_uid (​
Many2one​
)
usuario que modificó por última vez el registro

Campos especiales
Por defecto, Odoo también requiere un campo nombre en todos los modelos para diversas búsquedas y mostrar
comportamientos. El campo que se utiliza para estos fines puede ser anulado por el establecimiento ​
_rec_name​
.


Ejercicio​
Definir un modelo

Definir un nuevo modelo Curso en el módulo openacademy. Un curso tiene un título y una descripción. Los cursos
deben tener un título.

Editar el archivo openacademy/models.py para incluir la clase curso.

openacademy/models.py

from openerp import models, fields, api


class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string="Title", required=True)
description = fields.Text()

Los archivos de datos

Odoo es un sistema altamente datos impulsada. Aunque el comportamiento es personalizado utilizando ​


Python
código parte del valor de un módulo está en los datos en ella se establece cuando se carga.

Tip​

Existen algunos módulos únicamente para agregar datos en Odoo

los archivos de datos​


Módulo de datos se declara a través de ​ , archivos XML con elementos <record>. Cada
<record> crea o actualiza un registro de base de datos.

<openerp>
<data>
<record model="{model name}" id="{record identifier}">
<field name="{a field name}">{a value}</field>
</record>
</data>
<openerp>

➢ model es el nombre del modelo Odoo para el registro

identificador externo​
➢ id es un ​ , permite referir al registro (sin necesidad de conocer su identificador en la
base de datos)

➢ <field> elementos tienen un name que es el nombre del campo en el modelo (por ejemplo,
description). Su cuerpo es el valor del campo.


Los archivos de datos tienen que ser declarados en el archivo de manifiesto para ser cargado, pueden ser
declaradas en el 'data' list (always loaded) (siempre cargado) o en la 'demostración' lista (sólo cargan en modo
de demostración).

Ejercicio​
Definir los datos de demostración

Crear datos de demostración que llenan el modelo Cursos con algunos cursos de demostración.

Editar el archivo openacademy/demo.xml para incluir algunos datos.

openacademy/demo.xml

<openerp>
<data>
<record model="openacademy.course" id="course0">
<field name="name">Course 0</field>
<field name="description">Course 0's description

Can have multiple lines


</field>
</record>
<record model="openacademy.course" id="course1">
<field name="name">Course 1</field>
<!-- no description for this one -->
</record>
<record model="openacademy.course" id="course2">
<field name="name">Course 2</field>
<field name="description">Course 2's description</field>
</record>
</data>
</openerp>

Acciones y Menús

Son registros regulares en la base de datos, por lo general a través de los archivos de datos declarados. Las
acciones pueden ser desencadenados por tres vías:

1. haciendo clic en los elementos de menú (vinculado a acciones específicas)


2. haciendo clic en los botones en las vistas (si éstos están conectados a las acciones)
3. acciones como contextuales sobre objeto


Debido a que los menús son algo complejos para declarar hay un acceso directo <menuitem> declarado en
ir.ui.menu para conectarlo a la acción correspondiente con más facilidad.

<record model="ir.actions.act_window" id="action_list_ideas">


<field name="name">Ideas</field>
<field name="res_model">idea.idea</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_ideas" parent="menu_root" name="Ideas" sequence="10"
action="action_list_ideas"/>

Tip​
La acción debe ser declarado antes de su correspondiente menú en el archivo XML.

Los archivos de datos se ejecutan secuencialmente, el id de la acción debe estar presente en la base de datos
antes de que el menú se puede crear.

Ejercicio​
Definir nuevas entradas de menú

Definir nuevas entradas de menú para acceder a los cursos y sesiones en virtud de la entrada del menú
OpenAcademy. Un usuario debe ser capaz de

➢ mostrar una lista de todos los cursos


➢ crear / modificar los cursos

1. Crear openacademy/views/openacademy.xml con una acción y los menús de disparo la acción


2. Añadir a la data list de openacademy/__openerp__.py

openacademy/__openerp__.py

'data': [
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
],
# only loaded in demonstration mode
'demo': [

10 
openacademy/views/openacademy.xml

<?xml version="1.0" encoding="UTF-8"?>


<openerp>
<data>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
that is an action opening a view or a set of views
-->
<record model="ir.actions.act_window" id="course_list_action">
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">Create the first course
</p>
</field>
</record>

<!-- top level menu: no parent -->


<menuitem id="main_openacademy_menu" name="Open Academy"/>
<!-- A first level in the left side menu is needed
before using action= attribute -->
<menuitem id="openacademy_menu" name="Open Academy"
parent="main_openacademy_menu"/>
<!-- the following menuitem should appear *after*
its parent openacademy_menu and *after* its
action course_list_action -->
<menuitem id="courses_menu" name="Courses" parent="openacademy_menu"
action="course_list_action"/>
<!-- Full id location:
action="openacademy.course_list_action"
It is not required when it is the same module -->
</data>
</openerp>

11 
Vistas básicas
Definen la forma en que se muestran los registros de un modelo. Cada tipo de vista representa un modo de
visualización (una lista de registros, un gráfico de su agregación, ...). Las vistas pueden ser solicitadas ya sea de
forma genérica a través de su tipo (por ejemplo, una lista de socios ) o específicamente a través de su
identificación. Para las solicitudes de genéricos, se utilizará la vista con el tipo correcto y la prioridad más baja (lo
que la vista de menor prioridad de cada tipo es la vista por defecto para ese tipo).

Ver herencia​
permite alterar vistas declarados en otra parte (la adición o eliminación del contenido).

Declaración de vista genérico

Una vista es declarado como un registro del modelo ir.ui.view . El tipo de vista está implícita en el elemento raíz del
campo arch:

<record model="ir.ui.view" id="view_id">


<field name="name">view.name</field>
<field name="model">object_name</field>
<field name="priority" eval="16"/>
<field name="arch" type="xml">
<!-- view content: <form>, <tree>, <graph>, ... -->
</field>
</record>

Tip El contenido de la vista es XML. El campo arch por lo tanto debe ser declarada como type = "xml" para que sea
analizado correctamente.

Vistas de árbol

Los registros de visualización en forma de tabla. Su elemento raíz es <tree> . La forma más simple de la vista de árbol
se limita a enumerar todos los campos para mostrar en la tabla (cada campo como una columna):

<tree string="Idea list">


<field name="name"/>
<field name="inventor_id"/>
</tree>

Vistas de formulario

Los formularios se utilizan para crear y editar registros individuales. Su elemento raíz es <form> . Se componen de
elementos de alto nivel de la estructura (gruops, notebooks) y elementos interactivos (buttons y fields):

12 
<form string="Idea form">
<group colspan="4">
<group colspan="2" col="2">
<separator string="General stuff" colspan="2"/>
<field name="name"/>
<field name="inventor_id"/>
</group>

<group colspan="2" col="2">


<separator string="Dates" colspan="2"/>
<field name="active"/>
<field name="invent_date" readonly="1"/>
</group>

<notebook colspan="4">
<page string="Description">
<field name="description" nolabel="1"/>
</page>
</notebook>

<field name="state"/>
</group>
</form>

Ejercicio​
Personalice el formulario utilizando XML

Crea tu propia vista de formulario para el objeto del curso. Datos muestra debe ser: el nombre y la descripción del
curso.

openacademy/views/openacademy.xml

<?xml version="1.0" encoding="UTF-8"?>


<openerp>
<data>
<record model="ir.ui.view" id="course_form_view">
<field name="name">course.form</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<form string="Course Form">
<sheet>

13 
<group>
<field name="name"/>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>

<!-- window action -->


<!--
The following tag is an action definition for a "window action",

Ejercicio​
Notebooks

En la vista de formulario de Curso, poner el campo de descripción bajo una pestaña, de manera que será más fácil para
agregar otras fichas más tarde, que contiene información adicional. Modificar la vista de formulario de Curso de la
siguiente manera:

openacademy/views/openacademy.xml

<sheet>
<group>
<field name="name"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="About">
This is an example of notebooks
</page>
</notebook>
</sheet>
</form>
</field>

14 
Las vistas de formulario también pueden usar HTML plano para diseños más flexibles:

<form string="Idea Form">


<header>
<button string="Confirm" type="object" name="action_confirm"
states="draft" class="oe_highlight" />
<button string="Mark as done" type="object" name="action_done"
states="confirmed" class="oe_highlight"/>
<button string="Reset to draft" type="object" name="action_draft"
states="confirmed,done" />
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only" string="Idea Name" />
<h1><field name="name" /></h1>
</div>
<separator string="General" colspan="2" />
<group colspan="2" col="2">
<field name="description" placeholder="Idea description..." />
</group>
</sheet>
</form>

Vistas de búsquedas

Personaliza el campo de búsqueda asociado a la vista de la lista (y otras vistas agregadas). Su elemento raíz es
<search> y se compone de campos que definen porqué campos se pueden buscar:

<search>
<field name="name"/>
<field name="inventor_id"/>
</search>

Si no existe una vista de búsqueda para el modelo, Odoo genera uno que sólo permite la búsqueda por el campo name

Ejercicio​
Buscar cursos

Permitir la búsqueda de cursos en función de su título o su descripción.

openacademy/views/openacademy.xml

15 
</field>
</record>

<record model="ir.ui.view" id="course_search_view">


<field name="name">course.search</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="description"/>
</search>
</field>
</record>

<!-- window action -->


<!--
The following tag is an action definition for a "window action",

Relaciones entre modelos


Un registro de un modelo puede estar relacionado con un registro de otro modelo. Por ejemplo, un registro para la
venta se relaciona con un registro de cliente que contiene los datos del cliente; sino que también está relacionada
con sus registros de línea de orden de venta.

Ejercicio​
Crear un modelo de sesión

Para el módulo de la Academia Americana, consideramos un modelo para sesiones : una sesión es una ocurrencia de
un curso impartido en un momento dado para un público determinado.

Crear un modelo para las sesiones. Una sesión tiene un nombre, una fecha de inicio, una duración y un número de
sillas. Añadir una acción y un elemento de menú para mostrarlos. Hacer que el nuevo modelo visible a través de un
elemento de menú.

1. Crear la clase Session en openacademy/models.py.


2. Añadir el acceso al objeto session en openacademy/view/openacademy.xml.

openacademy/models.py

16 
name = fields.Char(string="Title", required=True)
description = fields.Text()

class Session(models.Model):
_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")

openacademy/views/openacademy.xml

<!-- Full id location:


action="openacademy.course_list_action"
It is not required when it is the same module -->
<!-- session form view -->
<record model="ir.ui.view" id="session_form_view">
<field name="name">session.form</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<sheet>
<group>
<field name="name"/>
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</sheet>
</form>
</field>
</record>

<record model="ir.actions.act_window" id="session_list_action">


<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>

17 
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
action="session_list_action"/>
</data>
</openerp>

Nota
digits = (6, 2) especifica la precisión de un número float: 6 es el número total de dígitos, mientras que 2 es el número
de dígitos después de la coma. Tenga en cuenta que resulta en los dígitos de los números antes de la coma es de un
máximo de 4

Campos relacionales

Campos relacionales vinculan registros, ya sea del mismo modelo (jerarquías) o entre diferentes modelos. Los tipos de
campo relacionales son:

Many2one(other_model, ondelete='set null')


Un simple enlace a otro objeto:
print foo.other_id.name

One2many(other_model, related_field)
Una relación virtual, inversa de una ​
Many2one​
. Un ​
one2many se comporta como contenedor de los registros, el
acceso se traduce en un conjunto de registros (posiblemente vacía):

for other in foo.other_ids:


print other.name

Nota
Debido a que un ​
one2many es una relación virtual, debe haber un campo ​
Many2one en el other_model, y su nombre
debe ser related_field

Many2many(other_model)
Relación múltiple bidireccional, cualquier registro en un lado puede estar relacionada con cualquier número de
registros en el otro lado. Se comporta como un contenedor de archivos, acceder a él también se traduce en una serie
posiblemente vacía de registros:

for other in foo.other_ids:


print other.name

Ejercicio​
Relaciones Many2one

18 
El uso de un many2one, modifique los modelos del curso y Sesión para reflejar su relación con otros modelos:

➢ Un curso tiene un usuario responsable; el valor de ese campo es un registro del modelo integrado res.users.
➢ Una sesión tiene un instructor ; el valor de ese campo es un registro del modelo integrado res.partner.
➢ Una sesión se relaciona a un curso ; el valor de ese campo es un registro del modelo openacademy.course y se
requiere.
➢ Adaptar las vistas.
1. Añadir las pertinentes Many2one campos a los modelos, y
2. añadirlos en las vistas.

openacademy/models.py

name = fields.Char(string="Title", required=True)


description = fields.Text()

responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)

class Session(models.Model):
_name = 'openacademy.session'

start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")

instructor_id = fields.Many2one('res.partner', string="Instructor")


course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)

openacademy/views/openacademy.xml

<sheet>
<group>
<field name="name"/>
<field name="responsible_id"/>
</group>
<notebook>
<page string="Description">

19 
</field>
</record>

<!-- override the automatically generated list view for courses -->
<record model="ir.ui.view" id="course_tree_view">
<field name="name">course.tree</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<tree string="Course Tree">
<field name="name"/>
<field name="responsible_id"/>
</tree>
</field>
</record>

<!-- window action -->


<!-- The following tag is an action definition for a "window action",

<form string="Session Form">


<sheet>
<group>
<group string="General">
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
</group>
<group string="Schedule">
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</group>
</sheet>
</form>
</field>
</record>

<!-- session tree/list view -->


<record model="ir.ui.view" id="session_tree_view">
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>

20 
<field name="arch" type="xml">
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>

Ejercicio​
Relaciones One2Many inversas

Utilizando la inversa one2many, modificar los modelos para reflejar la relación entre los cursos y sesiones.

1. Modifique la clase Curso , y


2. agregar el campo en la vista formulario supuesto.

openacademy/models.py

responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
class Session(models.Model):

openacademy/views/openacademy.xml

<page string="Description">
<field name="description"/>
</page>
<page string="Sessions">
<field name="session_ids">
<tree string="Registered sessions">
<field name="name"/>
<field name="instructor_id"/>
</tree>
</field>
</page>
</notebook>
</sheet>

21 
Ejercicio​
Múltiples relaciones Many2Many

Utilizando el Many2Many, modificar el período de sesiones de modelo para relacionar cada sesión a un conjunto de
asistentes. Los asistentes estarán representados por los registros asociados, por lo que se relacionan con el modelo
integrado res.partner . Adaptar las vistas en consecuencia.

1. Modifique la clase sesión, y


2. agregar el campo en la vista formulario.

openacademy/models.py

instructor_id = fields.Many2one('res.partner', string="Instructor")


course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

openacademy/views/openacademy.xml

<field name="seats"/>
</group>
</group>
<label for="attendee_ids"/>
<field name="attendee_ids"/>
</sheet>
</form>
</field>

Herencia
Modelo de herencia

Odoo ofrece dos mecanismos de herencia para extender un modelo existente de forma modular. El primer
mecanismo de herencia permite a un módulo modificar el comportamiento de un modelo definido en otro
módulo:

➢ agregar campos a un modelo,


➢ anular la definición de campos en un modelo,
➢ añadir restricciones a un modelo,
➢ añadir métodos a un modelo,
➢ reemplazar los métodos existentes en un modelo.

22 
El segundo mecanismo de herencia (delegación) permite vincular cada registro de un modelo para un registro en
un modelo de los padres, y proporciona un acceso transparente a los campos del registro padre.

Herencia de vista

En lugar de modificar las vistas existentes en su lugar (sobrescribiendolas), Odoo ofrece herencia de vistas donde
se aplica a los hijos vistas "de extensión" en la parte superior de la vista de la raíz, y pueden añadir o eliminar el
contenido de sus padres.

Una vista de extensión hace referencia a su padre con el campo inherit_id, y en lugar de una sola vista de su
campo arch se compone de un número cualquiera de elementos XPath de selección y alterando el contenido de
su vista padre:

<!-- improved idea categories list -->


<record id="idea_category_list2" model="ir.ui.view">
<field name="name">id.category.list2</field>
<field name="model">ir.ui.view</field>
<field name="inherit_id" ref="id_category_list"/>
<field name="arch" type="xml">

23 
<!-- find field description inside tree, and add the field
idea_ids after it -->
<xpath expr="/tree/field[@name='description']" position="after">
<field name="idea_ids" string="Number of ideas"/>
</xpath>
</field>
</record>

expr
Un expresión ​
XPath para seleccionar un solo elemento en la vista padre. Plantea un error si coincide con
ningún elemento o más de uno
position
Operación de aplicar al elemento coincidente:
inside
anexa XPath en el extremo del elemento emparejado
replace
reemplaza el elemento emparejado por el xpath
before
inserta el xpath como un hermano antes de que el elemento coincidente
after
inserta los XPaths como un hermano después del elemento emparejado
attributes
altera los atributos del elemento coincidente utilizando atributos especiales en el xpath

Ejercicio​
Alterar el contenido existente

➢ Utilizando el modelo de herencia, modificar el vigente modelo Partner para añadir un campo booleano
instructor, y un campo de Many2Many que corresponde a la relación de sesión-partner
➢ Usar la vista herencia, visualizar estos campos en la vista de formulario del partner

Nota Esta es la oportunidad para introducir el modo de programador para inspeccionar la vista, encontrará su
ID externo y el lugar para poner el nuevo campo.

1. Crear el archivo openacademy/partner.py e importarlo en __init__.py


2. Crear el archivo openacademy/views/partner.xml y añadirlo a __openerp__.py

24 
openacademy/__init__.py

# -*- coding: utf-8 -*-


from . import controllers
from . import models
from . import partner

openacademy/__openerp__.py

# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
],
# only loaded in demonstration mode
'demo': [

openacademy/partner.py

# -*- coding: utf-8 -*-


from openerp import fields, models

class Partner(models.Model):
_inherit = 'res.partner'
# Add a new column to the res.partner model, by default partners are not
# instructors
instructor = fields.Boolean("Instructor", default=False)
session_ids = fields.Many2many('openacademy.session',
string="Attended Sessions", readonly=True)

openacademy/views/partner.xml

<?xml version="1.0" encoding="UTF-8"?>


<openerp>
<data>
<!-- Add instructor field to existing view -->
<record model="ir.ui.view" id="partner_instructor_form_view">
<field name="name">partner.instructor</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>

25 
<field name="arch" type="xml">
<notebook position="inside">
<page string="Sessions">
<group>
<field name="instructor"/>
<field name="session_ids"/>
</group>
</page>
</notebook>
</field>
</record>
<record model="ir.actions.act_window" id="contact_list_action">
<field name="name">Contacts</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="configuration_menu" name="Configuration"
parent="main_openacademy_menu"/>
<menuitem id="contact_menu" name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
</data>
</openerp>

Dominios
En Odoo, ​
Dominios son valores que codifican las condiciones en los registros. Un dominio es una lista de los
criterios utilizados para seleccionar un subconjunto de los registros de un modelo. Cada criterio es un triple con
un nombre de campo, un operador y un valor.

Por ejemplo, cuando se utiliza en el ​


producto el siguiente modelo de dominio selecciona todos ​
los servicios con
1000​
un precio unitario mayor a ​ :

[('product_type', '=', 'service'), ('unit_price', '>', 1000)]

Por criterios predeterminados se combinan con una Y implícita. Los operadores lógicos Y (AND), | (OR) y ! (NO)
se puede utilizar para combinar explícitamente criterios. Se utilizan en posición prefijo (el operador se inserta
antes de que sus argumentos en lugar de entre).

Por ejemplo para seleccionar los productos "que son los servicios ​
O tienen un precio unitario que es ​
NO entre
1000 y 2000 ":

26 
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]

Un parámetro se puede añadir a los campos relacionales para limitar registros válidos para la relación cuando se
trata de seleccionar registros en la interfaz del cliente.

Ejercicio​
Dominios sobre campos relacionales

Al seleccionar el instructor para una reunión, sólo los instructores (Partner con instructor establecido en True )
deben ser visibles.

openacademy/models.py

duration = fields.Float(digits=(6, 2), help="Duration in days")


seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=[('instructor', '=', True)])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

Nota Un dominio declarado como una lista literal se evalúa del lado del servidor y no puede referir valores
dinámicos en el lado derecho, un dominio declarado como una cadena se evalúa el lado del cliente y permite
nombres de campo en la parte derecha.

Ejercicio​
Dominios más complejos

Crear una nueva categoria de partner Teacher / Level 1 and Teacher / Level 2. El instructor para una sesión puede
ser un instructor o un profesor (de cualquier nivel).

1. Modifique el dominio del modelo Session


2. Modifique openacademy/view/partner.xml para dar acceso a las categorias de partner:

27 
openacademy/models.py

seats = fields.Integer(string="Number of seats")

instructor_id = fields.Many2one('res.partner', string="Instructor",


domain=['|', ('instructor', '=', True),
('category_id.name', 'ilike', "Teacher")])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

openacademy/views/partner.xml

<menuitem id="contact_menu" name="Contacts"


parent="configuration_menu"
action="contact_list_action"/>
<record model="ir.actions.act_window" id="contact_cat_list_action">
<field name="name">Contact Tags</field>
<field name="res_model">res.partner.category</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="contact_cat_menu" name="Contact Tags"
parent="configuration_menu"
action="contact_cat_list_action"/>
<record model="res.partner.category" id="teacher1">
<field name="name">Teacher / Level 1</field>
</record>
<record model="res.partner.category" id="teacher2">
<field name="name">Teacher / Level 2</field>
</record>
</data>
</openerp>

Campos calculados y valores predeterminados


Hasta ahora los campos se han almacenado directamente en y recuperados directamente de la base de datos.
Los campos también se puede calcular. En ese caso, el valor del campo no se recupera de la base de datos pero
se calcula sobre la marcha llamando a un método del modelo.

Para crear un campo calculado, cree un campo y establezca su atributo ​


compute al nombre de un método. El
método de cálculo debería simplemente establecer el valor del campo para calcular en cada registro en ​
self​
.

28 
Nota self es una colección. El objeto ​
self es un conjunto de registros, es decir, una colección ordenada de
registros. Es compatible con las operaciones de Python estándar en las colecciones, como ​
len(self) y ​
iter(self)​
,
además de operaciones de conjuntos adicionales como ​ recs1 + recs2​
.

La iteración sobre ​
self da los registros uno por uno, donde cada registro es en sí mismo una colección de tamaño
1. Puede acceder/asignar campos en los registros individuales usando la notación de puntos, como ​ record.name​
.

import random
from openerp import models, fields

class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
@api.multi
def _compute_name(self):
for record in self:
record.name = str(random.randint(1, 1e6))

Nuestro método compute es muy simple: se realiza un bucle sobre ​


self y realiza la misma operación en cada
registro. Podemos hacer que sea un poco más fácil mediante el uso de un decorador ​
one() para recorrer
automáticamente la colección:

@api.one
def _compute_name(self):
self.name = str(random.randint(1, 1e6))

Dependencias

El valor de un campo calculado generalmente depende de los valores de otros campos en el registro
computarizado. El ORM espera que el desarrollador especifique esas dependencias en el método compute con el
decorador ​
depends()​
. Las dependencias dadas son utilizadas por el ORM para activar el recálculo del campo
siempre que algunas de sus dependencias se han modificado:

from openerp import models, fields, api

class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
value = fields.Integer()

29 
@api.one
@api.depends('value')
def _compute_name(self):
self.name = "Record with value %s" % self.value

Ejercicio ​
Campos calculados

➢ Añadir el porcentaje de sillas adoptadas para el modelo Sesión


➢ Muestra el campo en vista de árbol y de form
➢ Mostrar el campo como una barra de progreso
1. Añadir un campo calculado a Session
2. Mostrar el campo en la vista Session:

openacademy/models.py

course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
@api.one
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
if not self.seats:
self.taken_seats = 0.0
else:
self.taken_seats = 100.0 * len(self.attendee_ids) / self.seats

openacademy/views/openacademy.xml

<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
<field name="taken_seats" widget="progressbar"/>
</group>
</group>
<label for="attendee_ids"/>

30 
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>
</record>

Valores predeterminados

A cualquier campo se puede dar un valor predeterminado. En la definición de campo, añada la opción default=X
donde X es o bien un valor Python literal (boolean, integer, float, string), o una función que toma un conjunto de
registros y devuelve un valor:

name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)

Nota​

El objeto self.env da acceso a los parámetros de la petición y otras cosas útiles:

➢ self.env.cr o self._cr es el objeto cursor de base de datos; se utiliza para la consulta de la BD


➢ self.env.uid o self._uid es la identificación de base de datos del usuario actual
➢ self.env.user es el registro del usuario actual
➢ self.env.context o self._context es el diccionario contexto
➢ self.env.ref(xml_id) devuelve el registro correspondiente a un id XML
➢ self.env[model_name] devuelve una instancia del modelo dado

Ejercicio ​
Objetos activos – Valores predeterminados

➢ Defina start_date con el valor predeterminado today (see ​


Date​
).
➢ Añada el campo active en la clase Session, y setee las sesiones como activa por defecto.

openacademy/models.py

_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date(default=fields.Date.today)
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),

31 
openacademy/views/openacademy.xml

<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
<field name="active"/>
</group>
<group string="Schedule">
<field name="start_date"/>

Nota ​
Odoo tiene reglas incorporadas que hacen un campo invisible cuando un campo active se establece en
False.

Onchange
El mecanismo de "onchange" proporciona una forma para actualizar la interfaz de cliente cada vez que el usuario
ha rellenado un valor en un campo, sin guardar nada en la base de datos.

Por ejemplo, supongamos que un modelo tiene tres campos amount, unit_price y price, y desea actualizar el
precio en el formulario cuando cualquiera de los otros campos se modifica. Para lograr esto, definir un método
donde self representa el registro en la vista form, y se declara con ​
onchange() para especificar en qué campo
tiene que ser activado. Cualquier cambio que realice en self se reflejará en el formulario.

<!-- content of form view -->


<field name="amount"/>
<field name="unit_price"/>
<field name="price" readonly="1"/>

# onchange handler
@api.onchange('amount', 'unit_price')
def _onchange_price(self):
# set auto-changing field
self.price = self.amount * self.unit_price
# Can optionally return a warning and domains
return {
'warning': {
'title': "Something bad happened",
'message': "It was very bad indeed",
}
}

32 
Para los campos calculados, el comportamiento onchange está incorporado como se puede ver jugando con la
Session form: cambiar el número de asientos o los participantes, y la progressbar taken_seats se actualiza
automáticamente.

Ejercicio ​
Advertencia

Añadir un onchange explícito para advertir sobre los valores no válidos, al igual que un número negativo de
asientos, o más participantes que los asientos.

openacademy/models.py

self.taken_seats = 0.0
else:
self.taken_seats = 100.0 * len(self.attendee_ids) / self.seats

@api.onchange('seats', 'attendee_ids')
def _verify_valid_seats(self):
if self.seats < 0:
return {
'warning': {
'title': "Incorrect 'seats' value",
'message': "The number of available seats may not be negative",
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': "Too many attendees",
'message': "Increase seats or remove excess attendees",
},
}

Restricciones de Modelo
Odoo ofrece dos formas de configurar invariantes verificadas de forma automática: ​
Python constraints y ​
SQL
constraints​
.

Una restricción Python se define como un método decorado con ​


constrains()​
, e invocado en un conjunto de
registros. El decorador especifica qué campos están involucrados en la restricción, por lo que la restricción se
evalúa automáticamente cuando uno de ellos se modifica. Se espera que el método para lanzar una excepción si
esa invariante no está satisfecha:

33 
from openerp.exceptions import ValidationError

@api.constrains('age')
def _check_something(self):
for record in self:
if record.age > 20:
raise ValidationError("Your record is too old: %s" % record.age)
# all records passed the test, don't return anything

Ejercicio ​
Añadir una restriccion Python

Añadir una restricción que comprueba que el instructor no está presente en los asistentes de su propia sesión.

openacademy/models.py

# -*- coding: utf-8 -*-


from openerp import models, fields, api, exceptions

class Course(models.Model):
_name = 'openacademy.course'

'message': "Increase seats or remove excess attendees",


},
}

@api.one
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
if self.instructor_id and self.instructor_id in self.attendee_ids:
raise exceptions.ValidationError("A session's instructor can't be an attendee")

Las limitaciones de SQL se definen a través de los atributos ​


_sql_constraints​
. Este último es asignado a una lista
de triples de strings (name, sql_definition, message), donde name es un nombre de restricción de SQL válido,
sql_definition es una expresión ​
table_constraint​
, y message es un mensaje de error.

Ejercicio ​
Añadir restricciones SQL

Con la ayuda de la documentación de PostgreSQL, agregue las siguientes limitaciones:

1. Compruebe que la descripción del curso y el título del curso son diferentes
2. Hacer el nombre del curso único

34 
openacademy/models.py

session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")

_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
"The title of the course should not be the description"),

('name_unique',
'UNIQUE(name)',
"The course title must be unique"),
]

class Session(models.Model):
_name = 'openacademy.session'

Ejercicio ​
Añadir una opción duplicate

Desde que añadimos una restricción para el nombre del curso singularidad, no es posible utilizar la función
"duplicar" más (Formulario ‣Duplicar).

Vuelva a aplicar su propio método de "copiar", que permite duplicar el objeto del curso, cambiar el nombre
original en "Copia de [nombre original]".

openacademy/models.py
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")

@api.one
def copy(self, default=None):
default = dict(default or {})

copied_count = self.search_count(
[('name', '=like', u"Copy of {}%".format(self.name))])
if not copied_count:
new_name = u"Copy of {}".format(self.name)
else:
new_name = u"Copy of {} ({})".format(self.name, copied_count)
default['name'] = new_name
return super(Course, self).copy(default)

35 
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',

Vistas avanzadas
Vistas árbol

Las vistas de árbol pueden tomar atributos complementarios para personalizar aún más su comportamiento:

colors
asignaciones de colores a las condiciones. Si la condición se evalúa como True, el color correspondiente se
aplica a la fila:
<tree string="Idea Categories" colors="blue:state=='draft';red:state=='trashed'">
<field name="name"/>
<field name="state"/>
</tree>
Las cláusulas están separados por (;) , el color y el estado están separados por (:) .
editable
Cualquier "top" o "bottom". Hace que la vista de árbol editable en el lugar (en lugar de tener que ir a través de
la vista de formulario), el valor es la posición en la que aparecen nuevas filas.

Ejercicio ​
Coloración de las listas

Modificar la vista de árbol Sesión de tal manera que las sesiones que duran menos de 5 días son de color azul, y los que
duran más de 15 días son de color rojo.

Modificar la vista de árbol de sesión:

openacademy/views/openacademy.xml

<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<tree string="Session Tree" colors="#0000ff:duration&lt;5;red:duration&gt;15">
<field name="name"/>
<field name="course_id"/>
<field name="duration" invisible="1"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>

36 
Calendarios
Muestra los registros como los eventos del calendario. Su elemento raíz es <calendar> y sus atributos más comunes:

color
Los colores se distribuyen automáticamente a los eventos, pero los acontecimientos en el mismo segmento de
color (registros que tienen el mismo valor para su campocolor) se les dará el mismo color.

date_start
campo de registro de la celebración del inicio de fecha / hora para el evento

date_stop (optional)
campo de registro de la celebración de la final de fecha / hora para el evento

string
campo (para definir la etiqueta para cada evento de calendario)

<calendar string="Ideas" date_start="invent_date" color="inventor_id">


<field name="name"/>
</calendar>

Ejercicio​
Vista calendario

Agregar una vista de calendario para el modelo de sesión que permite al usuario ver los eventos.

1. Agregar un campo ​
end_date​
calculado a partir de ​
start_date​
y​
duration

Tip ​
La función inversa hace que el campo se pueda escribir, y permite mover las sesiones (a través de arrastrar y
soltar) en la vista del calendario

2. Agregar una vista calendario al modelo Sesion


3. Y añadir la vista de calendario a las acciones del modelo de Sesion

openacademy/models.py

# -*- coding: utf-8 -*-

from datetime import timedelta


from openerp import models, fields, api, exceptions

class Course(models.Model):

37 
attendee_ids = fields.Many2many('res.partner', string="Attendees")

taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')


end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')

@api.one
@api.depends('seats', 'attendee_ids')

}
@api.one
@api.depends('start_date', 'duration')
def _get_end_date(self):
if not (self.start_date and self.duration):
self.end_date = self.start_date
return

# Add duration to start_date, but: Monday + 5 days = Saturday, so


# subtract one second to get on Friday instead
start = fields.Datetime.from_string(self.start_date)
duration = timedelta(days=self.duration, seconds=-1)
self.end_date = start + duration

@api.one
def _set_end_date(self):
if not (self.start_date and self.end_date):
return

# Compute the difference between dates, but: Friday - Monday = 4 days,


# so add one day to get 5 days instead
start_date = fields.Datetime.from_string(self.start_date)
end_date = fields.Datetime.from_string(self.end_date)
self.duration = (end_date - start_date).days + 1

@api.one
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
if self.instructor_id and self.instructor_id in self.attendee_ids:

openacademy/views/openacademy.xml

38 
</field>
</record>

<!-- calendar view -->


<record model="ir.ui.view" id="session_calendar_view">
<field name="name">session.calendar</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<calendar string="Session Calendar" date_start="start_date"
date_stop="end_date"
color="instructor_id">
<field name="name"/>
</calendar>
</field>
</record>

<record model="ir.actions.act_window" id="session_list_action">


<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar</field>
</record>

<menuitem id="session_menu" name="Sessions"

Vistas de búsqueda
En vista búsqueda, los elementos ​
<field> pueden tener un filter_domain que anula el dominio generado para buscar
en el campo dado. En el dominio dado, ​
self representa los valores ingresados por el usuario. En el ejemplo siguiente,
se utiliza para buscar tanto en los campos nombre como descripción.
Search views tambien pueden contener elementos ​
<filter>​
, que actúan como palancas para búsquedas predefinidas.
Los filtros deben tener uno de los siguientes atributos:

domain
añadir el dominio dado a la búsqueda actual

context
añadir un poco de contexto a la búsqueda actual

<search string="Ideas">
<field name="name"/>

39 
<field name="description" string="Name and description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
<field name="inventor_id"/>
<field name="country_id" widget="selection"/>
<filter name="my_ideas" string="My Ideas"
domain="[('inventor_id', '=', uid)]"/>
<group string="Group By">
<filter name="group_by_inventor" string="Inventor"
context="{'group_by': 'inventor'}"/>
</group>
</search>

Para utilizar una vista de búsqueda no predeterminado en una acción, debe estar vinculada con el campo
search_view_id del registro action.

La acción también puede establecer valores predeterminados para los campos de búsqueda a través de su campo
context​
: las claves de context, search_default_field_name inicializará field_name con el valor proporcionado. Los
filtros de búsqueda deben tener un opcional @name por defecto y comportarse como booleanos ( que sólo pueden
ser activadas por defecto ).

Ejercicio ​
Vistas de búsqueda

1. Agregue un botón para filtrar los cursos para los cuales el usuario actual es el responsable en la vista de
búsqueda de cursos. Que sea seleccionada por defecto.
2. Añadir un botón para agrupar los cursos por usuario responsable.

openacademy/views/openacademy.xml

<search>
<field name="name"/>
<field name="description"/>
<filter name="my_courses" string="My Courses"
domain="[('responsible_id', '=', uid)]"/>
<group string="Group By">
<filter name="by_responsible" string="Responsable"
context="{'group_by': 'responsible_id'}"/>
</group>
</search>
</field>
</record>

40 
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context" eval="{'search_default_my_courses': 1}"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">Create the first course
</p>

Gantt
Los gráficos de barras horizontales suelen utilizar para mostrar la planificación y el avance del proyecto, su elemento
raíz es ​
<gantt>​
.

<gantt string="Ideas" date_start="invent_date" color="inventor_id">


<level object="idea.idea" link="id" domain="[]">
<field name="inventor_id"/>
</level>
</gantt>

Ejercicio ​
Diagrama de Gantt

Añadir un diagrama de Gantt que permite al usuario ver la programación de sesiones relacionado con el módulo
openacademy. Las sesiones deben ser agrupadas por el instructor.

1. Crear un campo calculado expresando duración de la sesión en horas


2. Añadir la definición de la vista de Gantt, y añadir la vista de Gantt a la acción del modelo Sesión

openacademy/models.py

end_date = fields.Date(string="End Date", store=True,


compute='_get_end_date', inverse='_set_end_date')

hours = fields.Float(string="Duracion en horas",


compute='_get_hours', inverse='_set_hours')

@api.one
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
self.duration = (end_date - start_date).days + 1

41 
@api.one
@api.depends('duration')
def _get_hours(self):
self.hours = self.duration * 24

@api.one
def _set_hours(self):
self.duration = self.hours / 24

@api.one
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
if self.instructor_id and self.instructor_id in self.attendee_ids:

openacademy/views/openacademy.xml

</field>
</record>

<record model="ir.ui.view" id="session_gantt_view">


<field name="name">session.gantt</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<gantt string="Session Gantt" color="course_id"
date_start="start_date" date_delay="hours"
default_group_by='instructor_id'>
<field name="name"/>
</gantt>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,gantt</field>
</record>
<menuitem id="session_menu" name="Sessions"

42 
Vistas de gráfico
Permiten visión general y análisis de los modelos agregados, su elemento raíz es ​
<graph>​
. Tienen 4 modos de
visualización, el modo por defecto se selecciona mediante el atributo ​
@type​
.

Pivot
una tabla multidimensional, permite la selección de contribuyentes y dimensiones para obtener el conjunto de
datos agregados derecha antes de pasar a una visión más gráfica

Bar (default)
un gráfico de barras, la primera dimensión se utiliza para definir los grupos en el eje horizontal, otras
dimensiones definen bares agregados dentro de cada grupo.
Por bares predeterminados son de lado a lado, que pueden ser apilados mediante el uso de ​
@stacked="True"
en el ​
<graph>

Line
Gráfico de líneas de 2 dimensiones

Pie
Pastel de 2 dimensiones

Las vistas de gráficos contienen <field > con un atributo type obligatorio tomar los valores:

row (default)
el campo debe ser agregada por defecto

measure
el campo debe ser agregado en lugar de agruparse

<graph string="Total idea score by Inventor">


<field name="inventor_id"/>
<field name="score" type="measure"/>
</graph>

Nota ​
Las vistas de gráfico pueden realizar agregaciones en los valores de base de datos, que no funcionan con
campos que no son almacenados computadas

Ejercicio​
Vista de gráfico

Añadir una vista de gráfico en el objeto Session que se muestra, para cada curso, el número de asistentes bajo la forma
de un gráfico de barras.

1. Añadir el número de asistentes como un campo calculado almacenada


2. A continuación, agregue la vista relevante

43 
openacademy/models.py

hours = fields.Float(string="Duration in hours",


compute='_get_hours', inverse='_set_hours')

attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)

@api.one
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):

self.duration = self.hours / 24

@api.one
@api.depends('attendee_ids')
def _get_attendees_count(self):
self.attendees_count = len(self.attendee_ids)

@api.one
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
if self.instructor_id and self.instructor_id in self.attendee_ids:

openacademy/views/openacademy.xml

</record>

<record model="ir.ui.view" id="openacademy_session_graph_view">


<field name="name">openacademy.session.graph</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<graph string="Participations by Courses">
<field name="course_id"/>
<field name="attendees_count" type="measure"/>
</graph>
</field>
</record>

44 
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,gantt,graph</field>
</record>

<menuitem id="session_menu" name="Sessions"


parent="openacademy_menu"

Kanban

Se utiliza para organizar las tareas, procesos de producción, etc ... su elemento raíz es ​
<kanban>​
.

Una vista kanban muestra un conjunto de cartas posiblemente agrupados en columnas. Cada carta representa un
registro, y cada columna los valores de un campo de agregación.

Por ejemplo, las tareas del proyecto pueden ser organizados por etapa (cada columna es una etapa), o por
responsable (cada columna es un usuario), y así sucesivamente.

Las viistas Kanban definen la estructura de cada tarjeta como una mezcla de elementos de formulario (incluyendo
HTML básico) y ​
QWeb​
.

Ejercicio ​
Vista Kanban

Añadir un punto de vista Kanban que muestra sesiones agrupadas por supuesto (columnas son los cursos).
1. Añadir un campo entero color para el modelo de sesión
2. Añadir una vista kanban y actualizar la acción

openacademy/models.py

duration = fields.Float(digits=(6, 2), help="Duration in days")


seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
color = fields.Integer()

instructor_id = fields.Many2one('res.partner', string="Instructor",


domain=['|', ('instructor', '=', True),

45 
openacademy/views/openacademy.xml

</record>

<record model="ir.ui.view" id="view_openacad_session_kanban">


<field name="name">openacad.session.kanban</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<kanban default_group_by="course_id">
<field name="color"/>
<templates>
<t t-name="kanban-box">
<div
t-attf-class="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}}
oe_kanban_global_click_edit oe_semantic_html_override
oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
<div class="oe_dropdown_kanban">
<!-- dropdown menu -->
<div class="oe_dropdown_toggle">
<span class="oe_e">#</span>
<ul class="oe_dropdown_menu">
<li>
<a type="delete">Delete</a>
</li>
<li>
<ul class="oe_kanban_colorpicker"
data-field="color"/>
</li>
</ul>
</div>
<div class="oe_clear"></div>
</div>
<div t-attf-class="oe_kanban_content">
<!-- title -->
Session name:
<field name="name"/>
<br/>
Start date:
<field name="start_date"/>
<br/>
duration:

46 
<field name="duration"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>

<record model="ir.actions.act_window" id="session_list_action">


<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,gantt,graph,kanban</field>
</record>

<menuitem id="session_menu" name="Sessions"


parent="openacademy_menu"

Workflows
Los flujos de trabajo son modelos asociados a los objetos de negocio que describen su dinámica. Los flujos de trabajo
también se utilizan para realizar un seguimiento de los procesos que evolucionan con el tiempo.

Ejercicio ​
Añadir un campo de estado para el modelo de sesión. Se utiliza para definir un flujo de trabajo -ish.

Una sesion puede tener tres estados posibles : Borrador ( predeterminado) , confirmados y hecho.

En el form de sesiones, agregar un campo (sólo lectura ) para visualizar el estado , y los botones para cambiarlo . Las
transiciones válidas son:

➢ Borrador -> Confirmado


➢ Confirmado -> Borrador
➢ Confirmado -> Realizado
➢ Realizado -> Borrador

1. Añadir un nuevo campo ​


state
2. Añadir los métodos de transición del estado, los que pueden ser llamados de vista botones para cambiar el
estado del registro
3. Y añadir los botones correspondientes a la vista form de la sesión

47 
openacademy/models.py

attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)

state = fields.Selection([
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
], default='draft')

@api.one
def action_draft(self):
self.state = 'draft'

@api.one
def action_confirm(self):
self.state = 'confirmed'

@api.one
def action_done(self):
self.state = 'done'

@api.one
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):

openacademy/views/openacademy.xml

<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<header>
<button name="action_draft" type="object"
string="Reset to draft"
states="confirmed,done"/>
<button name="action_confirm" type="object"
string="Confirm" states="draft"
class="oe_highlight"/>

48 
<button name="action_done" type="object"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>
</header>

<sheet>
<group>
<group string="General">

Los flujos de trabajo pueden estar asociados con cualquier objeto en Odoo, y son totalmente personalizable. Los flujos
de trabajo se utilizan para estructurar y gestionar los ciclos de vida de los objetos de negocio y documentos, y definir
las transiciones, triggers, etc. con herramientas gráficas. Flujos de trabajo, actividades (nodos o acciones) y
transiciones (condiciones) se declaran como registros XML, como de costumbre. Las fichas que navegan en los flujos
de trabajo se denominan elementos de trabajo.

Nota Un flujo de trabajo asociado a un modelo sólo se crea cuando se crean los registros de la modelo. Por lo tanto
no hay ninguna instancia de flujo de trabajo asociado con casos de sesión creadas antes de la definición del flujo de
trabajo

Ejercicio​
Workflow

Reemplace el flujo de trabajo de sesiones ad-hoc por un flujo de trabajo real. Transformar la vista de formulario de
sesiones por lo que sus botones puedan llamar el flujo de trabajo en lugar de los métodos del modelo.

openacademy/__openerp__.py

'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
],
# only loaded in demonstration mode
'demo': [

49 
openacademy/models.py

('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
])

@api.one
def action_draft(self):

openacademy/views/openacademy.xml

<field name="arch" type="xml">


<form string="Session Form">
<header>
<button name="draft" type="workflow"
string="Reset to draft"
states="confirmed,done"/>
<button name="confirm" type="workflow"
string="Confirm" states="draft"
class="oe_highlight"/>
<button name="done" type="workflow"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>

openacademy/views/session_workflow.xml

<openerp>
<data>
<record model="workflow" id="wkf_session">
<field name="name">OpenAcademy sessions workflow</field>
<field name="osv">openacademy.session</field>
<field name="on_create">True</field>
</record>
<record model="workflow.activity" id="draft">
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
<field name="kind">function</field>
<field name="action">action_draft()</field>

50 
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">function</field>
<field name="action">action_confirm()</field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">function</field>
<field name="action">action_done()</field>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">
<field name="act_from" ref="draft"/>
<field name="act_to" ref="confirmed"/>
<field name="signal">confirm</field>
</record>
<record model="workflow.transition" id="session_confirmed_to_draft">
<field name="act_from" ref="confirmed"/>
<field name="act_to" ref="draft"/>
<field name="signal">draft</field>
</record>
<record model="workflow.transition" id="session_done_to_draft">
<field name="act_from" ref="done"/>
<field name="act_to" ref="draft"/>
<field name="signal">draft</field>
</record>
<record model="workflow.transition" id="session_confirmed_to_done">
<field name="act_from" ref="confirmed"/>
<field name="act_to" ref="done"/>
<field name="signal">done</field>
</record>
</data>
</openerp>

Tip​

Con el fin de comprobar si las instancias del flujo de trabajo se crean correctamente junto a las sesiones, vaya a
Settings ‣Technical ‣Workflows ‣Instances

51 
Ejercicio ​
Transiciones automáticas

Transición automática de sesiones de Borrador a Confirmado, cuando más de la mitad de los asientos de la sesión son
reservados.

openacademy/views/session_workflow.xml

<field name="act_to" ref="done"/>


<field name="signal">done</field>
</record>

<record model="workflow.transition" id="session_auto_confirm_half_filled">


<field name="act_from" ref="draft"/>
<field name="act_to" ref="confirmed"/>
<field name="condition">taken_seats &gt; 50</field>
</record>
</data>
</openerp>

Ejercicio ​
Acciones del servidor

Reemplace los métodos de Python para la sincronización de estado de la sesión por las acciones del servidor.

Tanto el flujo de trabajo y las acciones del servidor podrían haber sido creadas por completo de la interfaz de usuario.

openacademy/views/session_workflow.xml

<field name="on_create">True</field>
</record>

<record model="ir.actions.server" id="set_session_to_draft">


<field name="name">Set session to Draft</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_draft()
</field>
</record>
<record model="workflow.activity" id="draft">

52 
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_draft"/>
</record>

<record model="ir.actions.server" id="set_session_to_confirmed">


<field name="name">Set session to Confirmed</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_confirm()
</field>
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_confirmed"/>
</record>

<record model="ir.actions.server" id="set_session_to_done">


<field name="name">Set session to Done</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_done()
</field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_done"/>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">

53 
Seguridad
Mecanismos de control de acceso se deben configurar para lograr una política de seguridad coherente.

Mecanismos de control de acceso basado en grupo


Los grupos se crean como registros normales en los modelos res.groups, y el acceso otorgado a través de las
definiciones de menú. Sin embargo, incluso sin un menú, los objetos pueden todavía ser accesible indirectamente, por
lo permisos de nivel de objeto real ( leer, escribir, crear, unlink ) debe definirse para grupos. Por lo general, se insertan
a través de archivos CSV dentro de módulos. También es posible restringir el acceso a los campos específicos en una
visión o un objeto utilizando el campo groups attribute.

Derechos de acceso
Los derechos de acceso se definen como los registros del modelo ir.model.access. Cada derecho de acceso está
asociado a un modelo, un grupo ( o ningún grupo de acceso global ), y un conjunto de permisos: leer, escribir, crear,
desvincular. Estos derechos de acceso suelen ser creados por un archivo CSV lleva el nombre de su modelo:
ir.model.access.csv .

id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_idea_idea,idea.idea,model_idea_idea,base.group_user,1,1,1,0
access_idea_vote,idea.vote,model_idea_vote,base.group_user,1,1,1,0

Ejercicio ​
Agregar el control de acceso a través de la interfaz de OpenERP

Crear un nuevo usuario " John Smith ". A continuación, cree un grupo " OpenAcademy / Sesión Lee " con acceso de
lectura a la modelo de sesión.

1. Crear un nuevo usuario John Smith a través de ​


Settings ‣Users ‣Users
2. Crear un nuevo grupo ​
session_read a través ​
Settings ‣Users ‣Groups​
, debe tener acceso de lectura en el
modelo Session
3. Editar John Smith y hacerlo un miembro del grupo ​
session_read
4. Loguearse como John Smith para chequear si los permisos de acceso son correctos

Ejercicio ​
Agregar el control de acceso a través de los archivos de datos en el módulo
Usando los archivos de datos,

➢ Crear un grupo OpenAcademy / Manager con acceso total al modelo OpenAcademy


➢ Hacer Session y Course legible por todos los usuarios

54 
1. Crear un nuevo archivo ​
openacademy/security/security.xml​
para mantener OpenAcademy Manager group
2. Editar el archivo ​
openacademy/security/ir.model.access.csv​
con derecho de acceso a los modelos
3. Actualizar ​
openacademy/__openerp__.py​
para añadir los nuevo archivos de datos

openacademy/__openerp__.py

# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',

openacademy/security/ir.model.access.csv

id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1
session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1
course_read_all,course all,model_openacademy_course,,1,0,0,0
session_read_all,session all,model_openacademy_session,,1,0,0,0

openacademy/security/security.xml

<openerp>
<data>
<record id="group_manager" model="res.groups">
<field name="name">OpenAcademy / Manager</field>
</record>
</data>
</openerp>

Reglas de registro
Una regla registro restringe los derechos de acceso a un subconjunto de registros del modelo dado. Una regla es un
registro del modelo ir.rule , y se asocia a un modelo, un número de grupos (campo Many2Many ), los permisos a los
que se aplica la restricción, y un dominio. El dominio especifica a que registro los derechos de acceso son limitados.

55 
He aquí un ejemplo de una regla que impide la eliminación de los cables que no están en estado de cancelar. Observe
que el valor de los grupos de campos debe seguir la misma convención como el método write() de la ORM.

<record id="delete_cancelled_only" model="ir.rule">


<field name="name">Only cancelled leads may be deleted</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="groups" eval="[(4, ref('base.group_sale_manager'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('state','=','cancel')]</field>
</record>

Ejercicio ​
Regla de registro

Añadir una regla de registro para el modelo Curso y el grupo " OpenAcademy / Manager" , que restringe escribir y
desvincular accesos al responsable de un curso. Si un curso no tiene ningun responsable, todos los usuarios del grupo
debe ser capaz de modificarlo. Crear una nueva regla en openacademy/security/security.xml:

openacademy/security/security.xml

<record id="group_manager" model="res.groups">


<field name="name">OpenAcademy / Manager</field>
</record>
<record id="only_responsible_can_modify" model="ir.rule">
<field name="name">Only Responsible can modify Course</field>
<field name="model_id" ref="model_openacademy_course"/>
<field name="groups" eval="[(4, ref('openacademy.group_manager'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">
['|', ('responsible_id','=',False),
('responsible_id','=',user.id)]
</field>
</record>
</data>
</openerp>

56 
Wizards
Wizards describen sesiones interactivas con el usuario (o cuadros de diálogo ) a través de formularios dinámicos. Un
wizard no es más que un modelo que amplía la clase TransientModel en lugar de Modelo. La clase TransientModel
extiende modelo y reutiliza todos sus mecanismos existentes, con las siguientes particularidades:

➢ Wizard de registros no están destinados a ser persistente; que se eliminan automáticamente de la base de
datos después de un cierto tiempo. Es por eso que se llaman transitoria.
➢ Wizard de modelos no requieren derechos de acceso explícitos: los usuarios tienen todos los permisos en
los registros del wizard.
➢ Wizard de registros pueden referirse a los registros regulares o registros asistente a través de campos
many2one, pero los registros regulares no pueden hacer referencia a los registros del asistente a través de
un campo de many2one.

Queremos crear un wizard que permite a los usuarios crear asistentes para una sesión en particular, o para obtener
una lista de las sesiones a la vez.

Ejercicio ​
Definir el aistente

Crear un asistente de modelo con una relación many2one con el modelo de sesión y una relación Many2Many con el
modelo partner. Añadir un nuevo archivo openacademy/wizard.py:

openacademy/__init__.py

from . import controllers


from . import models
from . import partner
from . import wizard

openacademy/wizard.py

# -*- coding: utf-8 -*-


from openerp import models, fields, api

class Wizard(models.TransientModel):
_name = 'openacademy.wizard'

session_id = fields.Many2one('openacademy.session',
string="Session", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

57 
Lanzamiento de wizards
Los wizards son lanzados por ​
ir.actions.act_window​
, con el campo ​
target seteado con el valor ​
new​
. Este último se
abre la vista del asistente en una ventana emergente. La acción puede ser provocada por un elemento de menú.
Hay otra manera de iniciar el asistente: utilizar un registro ​
ir.actions.act_window como el anterior, pero con un
campo extra ​
src_model que especifica en el contexto de qué modelo de la acción está disponible. El asistente
aparecerá en las acciones contextuales del modelo, por encima de la vista principal. Debido a algunos ganchos
internos en el ORM , tal acción se declara en XML con el tag ​
act_window​
.

<act_window id="launch_the_wizard"
name="Launch the Wizard"
src_model="context_model_name"
res_model="wizard_model_name"
view_mode="form"
target="new"
key2="client_action_multi"/>

Los wizards usan visitas regulares y sus botones pueden utilizar el atributo ​
special="cancel" para cerrar la ventana del
asistente sin guardar.

Ejercicio ​
Lanzar el wizard
1. Definir una vista form para el wizard.
2. Añadir la acción para lanzar en el contexto del modelo de Sesión
3. Definir un valor por defecto para el campo session en el wizard; usar el parámetro de contexto ​
self._context
para recuperar la sesion actual.

openacademy/wizard.py

class Wizard(models.TransientModel):
_name = 'openacademy.wizard'

def _default_session(self):
return self.env['openacademy.session'].browse(self._context.get('active_id'))

session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

58 
openacademy/views/openacademy.xml

parent="openacademy_menu"
action="session_list_action"/>

<record model="ir.ui.view" id="wizard_form_view">


<field name="name">wizard.form</field>
<field name="model">openacademy.wizard</field>
<field name="arch" type="xml">
<form string="Add Attendees">
<group>
<field name="session_id"/>
<field name="attendee_ids"/>
</group>
</form>
</field>
</record>

<act_window id="launch_session_wizard"
name="Add Attendees"
src_model="openacademy.session"
res_model="openacademy.wizard"
view_mode="form"
target="new"
key2="client_action_multi"/>
</data>
</openerp>

Ejercicio ​
Registrar asistentes

Añadir un boton para el wizard, e implementar el correspondiente método para añadir los asistentes a la session dada.

openacademy/views/openacademy.xml

<field name="attendee_ids"/>
</group>
<footer>
<button name="subscribe" type="object"
string="Subscribe" class="oe_highlight"/>

59 
or
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>

openacademy/wizard.py

session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

@api.multi
def subscribe(self):
self.session_id.attendee_ids |= self.attendee_ids
return {}

Ejercicio ​
Registrar los asistentes a varias sesiones

Modificar el wizard del modelo para que los asistentes puedan ser registrados en varias sesiones.

openacademy/views/openacademy.xml

<form string="Add Attendees">


<group>
<field name="session_ids"/>
<field name="attendee_ids"/>
</group>
<footer>
<button name="subscribe" type="object"

openacademy/wizard.py

class Wizard(models.TransientModel):
_name = 'openacademy.wizard'

def _default_sessions(self):

60 
return self.env['openacademy.session'].browse(self._context.get('active_ids'))

session_ids = fields.Many2many('openacademy.session',
string="Sessions", required=True, default=_default_sessions)
attendee_ids = fields.Many2many('res.partner', string="Attendees")

@api.multi
def subscribe(self):
for session in self.session_ids:
session.attendee_ids |= self.attendee_ids
return {}

Internationalización
Cada módulo puede proporcionar sus propias traducciones en el directorio i18n, teniendo archivos denominados
LANG.po donde LANG es el código de configuración regional para la lengua, o la combinación de idioma y país cuando
difieren (por ejemplo pt.po o pt_BR.po). Las traducciones se cargarán automáticamente por Odoo para todos los
idiomas habilitados.
Los desarrolladores siempre utilizan Inglés al crear un módulo, a continuación, exportar los términos del módulo
utilizando característica de exportación POT gettext de Odoo (​
Settings ‣Translations ‣Import/Export ‣Export
Translation sin especificar un idioma), para crear el archivo POT plantilla de módulo, y luego derivan del traducidos
archivos PO. Muchos IDE tienen plugins o modos de edición y fusión de archivos PO/POT.

Tip

El formato gettext GNU (Portable Object) utilizado por Odoo está integrado en LaunchPad, lo que es una plataforma
de traducción colaborativa en línea.

|- idea/ # The module directory


|- i18n/ # Translation files
| - idea.pot # Translation Template (exported from Odoo)
| - fr.po # French translation
| - pt_BR.po # Brazilian Portuguese translation
| (...)

Tip

61 
Por defecto POT exportación de Odoo sólo extrae etiquetas dentro de archivos XML o definiciones de campo dentro
de código Python, pero ningun string de Python se puede traducir de esta manera por lo rodeándolo con la función de
OpenERP ._ ( ) (por ejemplo _ ( "Label" ) )

Ejercicio ​
Traducir un módulo

Elegir un segundo idioma para la instalación Odoo. Traducir el módulo usando las facilidades proporcionadas por
Odoo.

1. Crear un directorio ​
openacademy/i18n/
2. Instalar cualquier idioma que quiera ( ​
Administration ‣Translations ‣Load an Official Translation​
)
3. Sincronizar los términos traducibles (​
Administration ‣Translations ‣Application Terms ‣Synchronize
Translations​
)
4. Crear un archivo de traducción de plantilla para exportar ( ​
Administration ‣Translations -> Import/Export ‣
Export Translation​
) sin especificar un idioma, guardarlo en ​
openacademy/i18n/
5. Crear un archivo de traducción para exportar ( ​
Administration ‣Translations ‣Import/Export ‣Export
Translation​
) y especificar un idioma. Guardarlo en ​
openacademy/i18n/
6. Abrir el archivo de traducción exportado (con un editor de texto básico o un editor de archivos PO dedicado
como por ejemplo ​
POEdit​
y traducir los términos que faltan.
7. En ​
models.py​
, añadir una sentencia import para la función ​
openerp._ y marcar las cadenas traducibles
como desaparecidas
8. Repetir los pasos del 3 al 6

openacademy/models.py

# -*- coding: utf-8 -*-

from datetime import timedelta


from openerp import models, fields, api, exceptions, _

class Course(models.Model):
_name = 'openacademy.course'
default = dict(default or {})

copied_count = self.search_count(
[('name', '=like', _(u"Copy of {}%").format(self.name))])
if not copied_count:
new_name = _(u"Copy of {}").format(self.name)
else:

62 
new_name = _(u"Copy of {} ({})").format(self.name, copied_count)

default['name'] = new_name
return super(Course, self).copy(default)

if self.seats < 0:
return {
'warning': {
'title': _("Incorrect 'seats' value"),
'message': _("The number of available seats may not be negative"),
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': _("Too many attendees"),
'message': _("Increase seats or remove excess attendees"),
},
}

@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
if self.instructor_id and self.instructor_id in self.attendee_ids:
raise exceptions.ValidationError(_("A session's instructor can't be an attendee"))

Reporting
Reportes impresos
Odoo 8.0 viene con un nuevo motor de informes basado en QWEB , Twitter Bootstrap y wkhtmltopdf .
Un informe es una combinación de dos elementos:

➢ un ir.actions.report.xml, para lo cual se proporciona un elemento de acceso directo <report>, establece


varios parámetros básicos para el informe (tipo predeterminado, si el informe debe ser guardado en la
base de datos tras la generación, ... )

➢ <report
id="account_invoices"
model="account.invoice"
string="Invoices"

63 
report_type="qweb-pdf"
name="account.report_invoice"
file="account.report_invoice"
attachment_use="True"
attachment="(object.state in ('open','paid')) and
('INV'+(object.number or '').replace('/','')+'.pdf')"
/>

➢ Una vista QWEB estándar para el informe real :

➢ <t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="report.external_layout">
<div class="page">
<h2>Report title</h2>
</div>
</t>
</t>
</t>

el contexto de representación estándar proporciona una serie de elementos, siendo los más importantes:

`` docs``
los registros para los que se imprime el informe
`` user``
al usuario imprimir el informe

Dado que los informes son páginas web estándar, están disponibles a través de un URL y la salida de parámetros
pueden ser manipulados a través de esta URL, por ejemplo, está disponible la versión HTML del informe de facturas a
través de ​
http://localhost:8069/report/html/account.report_invoice/1 (si ​
account está instalado) y la versión PDF a
través de ​
http://localhost:8069/report/pdf/account.report_invoice/1​
.

Ejercicio ​
Crear un reporte para el modelo Session

Para cada sesión, debe mostrar el nombre de la sesión, el inicio y el final, y la lista de los asistentes a la sesión.
openacademy/__openerp__.py

'views/openacademy.xml',
'views/partner.xml',

64 
'views/session_workflow.xml',
'reports.xml',
],
# only loaded in demonstration mode
'demo': [

openacademy/reports.xml
<openerp>
<data>
<report
id="report_session"
model="openacademy.session"
string="Session Report"
name="openacademy.report_session_view"
file="openacademy.report_session"
report_type="qweb-pdf" />
<template id="report_session_view">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="report.external_layout">
<div class="page">
<h2 t-field="doc.name"/>
<p>From <span t-field="doc.start_date"/> to <span t-field="doc.end_date"/></p>
<h3>Attendees:</h3>
<ul>
<t t-foreach="doc.attendee_ids" t-as="attendee">
<li><span t-field="attendee.name"/></li>
</t>
</ul>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

Dashboards

65 
Ejercicio ​
Definir un Dashboard

Definir un panel que contiene la vista de gráfico que ha creado, la vista del calendario de sesiones y una vista de lista
de los cursos (conmutable a una vista de formulario). Este panel debe estar disponible a través de un elemento de
menú en el menú, y se muestran automáticamente en el cliente web cuando se selecciona el menú principal
OpenAcademy .

1. Crear un archivo ​
openacademy/views/session_board.xml​
. Debe contener la vista tabla, las acciones que
se hace referencia en esta vista, una acción para abrir el tablero de instrumentos y una re-definición del
elemento del menú principal para agregar la acción dashboard
Nota
Los estilos de dashboard disponibles son ​
1​
,​
1-1​
,​
1-2​
,​
2-1​
y​
1-1-1
2. Actualizar ​
openacademy/__openerp__.py​
para hacer referencia al nuevo archivo de datos

openacademy/__openerp__.py

'version': '0.1',

# any module necessary for this one to work correctly


'depends': ['base', 'board'],

# always loaded
'data': [

'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'views/session_board.xml',
'reports.xml',
],
# only loaded in demonstration mode

openacademy/views/session_board.xml

<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.actions.act_window" id="act_session_graph">

66 
<field name="name">Attendees by course</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">graph</field>
<field name="view_id"
ref="openacademy.openacademy_session_graph_view"/>
</record>
<record model="ir.actions.act_window" id="act_session_calendar">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">calendar</field>
<field name="view_id" ref="openacademy.session_calendar_view"/>
</record>
<record model="ir.actions.act_window" id="act_course_list">
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<record model="ir.ui.view" id="board_session_form">
<field name="name">Session Dashboard Form</field>
<field name="model">board.board</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Session Dashboard">
<board style="2-1">
<column>
<action
string="Attendees by course"
name="%(act_session_graph)d"
height="150"
width="510"/>
<action
string="Sessions"
name="%(act_session_calendar)d"/>
</column>
<column>
<action
string="Courses"
name="%(act_course_list)d"/>

67 
</column>
</board>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_board_session">
<field name="name">Session Dashboard</field>
<field name="res_model">board.board</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="usage">menu</field>
<field name="view_id" ref="board_session_form"/>
</record>

<menuitem
name="Session Dashboard" parent="base.menu_reporting_dashboard"
action="open_board_session"
sequence="1"
id="menu_board_session" icon="terp-graph"/>
</data>
</openerp>

WebServices
El módulo de servicios web ofrece una interfaz común para todos los servicios web:

➢ XML-RPC
➢ JSON-RPC

Los objetos de negocio también se puede acceder a través del mecanismo de objetos distribuidos. Todos ellos pueden
ser modificados a través de la interfaz de cliente con vistas contextuales.

Odoo es accesible a través de interfaces XML-RPC / JSON-RPC, para los que existen bibliotecas en muchos idiomas.

XML-RPC Library

El siguiente ejemplo es un programa de Python que interactúa con un servidor Odoo con la biblioteca xmlrpclib:

68 
import xmlrpclib

root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)

uid = xmlrpclib.ServerProxy(root + 'common').login(DB, USER, PASS)


print "Logged in as %s (uid: %d)" % (USER, uid)

# Create a new note


sock = xmlrpclib.ServerProxy(root + 'object')
args = {
'color' : 8,
'memo' : 'This is a note',
'create_uid': uid,
}
note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)

Ejercicio ​
Añadir un nuevo servicio al cliente

Escriba un programa Python capaz de enviar peticiones XML-RPC a un PC con Odoo (el suyo o el de su instructor). Este
programa debe mostrar todas las sesiones, y su correspondiente número de sillas. También debe crear una nueva
sesión de uno de los cursos.

import functools
import xmlrpclib
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT)

# 1. Login
uid = xmlrpclib.ServerProxy(ROOT + 'common').login(DB,USER,PASS)
print "Logged in as %s (uid:%d)" % (USER,uid)

call = functools.partial(
xmlrpclib.ServerProxy(ROOT + 'object').execute,
DB, uid, PASS)

# 2. Read the sessions

69 
sessions = call('openacademy.session','search_read', [], ['name','seats'])
for session in sessions:
print "Session %s (%s seats)" % (session['name'], session['seats'])

# 3.create a new session


session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : 2,
})

En lugar de utilizar un identificador de curso no modificable, el código puede buscar un curso por nombre:

# 3.create a new session for the "Functional" course


course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0]
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : course_id,
})

JSON-RPC Library

El siguiente programa Python interactúa con un servidor Odoo con las bibliotecas estándar urllib2 y JSON:

import json
import random
import urllib2

def json_rpc(url, method, params):


data = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": random.randint(0, 1000000000),
}
req = urllib2.Request(url=url, data=json.dumps(data), headers={
"Content-Type":"application/json",
})
reply = json.load(urllib2.urlopen(req))
if reply.get("error"):
raise Exception(reply["error"])
return reply["result"]

70 
def call(url, service, method, *args):
return json_rpc(url, "call", {"service": service, "method": method, "args": args})

# log in the given database


url = "http://%s:%s/jsonrpc" % (HOST, PORT)
uid = call(url, "common", "login", DB, USER, PASS)

# create a new note


args = {
'color' : 8,
'memo' : 'This is another note',
'create_uid': uid,
}
note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args)

Aquí está el mismo programa, utilizando la biblioteca jsonrpclib:

import jsonrpclib

# server proxy object


url = "http://%s:%s/jsonrpc" % (HOST, PORT)
server = jsonrpclib.Server(url)

# log in the given database


uid = server.call(service="common", method="login", args=[DB, USER, PASS])

# helper function for invoking model methods


def invoke(model, method, *args):
args = [DB, uid, PASS, model, method] + list(args)
return server.call(service="object", method="execute", args=args)

# create a new note


args = {
'color' : 8,
'memo' : 'This is another note',
'create_uid': uid,
}
note_id = invoke('note.note', 'create', args)

Estos ejemplos pueden ser adaptados fácilmente de XML-RPC para JSON-RPC .

71 

You might also like