Documentos de Académico
Documentos de Profesional
Documentos de Cultura
F P
¿Qué aprenderás? A
K I
N
• Conocerás los archivos mínimos necesarios para generar un
módulo en Odoo.
LI
O
• Revisarás el modelo de Odoo, con sus atributos posibles, para
N
generar clases dentro de un módulo.
U M
• Generarás las vistas necesarias para que los usuarios puedan
utilizar el módulo.
AL
• Utilizarás la herencia para modificar un módulo y personalizarlo
a gusto del cliente. L E
IB
• Crearás un esquema de seguridad para asegurar que cada
I M
usuario sólo accede a los datos que le corresponden.
P R
I
¿Sabías que…?
M
N
I Ó
•SEn términos de seguridad, es mucho más probable que alguien
R
E se introduzca en el sistema por una mala gestión de los perfiles
V de usuario que por agujeros en el código. La creación de perfiles
es una de las partes centrales del diseño de módulos en un ERP.
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
P
IM
Este software permite la realización de distintos tipos de diagrama. Evidentemente hemos
de tener seleccionados los diagramas UML en la parte izquierda de la pantalla.
N
Se han modelado clases para modelar los conceptos máquina, orden de trabajo, gama de
I Ó
mantenimiento e instrucción. Las clases creadas son:
•
LI
Una orden de trabajo tiene una o varias máquinas asociadas, que serán sobre las
que se ha de actuar. Es una relación many2many.
•
N O
Una gama de mantenimiento tiene una o varias instrucciones asociadas. Es una
•
relación one2many.
U M
Aunque no aparecen en el diagrama, la clase manteni.workorder está asociada con
AL
la tabla hr_employee mediante el atributo employee_id (empleado que realizará el
mantenimiento). Es una relación many2one.
•
L E
Aunque no aparecen en el diagrama, la clase manteni.machine está asociada con la
I M
P R
IM
N
I Ó
R S
VE
3
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Una vez hemos diseñado de manera teórica nuestro módulo, vamos a comenzar a escribir
su código Python.
Un módulo incorpora todos los elementos del patrón modelo-vista-controlador que es la
base del desarrollo de Odoo, estos son:
F P
IA
N K
LI
N O
U M
AL
L E
I B
I M
Figura 31: Patrón modelo vista controlador en Odoo
•
P R
El modelo: se define vía lenguaje Python, son los objetos que declararemos, y que
•
IM
se traducirán en tablas PostgreSQL.
Una vista: se define con lenguaje XML.
N
•
Ó
Un controlador: Podremos diseñar métodos en lenguaje Python que controlarán el
I
funcionamiento de Odoo.
R S
Vamos a ver ahora como diseñar módulos usando directamente el lenguaje Python. Para
V E ello empezamos viendo como es la estructura de un módulo, generando uno vacío. Para
instalaciones finales de Odoo, solamente hay que buscar el archivo odoo-bin y escribir el
siguiente comando:
odoo-bin scaffold <module name> <where to put it>
Esta instrucción nos creará toda la estructura de archivos y carpetas de un módulo vacío. Si
no hemos instalado Odoo, ya que estamos utilizando el código fuente sobre Pycharm, no
hay que preocuparse, ya que iremos creando uno por uno todos los archivos y carpetas.
4
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Comenzaremos por los dos archivos que aparecen en la carpeta del módulo, __init__.py y
__manifest__.py.
F P
IA
N K
LI
N O
U M
Figura 32: Carpeta que contiene un módulo nuevo
AL
Archivo __init__.py
L E
I B
I M
El archivo __init__.py (dos guiones bajos antes y después de init) en el proceso de
inicialización del programa. En se colocan todos los archivos Python que hay que cargar
R
para la ejecución del programa. Por defecto se cargar los controladores y modelos.
P
IM
N
I Ó
R S
V E Figura 33: Archivo __init__.py
5
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Archivo __manifest__.py
Este archivo debe estar ubicado en la raíz de la carpeta del módulo y debe tener el formato
de un diccionario de Python. Es el responsable de determinar:
• Nombre, descripción, autores, licencia, versión, categoría,… del módulo.
•
•
Los archivos XML que se analizaran durante el arranque del servidor Odoo.
Las dependencias con otros módulos. F P
Los valores del diccionario Python que contiene este archivo son los siguientes: I A
•
N K
LI
name: Nombre del módulo en inglés.
• summary: Descripción del propósito del módulo en una frase.
• description: Texto que describe el módulo.
N O
• author: Autor del módulo.
• website: web del autor del módulo.
U M
•
AL
category: Categoría a la que pertenece el módulo. Texto separado por barras / con
la ruta jerárquica de las categorías.
• version: Versión del módulo.
L E
•
IB
depends: Lista Python de los módulos de los que depende este módulo.
•
I M
data: Lista Python de los nombres de archivos XML que se al instalar/actualizar el
módulo.
P R
módulo. La ruta de los archivos debe ser relativa a la carpeta donde se encuentra el
•
IM
demo: Lista de Python de nombres de ficheros XML que se cargaran si se ha
instalado Odoo con los datos de demostración o ejemplo. Las rutas de los archivos
N
deben ser relativas al directorio donde está el módulo.
I Ó
R S
V E
6
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
F P
IA
N K
LI
Figura 34: Archivo __manifest__.py
N O
U M
Los diccionarios de Python se definen entre {} y las palabras clave:valor separadas por
AL
comas. Las listas se definen entre [] y sus valores también van separados por comas.
Éste es el archivo __manifest__.py que resulta para el módulo “manteni”.
L E
I B
I M
P R
IM
N
I Ó
R S
VE
7
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
El modelo de Odoo contendrá todos los objetos (clases) que se han definido anteriormente
con DIA. Toda la información se introducirá en la carpeta “models”. Esta carpeta contendrá
como mínimo dos archivos:
• P
__init__.py donde se mencionan todos los archivos que contiene la carpeta, y se
importarán para crear el módulo. F
• I A
nombre_modulo.py donde se generarán todas las clases que componen el módulo.
N K
El archivo nombre_modulo.py contiene, como hemos dicho, la definición del modelo del
LI
módulo de Odoo. Para definir un objeto (clase de Odoo), es necesario definir una clase de
Python y posteriormente instanciarla.
N O
U M
AL
Figura 36: Clase en Odoo
L E
El atributo _name es obligatorio, y tendrá el formato similar al que hemos utilizado para
definir nuestra cases en DIA.
I B
I M
P R
IM
N
I Ó
R S
V E
8
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
L E
I B
M
Figura 37: Ejemplo de uso del atributo _sql_constraints
I
P R
IM
N
I Ó
R S
VE
9
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
A continuación se generan los atributos (campos), que pueden ser de varios tipos:
• Campos básicos
• Campos calculados (computed)
•
•
Campos relacionales (relational)
Campos relacionados (related) F P
I A
N K
5.4.2.1. Campos básicos
LI
N O
Se trata de campos simples, que contienen información que se almacena en la base de
datos. Pueden ser de varios tipos (numérico, carácter, texto, fecha…)
U M
Algunos de los parámetros comunes (todos son opcionales) a cualquier campo básico son:
AL
• string: Texto que verán los usuarios relacionado con el campo. Si no se incluye, los
usuarios verán el nombre del campo.
•
L E
help: Tooltip con ayuda que verán los usuarios.
•
IB
readonly: Campo de sólo lectura (FALSE por defecto).
•
M
required: Campo obligatorio (FALSE por defecto).
I
•
• P R
index: Generar un índice en la base de datos (FALSE por defecto).
default: valor por defecto que toma el campo. Puede utilizarse un valor concreto o
IM
llamar a una función (que se describirá después en el mismo archivo), para el
cálculo.
N
•
Ó
groups: lista separada por comas de los grupos con acceso a este campo.
I
R
•
•
SLos campos que se pueden utilizar en Odoo son:
Char: campo básico de texto. Tiene dos prámetros:
10
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
• Text: es similar a char, pero utilizado para cadenas más largas. No se indica el
tamaño, y se muestra en pantalla mediante una caja multilínea. Tiene un
parámetro:
o translate: activa la traducción del campo si lo ponemos como TRUE.
• Selection: crea un combo desplegable para elegir un valor. Tiene un parámetro
obligatorio:
o selection: valores posibles del campo. Se presentan como una lista de
•
pares (valor, texto), separados por comas.
Date: campo tipo fecha. Se almacena en formato YYYY-MM-DD F P
IA
N K
LI
N O
U M
AL
L E
B
Figura 38: Clase "manteni.workorder”
I
I M
Es obligatorio e importante que en todas las clases se defina un campo llamado “name”, ya
que se utilizará para hacer referencia a la clase cuando se creen campos relacionales.
P R
A continuación se muestran varios un ejemplos del atributo default, tanto dándole un valor
IM
concreto como llamando a una función de Python.
N
S IÓ Figura 39: Campo "date_begin" de la clase "manteni.workorder"
R
VE Figura 40: Campo "state" de la clase "manteni.workorder"
11
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Puede que nos interese crear un campo que, en lugar de utilizarse almacenando un valor
en la base de datos, se genere mediante un cálculo. Por ejemplo, podríamos calcular de
forma dinámica la edad de un trabajador de la empresa mediante su fecha de nacimiento y
el día actual. De esta manera, sin necesidad de ocupar espacio en la base de datos
tendríamos un dato actualizado y utilizable en cada momento.
Hacen referencia a un método que deberá definirse a continuación en lenguaje Python.
F P
I A
N K
Figura 41: Campo calculado
LI
N O
U M
AL
Figura 42: Función Python para calcular el campo
L E
IB
Campos relacionados (related)
5.4.2.3.
I M
PR
Los campos relacionados sirven para obtener un valor de otro modelo, siguiendo las
relaciones entre atributos. Esto evita tener que duplicar información en la base de datos.
IM
Por ejemplo, en la clase “manteni.machine” hemos creado el campo “suplier_id”, que hace
referencia a la tabla “res_partner”. Si además del nombre de la empresa quisiéramos
N
mostrar su dirección, podríamos utilizar un campo related, que utilizara “suplier_id” para
I Ó
consultar en la tabla “res_partner” sin necesidad de duplicar la información.
R S
VE Figura 43: Campo relacionado
12
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Entre las clases de Odoo se pueden establecer tres tipos de relaciones, similares a las del
modelo entidad-relación para las bases de datos. Estas relaciones se definen mediante los
atributos referenciales siguientes:
• many2one
• one2many
• many2many
F P
IA
a) Many2one
N K
LI
Representa una relación hacia una clase padre. Muchos objetos de la clase que contiene el
atributo pueden estar relacionados con el mismo objeto de la clase padre.
N O
Odoo muestra los campos many2one acompañados por una lista para seleccionar el objeto
de la clase padre. Esto obliga a que la case referenciada por el campo many2one contenga
el campo name.
U M
AL
L E
IB
I M
R
Figura 44: Campo many2one "employee" de la clase "manteni.workorder"
P
IM
N
Para definir un atributo many2one en Odoo podemos utilizar (entre otros) los siguientes
parámetros:
• I Ó
R •
Scomodel_name: nombre de la clase destino
domain (opcional): permite filtrar para no ver todos los valores.
VE • Ondelete: qué hacer cuando se borra el registro referido. Los valores posibles son
‘set_null”, ‘restrict’ y ‘cascade’.
13
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
b) One2many
Representa una relación hacia una clase hija. Un objeto de la clase que contiene el atributo
puede estar relacionado con muchos objetos de la clase hija. Es evidente que cada atributo
one2many debe ser complementario de un atributo may2one que debe estar
obligatoriamente en la clase hija.
Odoo muestra para los campos one2many una lista con todos los objetos relacionados, y la
posibilidad de añadir más.
F P
IA
N K
LI
N O
M
Figura 46: Campo one2many de la clase “manteni.program”
U
AL
Para definir un atributo one2many en Odoo podemos utilizar (entre otros) los siguientes
parámetros:
•
L
comodel_name: nombre de la clase destino.E
•
I B
inverse_name: nombre del campo inverso many2one que se encuentra en la clase
destino.
I M
•
R
domain (opcional): permite filtrar para no ver todos los valores.
P
IM Figura 47: Campo one2many
N
I Ó
R S Figura 48: Campo many2one relacionado con el one2many de la figura anterior
VE La existencia de un atributo one2many implica que debe existir un atributo may2one. Sin
embargo, la existencia de un atributo may2one no implica que deba existir un atributo
one2may.
14
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
c) Many2many
Representa una relación de muchos a muchos entre dos objetos. Cada clase de una clase A
pues estar relacionado con muchos objetos de la clase B y cada objeto de la clase B puede
estar relacionado con muchos objetos de la clase A.
En nuestro caso, en una orden de trabajo se puede pedir el mantenimiento de varias
máquinas, y una máquina aparecerá en varias órdenes de trabajo, a medida que va
sufriendo averías.
Odoo muestra para los campos many2many una lista con todos los objetos relacionados, y
la posibilidad de añadir más, al igual que en one2many.
F P
IA
N K
LI
N O
U M
AL
Figura 49: Campo many2many de la clase “manteni.workorder”
parámetros: L E
Para definir un atributo many2many en Odoo podemos utilizar (entre otros) los siguientes
• IB
comodel_name: nombre de la clase destino.
• I M
relation (opcional): nombre de la tabla de la base de datos que contendrá la
R
relación entre las dos clases.
P
•
IM
column1 (opcional): nombre de la columna referida para el registro de la clase
actual.
• N
column2 (opcional): nombre de la columna referida para el registro de la clase
I Ó
destino.
R
• Sdomain (opcional): permite filtrar para no ver todos los valores.
VE Los tres valores relation, column1 y column2 eran obligatorios en versiones anteriores
(cuando el ERP se llamaba OpenERP), y se pueden seguir añadiendo si se quisiera.
15
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
A veces puede resultarnos útil cambiar un valor antes manera inmediata, dependiendo del
valor que ha tomado otro campo, para ello utilizamos las funciones onchange.
Los campos calculados, definidos anteriormente, no necesitan una función onchange, ya
que se actualizan automáticamente. Esto es una novedad respecto a versiones anteriores.
La sintaxis será la siguiente:
F P
IA
N K
LI
Figura 51: Sintaxis para las funciones onchange
N O
Para la clase “manteni.workorder” hemos creado dos funciones de este tipo. La primera
U M
actualiza la fecha de finalización de la orden de trabajo si se elige el estado “Closed” a la
fecha actual. La segunda busca el identificador de usuario de un trabajador cuando
AL
cambiamos el empleado que llevará a cabo la orden de trabajo.
L E
I B
I M
P R
IM
Figura 52: Funciones onchange en la clase "manteni.workorder"
N
I Ó
R S
VE
16
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
F P
IA
N K
LI
N O
U M
AL
Figura 53: Carpeta "models" del módulo "manteni"
Tal y como hemos definido anteriormente, existen los archivos __init__.py y manteni.py.
I B
5.4.4.1. Archivo __init__.py
I M
P R
Contendrá la importación de los dos archivos que componen el modelo.
# -*- coding: utf-8 -*-
IM
from . import manteni
N
I Ó
from . import partner
R S
5.4.4.2. Archivo manteni.py
class Machine(models.Model):
17
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
_name = 'manteni.machine'
string='State', default='on_use')
LI
city = fields.Char(related='suplier_id.city', store=False)
L E
@api.multi
I B
def _maintenance_date(self):
I M
for record in self:
fecha=record.date_begin P R
IM
fecha_fin=datetime.strptime(fecha,'%Y-%m-%d')
N
record.date_first_maintenance = fecha_fin + timedelta(hours=record.hours_maint)
I Ó
S
class Program(models.Model):
R
V E
#Normas de mantenimiento, conjunto de medidas para el mantenimiento de una parte determinada"""
_name = 'manteni.program'
18
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
class ProgramInstruction(models.Model):
_name = 'manteni.program.instruction'
N K
LI
# Orden de trabajo para un trabajador concreto, repara una o varias maquinas"""
_name = 'manteni.workorder'
N O
name = fields.Char('Title', size=64, required=True, translate=True)
L E
domain="[('department_id','=','Maintenance')]")
IB
employee_id = fields.Many2one('hr.employee', string='Employee',
employee_user_id = fields.Integer()
I M
R
machine_ids = fields.Many2many('manteni.machine', string='Machines')
P
IM
program_id = fields.Many2one('manteni.program', string='Program')
R S
type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Type of maintenance',
default='corrective')
V E
closing_info = fields.Text('Closing information')
@api.onchange('state')
def _onchange_state(self):
if self.state == 'closed':
self.date_end=datetime.now()
19
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.employee_user_id=self.employee_id.resource_id.user_id
F P
I A
N K
LI
N O
U M
AL
L E
I B
I M
P R
IM
N
I Ó
R S
VE
20
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
El concepto vista hace referencia a las pantallas que permiten que el usuario visualice la
información o la edite. Una vista (view) también genera los menús que permiten un acceso
organizado a la a las vistas. Estos menús y vistas se diseñan mediante archivos XML que, al
instalarse en una empresa de Odoo, obtienen un identificador único y que el sistema usa
para la gestión de la aplicación. Por ello, en la definición XML de menús y vistas se incluye
un identificador (texto) que debe ser único dentro del módulo y al que se puede
referenciar desde el propio módulo por su nombre o desde cualquier otro módulo vía la F P
sintaxis módulo.identificador.
I A
K
Todos los archivos xml necesarios para generar las vistas de Odoo se introducirán en la
N
LI
carpeta “Views”. De manera obligatoria tendremos que incluir solamente un archivo
llamado nombre_modulo.xml.
5.5.1. Vistas N O
U M
Las vistas son aquellas pantallas que facilitan el acceso la información al usuario. Desde
AL
estas pantallas, un usuario puede consultar y modificar la información. Tenemos diferentes
pantallas que sirven de interfaz entre el usuario y el programa. Todas ellas son dinámicas.
L E
Entendemos por vistas dinámicas aquellas que se van construyendo en tiempo de ejecución a
I M
Las vistas se declaran en el archivo XML nombre_modulo.xml (al igual que acciones y
menús).
P R
La sintaxis para definir una vista es la siguiente:
IM
<record model="ir.ui.view" id="view_id">
N
<field name="name">view.name</field>
I Ó
<field name="model">object_name</field>
R S
<field name="type">xxx</field>
</field>
</record>
21
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
LI
Elemento field name=”priority”: prioridad de la vista. Cuanto más pequeña, más
prioridad. Valor por defecto 16.
•
O
Elemento field name=”arch” type=”xml”: arquitectura o estructura de la vista. Se
N
define mediante diferentes etiquetas XML y es distinta para cada tipo de vista.
I M
Utilizando la sintaxis que hemos definido para una vista, declararemos el tipo form de la
<field name="type">form</field> PR
siguiente manera (recordando que no es una sintaxis obligatoria):
IM
N
a) Colocación de los elementos
I Ó
Para colocar los elementos en el formulario de la manera adecuada, deben utilizarse
R S
etiquetas xml.
22
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
•
crear un título.
F P
Sheet: Puede ir inmediatamente debajo de form, para crear que el layout sea más
estrecho y “responsive” (adaptable a cualquier formato de pantalla). IA
• Header: título cuando se utiliza sheet.
N K
LI
b) Utilización de etiquetas CSS
N O
Pueden utilizarse las etiquetas <div>, <h1>, <h2>… para producir los títulos. También
clase “oe_edit_only”.
U M
puede hacerse que la etiqueta de un campo sólo aparezca en modo edición, mediante la
AL
L E
IB
I M
P R
IMFigura 54: Etiquetas CSS para dar formato a un formulario
N
• I Ó
También podemos utilizar las siguientes clases:
R •
Soe_inline: cancela el salto de línea tras un campo
oe_left, oe_right: hace el campo flotante en la dirección correspondiente
23
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
<form> P R
<sheet>
IM
N
<div class="oe_title">
I Ó
<label for="name" string="Machine's denomination" class="oe_edit_only"/>
R <h1> S
V E <field name="name" placeholder="Machine's denomination" modifiers="{'required': true}"/>
</h1>
</div>
<group>
<group>
<field name="serial"/>
<field name="state"/>
24
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
</group>
<group>
<field name="suplier_id"/>
</group>
</group>
<group>
F P
<field name="date_begin"/>
IA
<field name="hours_maint" />
N K
LI
</group>
</form>
U M
</field>
AL
</record>
L E
IB
I M
P R
IM
N
I Ó
R S Figura 55: Formulario para la clase "machine"
V E
<record model="ir.ui.view" id="manteni.program_form">
<field name="name">manteni.program.form</field>
<field name="model">manteni.program</field>
<field name="type">form</field>
<group>
25
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
</group>
<group>
</group>
F P
</form>
IA
</field>
N K
LI
</record>
N O
U M
AL
L E
Figura 56: Formulario para la clase "program"
IB
M
<record model="ir.ui.view" id="manteni.manager_workorder_form">
I
P R
<field name="name">manteni.workorder.form</field>
<field name="model">manteni.workorder</field>
IM
<field name="type">form</field>
N
<field name="arch" type="xml">
I Ó
<form string="manteni.workorder">
R S
<div class="oe_title">
<h1>
</h1>
</div>
<group>
</group>
<group string="Assignation">
N K
LI
</group>
<group>
L E
<group>
I B
<field name="closing_info" attrs="{'invisible':[('state','!=', 'closed')]}"/>
</group> I M
</form>
P R
</field>
IM
</record>
N
I Ó
R S
VE
27
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
F P
IA
N K
LI
N O
U M
Figura 57: Formulario para la clase "workorder"
AL
e) Vistas list (tree)
L E
IB
Este tipo de vistas consisten en una distribución de líneas con el objetivo de facilitar la
I M
visualización y/o edición de un conjunto de recursos de un objeto.
P R
IM
Al igual que para el resto de vistas, generaremos el código estándar, personalizando la
siguiente linea:
N
I Ó
<field name="type">tree</field>
R S
Existen, entre otro, los siguientes parámetros:
V E • editable: por defecto, para editar una lista hacemos clic sobre una de las líneas, y se
abrirá el formulario definido para la edición. Sin embargo, el atributo “editable” nos
permite hacerlo sobre la misma lista. Puede tomar los valores “top” o “bottom”.
• default_order: cambia el orden por defecto por uno personalizado.
• decoration-{$name}: permite cambiar el color de las líneas de la lista, en función de
una variable determinada. Se introduce una expresión Python para ser evaluada.
{$name} puede ser sustituido por los valores siguientes:
o Valores html (por ejemplo bf, font-weight: bold, it, font-style: italic)
28
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
•
valor FALSE.
Bajo la etiqueta <tree> podemos encontrar los siguientes elementos: F P
• button: muestra un botón. Entre otros lleva los atributos: I A
o icon: icono del botón
N K
o string: texto del botón LI
o type: tipo del botón (workflow, object, action)
N O
o states: hace el botón invisible si no se cumple la condición
M
o confirm: mensaje de confirmación para que el usuario acepte.
U
AL
• Field: muestra un campo. Entre otros lleva los atributos:
o name: nombre del campo.
L E
o string: texto de la columna (por defecto la etiqueta del campo)
I B
o invisible: se tiene en cuenta por Odoo, pero no aparece en la lista.
I M
Imprescindible si el campo es una de las condiciones para cambiar la
decoración de la tabla.
P R
o groups: grupos que pueden ver el campo.
IM
o A continuación mostramos un ejemplo de vista de lista para la clase
“manteni.workorder”.
N
Ó
<record model="ir.ui.view" id="manteni.workorder_tree">
I
R S
<field name="name">manteni.workorder.tree</field>
<field name="model">manteni.workorder</field>
VE <field name="type">tree</field>
<tree string="manteni.workorder">
<field name="date_begin"/>
<field name="name"/>
<field name="employee_id"/>
29
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
</tree>
</field>
</record>
F P
I A
N K
LI
N O
U M
AL
L E
I B
M
Figura 59: Formulario para la clase "manteni.workorder"
I
f) Vista calendar
P R
IM
Permite añadir un calendario a partir de la información de la clase. Su elemento raíz es
<calendar>. Sus atributos son:
• N
date_start (obligatorio): campo que marca la fecha de comienzo del evento.
• I Ó
date_stop: campo que marca la fecha de finalización. Si está incorporado el evento
VE •
date_delay: alternativo a date_stop. Duración del evento.
color
• all_day: booleano para indicar que el registro dura todo el día.
• mode: modo de visualización por defecto. Puede ser day, week o month.
A continuación mostramos un ejemplo:
<record model="ir.ui.view" id="manteni.workorder_calendar">
<field name="name">workorder.calendar</field>
<field name="model">manteni.workorder</field>
30
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
color="program_id" mode="month">
<field name="name"/>
</calendar>
</field>
</record>
F P
I A
N K
LI
N O
U M
AL
L E
I B
I M
P R
IM
Figura 60: Vista calendar para la clase "workorder"
N
I Ó
g) Vista search
R • S Se utilizan para definir criterios de búsqueda por defecto en las vistas de lista.
Cualquier usuario podría crear estas búsquedas por el interfaz web, pero esta vista
N K
LI
<record id="manteni.workorder_search_view" model="ir.ui.view">
<field name="model">manteni.workorder</field>
<field name="name"/>
U M
<field name="description" string="Name and description"
AL
E
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
L
<field name="employee_id"/>
P R
domain="[('employee_id', '=', uid)]"/>
IM
<group string="Group By">
N
<filter name="group_by_program" string="Program"
I Ó
context="{'group_by': 'program_id'}"/>
R
</group>S
V E </search>
</field>
</record>
32
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
5.5.2. Acciones
Las acciones definen el comportamiento del sistema en respuesta a una acción: login,
presionar un botón, seleccionar un valor…
Existen varios tipos:
• Abrir una ventana: ir.actions.act_window
• Abrir una web: ir.actions.act_url
• Ejecutar un método en el servidor: ir.actions.server
F P
• Ejecutar un informe: ir.actions.report.xml
I A
•
N K
Ejecutar una acción implementada enteramente en el cliente: ir.actions.client
En este manual nos ceñiremos exclusivamente a las acciones de ventana.
LI
N O
Acciones ir.actions.act_window
U M
AL
La más común, sirve para abrir una ventana de visualización de un modelo mediante sus
vistas. Se definirá el conjunto de vistas posibles a abrir.
L E
Una vista se ejecuta a partir de un evento creado por un usuario que está vinculado a una
• I M
name: nombre de la acción.
•
P R
res_model: clase de la que proviene la acción
•
IM
view_mode: lista separada por comas de las vistas disponibles, poniendo la primera
la que queremos que aparezca por defecto.
• N
Ó
view_type (optativo): indicar si la vista es jerárquica (tree) o no (form)
I
R S
A continuación presentamos un ejemplo para la clase “machine”.
<!-- actions opening views on models -->
VE
<record model="ir.actions.act_window" id="manteni.machine_action_window">
<field name="res_model">manteni.machine</field>
<field name="view_mode">tree,form</field>
</record>
33
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Para generar un menú para un módulo introduciremos código dentro del archivo
nombre_modulo.xml. Utilizaremos las etiquetas xml <menuitem>. Su formato será el
siguiente:
<menuitem
name="título_del_menú"
F P
id=”identificador”
I A
action="action_id"
N K
LI
web_icon="nombre_icono_web"
parent="menuitem_id"
groups="grupos_usuarios"/>
N O
Los valores específicos de cada menú son:
U M
AL
• Atributo name: es el título del menú, es opcional, si no lo ponemos el menú tomará
el nombre de la acción que ejecuta.
•
E
Atributo id: contiene el identificar XML del menú, debe ser único dentro del
módulo. L
• IB
Atributo action: identificador de la acción que ejecuta el menú. Si no especificamos
I M
la acción el sistema entiende que se trata de un menú raíz y por tanto que contiene
PR
otros menús i/u opciones. El diseño de la correspondiente acción depende del tipo
de ejecución que lleva asociada (abrir ventana, ejecutar informe, arrancar un
asistente,…). Se explicarán las acciones en el siguiente apartado.
• IM
Atributo web_icon: icono que muestra el cliente web en la página inicial.
• N
Atributo parent: identificador del menú padre al que pertenece. Si no lo
I Ó
especificamos el sistema entiende que se trata de un menú raíz (menú principal).
V E especificar más de un grupo hay que separarlos por comas (groups= “admin,user”).
34
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
F
recomendable. Existe el mecanismo de la herencia, que permite lo mismo pero de maneraP
más inocua para el sistema.
I A
N
nuevas, y en la vista, que nos permitirá adaptarla a nuestras necesidades.
K
Diferenciaremos herencia para el modelo, que nos permitirá ampliar las clases o diseñar
LI
N O
U M
AL
L E
IB
I M
P R
IM
N
I Ó
R S
V E
35
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Odoo permite tres tipos diferentes de herencia: herencia clásica o de clase, herencia de
prototipo y herencia por delegación.
F P
I A
N K
LI
N O
U M
AL
L E
I B
I M
P R
IM
N Figura 61: Herencia en el modelo de Odoo
I Ó
R S
5.6.1.1. Herencia clásica
VE Consiste en generar un nuevo modelo a partir de uno existente. El nuevo modelo contiene
todos los campos y métodos del anterior. Las vistas antiguas siguen siendo compatibles
con el nuevo modelo. No se crea ninguna tabla nueva, sino que se añaden las nuevas
columnas.
Para llevarlo a cabo utilizaremos el atributo _inherit, indicando la clase que estamos
heredando. La nueva clase deberá llamarse igual que la clase origen.
Para nuestro módulo “manteni”, se ha creado una clase que hereda la clase “res.partner”,
y le añade un campo para saber si esta empresa es vendedora de maquinaria. Esta
información se introduce en el archivo partner.py en la carpeta “models”.
36
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
class Partner(models.Model):
F P
_inherit = 'res.partner'
I A
N K
LI
machinery_seller=fields.Boolean(string="Machinery seller", default=False)
AL
y métodos que tenía el objeto del cual deriva, juntamente con los nuevos datos y métodos
que pueda incorporar el nuevo objeto. En este caso, siempre se crea una nueva tabla en la
E
base de datos para mapear el nuevo objeto.
L
Herencia por delegaciónIB
5.6.1.3.
I M
PR
La herencia multiple (_inherits), también denominada herencia por delegación, siempre
provoca la creación de una nueva tabla en la base de datos. El objeto derivado deberá
IM
incluir, para cada derivación, un campo many2one que apuntará al objeto del que deriva,
con la propiedad ondelete=’cascade’. Este tipo de herencia obliga a que cada recurso del
N
objeto derivado apunte a un recurso de cada uno de los objetos de los que deriva. Puede
I Ó
ocurrir que haya varios recursos del objeto derivado que apunten a un mismo recurso para
alguno de los objetos de los cuales deriva.
R S
VE
37
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Cuando hacemos herencia en una clase se pueden seguir usando las vistas del objeto padre.
Sin embargo también nos puede interesar disponer de una versión retocada. En tal caso, lo
mejor es heredar de las vistas existentes y modificar, añadir o eliminar campos y no
reemplazarlas totalmente.
El diseño de una vista heredada es idéntico al de la vista no heredada, sólo hay que añadir
el siguiente elemento:
F P
<field name="inherit_id" ref="id_xml_vista_padre"/>
IA
N
Si la vista id_xml_vista_padre reside en un módulo diferente al que estamos diseñando, K
LI
hemos de añadir delante el nombre del módulo:
<field name="inherit_id" ref="modulo.id_xml_vista_padre"/>
N O
Cuando el motor de herencia encuentra una vista heredada procesa el contenido del
U M
elemento arch. Para cada hijo de este elemento que tenga algún atributo, busca en la vista
padre una etiqueta con los atributos coincidentes, salvo el de la posición, y, acto seguido,
AL
combina los campos de la vista padre con los de las vista heredada. Establecerá la posición
de las nuevas etiquetas a partir de los siguientes valores:
•
•
E
inside (por defecto): los valores se añaden dentro de la etiqueta.
L
•
after: después de la etiqueta.
before: antes de la equiqueta. IB
• I M
P R
replace: reemplazando el contenido de la etiqueta
Por ejemplo, utilizamos el archivo partner.xml, para la vista de la clase heredada que
IM
hemos creado en el apartado anterior.
<?xml version="1.0" encoding="utf-8"?>
N
<odoo>
I Ó
<data>
R S
<record id="view_partner_seller" model="ir.ui.view">
V E<field name="name">partner.seller</field>
<field name="model">res.partner</field>
</field> -->
38
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
<group>
</group>
</page>
</page>
F P
</field>
IA
</record>
N K
LI
</data>
</odoo>
N O
U M
AL
L E
I B
I M
P R
IM
N
I Ó
R S
V E
39
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
5.7. Seguridad
N K
asociados usuarios. En la siguiente figura podemos ver los grupos de usuarios del usuario
demo, que se instaló durante la instalación de Odoo al activar la casilla datos de
demostración.
LI
N O
U M
AL
L E
I B
I M
P R
IM
N
I Ó
R S
V E Figura 62: Privilegios del usuario demo
40
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
F P
I A
N K
LI
N O
U M
AL
E
Figura 63: Grupos de usuario
L
I B
A continuación vamos a explicar cómo incorporamos esquemas de seguridad en los
I M
módulos que diseñemos, de forma que cuando se instale el módulo se instale también al
menos un grupo de privilegios y el administrador sólo tenga que asignar usuarios a este
grupo.
P R
IM
El esquema de seguridad básico de un módulo se define en dos archivos situados en la
carpeta security (no es obligatorio), estos archivos serán referenciados en el apartado
N
update_xml del archivo __manifest__.py. Veamos a continuación como se configuran estos
Ó
dos archivos:
I
R S
VE
41
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
5.7.1. security.xml
IA
<odoo>
N K
LI
<data noupdate="1">
<field name="name">nomGrup</field>
</record>
I M
P R
<record id="idMenu" model="ir.ui.menu">
IM
<!-- Crea/Modifica el menú "idMenu" asignándole grupos -->
</record>
N
Ó
<record id="idRegla" model="ir.rule">
I
R S
<!-- Definición de reglas de negocio y asignación a grupos y/o empresas -->
</record>
V E
</data>
</odoo>
El atributo noupdate del elemento data toma el valor 1 para indicar que en caso de
actualización del módulo no se instale el esquema de seguridad que incorpora el módulo,
de esta forma no se sobrescribirá el esquema configurado por el administrador de la
empresa. El valor 0 indica que se instala siempre, sobrescribiendo el esquema existente. Si
hay artes que no se deben sobrescribir y otras que sí, se separan con dos elementos data
diferentes, un con un noupdate=”1” y otro con un noupdate=”0”.
42
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Como puede verse en el esquema, podremos añadir grupos, usuarios, limitar el acceso a
menús (también podíamos hacerlo desde la vista con el atributo groups), o incluso
imponer reglas para diferentes usuarios.
Presentamos como ejemplo el archivo security.xml del módulo “manteni”.
F P
IA
N K
LI
N O
U M
AL
L E
IB
I M
P R
IM
N
I Ó
R S
VE
43
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
<odoo>
<data noupdate="0">
<field name="name">Maintenance</field>
F P
<field name="description">Helps you manage your factory's industrial maintenance</field>
IA
<field name="sequence">104</field>
N K
LI
</record>
</record>
U M
AL
E
<record id="group_manteni_manager" model="res.groups">
L
<field name="name">Maintenance Manager</field>
IB
<field name="category_id" ref="module_category_manteni"/>
</record> I M
P R
IM
<record id="view_your_own_workorders" model="ir.rule">
N
<field name="name">Only view your own workorders</field>
I Ó
<field name="model_id" ref="model_manteni_workorder"/>
S
<field name="groups" eval="[(4, ref('group_manteni_user'))]"/>
R
V E<field name="perm_read" eval="1"/>
<field name="domain_force">[('employee_user_id','=',user.id),('state','=','opened')]</field>
</record>
</data>
44
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
</odoo>
5.7.2. ir.model.access.csv
Es un archivo CSV, cuyo nombre no se puede cambiar. Contiene los privilegios de cada
grupo sobre los diferentes objetos del módulo. Si este archivo estuviese el XML podría
tener cualquier nombre, pero como la información que contiene proviene de una tabla es
más cómodo que sea del tipo CSV para exportarlo rápidamente desde cualquier hoja de
cálculo.
F P
El archivo ir.model.access.csv contiene toda la información de los privilegios asignados a
I
cada grupo de privilegios sobre la totalidad de los objetos del módulo. La primera línea A
contiene, obligatoriamente, el título de las columnas:
N K
LI
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
que:
N O
Las siguientes líneas contienen las diferentes asignaciones de privilegios. Hemos de saber
•
•
M
id es el identificador a decidir, debe ser único y no puede contener ningún punto.
U
AL
name es un nombre que describe el privilegio (puede contener puntos)
• model_id: id es el nombre del objeto del modelo sobre el que se aplica el privilegio
L E
y debe ir precedido, obligatoriamente, por el prefijo model_. Si se aplica sobre un
objeto definido en otro módulo, hay que usar el prefijo nombreMódulo.model_.
• IB
group_id: id es el identificador del grupo de privilegios (definido en el fichero XML
previo).
I M
•
R
perm_read, perm_write, perm_create i perm_unlink son valores 0/1 para indicar,
P
respectivamente, privilegios de lectura, escritura, creación y eliminación sobre el
IM
objeto.
Si en la definición de los grupos y privilegios se hace referencia a una categoría o grupo,
N
esta o este debe estar previamente definida. Por tanto, la inclusión del archivo
I Ó
ir.model.access.csv en el archivo __manifest__.py debe hacerse posteriormente a la
R S
inclusión del archivo security.xml.
V E
45
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
A veces hay que realizar una carga masiva de datos en los objetos de un módulo. Esto
puede darse por varios motivos:
• Migración de datos de arranque de Odoo al sustituir un sistema informático.
• Carga de datos de demostración.
F P
I A
Esta carga no se debe efectuar nunca actuando directamente sobre la base de datos, pues
N K
podría conducir al mal funcionamiento del sistema. Odoo proporciona mecanismos para la
carga de datos desde ficheros CSV (solo permite la carga de recursos, registros, de un
LI
mismo objeto, tabla) o XML (permite la introducción de datos que afectan a diversos
objetos, tablas).
N O
El nombre del archivo CSV debe coincidir con el nombre del objeto (tabla donde se
añadirán o actualizaran los recursos (registros). Si primera línea debe contener,
AL
Los archivos XML siguen una plantilla como la siguiente, en la que cada elemento record
contienen datos de un recurso y el modelo al que pertenece, pudiendo incorporar datos de
recursos de diferentes modelos.
L E
<?xml version="1.0" encoding="UTF-8"?>
IB
<odoo>
I M
<data noupdate="1">
P R
<record id="idRecurso" model="nombreObjeto">
IM
<field name="campo1">valor</field>
N
<field name="campo2">valor</field>
…I Ó
R
</record>S
V E <record id="idRecurso" model="nombreObjeto">
<field name="campo1">valor</field>
<field name="campo2">valor</field>
</record>
</data>
46
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
</odoo>
El atributo id de cada elemento record que ha de ser único dentro del módulo, es un
identificador XML para el recurso, al que se puede hacer referencia desde el propio
módulo vía su nombre o desde cualquier otro módulo vía la sintaxis
nombreMódulo.identificador.
Los archivos con datos de demostración (sean CSV o XML) se deben indicar en el archivo
__manifest__.py del módulo en el apartado init_xml.
A continuación se adjunta el archivo demo.xml del módulo “manteni”. Como puede verse,
se añaden usuarios de prueba (tienen información en la tabla res_users y en la
F P
hr_employees), gamas de mantenimiento, instrucciones, máquinas y órdenes de trabajo.
I A
<odoo>
N K
LI
<data>
<!--Users-->
N O
<record id="base.user_root" model="res.users">
U M
AL
<field name="groups_id" eval="[(4,ref('manteni.group_manteni_manager'))]" />
</record>
L E
<record id="base.user_paco" model="res.users">
I B
<field name="name">Paco</field>
I M
<field name="login">paco</field>
P R
<field name="password">paco</field>
IM
<field name="groups_id" eval="[(6, 0, [ref('group_manteni_user'), ref('base.group_user')])]" />
</record>
N
I Ó
R S
<!--Employees-->
<field name="work_location">Barcelona</field>
<field name="work_phone">+3281813700</field>
47
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
<field name="work_email">paco@odoo.com</field>
</record>
N K
LI
<field name="state">on_use</field>
<field name="date_begin">2017-05-01</field>
</record>
U M
AL
<!--Maitenance programs-->
L E
I
<record id="program0" model="manteni.program">
B
I M
<field name="name">Gama electrica tornos</field>
</record>
P R
IM
N
<record id="program1" model="manteni.program">
I Ó
<field name="name">Gama mecanica tornos</field>
R S
</record>
VE
<!--Maitenance instructions-->
</record>
<field name="name">Engrasado</field>
</record>
F P
I A
<record id="instruction0" model="manteni.program.instruction">
N K
LI
<field name="name">Comprobacion de continuidad</field>
U M
AL
<!-- Workorders -->
L E
<record id="workorder1" model="manteni.workorder">
P R
<field name="employee_id" ref="employee_paco"></field>
IM
<field name="employee_user_id" ref="base.user_paco" />
N
<field name="machine_ids" eval="[(6, 0, [ref('torno1'), ref('torno2'), ref('torno3'), ref('torno4'),
Ó
ref('torno5'), ref('torno6')])]"></field>
I
R S
<field name="program_id" ref="program0"></field>
</record>
</data>
</odoo>
49
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
F P
I A
N K
LI
N O
U M
AL
L E
IB
I M
P R
IM
N
I Ó
RS
VE
50
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Recursos y enlaces
Canal de Youtube de Marlon Hernández, experto en Odoo, con infinidad de tutoriales.
F P
I A
N K
Conceptos clave LI
N O
• M
El comando odoo-bin para crear el esqueleto de un módulo.
U
AL
• El archivo __manifest__.py. Incorpora toda la información que se necesita para
crear un módulo Odoo.
•
E
El modelo de Odoo. Conocer los diferentes tipos de campos que se pueden
L
incorporar, con sus correspondientes atributos.
• I B
Las vistas de Odoo. Permiten presentar la información de diferentes maneras, para
su explotación.
I M
• R
La herencia de Odoo. Permite modificar módulos existentes sin modificar su código
fuente. P
•
IM
La seguridad de Odoo. Permite que cada usuario sólo disponga de los permisos que
N
el administrador le quiera dar.
I Ó
R S
VE
51
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
Ponlo en práctica
Actividad 1
52
Desarrollo de Aplicaciones Multiplataforma. Sistemas de gestión empresarial
Tema 5. Desarrollo de módulos Odoo con Python
SOLUCIONARIOS
Ponlo en práctica
Actividad 1
U M
AL
L E
I B
I M
P R
IM
N
I Ó
R S
VE
53
F P
IA
N K
LI
N O
U M
AL
L E
I B
I M
P R
IM
N
I Ó
RS
VE