Advertencia
Este tutorial requiere haber instalado Odoo Este tutorial fue traducido al español por Vauxoo [http://www.vauxoo.com] para sus cursos. Para cualquier sugerencia de mejora, por favor enviarla a nhomar at vauxoo dot com.
Inicia/Detén el servidor Odoo
Odoo utiliza una arquitectura cliente/servidor en la que los clientes son los navegadores que acceden al servidor Odoo vía RPC.
La lógica de negocio y sus extensiones se realizan generalmente del lado del servidor, aunque se apoya en algunas características del cliente (por ejemplo, una representación de datos como mapas interactivos) que pueden ser añadidas al cliente.
Para iniciar el servidor, simplemente ejecuta el comando odoo-bin en la consola, agregando la ruta completa al archivo si es necesario:
odoo-bin
El servidor se detiene pulsando Ctrl-C
dos veces desde la terminal, o matando el proceso correspondiente.
Construir un módulo para Odoo
Las extensiones de servidor y cliente se empaquetan como módulos que opcionalmente se cargan en una base de datos.
Los módulos en Odoo pueden añadir nueva lógica de negocio a un sistema Odoo, o modificar y ampliar la lógica de negocios existente: puede crear un módulo para agregar las reglas de contabilidad de su país adicionándolo al soporte de contabilidad genérico de Odoo, mientras que otro módulo puede añadir soporte para la visualización en tiempo real de una flota de autobuses.
Todo en Odoo comienza y termina con módulos.
Composición de un módulo
Un módulo de Odoo puede contener una serie de elementos:
- Objetos de negocios
Declarado como clases de Python, estos recursos son automáticamente persistentes en Odoo basados en su configuración
- Archivos de datos
Los archivos XML o CSV declaran metadatos (vistas o reportes), datos de configuración (parametrizan los módulos), datos de demostración y más
- Controladores Web
Manejo de peticiones de los navegadores web
- Datos estáticos Web
Archivos de imágenes, CSS o javascript utilizados por la interfaz web o sitio web
Estructura de un módulo
Cada módulo es un directorio dentro de un directorio de módulos. Los directorios de módulos se especifican mediante la opción --addons-path
.
Truco
la mayoría de las opciones de línea de comandos también se pueden establecer usando un archivo de configuración
Un módulo de Odoo es declarado por su manifiesto. Ver la Documentación del manifiesto aquí.
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 dicho módulo.
Por ejemplo, si un módulo tiene un solo archivo mymodule.py
, el archivo __init__.py
podría contener:
from . import mymodule
Odoo ofrece un mecanismo para ayudar a comenzar un nuevo módulo, el comando odoo-bin tiene un subcomando scaffold para crear un módulo vacío:
$ odoo-bin scaffold <module name> <where to put it>
El comando crea un subdirectorio para un módulo y automáticamente crea un montón de archivos estándar para dicho módulo. La mayoría de ellos contienen sólo código comentado o XML. A lo largo de este tutorial se explicará el uso de la mayoría de estos archivos.
Exercise
Creación de un módulo
Utilizar la línea de comandos anterior para crear un módulo vacío Open Academy e instalarlo en Odoo.
Ejecuta el comando
odoo-bin scaffold openacademy addons
.Adapta el archivo de manifiesto al módulo.
No te preocupes por los demás archivos.
# -*- coding: utf-8 -*-
{
'name': "Open Academy",
'summary': """Manage trainings""",
'description': """
Open Academy module for managing trainings:
- training courses
- training sessions
- attendees registration
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/master/odoo/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',
],
}
# -*- coding: utf-8 -*-
from . import controllers
from . import models
# -*- coding: utf-8 -*-
from odoo 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
# })
<odoo>
<!-- -->
<!-- <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> -->
<!-- -->
</odoo>
# -*- coding: utf-8 -*-
from odoo import models, fields, api
# class openacademy(models.Model):
# _name = 'openacademy.openacademy'
# name = fields.Char()
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
<odoo>
<!-- <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> -->
</odoo>
Mapeo Objeto-Relacional
Un componente clave de Odoo es el ORM. Esta capa evita escribir la mayoría del SQL manualmente y proporciona extensibilidad y servicios de seguridad 2.
Los objetos de negocio se declaran como clases Python que extienden Model
la cual los integra en el sistema automatizado de persistencia.
Módulos pueden ser configurados para establecer un número de atributos en su definición. El atributo más importante es _name
el cuál es requerido y define el nombre para el modelo en el sistema de Odoo. Aquí es mínimamente completada la definición de un modelo:
from odoo import models
class MinimalModel(models.Model):
_name = 'test.model'
Campos en el Modelo
Los campos se utilizan para definir lo que puede almacenar el modelo y dónde. Los campos se definen como atributos en el modelo:
from odoo import models, fields
class LessMinimalModel(models.Model):
_name = 'test.model2'
name = fields.Char()
Atributos comunes
Así como en el modelo, los campos se pueden configurar pasando los atributos como parámetros normales:
name = field.Char(required=True)
Algunos atributos están disponibles en todos los campos, aquí están los más comunes:
string
(unicode
, default: field's name)La etiqueta del campo en la interfaz del usuario (lo que el usuario leerá).
required
(bool
, default:False
)Si es
Verdadero
, el campo no puede estar vacío, deberá tener un valor predeterminado o siempre se tendrá que asignar un valor al crear un registro.help
(unicode
, default:''
)Formato largo, proporciona una información de ayuda en la interfaz de usuario.
index
(bool
, default:False
)Solicita a Odoo la creación de un índice database index en la columna.
Campos Simples
En general hay dos categorías de campos: Los campos ”Simples" los cuales son valores atómicos almacenados directamente en la tabla correspondiente al modelo, y los campos “Relacionales” los cuales enlazan registros (registros del mismo modelo o de diferentes modelos).
Campos reservados
Odoo crea algunos campos en todos los modelos1. Estos campos son manejados por el sistema y no deberían ser escritos. Ellos pueden ser leídos si representan alguna utilidad o son necesarios:
id
(Id
)El identificador único para un registro en su modelo.
create_date
(Datetime
)Fecha de creación del registro.
create_uid
(Many2one
)Usuario quien creó el registro.
write_date
(Datetime
)Fecha de la última modificación del registro.
write_uid
(Many2one
)usuario quien hizo la última modificación del registro.
Campos Especiales
Por defecto, Odoo también requiere un campo name
en todos los modelos para varios comportamientos de búsqueda y visualización. El campo utilizado para estos fines se puede reemplazar estableciendo _rec_name
.
Exercise
Define un modelo
Define un nuevo modelo de datos Curso en el módulo openacademy. Un curso tiene un título y una descripción. Los cursos deben tener un título.
Edita el archivo openacademy/models/models.py
incluyendo una clase Course (intenta nombrar todo en inglés).
from odoo import models, fields, api
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string="Title", required=True)
description = fields.Text()
Archivos de datos
Odoo es un sistema altamente dirigido por datos. Aunque el comportamiento personalizado utiliza una parte de código Python el valor de un módulo está en los datos que éste establece cuando se carga.
Truco
algunos módulos existen únicamente para agregar datos a Odoo
Los datos del módulo son declarados vía Archivos de Datos, archivos XML con elementos <record>
. Cada elemento <record>
crea o actualiza registros en la base de datos.
<odoo>
<record model="{model name}" id="{record identifier}">
<field name="{a field name}">{a value}</field>
</record>
</odoo>
model
es el nombre para un registro en el modelo de Odoo.id
es un external identifier (identificador único), que permite hacer referencia a un registro (sin necesidad de conocer su identificador en base de datos).<field>
los elementos tienen unnombre
que es el nombre del campo en el modelo (por ejemplo,description
). Su cuerpo es el valor de campo.
Los archivos de datos tienen que ser declarados en el archivo manifiesto para que puedan cargarse, ellos pueden ser declarados en la lista 'data'
(la cual siempre se carga) o en la lista 'demo'
(que se cargará solo cuando modo demostración esté habilitado).
Exercise
Define los datos de demostración
Crea los datos de demostración llenando el model Courses con algunos pocos cursos.
Edita el archivo openacademy/demo/demo.xml
para incluir algunos datos.
<odoo>
<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>
</odoo>
Vistas Básicas
Las vistas definen el modo 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, …). Las vistas se pueden solicitar ya sea de manera genérica a través de su tipo (por ejemplo una lista de clientes) o especificando su id. Para solicitudes genéricas, se utilizará la vista con el tipo y la prioridad más baja (por lo que la vista de la prioridad más baja de cada tipo es la vista predeterminada para ese tipo).
La herencia de Vistas permite modificar vistas declaradas en otros lugares (agregar o quitar contenido).
Declaración de vista genérica
Una vista es declarada como un registro del modelo ir.ui.view
. El tipo de vista es 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>
Peligro
El contenido de la vista es XML.
El campo arch
debe ser declarado como type =“xml”
para que sea analizado correctamente.
Vistas de árbol
Vistas de árbol, también llamadas vistas de lista, mostrarán registros en forma de tabla.
Su elemento raíz es <tree>
. La forma más simple de la vista de árbol simplemente lista todos los campos a 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 individualmente.
Su elemento raíz es <form>
. Están compuestos por una estructura de alto nivel (groups, notebooks) y los elementos interactivos (buttons y fields):
<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>
Exercise
Personaliza la vista de formulario usando XML
Crea una vista de formulario propia para el objeto Course
. Los datos mostrados deben ser: el nombre y la descripción del curso.
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<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>
<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",
Exercise
Cuaderno de Notas (Notebooks)
En la vista formulario de un curso, poner el campo description
en una pestaña, de tal manera que será más fácil añadir más tarde otras pestañas que contengan información adicional.
Modifica la vista de formulario de curso como sigue:
<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>
Las vistas de formulario también pueden utilizar 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úsqueda
Las vistas de búsqueda personalizan los campos de búsqueda asociados con la vista de lista (y otras vistas agregadas). Su elemento raíz es <search>
y están compuestos por los campos definidos, por los cuáles pueden ser buscados:
<search>
<field name="name"/>
<field name="inventor_id"/>
</search>
Si no existe ninguna vista de búsqueda para el modelo, Odoo genera automáticamente una, que permite la búsqueda por el campo name
.
Exercise
Buscar Cursos
Permite buscar cursos basados en el título o la descripción.
</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 de orden de venta se relaciona con un registro de cliente que contiene los datos del cliente; también está relacionado con los registros de línea de orden de venta.
Exercise
Crea un modelo Session
Para el módulo Open Academy, consideramos un modelo sessions: una sesión es cuando un curso es impartido en un momento dado para un público determinado.
Crea un modelo para sessions. Una sesión tiene un nombre, una fecha de inicio, una duración y un número de asientos. Agrega una acción y un elemento de menú a los mismos. Visualizar el nuevo modelo a través de un menú.
Crea la clase Session en
openacademy/models/models.py
.Añade el acceso al objeto session en
openacademy/view/openacademy.xml
.
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")
<!-- 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>
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
action="session_list_action"/>
</odoo>
Nota
digits=(6, 2)
especifica la precisión de un número flotante: 6 es el número total de dígitos, mientras que 2 es el número de dígitos después de la coma. Ten en cuenta que, en el resultado, los dígitos antes de la coma pueden tener un máximo de 4
Campos Relacionales
Los campos relacionales vinculan registros, ya sea del mismo modelo (jerarquías) o entre diferentes modelos.
Los tipos de campos relacionales son:
Many2one(other_model, ondelete='set null')
Un simple enlace a otro objeto:
print foo.other_id.name
Ver también
foreign keys (llaves foráneas)
One2many(other_model, related_field)
Una relación virtual, inverso a un
Many2one
. UnOne2many
se comporta como contenedor de registros, accediendo a sus resultados en un (posiblemente vacío) conjunto de registros:for other in foo.other_ids: print other.name
Many2many(other_model)
Relación bidireccional múltiple, cualquier registro en un lado puede estar relacionada con cualquier número de registros en el otro lado. Se comporta como un contenedor de registros, al acceder a él también se puede traducir en una lista vacía de registros:
for other in foo.other_ids: print other.name
Exercise
Relaciones Many2one
Usando un many2one, modificar los modelos Course y Session para reflejar su relación con otros modelos:
Un curso tiene un usuario responsable (responsible); 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 está relacionada con un curso (course); el valor de ese campo es un registro del modelo
openacademy.course
y es requerido.Adapta las vistas.
Añade los campos
Many2one
correspondientes a los modelos, yañádelos en las vistas.
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)
<sheet>
<group>
<field name="name"/>
<field name="responsible_id"/>
</group>
<notebook>
<page string="Description">
</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>
<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>
Exercise
Relaciones One2Many inversas
Utilizando el campo relacional inverso one2many, modifica los modelos para reflejar la relación entre los cursos y sesiones.
Modifica la clase
Course
, yañade el campo en la vista de formulario del curso.
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):
<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>
Exercise
Relaciones Many2Many múltiples
Utilizando el campo relacional Many2Many, modifica el modelo de Session para relacionar cada sesión a un conjunto de asistentes (attendees). Los asistentes estarán representados por los registros partners, por lo que se relacionan con el modelo res.partner
. Adapta las vistas en consecuencia.
Modifica la clase `` Session``, y
agrega el campo en la vista de formulario del curso.
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")
<field name="seats"/>
</group>
</group>
<label for="attendee_ids"/>
<field name="attendee_ids"/>
</sheet>
</form>
</field>
Herencia
Herencia de modelos
Odoo proporciona 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:
agrega campos a un modelo,
sobrescribir la definición de campos en un modelo,
añade restricciones a un modelo,
añade métodos a un modelo,
reemplazar los métodos existentes en un modelo.
El segundo mecanismo de herencia (delegación) permite vincular cada registro de un modelo a un registro en un modelo padre, y proporciona un acceso transparente a los campos del registro padre.

Herencia de vistas
En lugar de modificar las vistas existentes en el lugar (sobrescribiéndolos), Odoo proporciona herencia de vistas, donde las vistas hijas extensión
se aplican en la parte superior de la vista de la raíz, y se puede añadir o eliminar contenido de sus padres.
Una vista de extensión hace referencia a su padre con el campo inherit_id
, y en lugar de una vista el campo arch
se compone de cualquier número de xpath
, elementos que seleccionan y modifican 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">idea.category</field>
<field name="inherit_id" ref="id_category_list"/>
<field name="arch" type="xml">
<!-- find field description and add the field
idea_ids after it -->
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" string="Number of ideas"/>
</xpath>
</field>
</record>
expr
Una expresión XPath es la selección de un solo elemento en la vista padre. Genera un error si no coincide con ningún elemento o más de uno
position
Operación para aplicar al elemento coincidente:
inside
anexa el cuerpo de
xpath
en el extremo del elemento emparejadoreplace
reemplaza el elemento que empate con el cuerpo del
xpath
, reemplazando cualquier ocurrencia de nodo$0
en el nuevo cuerpo con el elemento originalbefore
inserta el cuerpo del
xpath
como un hermano antes del elemento emparejadoafter
inserta el cuerpo de
xpaths
como un hermano después del elemento emparejadoattributes
altera los atributos del elemento coincidente utilizando elementos
attribute
especiales en el cuerpo dexpath
Truco
En la concordancia de un solo elemento, el atributo position
se puede establecer directamente sobre el elemento que se encuentra. Ambas herencias a continuación le dará el mismo resultado.
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field>
Exercise
Alterar el contenido existente
Utilizando el modelo de herencia, modifica el modelo Partner y añade un campo boolean llamado
Instructor
y un campo many2many que corresponde a la relación de session-partnerMediante herencia de vista, muestra este campo en la vista formulario de partner
Nota
Esta es la oportunidad para presentar el modo desarrollador para inspeccionar la vista, conocer su ID externo y el lugar para poner el nuevo campo.
Crea un archivo
openacademy/models/partner.py
e impórtalo en__init__.py
Crea un archivo
openacademy/views/partner.xml
y agrégalo al__manifest__.py
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import partner
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
],
# only loaded in demonstration mode
'demo': [
# -*- coding: utf-8 -*-
from odoo 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)
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- 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"/>
<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"/>
</odoo>
Dominios
En Odoo, Domains (dominios) son valores que representan condiciones en los registros. Un dominio es una lista de criterios utilizados para seleccionar un subconjunto de registros de un modelo. Cada criterio es una tripleta con un nombre de campo, un operador y un valor.
Por ejemplo, cuando se usa en el modelo product el siguiente dominio selecciona todos los services con un precio unitario mayor 1000:
[('product_type', '=', 'service'), ('unit_price', '>', 1000)]
Por defecto se combinan criterios con un AND implícito. Los operadores lógicos &
(AND), |
(OR) and !
(NOT) se pueden utilizar para combinar explícitamente criterios. Se utilizan en posición de prefijo (el operador se inserta antes de sus argumentos, y no entre). Por ejemplo para seleccionar productos "que son servicios OR tienen un precio unitario que NOT está entre 1000 y 2000":
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]
Un parámetro dominio
puede ser añadido a campos relacionales para limitar registros válidos para esa relación cuando se intenta seleccionar registros en la interfaz del cliente.
Exercise
Dominios en campos relacionales
Al seleccionar el instructor para una Session, sólo instructores (partners cuyo campo instructor
es True
) deben ser visibles.
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 hacer referencia a valores dinámicos en el lado derecho en su declaración, un dominio declarado como una cadena es evaluado del lado del cliente y permite nombres de campo en el lado derecho
Exercise
Dominios más complejos
Crea un par de nuevas categorías Teacher / Level 1 y Teacher / Level 2. El instructor de una sesión puede ser un instructor o un profesor (de cualquier nivel).
Modifica el dominio del modelo Session
Modifica
openacademy/view/partner.xml
para obtener acceso a las categorías que acabamos de crear:
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")
<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>
</odoo>
Campos y valores calculados
Hasta ahora los campos han sido almacenados y obtenidos directamente de la base de datos. Los campos también pueden ser calculados. En ese caso, el valor del campo no es obtenido de la base de datos sino calculado al vuelo llamando a un método en el modelo.
Para crear un campo calculado, crea un campo y establece su atributo: compute
con el nombre del método. El método de cómputo simplemente debe establecer el valor del campo calculado en cada registro en self
.
Peligro
self
es una (collection) colección
El objeto self
es un recordset, es decir, una colección ordenada de registros. Es compatible con las operaciones estándar de Python en colecciones, como len(self)
e iter(self)
, además de operaciones extra set como recs1 + recs2
.
Iterando sobre self
los registros uno por uno, donde cada registro es una colección de tamaño 1. Puedes acceder/asignar campos a registros individuales mediante el uso de la notación de puntos, algo así record.name
.
import random
from odoo import models, fields, api
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))
Dependencias
El valor de un campo calculado generalmente depende de los valores de otros campos en el registro calculado. El ORM espera que el programador especifique estas dependencias en el método de cálculo con el decorador depends()
. Las dependencias dadas son utilizadas por el ORM para desencadenar el recálculo del campo cada vez que se han modificado algunas de sus dependencias:
from odoo import models, fields, api
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
value = fields.Integer()
@api.depends('value')
def _compute_name(self):
for record in self:
record.name = "Record with value %s" % record.value
Exercise
Campos y valores calculados
Agrega el porcentaje de asientos ocupados para el modelo session
Muestra ese campo en la vista de árbol y formulario
Muestra el campo como una barra de progreso
Agrega un campo calculado a una Session
Muestra el campo en la vista Session:
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.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
if not r.seats:
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
<field name="taken_seats" widget="progressbar"/>
</group>
</group>
<label for="attendee_ids"/>
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>
</record>
Valores por defecto
Cualquier campo puede tener un valor por defecto. En la definición de campo, se agrega la opción default=X
donde X
es un valor literal Python (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 solicitar parámetros y otras cosas útiles:
self.env.cr
oself._cr
es el objeto cursor de la base de datos; se utiliza para consultar la base de datosself.env.uid
oself._uid
es el id de la base de datos del usuario actualself.env.user
es el registro del usuario actualself.env.context
oself._context
es el Diccionario de contextoself.env.ref(xml_id)
devuelve el registro correspondiente a un id XMLself.env[model_name]
devuelve una instancia del modelo dado
Exercise
Objetos activos – valores por defecto
Define el valor por defecto de start_date como hoy (ver
Date
).Agrega un campo
active
en la clase de Session y has las sesiones activas por defecto.
_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),
<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 que registros con el campo active
establecido a False
sean invisibles.
Onchange
El mecanismo "onchange" proporciona una forma para que la interfaz del cliente actualice los datos de un formulario cuando el usuario ha llenado un valor en un campo, sin guardar nada en la base de datos.
Por ejemplo, supóngase que un modelo tiene tres campos amount
, unit_price
y price
, y se desea actualizar el precio en el formulario cuando cualquiera de los otros campos es modificado. Para ello, se define un método donde self
representa el registro en la vista formulario y se decora con onchange()
para especificar en qué campo se activará. Cualquier cambio que se realice en self
será reflejado 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",
}
}
Para campos calculados, el comportamiento onchange
está incorporado como se aprecia trabajando con el formulario de una sesión (Session): cambia el número de asientos o los participantes, y se actualiza automáticamente la barra de progreso de taken_seats
.
Exercise
Cuidado
Añade un onchange explícito para advertir sobre valores no válidos, como un número negativo de asientos o más participantes que asientos.
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.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
Odoo ofrece dos formas de configurar automáticamente invariantes: Python constraints
y SQL constraints
.
Una restricción de Python se define como un método decorado con constrains()
y es invocado en un recordset. El decorador especifica los campos que están involucrados en la restricción, por lo que la restricción es automáticamente evaluada cuando uno de ellos es modificado. El método levantará una excepción si no se satisface su invariante:
from odoo.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
Exercise
Agrega restricciones de Python
Agrega una restricción que comprueba que el instructor no está presente en los asistentes de su propia sesión.
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
class Course(models.Model):
_name = 'openacademy.course'
'message': "Increase seats or remove excess attendees",
},
}
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError("A session's instructor can't be an attendee")
Las restricciones SQL se definen mediante el atributo modelo: _sql_constraints
. Este último se le asigna a una lista de tripletas de cadenas (name, sql_definition, message)`, donde ``name
es un nombre de restricción válido de SQL, sql_definition
es una expresión de table_constraint, y message
es el mensaje de error.
Exercise
Agrega restricciones SQL
Con la ayuda de la Documentación de PostgreSQL, añade las siguientes restricciones:
Comprueba que la descripción del curso y el título del curso son diferentes
Haz el nombre del Curso ÚNICO
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'
Exercise
Ejercicio 6 - Añade una opción para duplicar
Puesto que hemos añadido una restricción para la unicidad del nombre de curso, no es posible utilizar la función duplicate
( ).
Reimplementa tu propio método de "copy" que permita duplicar el objeto del curso, cambiando el nombre original por "Copy of [original name]".
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
@api.multi
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)
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
Vistas Avanzadas
Vistas de árbol
Las vistas de árbol pueden tener atributos suplementarios para personalizar su comportamiento:
decoration-{$name}
permite cambiar el estilo de texto de una fila basada en atributos del registro en cuestión.
Los valores son expresiones Python. Para cada registro, la expresión se evalúa con los atributos del registro como valores del contexto y si es
true
, se aplica el estilo correspondiente a la fila. Otros valores de contexto sonuid
(el id del usuario actual) ycurrent_date
(la fecha actual como una cadena de la formayyyy-MM-dd
).{$name}
puede serbf
(font-weight: bold
),it
(font-style: italic
), o cualquier color contextual de bootstrap (danger
,info
,muted
,primary
,success
orwarning
).<tree string="Idea Categories" decoration-info="state=='draft'" decoration-danger="state=='trashed'"> <field name="name"/> <field name="state"/> </tree>
editable
Ya sea
"top"
o"bottom"
. Hace la vista de árbol modificable (en lugar de tener que pasar por la vista formulario), el valor representa la posición donde aparecen nuevas filas bien sea arriba o abajo por su significado en inglés.
Exercise
Colores en la Lista
Modifica la vista de árbol de una sesión de tal manera que las sesiones que duran menos de 5 días sean de color azul, y los que duran más de 15 días sean de color rojo.
Modifica la vista de árbol de sesión:
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<tree string="Session Tree" decoration-info="duration<5" decoration-danger="duration>15">
<field name="name"/>
<field name="course_id"/>
<field name="duration" invisible="1"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>
Calendarios
Muestra los registros como calendario de eventos. Su elemento raíz es <calendar>
y sus atributos más comunes son:
color
El nombre del campo utilizado para colorear. Los colores se distribuyen automáticamente a los eventos y acontecimientos en el mismo segmento de color (los registros que tienen el mismo valor para su campo
@color
) se les dará el mismo color.date_start
campo del registro con la fecha de inicio del evento
date_stop
(opcional)campo de registro con la fecha final del evento
string
campo del registro para definir la etiqueta de cada evento del calendario
<calendar string="Ideas" date_start="invent_date" color="inventor_id">
<field name="name"/>
</calendar>
Exercise
Vista de calendario
Agrega una vista de calendario al modelo Session que permita visualizar los eventos asociados a la OpenAcademy.
Agrega un campo
end_date
calculado a partir destart_date
yduration
Truco
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 Calendario
Agrega una vista de calendario al modelo Session
Y añade la vista de calendario a la acción del modelo Session
# -*- coding: utf-8 -*-
from datetime import timedelta
from odoo import models, fields, api, exceptions
class Course(models.Model):
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.depends('seats', 'attendee_ids')
def _taken_seats(self):
},
}
@api.depends('start_date', 'duration')
def _get_end_date(self):
for r in self:
if not (r.start_date and r.duration):
r.end_date = r.start_date
continue
# Add duration to start_date, but: Monday + 5 days = Saturday, so
# subtract one second to get on Friday instead
start = fields.Datetime.from_string(r.start_date)
duration = timedelta(days=r.duration, seconds=-1)
r.end_date = start + duration
def _set_end_date(self):
for r in self:
if not (r.start_date and r.end_date):
continue
# 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(r.start_date)
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
</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 la vista de búsqueda los elementos <field>
pueden tener un @filter_domain
que reemplaza el dominio generado para la búsqueda en el campo de búsqueda. En el dominio dado, self
representa el valor introducido por el usuario. En el siguiente ejemplo, se utiliza para buscar en campos name
y description
.
Las vistas de búsqueda también pueden contener elementos <filter>
, que actúan como alternativa a búsquedas predefinidas. Los filtros deben tener uno de los siguientes atributos:
domain
añade el dominio dado a la búsqueda actual
context
añade algún contexto para la búsqueda actual; Utiliza la clave
group_by
para agrupar los resultados por el campo nombre
<search string="Ideas">
<field name="name"/>
<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_id'}"/>
</group>
</search>
Para utilizar una vista de búsqueda no predeterminada en una acción, debe vincularse con el campo de search_view_id
del registro de la acción.
La acción también puede establecer valores predeterminados para los campos de búsqueda a través de su campo context
: las claves del contexto deben tener la forma search_default_field_name
la cual inicializará field_name con el valor proporcionado. Los filtros de búsqueda deben tener un @name
opcional para tener un valor por defecto y comportarse como datos lógicos (booleans) (que pueden sólo ser activadas por defecto).
Exercise
Vistas de búsqueda
Añade un botón en la vista de búsqueda para filtrar los cursos de tal forma que encuentre solo los registros donde el usuario actual sea el responsable del curso. Y que esté seleccionada por defecto.
Agrega un botón para agrupar los cursos por el usuario responsable.
<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="Responsible"
context="{'group_by': 'responsible_id'}"/>
</group>
</search>
</field>
</record>
<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
Advertencia
La vista gantt requiere del módulo web_gantt, que está disponible en la versión enterprise edition.
Gráficos de barras horizontales normalmente se utilizan para mostrar la planificación del proyecto y su avance, su elemento raíz es <gantt>
.
<gantt string="Ideas"
date_start="invent_date"
date_stop="date_finished"
progress="progress"
default_group_by="inventor_id" />
Exercise
Diagramas de Gantt
Agrega un diagrama de Gantt que permita al usuario ver la programación de las sesiones relacionados con el módulo OpenAcademy. Las sesiones deben agruparse por instructor.
Crea un campo calculado expresando la duración de la sesión en horas
Agrega la definición de la vista de gantt y agrega la vista de gantt a la acción del modelo Session
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.depends('duration')
def _get_hours(self):
for r in self:
r.hours = r.duration * 24
def _set_hours(self):
for r in self:
r.duration = r.hours / 24
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
<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"
parent="openacademy_menu"
action="session_list_action"/>
Vistas de Gráfico
Las vistas de gráfico permiten agregar resumen y análisis de modelos, su elemento raíz es <graph>
.
Nota
Vista Pivote (elemento raíz <pivot>
) es una tabla multidimensional, permite la selección de dimensiones para obtener el conjunto de datos adecuado para presentar los datos de forma más gráfica. La vista pivote comparte la misma definición contenida como vistas gráfico.
Vistas de gráfico tienen 4 modos de visualización, se selecciona el modo predeterminado mediante el atributo @type
.
- Bar (por defecto)
un gráfico de barras, la primera dimensión se utiliza para definir los grupos en el eje horizontal, las otras dimensiones definen las barras agregadas dentro de cada grupo.
Por defecto las barras están lado a lado, éstas pueden apilarse utilizando
@stacked="True"
en el<graph>
- Line
Gráfica de Líneas XY
- Pie
Gráfica de pastel de 2 dimensiones
Las vistas de gráfico contienen <field>
con un atributo obligatorio @type
tomando los valores:
row
(default)el campo debe agregarse por defecto
measure
el campo debe ser agregado en lugar de agrupar por él
<graph string="Total idea score by Inventor">
<field name="inventor_id"/>
<field name="score" type="measure"/>
</graph>
Advertencia
Las vistas de gráfico suma valores de la base de datos, no funcionan con campos calculados que no estén almacenados.
Exercise
Vistas en Gráfico
Agrega una vista de gráfico en el objeto Session que muestre, para cada curso, el número de participantes bajo la forma de un gráfico de barras.
Agrega el número de asistentes como un campo calculado almacenado
Luego añade la vista relevante
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.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
for r in self:
r.duration = r.hours / 24
@api.depends('attendee_ids')
def _get_attendees_count(self):
for r in self:
r.attendees_count = len(r.attendee_ids)
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
<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>
<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"
action="session_list_action"/>
Kanban
Usada para organizar tareas, procesos de producción, etc… su elemento raíz es <kanban>
.
Una vista de kanban muestra un conjunto de tarjetas que pueden ser agrupadas en columnas. Cada tarjeta representa un registro y cada columna los valores de un campo de agregación.
Por ejemplo, se organizan las tareas del proyecto por etapa (cada columna es una etapa), o por responsable (cada columna es un usuario), y así sucesivamente.
Las vistas Kanban definen la estructura de cada tarjeta como una mezcla de elementos de un formulario (incluyendo HTML básico) y QWeb.
Exercise
Vista Kanban
Agrega una vista de Kanban que muestre sesiones agrupadas por curso (las columnas son cursos).
Agrega un campo entero de
color
al modelo SessionAñade la opción de kanban y actualiza la acción
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),
<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">
<i class="fa fa-bars fa-lg"/>
<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:
<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"
action="session_list_action"/>
Seguridad
Los mecanismos de control de acceso deben ser configurados para lograr una política coherente de seguridad.
Mecanismos de control de acceso basado en grupos
Los grupos se crean como registros convencionales del modelo res.groups
y garantizan el acceso a través de las definiciones de permisos en los menús. Sin embargo incluso si un objeto no tiene menú, éstos se les puede acceder indirectamente, entonces las reglas CRUD (leer, escribir, crear, borrar) deben definirse para los grupos a nivel de objetos. Estas reglas se insertan generalmente a través de archivos CSV dentro de los módulos. Es posible restringir el acceso a campos específicos de una vista o a un objeto utilizando el atributo groups
del campo.
Derechos de acceso
Los derechos de acceso se definen como registros del modelo ir.model.access
. Cada derecho de acceso está asociado a un modelo, un grupo (o no grupo para el caso de un acceso global) y un conjunto de permisos: leer, escribir, crear, borrar. Estos derechos de acceso son creados generalmente por un archivo CSV con 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
Exercise
Añade control de acceso mediante la interfaz de Odoo
Crea un nuevo usuario “John Smith”. Crea un grupo "OpenAcademy / Session Read" con acceso de lectura al modelo Session.
Crea un nuevo usuario John Smith a través de
Crea un nuevo grupo llamado
session_read
a través del menú , que debe tener acceso de lectura sobre el modelo SessionEdita John Smith para que sea un miembro de
session_read
Inicie la sesión como John Smith para comprobar que los derechos de acceso son correctos
Exercise
Añade el control de acceso a través de los archivos de datos en el módulo
Usando archivos de datos,
Crea un grupo OpenAcademy / Manager con pleno acceso a todos los modelos OpenAcademy
Haz Session y Course con acceso de lectura por todos los usuarios
Crea un nuevo archivo
openacademy/security/security.xml
que tenga el grupo OpenAcademy Manager en élEdita el archivo
openacademy/security/ir.model.access.csv
con los derechos de acceso a los modelosPor último, actualiza
openacademy/__manifest__.py
para añadir los nuevos archivos de datos en él
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
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
<odoo>
<record id="group_manager" model="res.groups">
<field name="name">OpenAcademy / Manager</field>
</record>
</odoo>
Reglas de Registro
Una regla de registro restringe los derechos de acceso a un subconjunto de registros de un 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 aplicarán, y un dominio. El dominio especifica sobre cuáles registros se limitará el acceso.
Aquí un ejemplo de una regla que impide la eliminación de los prospectos (leads) que no se encuentren en el estado cancel
. Observe que el valor del campo groups
debe seguir la misma convención como el método write()
del 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('sales_team.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>
Exercise
Regla de registro
Agrega una regla de registro para el modelo Course y el grupo "OpenAcademy / Manager", que restringe el write
y unlink
al responsable de un curso. Si un curso no tiene responsable, todos los usuarios del grupo deben ser capaces de modificarlo.
Crea una nueva regla en 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>
</odoo>
Asistentes
Los asistentes describen sesiones interactivas con el usuario (o cajas de diálogos) a través de formularios dinámicos. Un asistente es simplemente un modelo que extiende la clase TransientModel
en vez de Model
. La clase TransientModel
extiende Model
y re-usa todos sus mecanismos existentes, con las siguientes particularidades:
Los registros de un asistente no pretenden ser persistentes; se borran automáticamente de la base de datos después de cierto tiempo. Por esta razón se llaman transient.
Un asistente no requiere permisos explícitos: los usuarios tienen todos los permisos en cualquier asistente.
Los registros de un asistente pueden referirse a registros normales a través de la definición de campos many2one, pero los registros regulares no se pueden referir a los registros de un asistente a través de campos many2one.
Queremos crear un asistente que permita a los usuarios crear attendees (asistentes) para una determinada sesión, o para una lista de sesiones a la vez.
Exercise
Define el asistente
Crea un modelo asistente con una relación many2one al modelo Session y una relación many2many con el modelo Partner.
Agrega un nuevo archivo openacademy/wizard.py
:
from . import controllers
from . import models
from . import partner
from . import wizard
# -*- coding: utf-8 -*-
from odoo 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")
Ejecutar un asistente
Los asistentes se lanzan usando registros ir.actions.act_window
con el campo target
en new
. Este último abre la vista del asistente en una ventana emergente. La acción puede ser llamada por un elemento de menú.
Hay otra manera para iniciar el asistente y esta es usando un registro ir.actions.act_window
como arriba, pero con un campo extra src_model
que especifica en el contexto de cuál modelo la acción está disponible. El asistente aparecerá en la acción contextual del modelo, por encima de la vista principal. Debido a algunos disparadores internos en el ORM, tal acción es declarada en XML con la etiqueta 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 asistentes usan vistas regulares y sus botones pueden usar el atributo special="cancel"
para cerrar la ventana del asistente sin guardar los cambios.
Exercise
Inicia el asistente
Define una vista de formulario para el asistente.
Añade la acción para ejecutar en el contexto del modelo Session.
Define un valor predeterminado para el campo de sesión en el asistente; Utiliza el parámetro de
self._context
para recuperar la sesión actual.
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")
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"/>
</odoo>
Exercise
Registro de asistentes
Añade botones al asistente e implementa el método correspondiente para agregar a los asistentes a la sesión dada.
</group>
<footer>
<button name="subscribe" type="object"
string="Subscribe" class="oe_highlight"/>
or
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
<act_window id="launch_session_wizard"
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 {}
Exercise
Registro de asistentes a las sesiones múltiples
Modifica el modelo de asistente (wizard) para que los asistentes puedan registrarse a varias sesiones.
<group>
<field name="session_ids"/>
<field name="attendee_ids"/>
</group>
<footer>
<button name="subscribe" type="object"
string="Subscribe" class="oe_highlight"/>
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
def _default_sessions(self):
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 {}
Internacionalización
Cada módulo puede tener sus propias traducciones dentro del directorio i18n, con solo tener archivos llamados LANG.po donde LANG es el código local del idioma, o el código del idioma y el país combinados cuando son diferentes (por ejemplo pt.po o pt_BR.po). Las traducciones serán cargadas automáticamente por Odoo para todos los idiomas habilitados. Los desarrolladores deberían utilizar inglés cuando crean un módulo, una vez listo exportan los términos de ese módulo usando la característica de exportar archivos POT la cual usa gettext
para tal fin ( sin especificar un lenguaje) para crear el archivo plantilla POT del módulo, y después usarlo para crear las traducciones derivadas, que serían archivos PO. Varios IDEs tienen plugins o modos de edición y unión de archivos PO/POT, por ejemplo poEdit
.
Truco
Los archivos del Objeto Potable generados por Odoo son publicados en Transifex, haciendo fácil la traducción del programa.
|- idea/ # The module directory
|- i18n/ # Translation files
| - idea.pot # Translation Template (exported from Odoo)
| - fr.po # French translation
| - pt_BR.po # Brazilian Portuguese translation
| (...)
Truco
Por defecto la exportación de los POT en Odoo sólo extrae etiquetas dentro de archivos XML o dentro de las definiciones de campo en código Python, pero cualquier cadena Python puede ser traducida de esta manera envolviéndola con la función odoo._()
(e.g. _("Label")
)
Exercise
Traduce un Módulo
Elige un segundo idioma para la instalación de Odoo. Traduce su módulo utilizando los servicios proporcionados por Odoo.
Crea un directorio
openacademy/i18n/
Instala cualquier idioma que desee (
)Sincroniza términos traducibles (
)Crea un archivo de plantilla de traducción por exportación (
) sin especificar un idioma, guardar enopenacademy/i18n/
Crea un archivo de traducción mediante la exportación (
) especifica un idioma. Guárdalo enopenacademy/i18n/
Abre el archivo exportado de la traducción (con un editor de texto básico o un editor de fichero PO dedicado por ejemplo POEdit) y traduce los términos que faltan
En
models.py
, agrega una instrucción import para la funciónodoo._
y marca las cadenas restantes como traduciblesRepite los pasos 3-6
# -*- coding: utf-8 -*-
from datetime import timedelta
from odoo 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:
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"),
},
}
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError(_("A session's instructor can't be an attendee"))
Informes
Informes impresos
Odoo, A partir de la versión 8.0, viene con un nuevo motor de informe basado en QWeb, Twitter Bootstrap y Wkhtmltopdf.
Un informe es un elemento de la combinación de dos elementos:
un
ir.actions.report
, para el cual se proporciona un enlace directo<report>
, que configura diversos parámetros básicos para el informe (por defecto el tipo, si el informe debe guardarse en la base de datos después de su generación,…)<report id="account_invoices" model="account.invoice" string="Invoices" 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')" />
Un estándar QWeb view 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> the standard rendering context provides a number of elements, the most important being: ``docs`` the records for which the report is printed ``user`` the user printing the report
Debido a que los informes son páginas web estándar, están disponibles a través de una URL y los parámetros de salida pueden ser manipulados a través de esta URL, por ejemplo la versión HTML de la factura está disponible a través de http://localhost:8069/report/html/account.report_invoice/1 (si está instalado account
) y la versión en PDF a través de http://localhost:8069/report/pdf/account.report_invoice/1.
Peligro
Si parece que el informe PDF carece de estilos (es decir, aparece el texto pero el estilo/diseño es diferente de la versión html), probablemente su proceso wkhtmltopdf no puede encontrar el servidor web para convertir y descargar los estilos.
Si revisas los logs de tu servidor y observas que los estilos CSS no están siendo descargados al generar un informe PDF, seguramente ese es el problema.
El proceso de wkhtmltopdf utiliza el parámetro del sistema web.base.url
como la root path a archivos de todos los links
, pero este parámetro se actualiza automáticamente cada vez que el administrador ha iniciado sesión. Si su servidor se encuentra detrás de algún tipo de proxy, no podrá llegar. Puede solucionar esto mediante la adición de uno de estos parámetros del sistema:
report.url
, apuntando a una URL accesible desde el servidor (probablemente ‘’ http://localhost:8069'' o algo parecido). Se utilizará para este propósito particular solamente.web.base.url.freeze
, cuando se establece enTrue
, dejará las actualizaciones automáticas deweb.base.url
.
Exercise
Crea un informe para el modelo de sesión
Para cada sesión, debe mostrar el nombre de sesión, su inicio y final y una lista de los asistentes de la sesión.
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'report'],
# always loaded
'data': [
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'reports.xml',
],
# only loaded in demonstration mode
'demo': [
<odoo>
<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>
</odoo>
Paneles de control
Exercise
Define un panel de control
Define un tablero que contenga la vista gráfico que creaste, la vista de calendario de sesiones y una vista de lista de los cursos (conmutables a una vista de formulario). Este panel debe estar disponible a través de un menuitem en el menú y aparecerá automáticamente en el cliente web cuando se selecciona el menú principal de OpenAcademy.
Crea un archivo
openacademy/views/session_board.xml
. Este debe contener la vista de tablero, las acciones referenciadas en dicha vista, una acción para abrir el tablero y una nueva definición de la opción del menú principal para agregar la acción del tableroNota
Los estilos disponibles de tableros son
1
,1-1
,1-2
,2-1
y1-1-1
Actualiza
openacademy/__manifest__.py
para hacer referencia al nuevo archivo de datos
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'report', 'board'],
# always loaded
'data': [
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_board.xml',
'reports.xml',
],
# only loaded in demonstration mode
<?xml version="1.0"?>
<odoo>
<record model="ir.actions.act_window" id="act_session_graph">
<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"/>
</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"/>
</odoo>
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 pueden acceder mediante el mecanismo de objetos distribuidos. Pueden ser modificables mediante la interfaz de cliente con vistas contextuales.
Odoo es accesible a través de interfaces XML-RPC/JSON-RPC, hay bibliotecas en muchos lenguajes.
Librería XML-RPC
El siguiente ejemplo es un programa Python que interactúa con un servidor de Odoo con la librería xmlrpclib
:
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)
Exercise
Añade un nuevo servicio al cliente
Escribe un programa de Python capaz de enviar peticiones XML-RPC a un PC Odoo (tuyo o de tu instructor). Este programa debe mostrar todas las sesiones y su correspondiente número de asientos. También debe crear una nueva sesión para 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
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 id de curso en el código, 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,
})
Biblioteca de JSON-RPC
El siguiente ejemplo es un programa Python que interactúa con un servidor de Odoo con las librerías estándar de Python 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"]
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)
Los ejemplos pueden ser fácilmente adaptados de XML-RPC a JSON-RPC.
Nota
Hay una serie de APIs de alto nivel en varios idiomas para acceder a los sistemas de Odoo sin pasar explícitamente por XML-RPC o JSON-RPC, tales como:
escribir consultas SQL directas es totalmente posible, pero requiere cuidado, ya que ignora todos los mecanismos de autenticación y seguridad y de Odoo.