Toggle navigation

Construyendo un Módulo

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.

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.

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).

Ejemplo de campos simples son los Boolean, Date, Char.

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.

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.

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 un nombre 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).

Acciones y menús

Las acciones y menús son registros regulares en la base de datos, usualmente a través de archivos de datos. Las acciones pueden ser disparadas en tres formas:

  1. haciendo click en los menús (enlazados a una acción específica)

  2. haciendo click en botones en las vistas (si éstos están conectados a acciones)

  3. como acciones contextuales en el objeto

Ya que los menús son algo complejos de declarar, hay un acceso directo <menuitem> para declarar un ir.ui.menu y así enlazarlo con mayor facilidad a la acción correspondiente.

<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"/>

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>

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>

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.

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.

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
One2many(other_model, related_field)

Una relación virtual, inverso a un Many2one. Un One2many 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

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 emparejado

replace

reemplaza el elemento que empate con el cuerpo del xpath, reemplazando cualquier ocurrencia de nodo $0 en el nuevo cuerpo con el elemento original

before

inserta el cuerpo del xpath como un hermano antes del elemento emparejado

after

inserta el cuerpo de xpaths como un hermano después del elemento emparejado

attributes

altera los atributos del elemento coincidente utilizando elementos attribute especiales en el cuerpo de xpath

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.

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.

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

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)

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.

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

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.

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 son uid (el id del usuario actual) y current_date (la fecha actual como una cadena de la forma yyyy-MM-dd).

{$name} puede ser bf (font-weight: bold), it (font-style: italic), o cualquier color contextual de bootstrap (danger, info, muted, primary, success or warning).

<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.

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>

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).

Gantt

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" />

Vistas de Gráfico

Las vistas de gráfico permiten agregar resumen y análisis de modelos, su elemento raíz es <graph>.

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>

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.

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

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>

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.

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.

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 (Settings ‣ Translations ‣ Import/Export ‣ Export Translation 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.

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

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.

Paneles de control

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)

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.

[2]

escribir consultas SQL directas es totalmente posible, pero requiere cuidado, ya que ignora todos los mecanismos de autenticación y seguridad y de Odoo.