-
Notifications
You must be signed in to change notification settings - Fork 1
2.3. Creació de mòduls per a Odoo
Aquest exemple parteix del supòsit que es disposa d'una màquina virtual o contenidor amb una instància d'Odoo v16 instal·lada i operativa.
Per a facilitar les tasques de desenvolupament es recomana configurar una connexió remota que simplifiqui el procés, per aquest motiu el primer que es farà serà generar una nova entrada al fitxer de hosts /etc/hosts
:
Després es procedirà a generar un parell de claus pública/provada i instal·lar la pública a la màquina remota, de tal manera que es podran fer connexions SSH sense haver d'introduir la contrasenya (procés més aviat farragós i repetitiu quan es treballa amb Visual Studio Code):
ssh-keygen -t rsa
ssh-copy-id -i $HOME/.ssh/id_rsa.pub <username>@ims-devel
Primerament, caldrà instal·lar el plugin que permet connectar-se remotament a les màquines:
Un cop instal·lat, s'afegirà el host al llistat del Visual Studio Code:
- Prémer la combinació de tecles
Ctrl+Shift+P
. - Escriure
host
i escollir l'opcióAdd new SSH Host
:
- Escriure la comanda que permet una connexió via terminal:
ssh <username>@ims-devel
- Guardar la configuració per a l'usuari actual:
Finalment cal connectar el Visual Studio Code a la màquina remota:
- Prémer la combinació de tecles
Ctrl+Shift+P
. - Escriure
host
i escollir l'opcióConnect to Host
:
- Escollir
ims-devel
:
- Això obrirà una nova finestra del Visual Studio Code, cal acceptar els missatges que puguin sortir; la primera connexió pot trigar una mica.
- Un cop acaba el procés, cal obrir un directori amb el Visual Studio Code:
- Es recomana obrir
/home/<username>
donat que és el directori en el qual es tenen permisos i, per tant, on es configurarà l'Odoo per anar a buscar el mòdul que s'està desenvolupant:
- Finalment, després d'acceptar els missatges que hagin pogut sortir, cal obrir un terminal (remot):
A partir d'aquest punt, tot el desenvolupament es pot fer sense haver de sortir del Visual Studio Code, el qual pot resultar molt còmode.
Cal indicar-li a Odoo en quin directori es desenvoluparan les mòduls propis, això resulta útil per a evitar problemes amb els permisos i per a no barrejar els mòduls de producció amb els de desenvolupament. Cal:
- Crear la carpeta on es vol treballar amb
mkdir myModules
,/root/myModules
(si es fa servir l'usuari root) o/home/<username>/myModules
(si es fa servir un altre usuari). - Editar el fitxer de configuració d'Odoo amb
sudo nano /etc/odoo/odoo.conf
i afegir la ruta amyModules
:
[options]
db_host = False
db_port = False
db_user = odoo
db_password = False
default_productivity_apps = True
admin_passwd = $pbkdf2-sha512$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
addons_path = /usr/lib/python3/dist-packages/odoo/addons, <path>/myModules
- Un cop editat el fitxer de configuració d'Odoo, cal reiniciar el servei amb
sudo service odoo restart
.
Cal donar permisos a l'usuari odoo
perque pugui accedir a la carpeta de desenvolupament del mòdul.
Si es fa servir una màquina amb l'usuari root
(per exemple, l'entorn de desenvolupament sota IsardVDI que fa servir contenidors LXC/LXD), caldrà donar els següents permisos:
sudo adduser root odoo
sudo chown root:odoo /root
sudo chmod 750 /root
cd /root
sudo chown -R root:odoo myModules
sudo chmod g+s myModules
Si es fa servir una màquina amb un usuari que no sigui root (per exemple, l'entorn de desenvolupament sota IsardVDI que fa servir l'usuari usuario
), caldrà donar els següents permisos:
sudo adduser usuario odoo
cd /home
sudo chown usuario:odoo usuario
sudo chmod 750 usuario
cd usuario
sudo chown -R usuario:odoo myModules
sudo chmod g+s myModules
Odoo incorpora una eina que prepara l'esquelet d'un mòdul perquè sigui fàcil de crear-ne un des de zero, basta fer servir la comanda odoo scaffold <modul> <ruta>
on "modul" és el nom del mòdul que es vol crear i "ruta" el path on es vol crear el mòduo. Per exemple, es podria crear un mòdul de proves amb la comanda `odoo scaffold myTestModule $HOME/myModules/".
Un cop executada la comanda, es pot comprovar com ha creat una carpeta (normalitzant el nom) i a dins es troben tots els fitxers necessaris per a treballar:
A partir d'aquest punt, es pot fer servir la connexió remota de Visual Studio Code per obrir la carpeta d'aquest mòdul i facilitar-ne la modificació:
L'estructura completa d'un mòdul segueix el patró de "Model / Vista / Controlador" i, un cop generat l'esquelet, és la següent:
El document __manifest__.py
inclou informació sobre el mòdul entre d'altres:
# -*- coding: utf-8 -*-
{
'name': "myTestModule",
'summary': """
Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "https://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/views.xml',
'views/templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
}
Respecte del contingut, caldria destacar el següent:
* name: És el nom del mòdul, es mostrarà al llistat d'aplicacions d'Odoo a l'hora d'instal·lar-lo.
* summary: És un resum del que fa el mòdul, es mostrarà al llistat d'aplicacions d'Odoo a l'hora d'instal·lar-lo.
* description: És la versió detallada del resum anterior.
* author: Quí és l'autor del mòdul, es mostrarà al llistat d'aplicacions d'Odoo a l'hora d'instal·lar-lo.
* website: Quina és la pàgina oficial del mòdul, es mostrarà al llistat d'aplicacions d'Odoo a l'hora d'instal·lar-lo.
* category: En quina categoria encaixa el mòdul, es fa servir per a cercar mòduls.
* version: Quina és la versió actual del mòdul.
* depends: Permet establir dependències entre el mòdul actual i altres mòduls.
* data: Les dades que carregarà el mòdul en el moment de la instal·lació.
* demo: Les dades de demostració que es carregaràn i s'ha activat l'opció en generar la BBDD d'Odoo.
Els controladors permeten fer gestions d'una forma similar a la que fan els models, però cal tenir en compte que podrien activar-se en un moment en el qual encara no es disposa dels prerequisits necessaris (com per exemple, la connexió a la base de dades). Usualment, un controlador es fa servir per a definir les accions que cal executar en rebre una petició via HTTP, com per exemple servir una web completa, una porció d'aquesta (un bloc HTML que s'injecta dinàmicament via AJAX) o realitzar una acció sobre la base de dades.
Exemple del controlador comentat que genera l'esquelet, és a dir, no es fa servir cap controlador propi de forma predeterminada:
Font: Web Controllers
Es tracta d'un fitxer XML que serveix per a definir les dades de demostració que cal inserir a la base de dades quan s'instal·la el mòdul que s'està desenvolupant. Aquestes dades només s'insereixen si, durant la instal·lació de la base de dades d'Odoo, s'ha activat l'opció de "dades de demostració":
Com es pot observar, el fitxer de demostració que ha generat l'esquelet té comentat el codi que permet fer les insercions:
Per cada registre de tipus "record" cal dir:
- id: quin és el ID de l'objecte que s'ha de crear.
- model: quin és el model de dades que es farà servir, en format <nom_del_modul>.<nom_del_model>
- tants camps "field" com atributs tingui el registre.
- name="name": el nom de l'atribut del registre com a valor del node.
- name="value": el valor de l'atribut del registre com a valor del node.
Font: Data Files
De forma predeterminada, l'esquelet es genera amb un únic fitxer de models:
Tanmateix, es recomana que cada model de dades ocupi un fitxer diferent donat que la lògica i complexitat inherents a un model en concret pot esdevenir en un codi prou extens que dificulti la comprensió si es fa servir un únic fitxer per a diversos models.
Per exemple, per a generar un model de dades anomenat "UF" i un altre anomenat "MP" caldria crear dos fitxers uf.py
i mp.py
(es poden generar fent una còpia del fitxer models.py
). Igualment, caldrà establir un nom per la classe (que ha de ser únic) així com el contingut del model en si:
mp.py:
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class my_test_module_mp(models.Model):
_name = 'my_test_module.mp'
_description = 'Model de dades per a un Mòdul Professional'
code = fields.Char('Codi')
name = fields.Char('Nom')
teacher = fields.Char('Professor')
startDate = fields.Date('Data inici')
started = fields.Boolean('Començat')
students = fields.Integer('Estudiants')
image = fields.Binary('Image')
notes = fields.Text('Anotacions')
ufs = fields.One2many(comodel_name="my_test_module.uf", inverse_name="mp", string="Unitats Formatives")
uf.py:
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class my_test_module_uf(models.Model):
_name = 'gestio_academica.uf'
_description = 'Model de dades per a una Unitat Formativa'
name = fields.Char('Nom')
code = fields.Char('Codi')
teacher = fields.Char('Professor')
startDate = fields.Date('Data inici')
endDate = fields.Date('Data final')
mp = fields.Many2one(comodel_name="my_test_module.mp", string="Modul Professional")
Respecte de les propietats:
- name: és el nom que tindrà el model, en forma de <nom_del_modul>.<nom_del_model> i, per tant, Odoo crearà una taula nova dins la BBDD amb el nom de <nom_del_modul><nom_del_model>.
- _description: és la descripció del model de dades, útil per als desenvolupadors que treballen amb el backoffice d'Odoo.
La resta de propietats del model seran les dades que es volen guardar. Més informació aquí.
Cal tenir en compte, però, que els nous fitxers de models de dades no seran visibles per Odoo a menys que s'incloguin dins del fitxer d'importació de models: init.py:
# -*- coding: utf-8 -*-
from . import mp
from . import uf
Font: ORM API
Font: Model fields
Els models de dades es poden relacionar uns amb altres, en un tipus de relació anomenat "1 a N" o "OneToMany". Per a Odoo, cal que els dos models de dades es relacionen entre si de forma bidireccional per a funcionar correctament, i un bon exemple són els modles uf
i mp
que s'han mostrat anteriorment:
mp.py
Aquest model inclou una propietat anomenada "ufs", com que un MP pot tenir moltes UFs, cal fer la relació cap al les seves UFs en forma de One2Many
:
ufs = fields.One2many(comodel_name="my_test_module.uf", inverse_name="mp", string="Unitats Formatives")
uf.py
Aquest model inclou una propietat anomenada "mp", com que una UF pertany només a un MP, cal fer la relació cap a la UF en forma de ManyToOne
:
mp = fields.Many2one(comodel_name="my_test_module.mp", string="Modul Professional")
Font: Relational Fields
Similar a l'exemple anterior, es poden relacionar models entre si de la forma "N-N" o "Many2Many"; per exemple: un alumne té molts professors i un professor té molts alumnes.
No es disposa d'un exemple funcional d'una relació "Many2Many" però el funcionament hauria de ser similar al de les relaciones "Many2One".
Font: Relational Fields
En ocasions resulta interessant calcular de forma automàtica els camps, però el mecanisme funciona diferent si es vol fer visible per l'usuari o no. Cal tenir sempre present que, quan s'obre un formulari per a crear un nou model, aquestes dades no es troben a la base de dades i, per tant, el que es pot fer de cara a l'usuari queda limitat (o requereix de dades addicionals).
Es poden establir valors predeterminats que no requereixen de càlculs, fent servir el camp apropiat quan es defineix l'atribut dins el model. Per exemple, amb level = fields.Integer(string="Level", default=1)
el valor per a level
que veurà l'usuari en obrir un formulari nou serà 1
.
També es poden realitzar càlculs per a mostrar valors, però cal tenir en compte que un formulari nou no disposa de dades guardades a la BBDD tot i que podria fer-ne servir d'altres que si que es trobin prèviament emmagatzemades. A continuació un exemple: _display_warning = fields.Boolean(default=lambda self: self._default_display_warning(), store=False)
. El mètode _default_display_warning
retorna True
o False
en funció dels valors d'altres dades que es troben guardades a la BBDD.
També es pot passar un valor predeterminat d'un formulari cap a un altre, com quan es vol crear una assignatura que ha de ser filla d'una altra, fent servir l'atribut context
del field
. Per exemple <field name="subject_ids" widget="one2many" context="{'default_subject_id': id, 'default_study_ids': study_ids, 'default_code': code}">
, tots quests valors s'estableixen de forma automàtica al formulari i l'usuari els veu establerts (i els pot canviar si vol).
Si es vol que l'usuari vegi canvis instantanis mentre treballa, es pot fer servir una capçalera onchange
a un mètode, que l'invocarà en funció de l'atribut modificat. Per exemple:
@api.onchange("subject_id")
def _onchange_subject_id(self):
for rec in self:
rec.study_ids = rec.subject_id.study_ids
Cada cop que l'usuari modifica el valor del desplegable assignat a subject_id
, veu com es carreguen automàticament els valors de study_ids
.
Important: aquests canvis només apliquen quan l'usuari modifica el formulari, no s'activa quan es carreguen dades de demo o via CSV.
Aquests càlculs es realitzen cada cop que hi ha un canvi a la base de dades i, per tant, si que s'activen en carregar dades de demo o un document CSV, així com quan es guarden les dades d'un formulari, però és transparent a l'usuari.
Per una banda, cal indicar que un camp es calcula mitjançant el paràmetre _compute
de l'atribut, per exemple: level = fields.Integer(string="Level", default=1, compute="_compute_level", store=True)
. A continuació cal definir el mètode _compute_level
:
@api.depends("subject_id")
def _compute_level(self):
for rec in self:
if rec.subject_id.id != False: rec.level = rec.subject_id.level + 1
Cada cop que l'atribut subject_id
es vegi alterat a la BBDD, s'invocarà aquest mètode i actualitzarà el valor de l'atribut level
(tal com defineix el mètode que ha de fer). En aquest cas es recomana que el camp level
sigui ocult, perquè l'usuari no veurà el canvi fins que no es guardin.
Dins el fitxer csv
que hi ha dins la carpeta security
es poden definir els permisos predeterminats que tindrà el mòdul:
Cal tenir en compte que els permisos predeterminats són tan estrictes que només l'usuari "root" d'Odoo serà capaç de veure'l i fer-lo servir. El següent exemple mostra un permís que fa visible el mòdul als usuaris un cop instal·lat sense necessitat de ser "root":
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_my_test_module_my_test_module,my_test_module.my_test_module,model_my_test_module_my_test_module,,1,0,0,0
L'esquelet que s'ha generat crea totes les vistes necessàries dins un únic document anomenat views.xml
:
Aquesta forma de treballar fa que la complexitat dels fitxers augmenti i gestionar-los es converteixi en quelcom tediós; per aquest motiu, es recomana generar fitxers separats per a cada vista que es vulgui crear i afegir aquestes vistes dins el fitxer __manifest__.py
dins la secció data
. Per exemple:
'data': [
'security/ir.model.access.csv',
'views/menu.xml',
'views/uf_list.xml',
'views/uf_form.xml',
'views/mp_list.xml',
'views/mp_form.xml',
'reports/mp.xml',
],
Perquè es pugui fer servir un mòdul cal generar un menú que inclogui les seves opcions disponibles. El següent seria un exemple:
menu.xml
<odoo>
<data>
<act_window id="action_my_test_module_uf_tree" name="Gestio Academica" res_model="my_test_module.uf" view_mode="tree,form" />
<act_window id="action_my_test_module_mp_tree" name="Gestio Academica" res_model="my_test_module.mp" view_mode="tree,form" />
<menuitem name="Gestio Academica" id="my_test_module.menu_root"/>
<menuitem name="Gestio de MPs" id="my_test_module.mps" parent="my_test_module.menu_root" action="action_my_test_module_mp_tree" />
<menuitem name="Gestio de UFs" id="my_test_module.ufs" parent="my_test_module.menu_root" action="action_my_test_module_uf_tree" />
</data>
</odoo>
Per una banda es disposen de les act_window
i per l'altra dels menuitem
:
- menuitem: Una opció del menú ha d'incloure altres opcions o bé apuntar cap a una acció, que serà el que es farà en clicar.
- act_window: Les dues accions que s'han posat d'exemple fan el mateix per a mòduls diferents, és a dir, mostres el llistat de registres (ufs o mps) que s'han creat (per la opció tree, la opció form no arriba a entrar en joc).
Per les llistes, cal definir quin model de dades cal fer servir i quines columnes (propietats del model) es volen mostrar. Exemples: mp_list.xml
<odoo>
<data>
<record model="ir.ui.view" id="my_test_module.mp_list">
<field name="name">my_test_module mp list</field>
<field name="model">my_test_module.mp</field>
<field name="arch" type="xml">
<tree>
<field name="code"/>
<field name="name"/>
<field name="teacher"/>
<field name="startDate"/>
</tree>
</field>
</record>
</data>
</odoo>
uf_list.xml
<odoo>
<data>
<record model="ir.ui.view" id="my_test_module.uf_list">
<field name="name">my_test_module uf list</field>
<field name="model">my_test_module.uf</field>
<field name="arch" type="xml">
<tree>
<field name="code"/>
<field name="name"/>
</tree>
</field>
</record>
</data>
</odoo>
Pels formularis, cal definir quin model de dades cal fer servir i quines propietats del model es volen mostrar i com (es pot fer servir HTML CSS, etc). Exemple: mp_form.xml
<odoo>
<data>
<record id="view_my_test_module_mp_form" model="ir.ui.view">
<field name="name">MP form</field>
<field name="model">my_test_module.mp</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="MP">
<style>
.preview {
float: left;
}
.preview img {
width: 100px;
}
h1 {
float: left;
width: 84%;
}
</style>
<sheet>
<field name="image" widget="image" class="preview" />
<h1>
<field name="name"/>
</h1>
<group col="2">
<group string="Dades acadèmiques">
<field name="code"/>
<field name="teacher"/>
</group>
<group string="Dates i alumnat">
<field name="startDate"/>
<field name="started"/>
<field name="students"/>
</group>
</group>
<notebook>
<page string="Unitats Formatives">
<field name="ufs" widget="one2many_list" context="{'show_attribute': False}">
<tree>
<field name="code"/>
<field name="name"/>
</tree>
</field>
</page>
<page string="Anotacions">
<field name="notes" placeholder="Espai reservat per a anotacions..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</data>
</odoo>
L'exemple amb el formulari del model de dades UF seria molt similar a l'anterior.
Tot i que l'esquelet no ho fa de forma automàtica, es recomana crear un nou directori reports
per a emmagatzemar a dins els informes. A continuació es mostra un informe que, donat un MP, mostra la seva informació junt amb la de les seves UFs.
mp_report.xml
<odoo>
<data>
<report
id="report_mp"
model="my_test_module.mp"
string="Report de MP"
name="my_test_module.report_mp_view"
file="my_test_module.report_mp"
menu="True"
report_type="qweb-pdf"
/>
<template id="report_mp_view">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="report.external_layout">
<div class="page">
<span t-field="doc.image" style="float:right" t-field-options="{"widget": "image", "class": "img-rounded"}"/>
<h1 t-field="doc.code"/>
<h2 t-field="doc.name"/>
<p>Professor: <span t-field="doc.teacher"/></p>
<p>Data d'inici: <span t-field="doc.startDate"/></p>
<p>Començat: <span t-field="doc.started"/></p>
<p>Alumnes matriculats: <span t-field="doc.students"/></p>
<br/>
<h2>Unitats Formatives</h2>
<t t-foreach="doc.ufs" t-as="uf">
<h3 t-field="uf.code"/>
<h4 t-field="uf.name"/>
<p>Professor: <span t-field="uf.teacher"/></p>
<p>Data d'inici: <span t-field="uf.startDate"/></p>
<p>Data de final: <span t-field="uf.endDate"/></p>
</t>
</div>
</t>
</t>
</t>
</template>
</data>
</odoo>
Les plantilles permeten generar blocs de vistes (formulatris, informes, llistats, etc) que poden invocar-se des les mateixes vistes per tal de reaprofitar el codi.
Font: QWeb Templates