Modelos – Estructura de los Datos de la Aplicación
En los capítulos anteriores, vio un resumen de extremo a extremo sobre la creación de módulos nuevos para Odoo. En el Capítulo 2, se construyo una aplicación totalmente nueva, y en el Capítulo 3, exploro la herencia y como usarla para crear un módulo de extensión para su aplicación. En el Capítulo 4, se discute como agregar datos iniciales y de demostración a sus módulos.
En estos resúmenes, se toco todas las capas que componen el desarrollo de aplicaciones "backend" para Odoo. Ahora, en los siguientes capítulos, es hora de explicar con más detalle todas estas capas que conforman una aplicación: modelos, vistas, y lógica de negocio.
En este capítulo, aprenderá como diseñar las estructuras de datos que soportan una aplicación, y como representar las relaciones entre ellas.
Organizar las características de las aplicaciones en módulos
Como hizo anteriormente, usara un ejemplo para ayudar a explicar los conceptos. Una de las mejores cosas de Odoo es tener la capacidad de tomar una aplicación o módulo existente y agregar, sobre este, las características que necesite. Así que continuara mejorando sus módulos to-do, y pronto formaran una aplicación completa!
Es una buena práctica dividir las aplicaciones Odoo en varios módulos pequeños, cada uno responsable de una característica específica. Esto reduce la complejidad general y hace el mantenimiento y la actualización más fácil.
El problema de tener que instalar todos esos módulos individuales puede ser resuelto proporcionando un módulo de la aplicación que empaquete todas esas características, a través de sus dependencias. Para ilustrar este enfoque implementara las características adicionales usando módulos to-do nuevos.
Introducción al módulo todo_ui
En el capítulo anterior, primero creo una aplicación para tareas por hacer personales, y luego la aplico para que las tareas por hacer pudieran ser compartidas con otras personas.
Ahora querrá llevar a su aplicación a otro nivel agregándole una
pizarra kanban
y otras mejoras en la interfaz. La pizarra kanban
nos
permitirá organizar las tareas en columnas, de acuerdo a sus estados,
como En Espera, Lista, Iniciada o Culminada.
Comenzara agregando la estructura de datos para permitir esa visión. Necesitara agregar los estados y sería bueno si añade soporte para las etiquetas, permitiendo que las tareas estén organizadas por categoría.
La primera cosa que tiene que comprender es como su data estará estructurada para que pueda diseñar los Modelos que la soportan. Ya tiene la entidad central: las tareas por hacer. Cada tarea estará en un estado, y las tareas pueden tener una o más etiquetas. Esto significa que necesitara agregar dos modelos adicionales, y tendrán estas relaciones:
- Cada tarea tiene un estado, y puede haber muchas tareas en un estado.
- Cada tarea puede tener muchas etiquetas, y cada etiqueta puede estar en muchas tareas.
Esto significa que las tareas tiene una relación muchos a uno con los estados, y una relación muchos a muchos con las etiquetas. Por otra parte, las relaciones inversas son: los estados tiene una relación uno a muchos con las tareas y las etiquetas tienen una relación muchos a muchos con las tareas.
Comenzara creando el módulo nuevo todo_ui
y agregara los
estados y los modelos de etiquetas.
Ha estado usado el directorio ~/odoo-dev/custom-addons/
para
alojar sus módulos. Para crear el módulo nuevo junto a los
existentes, podrá usar estos comandos en la terminal:
$ cd ~/odoo-dev/custom-addons
$ mkdir todo_ui
$ cd todo_ui
$ touch __openerp__.py
$ touch todo_model.py
$ echo "import todo_model" > __init__.py
Luego, debe editar el archivo manifiesto __openerp__.py
con este
contenido:
{
'name': 'User interface improvements to the To-Do app',
'description': 'User friendly features.',
'author': 'Daniel Reis',
'depends': ['todo_app']
}
Note que depende de todo_app
y no de todo_user
. En general,
es buena idea mantener los módulos tan independientes como sea posible.
Cuando un módulo aguas arriba es modificado, puede impactar todos los
demás módulos que directa o indirectamente dependen de el. Es mejor si
puede mantener al mínimo el número de dependencias, e igualmente
evitar concentrar un gran número de dependencias, como:
todo_ui → todo_user → todo_app
en este caso.
Ahora podrá instalar el módulo en su base de datos de trabajo y comenzar con los modelos.
Crear modelos
Para que las tareas por hacer tengan una pizarra kanban
, necesita
estados. Los estados son columnas de la pizarra, y cada tarea se
ajustará a una de esas columnas.
Agregue el siguiente código al archivo todo_ui/todo_model.py
:
#-*- coding: utf-8 -*-
from openerp import models, fields, api
class Tag(models.Model):
_name = 'todo.task.tag'
name = fields.Char('Name', 40, translate=True)
class Stage(models.Model):
_name = 'todo.task.stage'
_order = 'sequence,name'
_rec_name = 'name' # predeterminado
_table = 'todo_task_stage' # predeterminado
name = fields.Char('Name', 40, translate=True)
sequence = fields.Integer('Sequence')
Aquí, crea los dos modelos nuevos, a los cuales, hará referencia en las tareas por hacer.
Enfocándose en los estados de las tareas, tiene una clase Python,
Stage, basada en la clase models.Model
, que define un modelo nuevo,
todo.task.stage
. También defina dos campos, name
y sequence
.
Podrá ver algunos atributos del modelo, (con el guión bajo, _
,
como prefijo) esto es nuevo para nosotros. Dele una mirada más profunda.
Atributos del modelo
Las clases del modelo pueden tener atributos adicionales usados para controlar alguno de sus comportamientos:
_name
: Este es el identificador interno para el modelo que esta creando._order
: Este fija el orden que será usado cuando se navega por los registros del modelo. Es una cadena de texto que es usada como una clausula SQLorder by
, así que puede ser cualquier cosa permitida._rec_name
: Este indica el campo a usar como descripción del registro cuando se hace referencia a él desde campos relacionados, como una relación muchos a uno. De forma predeterminada usa el camponame
, el cual esta frecuentemente presente en los modelos. Pero este atributo le permite usar cualquier otro campo para este propósito._table
: Este es el nombre de la tabla de la base de datos que soporta el modelo. Usualmente, se deja para que sea calculado automáticamente, y es el nombre del modelo con el carácter de piso bajo (_
) que reemplaza a los puntos. Pero puede ser configurado para indicar un nombre de tabla específico.
Para completar, también podrá tener atributos inherit
e
_inherits
, como se explicara en el Capítulo 3.
Modelos y clases Python
Los modelos de Odoo son representados por las clases Python. En el
código precedente, tiene una clase Python llamada Stage, basada en la
clase models.Model
, usada para definir el modelo nuevo
todo.task.stage
.
Los modelos de Odoo son mantenidos en un registro central, también
denominado como piscina - pool - en las versiones anteriores. Es un
diccionario que mantiene las referencias de todas las clases de modelos
disponibles en la instancia, a las cuales se les puede hacer referencia
por el nombre del modelo. Específicamente, el código en un método del
modelo puede usar self.env['x]
o self.env.get('x')
para obtener
la referencia a la clase que representa el modelo x.
Puede observar que los nombres del modelo son importantes ya que son la
llave para acceder al registro. La convención para los nombres de modelo
es usar una lista de palabras en minúscula unidas con puntos, como
todo.task.stage
. Otros ejemplos pueden verse en los módulos raíz de
Odoo project.project
, project.task
o project.task.type
.
Debe usar la forma singular: todo.task
en vez de todo.tasks
.
Por cuestiones históricas se pueden encontrar módulos raíz, que no sigan
dicha convención, como res.users
, pero no es la norma.
Los nombres de modelo deben ser únicos. Debido a esto, la primera
palabra deberá corresponder a la aplicación principal con la cual esta
relacionada el módulo. En su ejemplo, es "todo". De los módulos
raíz tiene, por ejemplo, project
, crm
, o sale
.
Por otra parte, las clases Python, son locales para el archivo Python en la cual son declaradas. El identificador usado en ellas es solo significativo para el código en ese archivo.
Debido a esto, no se requiere que los identificadores de clase tengan
como prefijo a la aplicación principal a la cual están relacionados. Por
ejemplo, no hay problema en llamar simplemente Stage a su clase
para el modelo todo.task.stage
. No hay riesgo de colisión con otras
posibles clases con el mismo nombre en otros módulos.
Se pueden usar dos convenciones diferentes para los identificadores de clase: snake_case o CamelCase. Históricamente, el código Odoo ha usado el snake_case, y es aún muy frecuente encontrar clases que usan esa convención. Pero la tendencia actual en usar CamelCase, debido a que es el estándar definido para Python por la convenciones de codificación PEP8. Puede haber notado que esta usando esta última forma.
Modelos transitorios y abstractos
En el código precedente, y en la vasta mayoría de los modelos Odoo, las
clases están basadas en el clase models.Model
. Este tipo de modelos
tienen bases de datos persistentes: las tablas de las bases de datos son
creadas para ellos y sus registros son almacenados hasta que son
borrados explícitamente.
Pero Odoo proporciona otros dos tipos de modelo: modelos Transitorios y Abstractos.
Los modelos transitorios están basados en la clase
models.TransientModel
y son usados para interacción tipo asistente
con el usuario. Sus datos son aún almacenados en la base de datos, pero
se espera que sea temporal. Un proceso de reciclaje limpia periódicamente
los datos viejos de esas tablas.
Los modelos abstractos están basados en la clase
models.AbstractModel
y no tienen almacén vinculado a ellos. Actúan
como una característica de re-uso configurada para ser mezclada con
otros modelos. Esto es hecho usando las capacidades de herencia de Odoo.
Gráfico 5.1 - Vista de la estructura de base de datos del modelo todo.task
Inspeccionar modelos existentes
La información sobre los modelos y los campos creados con clases Python esta disponible a través de la interfaz. En el menú principal de Configuración, seleccione la opción de menú Técnico > Estructura de base de datos > Modelos. Allí, encontrará la lista de todos los modelos disponibles en la base de datos. Al hacer clic en un modelo de la lista se abrirá un formulario con sus detalles.
Esta es una buena herramienta para inspeccionar la estructura de un
Modelo, ya que se tiene en un solo lugar el resultado de todas las
adiciones que pueden venir de diferentes módulos. En este caso, como
puede observar en el campo En los módulos, en la parte superior
derecha, las definiciones de todo.task
vienen de los módulos
todo_app
y todo_user
.
En el área inferior, tiene disponibles algunas etiquetas informativas: una referencia rápida de los Campos del modelo, los Derechos de Acceso concedidos, y también lista las Vistas disponibles para este modelo.
Podrá encontrar el Identificador Externo del modelo, activando el
Menú de Desarrollo y accediendo a la opción Ver metadatos. Estos
son generados automáticamente pero bastante predecibles: para el modelo
todo.task
, el Identificador Externo es model_todo_task
.
Truco
Los formularios del Modelo pueden ser editados! Es posible crear y modificar modelos, campos y vistas desde aquí. Puede usar esto para construir prototipos antes de colocarlos definitivamente dentro de los propios modelos.
Crear campos
Después de crear un modelo nuevo, el siguiente paso es agregar los campos. Va a explorar diferentes tipos de campos disponibles en Odoo.
Tipos básicos de campos
Ahora tiene un modelo Stage y va a ampliarlo para agregar algunos
campos adicionales. Debe editar el archivo todo_ui/todo_model.py
,
removiendo algunos atributos innecesarios incluidos antes con propósitos
descriptivos:
class Stage(models.Model):
_name = 'todo.task.stage'
_order = 'sequence,name'
# Campos de cadena de caracteres:
name = fields.Char('Name',40)
desc = fields.Text('Description')
state = fields.Selection(
[
('draft','New'),
('open','Started'),
('done','Closed')
], 'State')
docs = fields.Html('Documentation')
# Campos numéricos:
sequence = fields.Integer('Sequence')
perc_complete = fields.Float('% Complete',(3,2))
# Campos de fecha:
date_effective = fields.Date('Effective Date')
date_changed = fields.Datetime('Last Changed')
# Otros campos:
fold = fields.Boolean('Folded?')
image = fields.Binary('Image')
Aquí tiene un ejemplo de tipos de campos no relacionales disponibles en Odoo, con los argumentos básicos esperados por cada función. Para la mayoría, el primer argumento es el título del campo, que corresponde al atributo palabra clave de cadena. Es un argumento opcional, pero se recomienda colocarlo. De lo contrario, sera generado automáticamente un título por el nombre del campo.
Existe una convención para los campos de fecha que usa date
como
prefijo para el nombre. Por ejemplo, debería usar date_effective
en vez de effective_date
. Esto también puede aplicarse a otros
campos, como amount_
, price_
o qty_
.
Algunos otros argumentos están disponibles para la mayoría de los tipos de campo:
Char
, acepta un segundo argumento opcional,size
, que corresponde al tamaño máximo del texto. Es recomendable usarlo solo si se tiene una buena razón.Text
, se diferencia deChar
en que puede albergar texto de varias líneas, pero espera los mismos argumentos.Selecction
, es una lista de selección desplegable. El primer argumento es la lista de opciones seleccionables y el segundo es la cadena de título. La lista de selección es una tupla('value', 'Title')
para el valor almacenado en la base de datos y la cadena de descripción correspondiente. Cuando se amplía a través de la herencia, el argumentoselection_add
puede ser usado para agregar opciones a la lista de selección existente.Html
, es almacenado como un campo de texto, pero tiene un manejo específico para presentar el contenido HTML en la interfaz.Integer
, solo espera un argumento de cadena de texto para el campo de título.Float
, tiene un argumento opcional, una tupla(x,y)
con los campos de precisión: 'x' como el número total de dígitos; 'y' representa los dígitos decimales.Date
yDatetime
, estos datos son almacenados en formato UTC. Se realizan conversiones automáticas, basadas en las preferencias del usuario, disponibles a través del contexto de la sesión de usuario. Esto es discutido con mayor detalle en el Capítulo 6.Boolean
, solo espera sea fijado el campo de título, incluso si es opcional.Binary
también espera este único argumento.
Además de estos, también existen los campos relacionales, los cuales serán introducidos en este mismo capítulo. Pero por ahora, hay mucho que aprender sobre los tipos de campos y sus atributos.
Atributos de campo comunes
Los campos también tienen un conjunto de atributos los cuales podrá usar, y se explicara aquí con más detalle:
string
, es el título del campo, usado como su etiqueta en la UI. La mayoría de las veces no es usado como palabra clave, ya que puede ser fijado como un argumento de posición.default
, fija un valor predefinido para el campo. Puede ser un valor estático o uno fijado anticipadamente, pudiendo ser una referencia a una función o una expresiónlambda
.size
, aplica solo para los camposChar
, y pueden fijar el tamaño máximo permitido.translate
, aplica para los campos de texto,Char
,Text
yHtml
, hacen que los campos puedan ser traducidos: puede tener varios valores para diferentes idiomas.help
, proporciona el texto de ayuda desplegable mostrado a los usuarios.readonly = True
, hace que el campo no pueda ser editado en la interfaz.required = True
, hace que el campo sea obligatorio.index = True
, creara un índice en la base de datos para el campo.copy = False
, hace que el campo sea ignorado cuando se usa la función Copiar. Los campos no relacionados de forma predeterminada pueden ser copiados.groups
, permite limitar la visibilidad y el acceso a los campos solo a determinados grupos. Es una lista de cadenas de texto separadas por comas, que contiene los ID XML del grupo de seguridad.states
, espera un diccionario para los atributos de la UI dependiendo de los valores de estado del campo. Por ejemplo:states={'done':[('readonly', True)]}
. Los atributos que pueden ser usados son,readonly
,required
einvisible
.
Para completar, a veces son usados dos atributos más cuando se actualiza entre versiones principales de Odoo:
deprecated = True
, registra un mensaje de alerta en cualquier momento que el campo sea usado.oldname = 'field'
, es usado cuando un campo es re-nombrado en una versión nueva, permitiendo que la data en el campo viejo sea copiada automáticamente dentro del campo nuevo.
Nombres de campo reservados
Unos cuantos nombres de campo están reservados para ser usados por el ORM:
id
, es un número generado automáticamente que identifica de forma única a cada registro, y es usado como clave primaria en la base de datos. Es agregado automáticamente a cada modelo.
Los siguientes campos son creados automáticamente en los modelos nuevos,
a menos que sea fijado el atributo _log_access=False
:
create_uid
, para el usuario que crea el registro.created_date
, para la fecha y la hora en que el registro es creado.write_uid
, para el último usuario que modifica el registro.write_date
, para la última fecha y hora en que el registro fue modificado.
Esta información esta disponible desde el cliente web, usando el menú de Desarrollo y seleccionando la opción Ver metadatos.
Hay algunos efectos integrados que esperan nombres de campo específicos. Debe evitar usarlos para otros propósitos que aquellos para los que fueron creados. Algunos de ellos incluso están reservados y no pueden ser usados para ningún otro propósito:
name
, es usado de forma predeterminada como el nombre del registro que será mostrado. Usualmente es unChar
, pero se permiten otros tipos de campos. Puede ser sobre escrito configurando el atributo_rec_name
del modelo.active
(tipoBoolean
), permite desactivar registros. Registros conactive==False
serán excluidos automáticamente de las consultas. Para acceder a ellos debe ser agregada la condición('active','=', False)
al dominio de búsqueda o agregar'active_test':False
al contexto actual.sequence
(tipoInteger
), si esta presente en una vista de lista, permite definir manualmente el orden de los registros. Para funcionar correctamente debe estar también presente en el_order
del modelo.state
(tipoSelection
), representa los estados básicos del ciclo de vida del registro, y puede ser usado por el atributofield
del estado para modificar de forma dinámica la vista: algunos campos de formulario pueden ser de solo lectura, requeridos o invisibles en estados específicos del registro.parent_id
,parent_left
, yparent_right
; tienen significado especial para las relaciones jerárquicas padre/hijo. En un momento se discutirá con mayor detalle.
Hasta ahora ha discutido los valores escalares de los campos. Pero una buena parte de una estructura de datos de la aplicación es sobre la descripción de relaciones entre entidades. Vea algo sobre esto ahora.
Relaciones entre modelos
Viendo su diseño del módulo, tiene estas relaciones:
- Cada tarea tiene un estado – esta es una relación muchos a uno, también conocida como una clave foránea. La relación inversa es de uno a muchos, que significa que cada estado puede tener muchas tareas.
- Cada tarea puede tener muchas etiquetas – esta es una relación muchos a muchos. La relación inversa, obviamente, es también una relación muchos a muchos, debido a que cada etiqueta puede también tener muchas tareas.
Agregue los campos de relación correspondientes al archivo
todo_ui/todo_model.py
:
class TodoTask(models.Model):
_inherit = 'todo.task'
stage_id = fields.Many2one('todo.task.stage', 'Stage')
tag_ids = fields.Many2many('todo.task.tag', string='Tags')
El código anterior muestra la sintaxis básica para estos campos.
Configurando el modelo relacionado y el campo de título. La convención
para los nombres de campo relacionales es agregar a los nombres de
campos _id
o _ids
, para las relaciones de uno y muchos,
respectivamente.
Como ejercicio puede intentar agregar en los modelos relacionados, las
relaciones inversas correspondientes: La relación inversa de Many2one
es
un campo One2many
en los estados: cada estado puede tener muchas tareas.
Debería agregar este campo a la clase Stage. La relación inversa de
Many2many
es también un campo Many2many
en las etiquetas: cada etiqueta
puede ser usada en muchas tareas.
Vea con mayor detalle las definiciones de los campos relacionales.
Relaciones muchos a uno
Many2one
, acepta dos argumentos de posición: el modelo relacionado (que
corresponde al argumento de palabra clave del comodel
) y la cadena
de título. Este crea un campo en la tabla de la base de datos con una
clave foránea a la tabla relacionada.
Algunos nombres adicionales de argumentos también están disponibles para ser usados con estos tipos de campo:
ondelete
, define lo que pasa cuando el registro relacionado es eliminado. De forma predeterminada esta fijado comonull
, lo que significa que al ser eliminado el registro relacionado se fija a un valor vacío. Otros valores posibles sonrestrict
, que arroja un error que previene la eliminación, ycascade
que también elimina este registro.context
ydomain
, son significativos para las vistas del cliente. Pueden ser configurados en el modelo para ser usados de forma predeterminada en cualquier vista donde sea usado el campo. Estos serán explicados con más detalle en el Capítulo 6.auto_join = True
, permite que el ORM use uniones SQL haciendo búsquedas usando esta relación. De forma predeterminada esto esta fijado comoFalse
para reforzar las reglas de seguridad. Si son usadas uniones, las reglas de seguridad serán pasadas por alto, y el usuario podrá tener acceso a los registros relacionados que las reglas de seguridad no le permitirían, pero las consultas SQL serán más eficientes y se ejecutarán con mayor rapidez.
Relaciones muchos a muchos
La forma más simple de la relación Many2many
acepta un argumento para el
modelo relacionado, y es recomendable también proporcionar el argumento
de cadena con el título del campo.
En el nivel de base de datos, esto no agrega ninguna columna a las
tablas existentes. Por el contrario, automáticamente crea una tabla
nueva de relación de solo dos campos con las claves foráneas de las
tablas relacionadas. El nombre de la tabla de relación es el nombre de
ambas tablas unidos por un símbolo de guión bajo (_
) con _rel
anexado.
Estas configuraciones predeterminadas pueden ser sobre escritas manualmente. Una forma de hacerlo es usar la forma larga para la definición del campo:
# TodoTask class: Task <-> relación Tag (forma larga):
tag_ids = fields.Many2many('todo.task.tag', # modelo relacionado
'todo_task_tag_rel', # nombre de la tabla de relación
'task_id', # campo para "este" registro
'tag_id', # campo para "otro" registro
string='Tasks')
Note que los argumentos adicionales son opcionales. Podrá simplemente fijar el nombre para la tabla de relación y dejar que los nombres de los campos usen la configuración predeterminada.
Si prefiere, puede usar la forma larga usando los argumentos de palabra clave:
# TodoTask class: Task <-> relación Tag (forma larga):
tag_ids = fields.Many2many(comodel_name='todo.task.tag', # modelo relacionado
relation='todo_task_tag_rel', # nombre de la tabla de relación
column1='task_id', # campo para "este" registro
column2='tag_id', # campo para "otro" registro
string='Tasks')
Como los campos muchos a uno, los campos muchos a muchos también soportan los atributos de palabra clave de dominio y contexto.
En algunas raras ocasiones tendrá que usar estas formas largas para sobre escribir las configuraciones automáticas predeterminadas, en particular, cuando los modelos relacionados tengan nombres largos o cuando necesite una segunda relación muchos a muchos entre los mismos modelos.
Truco
Los nombres de las tablas PostgreSQL tienen 63 caracteres como
límite, y esto puede ser un problema si la tabla de relación generada
automáticamente excede ese limite. Este es uno de los casos cuando
tendrá que configurar manualmente el nombre de la tabla de
relación usando el atributo relation
.
Lo inverso a la relación Many2many
es también un campo Many2many
. Si
también agrega un campo Many2many
a las etiquetas, Odoo infiere que
esta relación de muchos a muchos es la inversa a la del modelo de
tareas.
La relación inversa entre tareas y etiquetas puede ser implementada así:
# class Tag(models.Model): #
_name = 'todo.task.tag'
#Tag class relación a Tasks:
task_ids = fields.Many2many('todo.task', # modelo relacionado
string='Tasks')
Relaciones inversas de uno a muchos
La inversa de Many2many
puede ser agregada al otro extremo de la
relación. Esto no tiene un impacto real en la estructura de la base de
datos, pero le permite navegar fácilmente desde "un" lado a "muchos"
lados de los registros. Un caso típico es la relación entre un
encabezado de un documento y sus líneas.
En su ejemplo, con una relación inversa One2many
en estados,
fácilmente podrá listar todas las tareas que se encuentran en un
estado. Para agregar esta relación inversa a los estados, agregue el
código mostrado a continuación:
# class Stage(models.Model): #
_name = 'todo.task.stage'
#Stage class relación con Tasks:
tasks = fields.One2many('todo.task', # modelo relacionado
'stage_id', # campo para "este" en el modelo relacionado
'Tasks in this stage')
One2many
acepta tres argumentos de posición: el modelo relacionado, el
nombre del campo en aquel modelo que referencia este registro, y la
cadena de título. Los dos primeros corresponden a los argumentos
comodel_name
e inverse_name
.
Los parámetros adicionales disponibles son los mismos que para el muchos
a uno: contexto, dominio, ondelete
(aquí actúa en el lado "muchos" de la
relación), y auto_join
.
Relaciones jerárquicas
Las relaciones padre-hijo pueden ser representadas usando una relación
Many2one
al mismo modelo, para dejar que cada registro haga referencia a
su padre. Y la inversa One2many
hace más fácil para un padre mantener el
registro de sus hijos.
Odoo también provee soporte mejorado para estas estructuras de datos
jerárquicas: navegación más rápida a través de árboles hermanos, y
búsquedas más simples con el operador child_of
en las expresiones de
dominio.
Para habilitar esas características debe configurar el atributo
_parent_store
y agregar los campos de ayuda: parent_left
y
parent_right
. Tenga en cuenta que estas operaciones adicionales
traen como consecuencia penalizaciones en materia de almacenamiento y
ejecución, así que es mejor usarlo cuando se espere ejecutar más
lecturas que escrituras, como es el caso de un árbol de categorías.
Revisando el modelo de etiquetas definido en el archivo
todo_ui/todo_model.py
, ahora edite para que luzca así:
class Tags(models.Model):
_name = 'todo.task.tag'
_parent_store = True
#_parent_name = 'parent_id'
name = fields.Char('Name')
parent_id = fields.Many2one('todo.task.tag','Parent Tag', ondelete='restrict')
parent_left = fields.Integer('Parent Left', index=True)
parent_right = fields.Integer('Parent Right', index=True)
Aquí tiene un modelo básico, con un campos parent_id
que
referencia al registro padre, y el atributo adicional _parent_store
para agregar soporte a búsquedas jerárquicas.
Se espera que el campo que hace referencia al padre sea nombrado
parent_id
. Pero puede usarse cualquier otro nombre declarándolo con
el atributo _parent_name
.
También, es conveniente agregar un campo con el hijo directo del registro:
child_ids = fields.One2many('todo.task.tag', 'parent_id', 'Child Tags')
Hacer referencia a campos usando relaciones dinámicas
Hasta ahora, los campos de relación que ha visto puede solamente
hacer referencia a un modelo. El tipo de campo Reference
no tiene esta
limitación y admite relaciones dinámicas: el mismo campo es capaz de
hacer referencia a más de un modelo.
Podrá usarlo para agregar un campo, "Refers to", a Tareas por Hacer que pueda hacer referencia a un User o un Partner:
# class TodoTask(models.Model):
refers_to = fields.Reference([
('res.user', 'User'),('res.partner', 'Partner')
], 'Refers to')
Puede observar que la definición del campo es similar al campo
Selection
, pero aquí la lista de selección contiene los modelos que
pueden ser usados. En la interfaz, el usuario seleccionará un modelo de
la lista, y luego elegirá un registro de ese modelo.
Esto puede ser llevado a otro nivel de flexibilidad: existe una tabla de
configuración de Modelos Referenciables para configurar los modelos que
pueden ser usados en campos Reference
. Esta disponible en el menú
Configuración > Técnico > Estructuras de base de datos.
Cuando se crea un campo como este podrá ajustarlo para que use
cualquier modelo registrado allí, con la ayuda de la función
referencable_models()
en el módulo
openerp.addons.res.res_request
. En la versión 8 de Odoo, todavía se
usa la versión antigua de la API, así que necesitara empaquetarlo para
usarlo con la API nueva:
from openerp.addons.base.res import res_request
def referencable_models(self):
return res_request.referencable_model(self, self.env.cr, self.env.uid, context=self.env.context)
Usando el código anterior, la versión revisada del campo "Refers to" sera así:
# class TodoTask(models.Model):
refers_to = fields.Reference(referencable_models, 'Refers to')
Campos calculados
Los campos pueden tener valores calculados por una función, en vez de
simplemente leer un valor almacenado en una base de datos. Un campo
calculado es declarado como un campo regular, pero tiene el argumento
compute
adicional con el nombre de la función que se usará para
calcularlo.
En la mayoría de los casos los campos calculados involucran alguna lógica de negocio, por lo tanto este tema se desarrollara con más profundidad en el Capítulo 7. Igual podrá explicarlo aquí, pero manteniendo la lógica de negocio lo más simple posible.
Trabaje en un ejemplo: los estados tienen un campo "fold". Agregue a las tareas un campo calculado con la marca "Folded?" para el estado correspondiente.
Debe editar el modelo TodoTask
en el archivo
todo_ui/todo_model.py
para agregar lo siguiente:
# class TodoTask(models.Model):
stage_fold = fields.Boolean('Stage Folded?', compute='_compute_stage_fold')
@api.one
@api.depends('stage_id.fold')
def _compute_stage_fold(self):
self.stage_fold = self.stage_id.fold
El código anterior agrega un campo nuevo stage_fold
y el método
_compute_stage_fold
que sera usado para calcular el campo. El nombre
de la función es pasado como una cadena, pero también es posible pasarla
como una referencia obligatoria (el identificador de la función son
comillas).
Debido a que esta usando el decorador @api.one
, self
tendrá un
solo registro. Si en vez de esto usa @api.multi
, representara un
conjunto de registros y su código necesitará gestionar la iteración
sobre cada registro.
El @api.depends
es necesario si el calculo usa otros campos: le dice
al servidor cuando re-calcular valores almacenados o en cache. Este
acepta uno o más nombres de campo como argumento y la notación de puntos
puede ser usada para seguir las relaciones de campo.
Se espera que la función de calculo asigne un valor al campo o campos a
calcular. Si no lo hace, arrojara un error. Debido a que self
es un
objeto de registro, su calculo es simplemente para obtener el campo
"Folded?" usando self.stage_id.fold
. El resultado es conseguido
asignando ese valor (escribiéndolo) en el campo calculado,
self.stage_fold
.
No trabajara aún en las vistas para este módulo, pero puede hacer una edición rápida al formulario de tareas para confirmar si el campo calculado esta funcionando como es esperado: usando el menú de Desarrollo escoja la opción Editar Vista y agregue el campo directamente en el XML del formulario. No se preocupe: será reemplazado por una vista limpia del módulo en la próxima actualización.
Buscar y escribir en campos calculados
El campo calculado que acabo de crear puede ser leído, pero no se puede realizar una búsqueda ni escribir en el. Esto puede ser habilitado proporcionando funciones especiales para esto. A lo largo de la función de calculo también podrá colocar una función de búsqueda, que implemente la lógica de búsqueda, y la función inversa, que implemente la lógica de escritura.
Para hacer esto, su declaración de campo calculado se convertirá en esto:
# class TodoTask(models.Model):
stage_fold = fields.Boolean
string = 'Stage Folded?',
compute ='_compute_stage_fold',
# store=False) # predeterminado
search ='_search_stage_fold',
inverse ='_write_stage_fold')
Las funciones soportadas son:
def _search_stage_fold(self, operator, value):
return [('stage_id.fold', operator, value)]
def _write_stage_fold(self):
self.stage_id.fold = self.stage_fold
La función de búsqueda es llamada en cuanto es encontrada en este campo
una condición (campo, operador, valor)
dentro de una expresión de
dominio de búsqueda.
La función inversa realiza la lógica reversa del cálculo, para hallar el
valor que sera escrito en el campo de origen. En su ejemplo, es
solo escribir en stage_id.fold
.
Guardar campos calculados
Los valores de los campos calculados también pueden ser almacenados en
la base de datos, configurando store
a True
en su definición. Estos
serán calculados cuando cualquiera de sus dependencias cambie. Debido a
que los valores ahora estarán almacenados, pueden ser buscados como un
campo regular, entonces no es necesaria una función de búsqueda.
Campos relacionados
Los campos calculados que implemento en la sección anterior son un caso especial que puede ser gestionado automáticamente por Odoo. El mismo efecto puede ser logrado usando campos Relacionados. Estos hacen disponibles, de forma directa en un módulo, los campos que pertenecen a un modelo relacionado, que son accesibles usando la notación de puntos. Esto posibilita su uso en los casos en que la notación de puntos no pueda usarse, como los formularos de UI.
Para crear un campo relacionado, declare un campo del tipo necesario,
como en los campos calculados regulares, y en vez de calcularlo, use
el atributo related
indicando la cadena de notación por puntos para
alcanzar el campo deseado.
Las tareas por hacer están organizadas en estados personalizables y a su vez esto forma un mapa en los estados básicos. Los pondrá disponibles en las tareas, y usara esto para la lógica del lado del cliente en la próximo capítulo.
Agregara un campo calculado en el modelo tarea, similar a como
hizo a "stage_fold", pero ahora usando un campo related
:
# class TodoTask(models.Model):
stage_state = fields.Selection(
related='stage_id.state',
string='Stage State'
)
Detrás del escenario, los campos "Related" son solo campos calculados que convenientemente implementan las funciones de búsqueda e inversa. Esto significa que podrá realizar búsquedas y escribir en ellos sin tener que agregar código adicional.
Restricciones del Modelo
Para reforzar la integridad de los datos, los modelos también soportan dos tipos de restricciones: SQL y Python.
Las restricciones SQL son agregadas a la definición de la tabla en la
base de datos e implementadas por PostgreSQL. Son definidas usando el
atributo de clase _sql_constraints
. Este es una lista de tuplas con
el nombre del identificador de la restricción, el SQL para la
restricción, y el mensaje de error que se usara.
Un caso común es agregar restricciones únicas a los modelos. Suponga que no querrá permitir que el mismo usuario tenga dos tareas activas con el mismo título:
# class TodoTask(models.Model):
_sql_constraints = [(
'todo_task_name_uniq',
'UNIQUE (name, user_id, active)',
'Task title must be unique!'
)]
Debido a que esta usando el campo user_id
agregado por el módulo
todo_user
, esta dependencia debe ser agregada a la clave depends
del archivo manifiesto __openerp__.py
.
Las restricciones Python pueden usar un pedazo arbitrario de código para
verificar las condiciones. La función de verificación necesita ser
decorada con @api.constrains
indicando la lista de campos
involucrados en la verificación. La validación es activada cuando
cualquiera de ellos es modificado, y arrojara una excepción si la
condición falla:
from openerp.exceptions import ValidationError
# class TodoTask(models.Model):
@api.one
@api.constrains('name')
def _check_name_size(self):
if len(self.name) < 5:
raise ValidationError('Must have 5 chars!')
El ejemplo anterior previene que el título de las tareas sean almacenados con menos de 5 caracteres.
Resumen
Vio una explicación minuciosa de los modelos y los campos, usándolos para ampliar la aplicación de Tareas por Hacer con etiquetas y estados de las tareas. Aprendió como definir relaciones entre modelos, incluyendo relaciones jerárquicas padre/hijo. Finalmente, vi ejemplos sencillos de campos calculados y restricciones usando código Python.
En el próximo capítulo, trabajara en la interfaz para las características "back-end" de ese modelo, haciéndolas disponibles para las vistas que se usan para interactuar con la aplicación.