Herencia - Extendiendo la Funcionalidad de las Aplicaciones Existentes
Una de las características más poderosas de Odoo es la capacidad para agregar características sin modificar directamente los objetos subyacentes. Esto se logra a través de mecanismos de herencia, que funcionan como capas para la modificación por encima de los objetos existentes.
Estas modificaciones puede suceder en todos los niveles: modelos, vistas, y lógica de negocio. En lugar de modificar directamente un módulo existente, creara un módulo nuevo para agregar las modificaciones previstas.
Aquí, aprenderá como escribir sus propios módulos de extensión, confiriéndole facultades para aprovechar las aplicaciones base o comunitarias. Como ejemplo, aprenderá como agregar las características de mensajería y redes sociales de Odoo a sus propios módulos.
Agregar la capacidad de compartir con otros a la aplicación To-Do
La aplicación To-Do actualmente permite a los usuarios y gestionar de forma privada sus tareas por hacer. ¿No sería grandioso llevar su aplicación a otro nivel agregando características colaborativas y de redes sociales? Sera capaz de compartir las tareas y discutirlas con otras personas.
Hará esto con un módulo nuevo para ampliar la funcionalidad de la aplicación To-Do creada anteriormente y agregar estas características nuevas. Esto es lo que esperara lograr al final de este capítulo:
Gráfico 3.1 - Nuevo módulo para la aplicación To-Do
Camino a seguir para las características colaborativas
Aquí esta su plan de trabajo para implementar la extensión de funcionalidades:
- Agregar campos al modelo Task, como el usuario quien posee la tarea.
- Modificar la lógica de negocio para operar solo en la tarea actual del usuario, en vez de todas las tareas disponibles para ser vistas por el usuario.
- Agregar los campos necesarios a las vistas.
- Agregar características de redes sociales: el muro de mensajes y los seguidores.
Comience creando la estructura básica para el módulo junto al módulo
todo_app
. Siguiendo el ejemplo de instalación del
Capítulo 1, sus módulos estarán alojados
en ~/odoo-dev/custom-addons/
:
$ mkdir ~/odoo-dev/custom-addons/todo_user
$ touch ~/odoo-dev/custom-addons/todo_user/__init__.py
Ahora cree el archivo __openerp__.py
, con el siguiente código:
{
'name': 'Multiuser To-Do',
'description': 'Extend the To-Do app to multiuser.',
'author': 'Daniel Reis',
'depends': ['todo_app'],
}
No ha hecho esto, pero incluir las claves "summary" y "category" puede ser importante cuando se publican módulos en la tienda de aplicaciones en línea de Odoo.
Ahora, podrá instalarlo. Debe ser suficiente con solo actualizar el Lista de módulos desde el menú Configuración, encuentre el módulo nuevo en la lista de Módulos locales y haga clic en el botón Instalar. Para instrucciones más detalladas sobre como encontrar e instalar un módulo puede volver al Capítulo 1.
Ahora, comience a agregar las nuevas características.
Ampliando el modelo de tareas por hacer
Los modelos nuevos son definidos a través de las clases Python. Ampliarlos también es hecho a través de las clases Python, pero usando un mecanismo específico de Odoo.
Para aplicar un modelo use una clase Python con un atributo
__inherit
. Este identifica el modelo que será ampliado. La clase
nueva hereda todas las características del modelo padre, y solo
necesite declarar las modificaciones que querrá introducir.
De hecho, los modelos de Odoo existen fuera de su módulo
particular, en un registro central. Podrá referirse a este registro
como la piscina, y puede ser accedido desde los métodos del modelo
usando self.env[<model name>]
. Por ejemplo, para referirse al
modelo res.partner
escribirá self.env['res.partner']
.
Para modificar un modelo de Odoo obtiene una referencia a la clase de registro y luego ejecuta los cambios en ella. Esto significa que esas modificaciones también estarán disponibles en cualquier otro lado donde el modelo sea usado.
En la secuencia de carga del módulo, durante un reinicio del servidor, las modificaciones solo serán visibles en los modelos cargados después. Así que, la secuencia de carga es importante y debe asegurarse que las dependencias del módulo están fijadas correctamente.
Agregar campos a un modelo
Ampliara el modelo todo.task
para agregar un par de campos: el
usuario responsable de la tarea, y la fecha de vencimiento.
Cree un archivo todo_task.py
nuevo y declare una clase que extienda
al modelo original:
#-*- coding: utf-8 -*-
from openerp import models, fields, api
class TodoTask(models.Model):
_inherit = 'todo.task'
user_id = fields.Many2one('res.users', 'Responsible')
date_deadline = fields.Date('Deadline')
El nombre de la clase es local para este archivo Python, y en general es
irrelevante para los otros módulos. El atributo _inherit
de la clase
es la clave aquí: esta le dice a Odoo que esta clase hereda el modelo
todo.task
. Note la ausencia del atributo _name
. Este no es
necesario porque ya es heredado desde el modelo padre.
Las siguientes dos líneas son declaraciones de campos comunes. El
user_id
representa un usuario desde el modelo Users, res.users
.
Es un campo de Many2one
equivalente a una clave foránea en el argot
de base de datos. El date_deadline
es un simple campo de fecha. En
el Capítulo 5, se explica
con más detalle los tipos de campos disponibles en Odoo.
Aun le falta agregar al archivo __init__.py
la declaración import
para incluirlo en el módulo:
from . import todo_task
Para tener los campos nuevos agregados a la tabla de la base de datos
soportada por el modelo, necesita ejecutar una actualización al
módulo. Si todo sale como es esperado, debería poder ver los campos
nuevos cuando revise el modelo todo.task
, en el menú Técnico,
Estructura de base de datos > Modelos.
Modificar los campos existentes
Como puede ver, agregar campos nuevos a un modelo existente es bastante directo. Desde Odoo 8, es posible modificar atributos en campos existentes. Esto es hecho agregando un campo con el mismo nombre, y configurando los valores solo para los atributos que serán modificados.
Por ejemplo, para agregar un comentario de ayuda a un campo name
,
podrá agregar esta línea en el archivo todo_task.py
:
name = fields.Char(help="What needs to be done?")
Si actualiza el módulo, va a un formulario de tareas por hacer, y posicione el ratón sobre el campo Descripción, aparecerá el mensaje de texto escrito en el código anterior.
Modificar los métodos del modelo
La herencia también funciona en la lógica de negocio. Agregar métodos nuevos es simple: solo declare las funciones dentro de la clase heredada.
Para ampliar la lógica existente, un método puede ser sobrescrito
declarando otro método con el mismo nombre, y el método nuevo
reemplazará al anterior. Pero este puede extender el código de la clase
heredada, usando la palabra clave de Python super()
para llamar al
método padre.
Es mejor evitar cambiar la función distintiva del método (esto es, mantener los mismos argumentos) para asegurarse que las llamadas a este sigan funcionando adecuadamente. En caso que necesite agregar parámetros adicionales, hágalos opcionales (con un valor predeterminado).
La acción original de Clear All Done
ya no es apropiada para su
módulos de tareas compartidas, ya que borra todas las tareas sin
importar a quien le pertenecen. Necesita modificarla para que borre
solo las tareas del usuario actual.
Para esto, se sobrescribirá el método original con una nueva versión que primero encuentre las tareas completadas del usuario actual, y luego las desactive:
@api.multi
def do_clear_done(self):
domain = [('is_done', '=', True), '|', ('user_id', '=', self.env.uid), ('user_id', '=', False)]
done_recs = self.search(domain)
done_recs.write({'active': False})
return True
Primero se listan los registros finalizados sobre los cuales se usa el
método search
con un filtro de búsqueda. El filtro de búsqueda sigue
una sintaxis especial de Odoo referida como domain
.
El filtro "domain" usado es definido en la primera instrucción: es una lista de condiciones, donde cada condición es una tupla.
Estas condiciones son unidas implícitamente con un operador AND
(&
en la sintaxis de dominio). Para agregar una operación OR
se
usa una "tubería" (|
) en el lugar de la tupla, y afectara las
siguientes dos condiciones. Ahondara más sobre este tema en el
Capítulo 6.
El dominio usado aquí filtra todas las tareas con su etapa finalizadas
('is_done', '=', True
) que también tengan al usuario actual como
responsable ('user_id','=',self.env.uid
) o no tengan fijado un
usuario ('user_id', '=', False
).
Lo que acaba de hacer fue sobrescribir completamente el método padre, reemplazándolo con una implementación nueva.
Pero esto no es lo que usualmente querrá hacer. En vez de esto,
ampliara la lógica actual y agregara operaciones adicionales. De
lo contrario podrá dañar operaciones existentes. La lógica existente
es insertada dentro de un método sobrescrito usando el comando
super()
de Python para llamar a la versión padre del método.
Vea un ejemplo de esto: podrá escribir una versión mejor de
do_toggle_done()
que solo ejecute la acción sobre las Tareas
asignadas a su usuario:
@api.one
def do_toggle_done(self):
if self.user_id != self.env.user:
raise Exception('Only the responsible can do this!')
else:
return super(TodoTask, self).do_toggle_done()
Estas son las técnicas básicas para sobrescribir y ampliar la lógica de negocio definida en las clases del modelo. Vera ahora como extender las vistas de la interfaz con los usuarios.
Ampliar las vistas
Vistas de formulario, listas y búsqueda son definidas usando las estructuras de arco de XML. Para ampliar las vistas necesita una manera de modificar este XML. Esto significa localizar los elementos XML y luego introducir modificaciones en esos puntos. Las vistas heredadas permiten esto.
Una vista heredada se ve así:
<record id="view_form_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task form – User extension</field>
<field name="model">todo.task</field>
<field name="inherit_id" ref="todo_app.view_form_todo_task"/>
<field name="arch" type="xml">
<!-- ...match and extend elements here! ... -->
</field>
</record>
El campo inherit_id
identifica la vista que será ampliada, a través
de la referencia de su identificador externo usando el atributo especial
ref
. Los identificadores externos serán discutidos con mayor detalle
en el Capítulo 4.
La forma natural de localizar los elementos XML es usando expresiones
XPath. Por ejemplo, tomando la vista que fue definida en el capítulo
anterior, la expresión XPath para localizar el elemento
<field name="is_done">
es //field[@name]='is_done'
. Esta
expresión encuentra un elemento field
con un atributo name
igual
a is_done
. Puede encontrar mayor información sobre XPath en:
https://docs.python.org/2/library/xml.etree.elementtree.html#xpath-support.
Tener atributos "name" en los elementos es importante porque los hace mucho más fácil de seleccionar como puntos de extensión. Una vez que el punto de extensión es localizado, puede ser modificado o puede tener elementos XML agregados cerca de él.
Como un ejemplo práctico, para agregar el campo date_deadline
antes
del campo is_done
, debe escribir en arch
:
<xpath expr="//field[@name]='is_done'" position="before">
<field name="date_deadline" />
</xpath>
Afortunadamente Odoo proporciona una notación simplificada para eso, así
que la mayoría de las veces podrá omitir la sintaxis XPath. En vez del
elemento xpath
anterior podrá usar el tipo de elementos que querrá
localizar y su atributo distintivo.
Lo anterior también puede ser escrito como:
<field name="is_done" position="before">
<field name="date_deadline" />
</field>`
Agregar campos nuevos, cerca de campos existentes es hecho
frecuentemente, por lo tanto la etiqueta <field>
es usada
frecuentemente como el localizador. Pero cualquier otra etiqueta puede
ser usada: <sheet>
, <group>
, <div>
, entre otras. El atributo
name
es generalmente la mejor opción para hacer coincidir elementos,
pero a veces, podrá necesitar usar string
(el texto mostrado en un
"label") o la clase CSS del elemento.
El atributo de posición usado con el elemento localizador es opcional, y puede tener los siguientes valores:
after
: Este es agregado al elemento padre, después del nodo de coincidencia.before
: Este es agregado al elemento padre, antes del nodo de coincidencia.inside
(el valor predeterminado): Este es anexado al contenido del nodo de coincidencia.replace
: Este reemplaza el nodo de coincidencia. Si es usado con un contenido vacío, borra un elemento.attributes
: Este modifica los atributos XML del elemento de coincidencia (más detalles luego de esta lista).
La posición del atributo le permite modificar los atributos del
elemento de coincidencia. Esto es hecho usando los elementos
<attribute name="attr-name">
con los valores del atributo nuevo.
En el formulario de Tareas, tendrá el campo Active, pero tenerlo
visible no es muy útil. Quizás podrá esconderlo al usuario. Esto puede
ser realizado configurando su atributo invisible
:
<field name="active" position="attributes">
<attribute name="invisible">1</attribute>
</field>
Configurar el atributo invisible
para esconder un elemento es una
buena alternativa para usar el localizador de reemplazo para eliminar
nodos. Debería evitarse la eliminación, ya que puede dañar las
extensiones de modelos que pueden depender del nodo eliminado.
Finalmente, podrá poner todo junto, agregar los campos nuevos, y obtener la siguiente vista heredada completa para ampliar el formulario de tareas por hacer:
<record id="view_form_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task form – User extension</field>
<field name="model">todo.task</field>
<field name="inherit_id" ref="todo_app.view_form_todo_task"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="user_id" />
</field>
<field name="is_done" position="before">
<field name="date_deadline" />
</field>
<field name="name" position="attributes">
<attribute name="string">I have to…</attribute>
</field>
</field>
</record>
Esto debe ser agregado al archivo todo_view.xml
en su módulo,
dentro de las etiquetas <openerp>
y <data>
, como fue mostrado en
el capítulo anterior.
Nota
Las vistas heredadas también pueden ser a su vez heredadas, pero debido a que esto crea dependencias más complicadas, debe ser evitado.
No podrá olvidar agregar el atributo datos al archivo descriptor
__openerp__.py
:
'data': ['todo_view.xml'],
Ampliando más vistas de árbol y búsqueda
Las extensiones de las vistas de árbol y búsqueda son también definidas
usando la estructura XML arch
, y pueden ser ampliadas de la misma
manera que las vistas de formulario. Seguidamente se muestra un ejemplo
de la ampliación de vistas de lista y búsqueda.
Para la vista de lista, querrá agregar el campo usuario:
<record id="view_tree_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task tree – User extension</field>
<field name="model">todo.task</field>
<field name="inherit_id" ref="todo_app.view_tree_todo_task"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="user_id" />
</field>
</field>
</record>
Para la vista de búsqueda, agregara una búsqueda por usuario, y filtros predefinidos para las tareas propias del usuario y tareas no asignadas a alguien.
<record id="view_filter_todo_task_inherited" model="ir.ui.view">
<field name="name">Todo Task tree – User extension</field>
<field name="model">todo.task</field>
<field name="inherit_id" ref="todo_app.view_filter_todo_task"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="user_id" />
<filter name="filter_my_tasks" string="My Tasks"
domain="[('user_id','in',[uid,False])]" />
<filter name="filter_not_assigned" string="Not Assigned"
domain="[('user_id','=',False)]" />
</field>
</field>
</record>
No se preocupe demasiado por la sintaxis específica de las vistas. Se describirá esto con más detalle en el Capítulo 6.
Más sobre el uso de la herencia para ampliar los modelos
Ha visto lo básico en lo que se refiere a la ampliación de modelos
"in place", lo cual es la forma más frecuente de uso de la herencia.
Pero la herencia usando el atributo _inherit
tiene mayores
capacidades, como la mezcla de clases.
También tiene disponible el método de herencia delegada, usando el
atributo _inherits
. Esto permite a un modelo contener otros modelos
de forma transparente a la vista, mientras por detrás de escena cada
modelo gestiona sus propios datos.
Explore esas posibilidades en más detalle.
Copiar características usando herencia por prototipo
El método que use anteriormente para ampliar el modelo solo usa el
atributo _inherit
. Defina una clase que hereda el modelo
todo.task
, y le agrega algunas características. La clase
_name
no fue fijada explícitamente; implícitamente fue también
todo.task
.
Pero usando el atributo _name
le permitió crear una mezcla de
clases (mixin), incorporándolo al modelo que querrá ampliar. Aquí
se muestre un ejemplo:
from openerp import models
class TodoTask(models.Model):
_name = 'todo.task'
_inherit = 'mail.thread'
Esto amplia el modelo todo.task
copiando las características del
modelo mail.thread
. El modelo mail.thread
implementa la
mensajería de Odoo y la función de seguidores, y es reusable, por lo
tanto es fácil agregar esas características a cualquier modelo.
Copiar significa que los métodos y los campos heredados estarán disponibles en el modelo heredero. Para los campos significa que estos serán creados y almacenados en las tablas de la base de datos del modelo objetivo. Los registros de datos del modelo original (heredado) y el nuevo modelo (heredero) son conservados sin relación entre ellos. Solo son compartidas las definiciones.
Estas mezclas son usadas frecuentemente como modelos abstractos, como el
mail.thread
usado en el ejemplo. Los modelos abstractos son como los
modelos regulares excepto que no es creada ninguna representación de
ellos en la base de datos. Actúan como plantillas, describen campos y la
lógica para ser reusadas en modelos regulares.
Los campos que definen solo serán creados en aquellos modelos regulares
que hereden de ellos. En un momento se discutirá en detalle como usar
eso para agregar mail.thread
y sus características de redes sociales
a su módulo. En la práctica cuando se usan las mezclas rara vez
hereda de modelos regulares, porque esto puede causar duplicación de
las mismas estructuras de datos.
Odoo proporciona un mecanismo de herencia delegada, el cual impide la duplicación de estructuras de datos, por lo que es usualmente usada cuando se hereda de modelos regulares. Vea esto con mayor detalle.
Integrar Modelos usando herencia delegada
La herencia delegada es el método de extensión de modelos usado con
menos frecuencia, pero puede proporcionar soluciones muy convenientes.
Es usada a través del atributo _inherits
(note la 's' adicional) con
un mapeo de diccionario de modelos heredados con campos relacionados a
él.
Un buen ejemplo de esto es el modelo estándar Users, res.users
, que
tiene un modelo Partner res.partner
anidado:
from openerp import models, fields
class User(models.Model):
_name = 'res.users'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one('res.partner')
Con la herencia delegada el modelos res.users
integra el modelo
heredado res.partner
, por lo tanto cuando un usuario (User) nuevo es
creado, un socio (Partner) también es creado y se mantiene una
referencia a este a través del campo partner_id
de User. Es similar
al concepto de polimorfismo en la programación orientada a objetos.
Todos los campos del modelo heredado, Partner, están disponibles como si fueran campos de User, a través del mecanismo de delegación. Por ejemplo, el nombre del socio y los campos de dirección son expuestos como campos de User, pero de hecho son almacenados en el modelo Partner enlazado, y no ocurre ninguna duplicación de la estructura de datos.
La ventaja de esto, comparada a la herencia por prototipo, es que no hay necesidad de repetir la estructura de datos en muchas tablas, como las direcciones. Cualquier modelo que necesite incluir un dirección puede delegar esto a un modelo Partner vinculado. Y si son introducidas algunas modificaciones en los campos de dirección del socio o validaciones, estas estarán disponibles inmediatamente para todos los modelos que vinculen con él!
Nota
Note que con la herencia delegada, los campos con heredados, pero los métodos no.
Modificar datos
A diferencia de las vistas, los registros de datos no tienen una estructura de arco XML y no pueden ser ampliados usando expresiones XPath. Pero aún pueden ser modificados reemplazando valores en sus campos.
El elemento <record id="x" model="y">
está realizando una operación
de inserción o actualización en un modelo: si x no existe, es creada; de
otra forma, es actualizada / escrita.
Debido a que los registros en otros módulos pueden ser accedidos usando
un identificador <model>.<identifier>
, es perfectamente legal para
su módulo sobrescribir algo que fue escrito antes por otro módulo.
Nota
Note que el punto esta reservado para separar el nombre del módulo
del identificador del objeto, así que no debe ser usado en
identificadores. Para esto use la barra baja (_
).
Como ejemplo, cambie la opción de menú creada por el módulo
todo_app
en "My To Do". Para esto agregar lo siguiente al archivo
todo_user/todo_view.xml
:
<!-- Modify menu item -->
<record id="todo_app.menu_todo_task" model="ir.ui.menu">
<field name="name">My To-Do</field>
</record>
<!-- Action to open To-Do Task list -->
<record model="ir.actions.act_window" id="todo_app.action_todo_task">
<field name="context">
{'search_default_filter_my_tasks': True}
</field>
</record>
Ampliando las reglas de registro
La aplicación Tareas-por-Hacer incluye una regla de registro para asegurar que cada tarea sea solo visible para el usuario que la ha creado. Pero ahora, con la adición de las características sociales, necesita que los seguidores de la tarea también tengan acceso. El modelo de red social no maneja esto por si solo.
Ahora las tareas también pueden tener usuarios asignados a ellas, por lo tanto tiene más sentido tener reglas de acceso que funcionen para el usuario responsable en vez del usuario que creo la tarea.
El plan será el mismo que para la opción de menú: sobrescribir
todo_app.todo_task_user_rule
para modificar el campo
domain_force
a un valor nuevo.
Desafortunadamente, esto no funcionará esta vez. Recuerde que el
<data no_update="1">
que use anteriormente en el archivo XML de
las reglas de seguridad: previene las operaciones posteriores de
escritura.
Debido a que las actualizaciones del registro no están permitidas, necesita una solución alterna. Este será borrar el registro y agregar un reemplazo para este en su módulo.
Para mantener las cosas organizadas, creara un archivo
security/todo_access_rules.xml
y agregara lo siguiente:
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<delete model="ir.rule" search="[('id''=',ref('todo_app.todo_task_user_rule'))]" />
<record id="todo_task_per_user_rule" model="ir.rule">
<field name="name">ToDo Tasks only for owner</field>
<field name="model_id" ref="model_todo_task"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">
['|', ('user_id','in', [user.id,False]), ('message_follower_ids','in',[user.partner_id.id])]
</field>
</record>
</data>
</openerp>
Esto encuentra y elimina la regla de registro todo_task_user_rule
del módulo todo_app
, y crea una nueva regla de registro
todo_task_per_user
. El filtro de dominio que usa ahora hace la
tarea visible para el usuario responsable user_id
, para todo el
mundo si el usuario responsable no ha sido definido (igual a False
), y
para todos los seguidores. La regla se ejecutará en un contexto donde el
usuario este disponible y represente la sesión del usuario actual. Los
seguidores son socios, no objetos User, así que en vez de user_id
,
necesita usar user.partner_id.id
.
Truco
Cuando se trabaja en campos de datos con <data noupdate="1">
puede ser engañoso porque cualquier edición posterior no será
actualizada en Odoo. Para evitar esto, use temporalmente
<data noupdate="0">
durante el desarrollo, y cámbielo solo
cuando haya terminado con el módulo.
Como de costumbre, no debe olvidar agregar el archivo nuevo al
archivo descriptor __openerp__.py
en el atributo "data":
'data': [
'todo_view.xml',
'security/todo_access_rules.xml'
],
Note que en la actualización de módulos, el elemento <delete>
arrojará un mensaje de advertencia, porque el registro que será
eliminado no existe más. Esto no es un error y la actualización se
realizará con éxito, así que no es necesario preocuparse por esto.
Resumen
Ahora debe ser capaz de crear módulos nuevos para ampliar los módulos existentes. Vio como ampliar el módulo To-Do creado en los capítulos anteriores.
Se agregaron nuevas características en las diferentes capas que forman la aplicación. Amplio el modelo Odoo para agregar campos nuevos, y amplié los métodos con su lógica de negocio. Luego, modifique las vistas para hacer disponibles los campos nuevos. Finalmente, aprendió como ampliar un modelo heredando de otros modelos, y use esto para agregar características de red social a su aplicación.
Con estos tres capítulos, tiene una vista general de las actividades mas comunes dentro del desarrollo en Odoo, desde la instalación de Odoo y configuración a la creación de módulos y extensiones.
Los siguientes capítulos se enfocarán en áreas específicas, la mayoría de las cuales ha tocado en estos primeros capítulos. En el siguiente capítulo, abordara la serialización de datos y el uso de archivos XML y CSV con más detalle.