Diferencia entre revisiones de «Odoo»

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda
Etiqueta: Reversión manual
 
(No se muestran 180 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
<div style="float:left; margin: 0 20px 10px 0; font-size:0.8em;">__TOC__</div>
<div style="float:left; margin: 0 20px 10px 0; font-size:0.8em;">__TOC__</div>
Articles relacionats:
[[Instal·lar Odoo]], [[Accions i menús en Odoo]], [[Millores en la vista en Odoo]], [[Odoo reports]], [[Wizards en Odoo]], [[El client Web Odoo]], [[Web Controller en Odoo]]


== El servidor Odoo ==
== El servidor Odoo ==


{{nota|Aquesta secció està molt més detallada en l'articul [[Instal·lar Odoo]]}}
{{nota|Aquesta secció està detallada en l'articul [[Instal·lar Odoo]]}}
 
Si el servidor odoo no està en martxa i no volem res especial, el podem arrancar simplement amb el comandament:
<pre class="code">
$ odoo
</pre>
Pot ser que estiga configurat el servici en l'arranc del sistema operatiu. No obstant, si volem depurar, cal parar el servici i [[Instal·lar_Odoo#Depurar_Odoo|arrancar de manera manual]].
 
En el cas d'un Odoo instal·lat en Debian amb els paquets, si fer un ps -aux, vorem que el comandament que té Odoo és:
<pre class="code">
$ /usr/bin/python3 /usr/bin/odoo --config /etc/odoo/odoo.conf --logfile /var/log/odoo/odoo-server.log
</pre>
Per reiniciar-lo de manera manual:
<pre class="code">
$ sudo systemctl restart odoo
o
$ sudo service odoo restart
</pre>
El servici Odoo proporciona accés als clients via '''RPC''', Odoo proporciona per defecte un client web.


== Arquitectura ==
== Arquitectura ==
Línea 32: Línea 17:
*    Una arquitectura '''MVC''' ([http://es.wikipedia.org/wiki/Modelo_Vista_Controlador model-vista-controlador]) en la qual el model resideix en les dades de les classes dissenyades amb Python, la vista resideix en els formularis, llistes, calendaris, gràfics… definits en fitxers XML i el controlador resideix en els mètodes definits en les classes que proporcionen la lògica de negoci.
*    Una arquitectura '''MVC''' ([http://es.wikipedia.org/wiki/Modelo_Vista_Controlador model-vista-controlador]) en la qual el model resideix en les dades de les classes dissenyades amb Python, la vista resideix en els formularis, llistes, calendaris, gràfics… definits en fitxers XML i el controlador resideix en els mètodes definits en les classes que proporcionen la lògica de negoci.
*    Odoo és un ERP amb una arquitectura [http://es.wikipedia.org/wiki/Tenencia_M%C3%BAltiple de Tenencia Múltiple]. És A dir, té una base de dades i un servidor comú per a tots els clients. El contrari sería tindre un servidor o base de dades per client o virtualitzar.
*    Odoo és un ERP amb una arquitectura [http://es.wikipedia.org/wiki/Tenencia_M%C3%BAltiple de Tenencia Múltiple]. És A dir, té una base de dades i un servidor comú per a tots els clients. El contrari sería tindre un servidor o base de dades per client o virtualitzar.
*    Un sistema de fluxos de treball o workflows.
*    Dissenyadors d’informes.
*    Dissenyadors d’informes.
*    Facilitats de traducció de l’aplicació a diversos idiomes.
*    Facilitats de traducció de l’aplicació a diversos idiomes.
Línea 81: Línea 65:
* '''Fitxers de dades''': Són fitxers XML que poden definir dades, vistes o configuracions.
* '''Fitxers de dades''': Són fitxers XML que poden definir dades, vistes o configuracions.
* '''Controladors web''': Gestionen les peticions dels navegadors web.
* '''Controladors web''': Gestionen les peticions dels navegadors web.
* '''Dades estàtiques''': Imatges, CSS, o javascript utilitzats per l'interficie web.
* '''Dades estàtiques''': Imatges, CSS, o javascript utilitzats per l'interficie web. És necessari que les dades estátiques es guarden en el directori '''static'''. Per exemple, l'icona del mòdul va en static/description/icon.png


==== Estructura de fitxers d'un mòdul ====
==== Estructura de fitxers d'un mòdul ====
Línea 115: Línea 99:


Odoo considera que un model és la referència a una o més taules en la base de dades. Un model no és una fila en la taula, és tota la taula.
Odoo considera que un model és la referència a una o més taules en la base de dades. Un model no és una fila en la taula, és tota la taula.
{{nota| En programació, el '''Model''' és una manera de relacionar el programa amb la base de dades. És de més alt nivell que les consultes directes en quant a base de dades i que les '''clases i objectes''' respecte a la programació orientada a objectes. El model junta en un únic concepte les '''estructures de dades''', les '''restriccions d'integritat''' i les opcions de '''manipulació''' de les dades. }}


Els models en Odoo poden heretar de '''models.Model''' i ser els normals mapejats i permanents en la base de dades. També poden ser '''models.TransientModel''' i són iguals, sols que no tenen permanència definitiva en la base de dades. Aquest són els recomanables per a fer [[#Wizards|wizards]]. També poden ser '''models.AbstractModel''' per a definir models abstractes per a després heretar.  
Els models en Odoo poden heretar de '''models.Model''' i ser els normals mapejats i permanents en la base de dades. També poden ser '''models.TransientModel''' i són iguals, sols que no tenen permanència definitiva en la base de dades. Aquest són els recomanables per a fer [[#Wizards|wizards]]. També poden ser '''models.AbstractModel''' per a definir models abstractes per a després heretar.  
Línea 210: Línea 196:
* Boolean
* Boolean
* Html : Guarda un text, però es representa de manera especial en el client.
* Html : Guarda un text, però es representa de manera especial en el client.
* Binary : Per guardar, per exemple, imatges. Utilitza codificació base64
* Binary : Per guardar, per exemple, imatges. Utilitza codificació base64 al enviar els fitxers al client. En realitat les guarda en '''/var/lib/odoo/.local/share/Odoo/filestore''' i la ruta als fitxers la diu la taula '''ir_attachment''' junt amb el id, nom del field i el model.   
* Image (Odoo13) : En el cas d'imatges, accepta els atributs '''max_width''' i '''max_height''' on es pot dir en píxel que ha de redimensionar la imatge a eixa mida màxima.
* Selection : Mostra un select amb les opcions indicades.
* Selection : Mostra un select amb les opcions indicades.
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
Línea 220: Línea 207:
Les relacions entre els models (en definitiva, entre les taules de la base de dades) també les simplifica l'ORM. D'aquesta maneram les relacions 1 a molts es fan en el Odoo anomena Many2one i les relacions Mols a Molts es fan el el Many2Many. Les relacions molts a molts, en una base de dades relacional, impliquen una tercera taula en mitg, però en Odoo no tenim que preocupar-nos d'aquestes coses si no volem, el mapat dels objectes el detectarà i farà les taules, claus i restriccions d'integritat necessaries. Anem a repasar un a un aquests camps:   
Les relacions entre els models (en definitiva, entre les taules de la base de dades) també les simplifica l'ORM. D'aquesta maneram les relacions 1 a molts es fan en el Odoo anomena Many2one i les relacions Mols a Molts es fan el el Many2Many. Les relacions molts a molts, en una base de dades relacional, impliquen una tercera taula en mitg, però en Odoo no tenim que preocupar-nos d'aquestes coses si no volem, el mapat dels objectes el detectarà i farà les taules, claus i restriccions d'integritat necessaries. Anem a repasar un a un aquests camps:   


* '''Reference''' : Una referència arbitrària a un model i un camp. [http://www.zbeanztech.com/blog/reference-fields-odoo]
====Reference====
Una referència arbitrària a un model i un camp. [http://www.zbeanztech.com/blog/reference-fields-odoo]
<syntaxhighlight lang="python" style="font-family:monospace">  
<syntaxhighlight lang="python" style="font-family:monospace">  
  aref = fields.Reference([('model_name', 'String')])
  aref = fields.Reference([('model_name', 'String')])
Línea 238: Línea 226:
Els fields reference no són molt utilitzats, ja que normalment les relacions entre models són sempre les mateixes.  
Els fields reference no són molt utilitzats, ja que normalment les relacions entre models són sempre les mateixes.  


* '''Many2one''' : Relació amb un altre model  
====Many2one====
Relació amb un altre model  
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
  arel_id = fields.Many2one('res.users')
  arel_id = fields.Many2one('res.users')
Línea 267: Línea 256:
Un altre argument addicional és '''ondelete''' que permet definir el comportament al esborrar l'element referenciat a '''set null''', '''restrict''' o '''cascade'''.
Un altre argument addicional és '''ondelete''' que permet definir el comportament al esborrar l'element referenciat a '''set null''', '''restrict''' o '''cascade'''.
{{nota| ondelete cascade esborra els fills a nivel de PostgreSQL, però no elimina en External Id, això es fa en unlink(), però no executa unlink() dels fills. Per tant, si volem que s'eliminen per complet, cal heretar el unlink del pare i afegir la cridada al dels fills. Mirar l'[[#unlink.28.29|exemple]] }}
{{nota| ondelete cascade esborra els fills a nivel de PostgreSQL, però no elimina en External Id, això es fa en unlink(), però no executa unlink() dels fills. Per tant, si volem que s'eliminen per complet, cal heretar el unlink del pare i afegir la cridada al dels fills. Mirar l'[[#unlink.28.29|exemple]] }}
* '''One2many''' : Inversa del Many2one. Necessita de la existència d'un Many2one en l'altre:
====One2many====
Inversa del Many2one. Necessita de la existència d'un Many2one en l'altre:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
  arel_ids = fields.One2many('res.users', 'arel_id')
  arel_ids = fields.One2many('res.users', 'arel_id')
Línea 278: Línea 268:
class pais(models.Model):
class pais(models.Model):
     _name = 'mon.pais'
     _name = 'mon.pais'
     ciutats = fields.One2many('mon.ciutat', 'pais', string='Ciutats', ondelete='restrict')
     ciutats = fields.One2many('mon.ciutat', 'pais', string='Ciutats')
</syntaxhighlight>
</syntaxhighlight>


{{nota|És important entendre que el One2many no implica dades addicionals en la base de dades i sempre és calculat com un ''select'' en la base de dades on el id del model actual coincidisca amb el Many2one (clau aliena) de l'altre model. Això fa que no tinga sentit fer One2many computed o ficar un domain.}}
{{nota|És important entendre que el One2many no implica dades addicionals en la base de dades i sempre és calculat com un ''select'' en la base de dades on el id del model actual coincidisca amb el Many2one (clau aliena) de l'altre model. Això fa que no tinga sentit fer One2many computed o ficar un domain per restringit els que es poden afegir.}}
 
{{nota|Els One2many poden tindre domain per no mostrar els que no compleixen una condició, això no significa que no existeixi aquesta relació.}}


* '''Many2many''' : Relació molts a molts.  
====Many2many====
Relació molts a molts.  
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
  arel_ids = fields.Many2many('res.users')
  arel_ids = fields.Many2many('res.users')
Línea 292: Línea 285:
</syntaxhighlight>
</syntaxhighlight>
El primer exemple sol funcionar directament, però si volem tindre més d'una relació Many2many entre els dos mateixos models, cal utilitzar la sintaxi completa on especifiquem el nom de la relació i el nom de les columnes que identifiquem els dos models. Pensem que una relació Many2many implica una taula en mig i estem especificant les seues claus alienes.  
El primer exemple sol funcionar directament, però si volem tindre més d'una relació Many2many entre els dos mateixos models, cal utilitzar la sintaxi completa on especifiquem el nom de la relació i el nom de les columnes que identifiquem els dos models. Pensem que una relació Many2many implica una taula en mig i estem especificant les seues claus alienes.  
{{nota|També és precís especificar la taula en mig si es fa una relació Many2many al propi model.}}


{{nota|Un Many2many implica una taula en mig. Si volem afegir atributs a aquesta relació, cal crear explícitament el model del mig.  
{{nota|Un Many2many implica una taula en mig. Si volem afegir atributs a aquesta relació, cal crear explícitament el model del mig.  
Línea 297: Línea 292:
El many2many pot ser ''computed'' i en el còmput es pot ordenar o filtrar. Un Many2many computed no crea la taula en mig.}}  
El many2many pot ser ''computed'' i en el còmput es pot ordenar o filtrar. Un Many2many computed no crea la taula en mig.}}  


* '''Related''' : Un camp d'un altre model, necessita una relació Many2one. Encara que estiga Store=True, Odoo 8.0 l'actualitza correctament. D'aquesta manera es poden aprofitar les funcionalitats de guardar, com ara les búsquedes o referències en funcions. En termes de bases de dades, un camp related trenca la tercera forma normal. Això sol ser problemàtic, però Odoo té mecanismes per a que no passe res. De totes maneres, si ens preocupa això, amb store=False no guarda res en la taula.
==== Related ====
Un camp d'un altre model, necessita una relació Many2one. Encara que estiga Store=True, Odoo 8.0 l'actualitza correctament. D'aquesta manera es poden aprofitar les funcionalitats de guardar, com ara les búsquedes o referències en funcions. En termes de bases de dades, un camp related trenca la tercera forma normal. Això sol ser problemàtic, però Odoo té mecanismes per a que no passe res. De totes maneres, si ens preocupa això, amb store=False no guarda res en la taula.
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
participant_nick = fields.Char(string='Nick name',
participant_nick = fields.Char(string='Nick name',
Línea 309: Línea 305:
{{nota|Un camp related pot ser perillós, ja que si es modifica, pot modificar l'original. Per tant, quasi sempre és necessari afegir '''readonly''', com en l'exemple anterior.}}
{{nota|Un camp related pot ser perillós, ja que si es modifica, pot modificar l'original. Per tant, quasi sempre és necessari afegir '''readonly''', com en l'exemple anterior.}}


* '''One2one''':
==== Many2oneReference ====
 
Un Many2one on es guardar també el model al qual fa referència amb el atribut: '''model_field'''.
 
==== One2one ====


Els camps '''One2one''' no existeixen en Odoo. Però si volem aquesta funcionalitat podem utilitzar varies tècniques:
Els camps '''One2one''' no existeixen en Odoo. Però si volem aquesta funcionalitat podem utilitzar varies tècniques:
Línea 350: Línea 350:
* Fer una herència múltiple. [http://blog.odoobiz.com/2014/10/openerp-one2one-relational-field-example.html]. Problemes:
* Fer una herència múltiple. [http://blog.odoobiz.com/2014/10/openerp-one2one-relational-field-example.html]. Problemes:
** Esta és, en teoría, la forma més oficial de fer-ho, però obliga a crear sempre la relació i els models en un ordre determinat.
** Esta és, en teoría, la forma més oficial de fer-ho, però obliga a crear sempre la relació i els models en un ordre determinat.
==== Filtres (Domains) ====
En ocasions és necessari afegir un filtre en el codi python per fer que un camp '''relacional''' no puga tindre certes referències. El comportament del domain és diferent depen del tipus de field.
* '''Domain en Many2one''': Filtra els elements del model referenciat que poden ser elegits per al field:
<syntaxhighlight lang="python" style="font-family:monospace">
parent = fields.Many2one('game.resource', domain="[('template', '=', True)]")
</syntaxhighlight >
* '''Domain en Many2many''': La llista d'elements a triar es filtra segons el domain:
<syntaxhighlight lang="python" style="font-family:monospace">
characters_attack = fields.Many2many('game.character',
                                      relation='characters_attack',
                                      domain="[('id', 'in', characters_attack_available)]")
</syntaxhighlight >
* '''Domain en One2many''': Al ser una relació que depen d'altre Many2one, no es pot filtrar, si fiquem un domain, sols deixarà de mostrar els que no compleixen el domain, però no deien d'existir:
<syntaxhighlight lang="python" style="font-family:monospace">
raws = fields.One2many('game.raws','clan', domain= lambda s: [('quantity','>',0)])
</syntaxhighlight >
Observem com hem fet un '''domain amb lambda''', és a dir, aquest domain crida a una funció lambda al ser aplicat.
[[Millores_en_la_vista_en_Odoo#Operadors_per_als_domains:]]


=== Fields Computed ===
=== Fields Computed ===
Línea 508: Línea 531:
El valor per defecte no pot dependre d'un field que està creant-se en eixe moment. En eixe cas es pot utilitzar un '''on_change'''.
El valor per defecte no pot dependre d'un field que està creant-se en eixe moment. En eixe cas es pot utilitzar un '''on_change'''.


{{nota|Veure també [[Odoo#Millores_en_les_vistes_form | La part de valors per defecte en un One2Many]] }}
{{nota|Veure també [[Millores_en_la_vista_en_Odoo#Millores_en_les_vistes_form | La part de valors per defecte en un One2Many]] }}
 
En cas de tindre molts valors per defecte o que depenen del context, es pot utilitzar la funció '''default_get''' que ja tenen els models.
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.model
    def default_get(self, default_fields):
        result = super(SelectSalePrice, self).default_get(default_fields)
        if self._context.get('default_picking_id') is not None:
            result['picking_id'] = self._context.get('default_picking_id')
        return result
</syntaxhighlight >
El que fa aquesta funció és un poc avançat de moment, ja que fa ús del [[Odoo#Context | context]] i l'herencia per afegir un valor per defecte al diccionari que retorna aquesta funció en la classe ''Model''


=== Restriccions (constrains) ===
=== Restriccions (constrains) ===
Línea 514: Línea 548:


<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
from openerp.exceptions import ValidationError
from odoo.exceptions import ValidationError


@api.constrains('age')
@api.constrains('age')
Línea 575: Línea 609:
<field name="product_id" eval="ref('product.product1')"/> # Equivalent a l'exemple anterior
<field name="product_id" eval="ref('product.product1')"/> # Equivalent a l'exemple anterior
<field name="price" eval="ref('product.product1').price"/>
<field name="price" eval="ref('product.product1').price"/>
<field name="avatar" model="school.template" eval="obj().env.ref('school.template_student1').image" ></field>  # Com que utilitza obj() necessita model="...
</syntaxhighlight>
</syntaxhighlight>


Línea 599: Línea 634:
{{nota|Si volem que sempre s'actualitzen les dades de demo (per exemple la data) podem esborrar i tornar a crear en el mateix fitxer de demo.}}
{{nota|Si volem que sempre s'actualitzen les dades de demo (per exemple la data) podem esborrar i tornar a crear en el mateix fitxer de demo.}}


=== Generar dades de demo ===
{{nota|Si falla l'actualització amb dades de demo, és possible que Odoo 12 deshabilite la possibilitat de tornar-les a instal·lar. Això és el field demo de ir.module.module que és readonly, per tant, cal modificar-lo a ma en la base de dades:}}
En postgresql:
update ir_module_module set demo = 't' where name='school';


Es poden crear fitxers de dades de demo amb qualsevol llenguatge de programació si necessitem moltes dades aleatòries. Per exemple:
== Accions i menús ==
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="xml" style="font-family:monospace">
#!/bin/bash


echo '<odoo><data>'
Si vols conèixer en més detall cóm funcionen les accions en Odoo, llig l'article [[Accions i menús en Odoo]].


for i in Esther,teacher Jose,teacher Carlos,teacher Maite,speech
El client web de Odoo conté uns menús dalt i a l'esquerra. Aquests menús, al ser accionats mostren altres menús i les pantalles del programa. Quant pulsem en un menú, canvia la pantalla perquè hem fet una [https://www.odoo.com/documentation/12.0/reference/actions.html acció].
do
nom=$(echo $i | cut -d "," -f1)
Una acció bàsicament té:
type=$(echo $i | cut -d "," -f2)
* '''type''': El tipus d'acció que és i cóm l'acció és interpretada. Quan la definim en el XML, el type no cal especificar-lo, ja que ho indica el model en que es guarda.
echo -n '<record id="escoleta.partner_'$nom'" model="res.partner">'
* '''name''': El nom, que pot ser mostrat en la pantalla o no. Es recomana que siga llegible per els humans.
echo -n '<field name="name">'$nom'</field><field name="escoleta">1</field>'
echo '<field name="escoleta_type">'$type'</field></record>'
done


for i in {1..10}
Les accions i els menús es declaren en fitxers de dades en XML o dirèctament si una funció retorna un diccionari que la defineix. Les accions poden ser cridades de tres maneres:
do
* Fent clic en un menú.
echo -n '<record id="escoleta.classroom_'$i'" model="escoleta.classroom">'
* Fent clic en botons de les vistes (han d'estar connectats amb accions).
echo '<field name="name">classe '$i'</field></record>'
* Com accions contextuals en els objectes.
done


for i in {1..99}
D'aquesta manera, el client web pot saber quina acció ha d'executar si rep alguna d'aquestes coses:
do
* '''false''': Indica que s'ha de tancar el diàleg actual.
nom="$(paste -d" "  <(shuf nombres.txt) <(shuf apellidos.txt) | head -1)"
* '''Una string''': Amb l'etiqueta de '''l'acció de client''' a executar.
classroom=$((1+$i/10))
* '''Un número''': Amb el ID o external ID de l'acció a trobar a la base de dades.
echo -n '<record id="escoleta.partner_'$i'" model="res.partner">'  
* '''Un diccionari''': Amb la definició de l'acció, aquesta no està ni en XML ni en la base de dades. En general, és la manera de cridar a un action al finalitzar una funció.
echo -n '<field name="name">'$nom'</field><field name="escoleta">1</field>'  
echo -n '<field name="escoleta_type">student</field>'
echo '<field name="classroom" ref="escoleta.classroom_'$classroom'"/></record>'
done


for i in monday tuesday wednesday thursday friday
=== Accions tipus ''window'' ===
do
echo -n '<record id="escoleta.menjador_2019_'$i'" model="escoleta.menjador">'
echo -n '<field name="name">menjador 2019 '$i'</field>'
echo -n '<field name="week_day">'$i'</field><field name="year">2019</field>'
echo -n '<field name="students" eval="[(6,0,[ref('\''escoleta.partner_1'\''),
ref('\''escoleta.partner_3'\''), ref('\''escoleta.partner_5'\''),
ref('\''escoleta.partner_20'\'')])]" />
</record>'
done
echo '</data></odoo>'
</syntaxhighlight>
</div>
 
== Accions i menús ==
<div style="float:right; font-size:0.7em; margin:5px;">
<pre>
Diagrama de cóm es comporta el client web quan carrega Odoo per primera
vegada i cóm crida a un action i carrega les vistes i les dades (records)
+----------------------+                            +----------------------+
|                      | GET / al port 8069        |                      |
|    Navegador Web    +--------------------------> |    Servidor Odoo    |
|                      |                            |                      |
+----------------------+  index.html (bàsic)        |                      |
|  Enllaços a JS i CSS <----------------------------+                      |
+----------------------+                            |                      |
|                      | GET JS i CSS  Qweb        +----------------------+
|                      +----------------------------> Crea els Assets      |
|                      |                            +----------------------+
+----------------------+ CSS i JS ASSETS Templates  |                      |
| Inicia Client Web    <----------------------------+                      |
+----------------------+                            |                      |
|                      | POST Load Views            +----------------------+
|                      +----------------------------> ir.ui.view          |
|                      | arch i json amb els fields |                      |
|                      +<---------------------------+                      |
|                      |                            +----------------------+
+----------------------+ POST load action          +----------------------+
|  Pulsem un menú      +----------------------------> ir.ui.action        |
+----------------------+ Definició de l'action      |                      |
|                      <----------------------------+                      |
| l'Action necessita  |                            +----------------------+
| vistes              | POST Load Views            |                      |
|                      +---------------------------->  ir.ui.view          |
|                      | Totes les vistes i fields  |                      |
| El client analitza  <----------------------------+                      |
| quins field necessita| POST Search read          +----------------------+
+---------------------------------------------------> Selecciona i computa |
|                      | Json amb els records      | el fields            |
|El client renderitza  <---------------------------------------------------+
|la vista amb els      |                            |                      |
|records              |                            |                      |
+----------------------+                            +----------------------+
 
</pre>
</div>
El client web de Odoo conté uns menús dalt i a l'esquerra. Aquests menús, al ser accionats mostren altres menús i les pantalles del programa. Quant pulsem en un menú, canvia la pantalla perquè hem fet una acció.
 
Les accions i els menús es declaren en fitxers de dades en XML. Les accions poden ser cridades de tres maneres:
* Fent clic en un menú.
* Fent clic en botons de les vistes (han d'estar connectats amb accions).
* Com accions contextuals en el objectes.


Les accions són un record més ('''ir.actions.act_window'''). No obstant, els menús, tenen una manera més ràpida de ser declarats amb una etiqueta '''menuitem''':
Les accions ''window'' són un record més ('''ir.actions.act_window'''). No obstant, els menús que les criden, tenen una manera més ràpida de ser declarats amb una etiqueta '''menuitem''':


<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
Línea 708: Línea 674:
</syntaxhighlight>
</syntaxhighlight>


{{nota | Les accions han de ser declarades al XML abans que els menús que les accionen }}
{{nota | Les accions han de ser declarades al XML abans que els menús que les accionen. }}
{{nota | A partir d'Odoo 12, cal donar permisos explícitament als usuaris per veure els menús. }}


Exemple:
Exemple:
Línea 754: Línea 721:
Sols el tercer nivell de menús pot tindre associada un action. El primer és el menú de dalt i el segon no es 'clicable'.
Sols el tercer nivell de menús pot tindre associada un action. El primer és el menú de dalt i el segon no es 'clicable'.


El que hem vist en esta secció és la definició d'una acció en un XML com a part de la vista, però una acció no és més que una forma còmoda d'escriure moltes coses que farà el client en javascript per demanar alguna cosa al servidor. Els actions separen i simplifiquen el desenvolupament de la interfície d'usuari que és el client web. Un menú o botó en html acciona una funció javascript que en principi no sap el que fer. Aquesta demana que es carregue la definició del seu action. Una vegada carregada la definició, queda clar tot el que ha de demanar (les vistes, context, dominis, vistes search, lloc on carregar-ho tot...) aleshores demana les vistes i amb ajuda de les vistes i els fields, demana els records que són les dades a mostrar. Per tant, un action és la definició sense programar javascript de coses que ha de fer el javascript. Odoo permet declarar actions com a resposta de funcions. Aquestes actions no estan en la base de dades, però són enviades igualment al client i el client fa en elles el mateix que en un action que ell ha demanat. Un exemple d'això són els actions que retornen els botons dels wizards. De fet, podem fer que un botó torne un action i, per tant, obrir una vista diferent.  
{{nota|El que hem vist en esta secció és la definició d'una acció en un XML com a part de la vista, però una acció no és més que una forma còmoda d'escriure moltes coses que farà el client en javascript per demanar alguna cosa al servidor. Els actions separen i simplifiquen el desenvolupament de la interfície d'usuari que és el client web. Un menú o botó en html acciona una funció javascript que en principi no sap el que fer. Aquesta demana que es carregue la definició del seu action. Una vegada carregada la definició, queda clar tot el que ha de demanar (les vistes, context, dominis, vistes search, lloc on carregar-ho tot...) aleshores demana les vistes i amb ajuda de les vistes i els fields, demana els records que són les dades a mostrar. Per tant, un action és la definició sense programar javascript de coses que ha de fer el javascript. Odoo permet declarar actions com a resposta de funcions. Aquestes actions no estan en la base de dades, però són enviades igualment al client i el client fa en elles el mateix que en un action que ell ha demanat. Un exemple d'això són els actions que retornen els botons dels wizards. De fet, podem fer que un botó torne un action i, per tant, obrir una vista diferent. }}


Aquest exemple és una funció cridada per un botó que retorna un action:
Si vols conèixer en més detall cóm funcionen les accions en Odoo, llig l'article [[Accions i menús en Odoo]].
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.multi    # Molt important que siga multi.
    def create_comments(self):
      clients=self.env['reserves.bookings'].search([('checking_day','<',fields.Date.today()),('room.hotel','=',self.id)]).mapped('client').ids
      print(clients)
      if len(clients)>0:
        print(clients)
        random.shuffle(clients)
        comment = self.env['reserves.comments'].create({'hotel':self.id,'client':clients[0],'stars':str(random.randint(1,5))})
        return {
    'name': 'Comment',
    'view_type': 'form',
    'view_mode': 'form',
    'res_model': 'reserves.comments',
    'res_id': comment.id,
    #'view_id': self.env.ref('reserves.comments_form').id,
    'type': 'ir.actions.act_window',
    'target': 'current',
        }
</syntaxhighlight>
Observem que li pasem el model i el res_id per a que puga obrir un formulari amb el comentari creat.  


Aquest és el json que rep el client després de cridar al botó:
== La vista ==
<syntaxhighlight lang="javascript" style="font-family:monospace">
{
  "jsonrpc":"2.0",
  "id":878622456,
  "result":{
      "name":"Comment",
      "view_mode":"form",
      "res_model":"reserves.comments",
      "res_id":20,
      "type":"ir.actions.act_window",
      "target":"current",
      "flags":{ },
      "views":[[false,"form"]]
  }
}
</syntaxhighlight>


Ara el client pot demanar un formulari i el record corresponent al model ''reserves.comments'' i el id ''20''.
Per saber més sobre les vistes i cómo millorar-les, consulta l'article de [[Millores en la vista en Odoo]].
</div>
 
=== ''Domains'' en les ''actions'' ===
 
En Odoo, el concepte de '''domain''' o domini està en varis llocs, encara que el seu funcionament sempre és el mateix. Es tracta d'un criteri de búsqueda o filtre sobre un model. La sintaxi dels domains és como veurem en aquest exemple:
<syntaxhighlight lang="python" style="font-family:monospace">
['|',('gender','=','male'),('gender','=','female')]
</syntaxhighlight>
Com es veu, cada condició va entre parèntesis amb el mon del field i el valor desitjat entre cometes si és un ''string'' i amb l'operador entre cometes i tot separat per comes. Les dues condicions tenen un '''|''' dabant, que significa la '''O''' lògica. Està dabant per utilitzar la [https://es.wikipedia.org/wiki/Notaci%C3%B3n_polaca_inversa notació polaca inversa].
 
Un action en domain treu vistes per als elements del model que coincideixen en les condicions del domini. El domain és trauit per el model en un ''where'' més a la consulta SQL. Per tant, al client no li arriben mai els registres que no pasen el filtre. Els ''domains'' en les [[Odoo#Vistes_search|vistes search]]
 
== Vistes Bàsiques ==


Les vistes són la manera en la que es representen els models. En cas de que no declarem les vistes, es poden referenciar per el seu tipus i Odoo generarà una vista de llista o formulari estandar per poder vorer els registres de cada model. No obstant, quasi sempre volem personalitzar les vistes i en aquest cas, es poden referenciar per un identificador.  
Les vistes són la manera en la que es representen els models. En cas de que no declarem les vistes, es poden referenciar per el seu tipus i Odoo generarà una vista de llista o formulari estandar per poder vorer els registres de cada model. No obstant, quasi sempre volem personalitzar les vistes i en aquest cas, es poden referenciar per un identificador.  
Línea 829: Línea 745:
{{nota|Les vistes es guarden en el model '''ir.ui.view'''. Tots els elements de interficie tenen en el seu nom ir.ui (Information Repository, User Interface). Els menús a ir.ui.menu o les accions a '''ir.actions.window'''}}
{{nota|Les vistes es guarden en el model '''ir.ui.view'''. Tots els elements de interficie tenen en el seu nom ir.ui (Information Repository, User Interface). Els menús a ir.ui.menu o les accions a '''ir.actions.window'''}}


Exemple:
Exemple de vista form:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
Línea 850: Línea 766:
</div>
</div>


== Vistes Millorades ==
Encara que Odoo ja proporciona un tree i un form per defecte, la vista cal millorar-la quasi sempre. Totes les vistes tenen fields que poden tindre widgets diferents. En les vistes form, podem adaptar molt l'aspecte amb grups de fields, pestanyes, camps ocults condicionalment...  
Encara que Odoo ja proporciona un tree i un form per defecte, la vista cal millorar-la quasi sempre. Totes les vistes tenen fields que poden tindre widgets diferents. En les vistes form, podem adaptar molt l'aspecte amb grups de fields, pestanyes, camps ocults condicionalment...  


=== Millores en les vistes tree ===
Per saber més sobre les vistes i cómo millorar-les, consulta l'article de [[Millores en la vista en Odoo]].
En les vistes tree es pot modificar el '''color''' en funció del contingut d'un field:
<syntaxhighlight lang="xml" style="font-family:monospace">
<tree colors="blue:state=='draft';red:state=='trashed'">
    <field name="name"/>
    <field name="state"/>
</tree>
</syntaxhighlight>
Encara que aquesta sintaxi es considera obsoleta i a partir de la versió 9 es recomana utilitzar '''decoration''', que utilitza colors contextuals de '''Bootstrap''':
    decoration-bf - Lineas en BOLD
    decoration-it - Lineas en ITALICS
    decoration-danger - Color LIGHT RED
    decoration-info - Color LIGHT BLUE
    decoration-muted - Color LIGHT GRAY
    decoration-primary - Color LIGHT PURPLE
    decoration-success - Color LIGHT GREEN
    decoration-warning - Color LIGHT BROWN
<syntaxhighlight lang="xml" style="font-family:monospace">
<tree  decoration-info="state=='draft'" decoration-danger="state=='trashed'">
    <field name="name"/>
    <field name="state"/>
</tree>
</syntaxhighlight>


En el cas de que es vulga comparar un field Date o Datetime es pot fer amb la variable global de QWeb '''current_date'''. Per exemple:
== Els reports ==
<syntaxhighlight lang="xml" style="font-family:monospace">
<tree  decoration-info="start_date==current_date">
...
</syntaxhighlight>


[[Odoo reports]]


També es pot fer '''editable''' per no tindre que obrir un form: '''editable="[top | bottom]"'''
== Herència ==


De vegades, un camp pot servir per a alguna cosa, però no cal que l'usuari el veja. El que cal fer és ficar el field , però dir que es '''invisible="1"'''
El framework d'Odoo facilita el mecanisme de l’herència per tal que els programadors puguin adaptar mòduls existents i garantir a la vegada que les actualitzacions dels mòduls no destrossin les adequacions desenvolupades.
<syntaxhighlight lang="xml" style="font-family:monospace">
<tree colors="#0000ff:duration&lt;5;red:duration&gt;15">
                    <field name="name"/>
                    <field name="course_id"/>
                    <field name="duration" invisible="1"/>
                    <field name="taken_seats" widget="progressbar"/>
                </tree>
</syntaxhighlight>


Els ''trees'' poden tindre '''buttons''' amb els mateixos atributs que els buttons dels forms.
L’herència es pot aplicar en els tres components del patró MVC:


En els trees es pot calcular totals amb aquesta etiqueta:
*    En el model: possibilita ampliar les classes existents o dissenyar noves classes a partir de les existents.
<syntaxhighlight lang="xml" style="font-family:monospace">
*    En la vista: possibilita modificar el comportament de vistes existents o dissenyar noves vistes.
<field name="amount" sum="Total Amount"/>
*    En el controlador: possibilita sobreescriure els mètodes existents o dissenyar-ne de nous.
</syntaxhighlight>
==== banner_route ====


A partir de la versió 12 d'Odoo, permet afegir als trees, forms, etc una capçalera obtinguda per una url. https://www.odoo.com/documentation/12.0/reference/views.html#common-structure


=== Millores en les vistes form ===
OpenObject proporciona tres mecanismes d’herència: l’herència de classe, l’herència per prototip i l’herència per delegació.


Per a que un form quede bé, es pot inclure la etiqueta '''<sheet>''', que fa que no ocupe tota la pantalla encara que siga panoràmica.  
{|  class="wikitable" 
 
!    Mecanisme
Tot sheet ha de tindre '''<group>''' i dins els fields. Es poden fer els group que vullgam i poden tindre string per mostrar un títol.
!    Característiques
 
!    Com es defineix
Si no utilitzem l'etiquet group, els fields no tindran label, no obstant, coses com el class="oe_edit_only" no funcionen en el group, per tant, cal utilitzar l'etiqueta '''<label for="name">'''
|- 
 
|    <strong>De classe</strong>
Per facilitar la gestió, un form pot tindre pestanyes temàtiques. Es fa en '''<notebook> <page string="titol">'''
|    - Herència simple. <br/>
 
- La classe original queda substituïda per la nova classe. <br/>
Es pot separar els grups amb '''<separator string="Description for Quotations"/>'''
- Afegeix noves funcionalitats (atributs i/o mètodes) a la classe original. <br/>
 
- Les vistes definides sobre la classe original continuen funcionant. <br/>
Alguns '''One2Many''' donen una vista tree que no es adequada, per això es pot modificar el tree per defecte:
- Permet sobreescriure mètodes de la classe original. <br/>
 
- En PostgreSQL, continua mapada en la mateixa taula que la classe original, ampliada amb els nous atributs que pugui incorporar.
<syntaxhighlight lang="xml" style="font-family:monospace">
|    - S’utilitza l’atribut <code>_inherit</code> en la definició de la nova classe Python: <code>_inherit = obj</code><br/>
<field name="subscriptions" colspan="4" mode=”tree”>
- El nom de la nova classe ha de continuar sent el mateix que el de la classe original: <code>_name = obj</code>
  <tree>...</tree>
|- 
</field>
|    <strong>Per prototip</strong>
</syntaxhighlight>
|    - Herència simple. <br/>
 
- Aprofita la definició de la classe original (com si fos un «prototipus»). <br/>
En un One2many es pot especificar també el '''form''' que en donarà quan anem a crear un nou element.
- La classe original continua existint. <br/>
 
- Afegeix noves funcionalitats (atributs i/o mètodes) a les aportades per la classe original. <br/>
Una altra opció és especificar la vista que insertarà en el field:
- Les vistes definides sobre la classe original no existeixen (cal dissenyar-les de nou). <br/>
<syntaxhighlight lang="xml" style="font-family:monospace">
- Permet sobreescriure mètodes de la classe original. <br/>
<field name="m2o_id" context="{'form_view_ref': 'module_name.form_id'}"/>
- En PostgreSQL, queda mapada en una nova taula.  
</syntaxhighlight>
|    - S’utilitza l’atribut <code>_inherit</code> en la definició de la nova classe Python: <code>_inherit = obj</code><br/>
 
- Cal indicar el nom de la nova classe: <code>_name = nou_nom</code>
'''Valors per defecte en un one2many'''
|- 
 
|    <strong>Per delegació</strong>
Quant creem un One2many en el mode form (o tree editable) ens permet crear elements d'aquesta relació. Per a aconseguir que, al crear-los, el camp many2one corresponga al pare des del que es crida, es pot fer amb el [[Odoo#Context|context]]: Dins del field one2many que estem fent fiquem aquest codi:
|    - Herència simple o múltiple. <br/>
 
- La nova classe «delega» certs funcionaments a altres classes que incorpora a l’interior. <br/>
<syntaxhighlight lang="xml" style="font-family:monospace">
- Els recursos de la nova classe contenen un recurs de cada classe de la que deriven. <br/>
context="{'default_<camp many2one>':active_id}"
- Les classes base continuen existint. <br/>
</syntaxhighlight>
- Afegeix les funcionalitats pròpies (atributs i/o mètodes) que correspongui. <br/>
 
- Les vistes definides sobre les classes bases no existeixen a la nova classe. <br/>
O este exemple per a dins d'un action:
- En PostgreSQL, queda mapada en diferents taules: una taula per als atributs propis, mentre que els recursos de les classes derivades resideixen en les taules corresponents a les dites classes.
 
|    - S’utilitza l’atribut <code>_inherits</code> en la definició de la nova classe Python: <code>_inherits = </code><br/>
<syntaxhighlight lang="xml" style="font-family:monospace">
- Cal indicar el nom de la nova classe: <code>_name = nou_nom</code>
<field name="context">{"default_doctor": True}</field>
|}
</syntaxhighlight>
[[Archivo:Inheritance methods.png]]


{{nota|Aquesta sintaxi funciona per a passar per context valors per defecte a un form cridat amb un action. Pot ser en One2many, botons o menús}}
El fitxer __openerp__.py ha de contindre les dependències de la clase heretada.


{{nota|'''active_id''' és una variable que apunta al id del element que està en aquest moment actiu. Com que estem en un formulari, és el que se està creant o modificant amb en formulari. En el cas de la creació, active_id no està encara apuntant a un element de la base de dades, però funciona internament, encara que en el field no diga res o diga False.}}
=== Herència en el Model ===


'''Domains en Many2ones'''
El disseny d’un objecte d’OpenObject heretat és paregut al disseny d’un objecte d’OpenObject no heretat; únicament hi ha dues diferències:


Els camps Many2one es poden filtrar, per exemple:
*  Apareix l’atribut '''_inherit''' o '''_inherits''' per indicar l’objecte (herència simple) o els objectes (herència múltiple) dels quals deriva el nou objecte. La sintaxi a seguir és:
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="hotel" domain="[('ishotel', '=', True)]"/>
</syntaxhighlight>


{{nota|Sembla que els domains sols funcionen en la vista en els Many2one, ja que en els One2many sols funcionen si estan en el codi python.}}
_inherit = 'nom.objecte.del.que.es.deriva'
_inherits = {'nom.objecte1':'nom_camp_FK1', ...}


====Widgets====
*    En cas d’herència simple, el nom (atribut _name) de l’objecte derivat pot coincidir o no amb el nom de l’objecte pare. També és possible no indicar l’atribut _name, fet que indica que el nou objecte manté el nom de l’objecte pare.


Alguns camps, com ara les imatges, es poden mostrar utilitzant un '''widget''' distint que el per defecte:
L’herència simple (_inherit) amb atribut _name idèntic al de l’objecte pare, s’anomena herència de classe i en ella el nou objecte substitueix l’objecte pare, tot i que les vistes sobre l’objecte pare continuen funcionant. Aquest tipus d’herència, la més habitual, s’utilitza quan es vol afegir ''fields'' i/o modificar propietats de dades existents i/o modificar el funcionament d’alguns mètodes. En cas d’afegir dades, aquestes s’afegeixen a la taula de la base de dades en la qual estava mapat l’objecte pare.


<syntaxhighlight lang="xml" style="font-family:monospace">
<div style="border: 1px dotted #ddd; width:80%; margin:auto; padding:10px; background-color:#fefffe">
<field name="image" widget="image" class="oe_left oe_avatar"/>
'''Exemple d'herència de classe'''
<field name="taken_seats" widget="progressbar"/>
L’herència de classe la trobem en molts mòduls que afegeixen dades i mètodes a objectes ja existents, com per exemple, el mòdul comptabilitat (account) que afegix dades i mètodes a l’objecte res.partner. Fixem-nos en el contingut del mòdul account:
<field name="country_id" widget="selection"/>
<syntaxhighlight lang="python" style="font-family:monospace">
<field name="state" widget="statusbar"/>
    class res_partner(Model.model):
    _name = 'res.partner'
    _inherit = 'res.partner'
    debit_limit = fields.float('Payable limit')
    ...
</syntaxhighlight>
</syntaxhighlight>
Podeu comprovar que la taula res_partner d’una empresa sense el mòdul account instal·lat no conté el camp debit_limit, que en canvi sí que hi apareix una vegada instal·lat el mòdul.


Llista de [[widgets d'Odoo]] disponibles per a camps dins de forms:
Odoo té molts mòduls que deriven de l’objecte res.partner per afegir-hi característiques i funcionalitats.
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
</div>
<syntaxhighlight lang="javascript" style="font-family:monospace">
 
instance.web.form.widgets = new instance.web.Registry({
L’herència simple (_inherit) amb atribut '''_name''' diferent al de l’objecte pare, s’anomena '''herència per prototip''' i en ella es crea un nou objecte que aglutina les dades i mètodes que tenia l’objecte del qual deriva, juntament amb les noves dades i mètodes que pugua incorporar el nou objecte. En aquest cas, sempre es crea una nova taula a la base de dades per mapar el nou objecte.  
    'char' : 'instance.web.form.FieldChar',
 
    'id' : 'instance.web.form.FieldID',
<div style="border: 1px dotted #ddd; width:80%; margin:auto; padding:10px; background-color:#fefffe">
    'email' : 'instance.web.form.FieldEmail',
'''Exemple d'herència per prototip'''
    'url' : 'instance.web.form.FieldUrl',
L’herència per prototip és difícil de trobar en els mòduls que incorpora Odoo. Un exemple el tenim en el mòdul base_calendar en el qual podem observar el mòdul comptabilitat (account) que afegix dades i mètodes a l’objecte res.partner. Fixem-nos en el contingut del mòdul account:
    'text' : 'instance.web.form.FieldText',
<syntaxhighlight lang="python" style="font-family:monospace">
    'html' : 'instance.web.form.FieldTextHtml',
     class res_alarm(Model.model):
    'char_domain': 'instance.web.form.FieldCharDomain',
     _name = 'res.alarm'
    'date' : 'instance.web.form.FieldDate',
     ...
    'datetime' : 'instance.web.form.FieldDatetime',
     class calendar_alarm(Model.model):
    'selection' : 'instance.web.form.FieldSelection',
     _name = 'calendar.alarm'
    'radio' : 'instance.web.form.FieldRadio',
     _inherit = 'res.alarm'
    'many2one' : 'instance.web.form.FieldMany2One',
     ...
    'many2onebutton' : 'instance.web.form.Many2OneButton',
    'many2many' : 'instance.web.form.FieldMany2Many',
    'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
    'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
    'one2many' : 'instance.web.form.FieldOne2Many',
     'one2many_list' : 'instance.web.form.FieldOne2Many',
    'reference' : 'instance.web.form.FieldReference',
     'boolean' : 'instance.web.form.FieldBoolean',
     'float' : 'instance.web.form.FieldFloat',
     'percentpie': 'instance.web.form.FieldPercentPie',
    'barchart': 'instance.web.form.FieldBarChart',
     'integer': 'instance.web.form.FieldFloat',
     'float_time': 'instance.web.form.FieldFloat',
     'progressbar': 'instance.web.form.FieldProgressBar',
    'image': 'instance.web.form.FieldBinaryImage',
    'binary': 'instance.web.form.FieldBinaryFile',
    'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
    'statusbar': 'instance.web.form.FieldStatus',
    'monetary': 'instance.web.form.FieldMonetary',
    'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
    'x2many_counter': 'instance.web.form.X2ManyCounter',
    'priority':'instance.web.form.Priority',
    'kanban_state_selection':'instance.web.form.KanbanSelection',
    'statinfo': 'instance.web.form.StatInfo',
});
</syntaxhighlight>
</syntaxhighlight>
Tret de: https://github.com/odoo/odoo/blob/8.0/addons/web/static/src/js/view_form.js#L6355
En una empresa que tingui el mòdul base_calendar instal·lat podeu comprovar l’existència de la taula res_alarm amb els camps definits a l’apartat _atributs de la classe res_alarm i la taula calendar_alarm amb camps idèntics als de la taula res_alarm més els camps definits a l’apartat _atributs de la classe calendar_alarm.  
</div>
</div>
{{nota|L'herència per prototip és la tradicional en els llenguatges orientats a objectes, ja que crea una nova classe vinculada}}


'''Reescalar les imatges'''
L’herència múltiple (_inherits) s’anomena herència per delegació i sempre provoca la creació d’una nova taula a la base de dades. L’objecte derivat ha d’incloure, per cada derivació, un camp many2one apuntant l’objecte del qual deriva, amb la propietat '''ondelete='cascade''''. L’herència per delegació obliga que cada recurs de l’objecte derivat apunte a un recurs de cadascun dels objectes dels quals deriva i es pot donar el cas que hi hagi diversos recursos de l’objecte derivat que apunten a un mateix recurs per algun dels objectes dels quals deriva.


Molt a sovint, tenim la necessitat de reescalar les imatges que l'usuari penja. Això es fa amb una utilitat d'Odoo en una funció de Python. Per exemple:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
from odoo import models, fields, api, tools
    class res_alarm(Model.model):
[...]
    _name = 'res.alarm'
     photo = fields.Binary()
    ...
     photo_small = fields.Binary(compute='_get_images',store=True)
     class calendar_alarm(Model.model):
     photo_medium = fields.Binary(compute='_get_images',store=True)
     _name = 'calendar.alarm'
    _inherits = {'res.alarm':'alarm_id'}
     ...
</syntaxhighlight>
 
=== Herència en la vista ===


    @api.one
L’herència de classe possibilita continuar utilitzant les vistes definides sobre l’objecte pare, però en moltes ocasions interessa disposar d’una versió retocada. En aquest cas, és molt millor heretar de les vistes existents (per afegir, modificar o eliminar camps) que reemplaçar-les completament.  
    @api.depends('photo')
    def _get_images(self):
        image = self.photo
        data = tools.image_get_resized_images(image)
        self.photo_small = data["image_small"]
        self.photo_medium = data["image_medium"]
</syntaxhighlight>
====buttons====
Podem introduir un botó en el form:
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
  <button name="update_progress" type="object" string="update" class="oe_highlight" />      
  <field name="inherit_id" ref="id_xml_vista_pare"/>
</syntaxhighlight>
</syntaxhighlight>
El name ha de ser igual que la funció a la que crida.
En cas que la vista id_xml_vista_pare estiga en un mòdul diferent del que estem dissenyant, cal afegir el nom del mòdul al davant:  
La funció pot ser un workflow, una del model en el que està o un action. En el type cal indicar el tipus amb: '''workflow, object o action'''
En l'exemple anterior, el button és de tipus object. Aixó vol dir que crida a una funció del model al que represente el formulari que el conté. Per a fer un butó que cride a un altre formulari, s'ha de fer en un tipus '''action'''. Amés, per ficar la id del xml del form al que es vol cridar, cal ficar el prefixe i sufixe '''%(...)d''', com en l'exemple:
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
  <button name="%(launch_mmog_fortress_wizard)d" type="action" string="Launch attack" class="oe_highlight" />
  <field name="inherit_id" ref="modul.id_xml_vista_pare"/>
</syntaxhighlight>
Els ''buttons'' poden tindre una icona. Odoo proporciona algunes que es poden trobar a aquesta web: [https://es.slideshare.net/TaiebKristou/odoo-icon-smart-buttons]
<syntaxhighlight lang="xml" style="font-family:monospace">
<button name="test" icon="fa-star-o"/>
</syntaxhighlight>
Els ''buttons'' es poden posar per el form, encara que es reconama en el header:
<syntaxhighlight lang="xml" style="font-family:monospace">
<header>
<field name="state" widget="statusbar"/>
<button name="accept" type="object" string="Accept" class="oe_highlight"/>
<button special="cancel" string="Cancel"/>
</header>
</syntaxhighlight>
</syntaxhighlight>
El motor d’herència d’OpenObject, en trobar una vista heretada, processa el contingut de l’element arch. Per cada fill d’aquest element que tingui algun atribut, OpenObject cerca a la vista pare una etiqueta amb atributs coincidents (excepte el de la posició) i, a continuació, combina els camps de la vista pare amb els de la vista heretada i estableix la posició de les noves etiquetes a partir dels següents valors:


Els botons sempre executen una funció de Javascript en la part del client web que demana alguna cosa al servidor. En el cas dels button '''action''', demana el action, per després executar aquesta. En el cas dels buttons '''object''' demana que s'execute una funció del model i recordset actual en el servidor. El client web es queda a l'espera d'una resposta del servidor, que si és un diccionari buit, provoca un refresc de la pàgina, però pot retornar moltes coses: '''warnings''', '''domains''', '''actions'''... i el client ha d'actuar en conseqüència. Els buttons poden tindre també [[Odoo#Context|'''context''']] per enviar alguna cosa extra al servidor.
*    inside (per defecte): els valors s’afegeixen “dins” de l’etiqueta.
 
*    after: afegeix el contingut després de l’etiqueta.
====Formularis dinàmics====
*    before: afegeix el contingut abans de l’etiqueta.
'''Ocultar condicionalment un field'''
*    replace: reemplaça el contingut de l’etiqueta.
 
*    attributes: Modifica [https://www.odoo.com/es_ES/forum/ayuda-1/question/xpath-how-to-replace-attributes-only-and-not-the-full-field-38192 els atributs].  
Es pot ocultar un field si algunes condicions no es cumpleixen. Per exemple:


'''Reemplaçar'''
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="boyfriend_name" attrs="{'invisible':[('married', '!=', False)]}" />
<field name="arch" type="xml">
<field name="boyfriend_name" attrs="{'invisible':[('married', '!=', 'selection_key')]}" />
  <field name="camp" position="replace">
    <field name="nou_camp" ... />
  </field>
</field>
</syntaxhighlight>
</syntaxhighlight>
 
'''Esborrar'''
Tambés es pot ocultar i mostrar sols en el mode edició o lectura:
 
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="partit" class="oe_edit_only"/>
<field name="arch" type="xml">
<field name="equip" class="oe_read_only"/>
  <field name="camp" position="replace"/>
</field>
</syntaxhighlight>
</syntaxhighlight>
 
'''Inserir nous camps'''
O mostrar si un camp anomenat '''state''' té un determinat valor:
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
  <group states="dia"><field name="dia"/></group>
  <field name="arch" type="xml">
</syntaxhighlight>
    <field name="camp" position="before">
      <field name="nou_camp" .../>
    </field>
</field>


En el següent exemple, introdueix dos conceptes nous: el '''column_invisible''' per ocultar una columna d'un tree i el '''parent''' per fer referència al valor d'un field de la vista pare:
<field name="arch" type="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
    <field name="camp" position="after">
<field name="lot_id" attrs="{'column_invisible': [('parent.state', 'not in', ['sale', 'done'])] />
      <field name="nou_camp" .../>
    </field>
</field>
</syntaxhighlight>
'''Fer combinacions'''
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="arch"type="xml">
  <data>
    <field name="camp1" position="after">
      <field name="nou_camp1"/>
    </field>
    <field name="camp2" position="replace"/>
    <field name="camp3" position="before">
        <field name="nou_camp3"/>
    </field>
  </data>
</field>
</syntaxhighlight>
</syntaxhighlight>


 
Per definir la posició dels elements que afegim, podem utilitzar una expresió '''xpath''':
'''Editar condicionalment un field'''
 
En '''attrs''' també es pot afegir '''readonly'''
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="name2" attrs="{'readonly': [('condition', '=', False)]}"/>
<xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before">
<xpath expr="//form/*" position="before">
  <header>
    <field name="status" widget="statusbar"/>
  </header>
</xpath>
</syntaxhighlight>
</syntaxhighlight>
 
És posssible que necessitem una vista totalment nova de l'objecte heredat. Si fem un action normal en l'XML es veuran els que més prioritat tenen. Si volem especificar quina vista volem en concret hem d'utilitzar '''view_ids''', observem aquest exemple:
Aquests exemples combinen tots els attrs:
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="name" attrs="{'invisible': [('condition1', '=', False)],  
        <record model="ir.actions.act_window" id="terraform.player_action_window">
                          'required': [('condition2', '=', True)],
            <field name="name">Players</field>
                          'readonly': [('condition3','=',True)]}" />
            <field name="res_model">res.partner</field>
 
            <field name="view_mode">tree,form,kanban</field>
<field name="suma" attrs="{'readonly':[('valor','=','calculat')],  
            <field name="domain"> [('is_player','=',True)]</field>
                          'invisible': ['|',('servici','in',['Reparacions','Manteniment']),
            <field name="context">{'default_is_player': True}</field>
                          ('client','=','Pepe')]}" />
            <field name="view_ids" eval="[(5, 0, 0),
            (0, 0, {'view_mode': 'tree', 'view_id': ref('terraform.player_tree')}),
            (0, 0, {'view_mode': 'form', 'view_id': ref('terraform.player_form')}),]" />
        </record>
</syntaxhighlight>
</syntaxhighlight>
En '''(0,0,{registre_a_crear})''' li diguem que a eixe Many2many hi ha que afegir un nou registre amb eixes dades en concret. El que necessita és el '''view_mode''' i el '''view_id''', com en els records anteriors.


====Workflows====
Si es vol especificar una vista search es pot inclourer la etiqueta '''search_view_id''':
https://www.odoo.com/documentation/8.0/reference/workflows.html
 
Els formularis poden tindre distint comportament depenent de l'estat en el que estan. L'exemple són els presuposts que tenen distints fields o buttons en funció de si estan en fase de presupost, comanda o facturació.
 
Abans d'explicar els workflows, cal dir que es pot fer alguna cosa pareguda amb les barres d'estatus. Els grups i fields es poden veure o ocultar en funció d'un camp '''state''' i afegint l'atribut '''states=""'''. Amés es pot fer una barra d'estatus
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="state" widget="statusbar" statusbar_visible="draft,sent,progress,invoiced,done" />
<field name="search_view_id" ref="cine.pos_order_line_search_view"/>
<button name="action_draft" type="object" string="Reset to draft" states="confirmed,done"/>
</syntaxhighlight>
</syntaxhighlight>
Exemple:


Però els workflows són molt més que això.
=== Vistes Kanban ===
Les vistes kanban són per a mostrar el model en forma de 'cartes'. Les vistes kanban se declaren amb una mescla de xml, html i plantilles '''Qweb'''.
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="python" style="font-family:monospace">
class socios(models.Model):
    _inherit = 'res.partner'
    _name = 'res.partner'
    #name = fields.Char()
    camions = fields.One2many('cooperativa.camion','socio',string='Trucks')
    n_camiones = fields.Integer(compute='_n_camiones',string='Number of Trucks')
    arrobas = fields.Float(compute='_n_camiones',string='@')
    @api.depends('camions')
    def _n_camiones(self):
      for i in self:
        for j in i.camions:
          i.arrobas = i.arrobas + j.arrobas
          i.n_camiones = i.n_camiones + 1
</syntaxhighlight>
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<record model="ir.ui.view" id="socio_kanban_view">
  <record model="ir.ui.view" id="socio_form_view">
             <field name="name">cooperativa.socio</field>
             <field name="name">socio</field>
             <field name="model">cooperativa.socio</field>
             <field name="model">res.partner</field>
            <field name="arch" type="xml">
  <field name="inherit_id" ref="base.view_partner_form"/>  
                <kanban>
          <field name="arch" type="xml">
                    <!--list of field to be loaded -->
    <field name="website" position="after">
                    <field name="name" />
                            <field name="camions"/>
                    <field name="id" /> <!-- És important afegir el id per al record.id.value -->
                            <field name="n_camiones"/>
                    <field name="foto" />
                            <field name="arrobas"/>
                    <field name="arrobas"/>
    </field>


                    <templates>
                    <t t-name="kanban-box">
                            <div class="oe_product_vignette">
                                <a type="open">
                                    <img class="oe_kanban_image"
                                        t-att-src="kanban_image('cooperativa.socio', 'foto', record.id.value)" />
                                </a>
                                <div class="oe_product_desc">
                                    <h4>
                                        <a type="edit">
                                            <field name="name"></field>
                                        </a>
                                    </h4>
                                    <ul>
                                      <li>Arrobas: <field name="arrobas"></field></li>
                                    </ul>
                                </div>
                            </div>
                        </t>
                    </templates>
                </kanban>
             </field>
             </field>
         </record>
         </record>
</syntaxhighlight>
</syntaxhighlight>
</div>
En l'anterior vista kanban cal comentar les línies.


Al principi es declaren els fields que han de ser mostrats. Si no es necessiten per a la lògica del kanban i sols han de ser mostrats no cal que estiguen declarats. No obstant, per que l'exemple estiga complet els he deixat.
<syntaxhighlight lang="xml" style="font-family:monospace">
    <!--Inherit quotations search view-->
A continuació ve un template '''Qweb''' en el que cal definir una etiqueta '''<t t-name="kanban-box">''' que serà renderitzada una vegada per cada element del model.
    <record id="view_sale_order_inherit_search" model="ir.ui.view">
      <field name="name">sale.order.search.expand.filter</field>
      <field name="model">sale.order</field>
      <field name="inherit_id" ref="sale.sale_order_view_search_inherit_quotation"/>
      <field name="arch" type="xml">
        <xpath expr="//search" position="inside">
          <filter string="Total &lt; 1000" name="total_under_1000" domain="[('amount_total', '&lt;', 1000)]"/>
          <filter string="Total &gt;= 1000" name="total_above_1000" domain="[('amount_total', '&gt;=', 1000)]"/>
        </xpath>
      </field>
    </record>
</syntaxhighlight>
</div>


Dins del template, es declaren divs o el que necessitem per donar-li el aspecte definitiu. Odoo ja té en el seu CSS unes classes per al productes o partners que podem aprofitar. Per exemple, els oe_kanban_image per a fer la imatge d'una mida adequada o el oe_product_desc que ajuda a colocar el text al costat de la foto. En l'exemple, usem uns '''<a>''' amb dos tipus: open i edit. Segons el que posem, al fer click ens obri el form en mode vista o edició.
'''Domains'''


Si ja volem fer un kanban més avançat, tenim aquestes opcions:
Si volem que el action heredat sols mostre els elements que volem, s'ha de ficar un domain en el action:
* En la etiqueta '''<kanban>''':
** '''default_group_by''' per agrupar segons algun criteri al agrupar apareixen opcions per crear nous elements sense necessitat d'entrar al formulari.
** '''default_order''' per ordenar segons algun criteri si no s'ha ordenat en el tree.
** '''quick_create''' a true o false segons vulguem que es puga crear elements sobre la marxa sense el form. Per defecte és false si no està agrupat i true si està agrupat.
* En cada '''field''':
** '''sum, avg, min, max, count''' com a funcions d'agregació en els kanbans agrupats.
* Dins del '''template''':
** Cada '''field''' pot tindre un '''type''' que pot ser open, edit, action, delete.
* Una serie de funcions javascript:
** '''kanban_image()''' que accepta com a argument: model, field, id, cache i retorna una url a una imatge. La raó és perquè la imatge està en base64 i dins de la base de dades i cal convertir-la per mostrar-la.
** '''kanban_text_ellipsis(string[, size=160]) ''' per acurtar textos llargs, ja que el kanban sols és una previsualització.
** '''kanban_getcolor(raw_value)''' per a obtindre un color dels 0-9 que odoo te predefinits en el CSS.
 
Un exemple més complet i correcte:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
      <record model="ir.ui.view" id="music_kanban_view">
<field name="domain"> [('isplayer','=',True)]</field>  
            <field name="name">conservatori.music</field>
            <field name="model">conservatori.music</field>
            <field name="arch" type="xml">
            <kanban default_group_by="instrument" default_order="instrument" quick_create="true">
                    <field name="numero" sum="numero"/>
                    <templates>
                    <t t-name="kanban-box">
                            <div  t-attf-class="oe_kanban_color_{{kanban_getcolor(record.numero.raw_value)}}
                                                  oe_kanban_global_click_edit oe_semantic_html_override
                                                  oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
                                <a type="open">
                                    <img class="oe_kanban_image"
                                        t-att-src="kanban_image('conservatori.music', 'foto', record.id.value)" />
                                </a>
                                <div t-attf-class="oe_kanban_content">
                                    <h4>
                                        <a type="edit">
                                            <field name="name"></field>
                                        </a>
                                    </h4>
                                    <ul>
                                      <li>Group: <field name="grup"></field></li>
                                    </ul>
                                </div>
                            </div>
                        </t>
                    </templates>
                </kanban>
            </field>
        </record>
</syntaxhighlight>
</syntaxhighlight>
</div>


I ara el kanban del magatzem que és realment potent:
Amés, es pot dir que, per defecte, quan es crea un nou registre a través d'aquest action, tinga el field a True:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<kanban class="oe_background_grey" create="0">
<field name="context">{'default_is_player': True}</field>
                    <field name="complete_name"/>
                    <field name="color"/>
                    <field name="count_picking_ready"/>
                    <field name="count_picking_draft"/>
                    <field name="count_picking_waiting"/>
                    <field name="count_picking_late"/>
                    <field name="count_picking_backorders"/>
                    <templates>
                        <t t-name="kanban-box">
                            <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_stock_picking_type">
                                <div class="oe_dropdown_toggle oe_dropdown_kanban" groups="stock.group_stock_manager">
                                    <span class="oe_e">í</span>
                                    <ul class="oe_dropdown_menu">
                                        <t t-if="widget.view.is_action_enabled('edit')"><li><a type="edit">Edit...</a></li></t>
                                        <t t-if="widget.view.is_action_enabled('delete')"><li><a type="delete">Delete</a></li></t>
                                        <t t-if="widget.view.is_action_enabled('edit')"><li><ul class="oe_kanban_colorpicker" data-field="color"/></li></t>
                                    </ul>
                                </div>
                                <div class="oe_kanban_content">
                                    <h4 class="text-center"><strong><field name="complete_name"/></strong></h4>
                                    <div class="oe_right">
                                        <a name="open_barcode_interface" type="object">
                                            <img src="/stock/static/src/img/scan.png" alt="Click to launch the barcode interface" class="oe_stock_scan_image" title="Click to launch the barcode interface"/>
                                        </a>
                                    </div>
                                    <div class="oe_items_list oe_kanban_ellipsis">
                                        <div>
                                            <a name="354" type="action">
                                                <field name="count_picking_ready"/> Ready
                                            </a>
                                            <a name="353" type="action" class="oe_sparkline_bar_link">
                                                <field name="last_done_picking" widget="sparkline_bar" options="{'type': 'tristate', 'colorMap': {'0': 'orange', '-1': 'red', '1': 'green'}}">Last 10 Done Operations</field>
                                            </a>
                                        </div>
                                        <div t-if="record.count_picking_waiting.raw_value &gt; 0">
                                            <a name="356" type="action">
                                                <field name="count_picking_waiting"/> Waiting Availability
                                            </a>
                                        </div>
                                        <div>
                                            <a name="359" type="action">All Operations</a>
                                        </div>
                                    </div>
                                    <div class="oe_picking_type_gauge">
                                        <field name="rate_picking_late" widget="gauge" style="width:150px; height: 110px;" options="{'levelcolors': ['#a9d70b', '#f9c802', '#ff0000'], 'action_jump': '357'}">Late (%)</field>
                                        <field name="rate_picking_backorders" widget="gauge" style="width:150px; height: 110px;">Backorders (%)</field>
                                        <div class="oe_gauge_labels">
                                            <div class="oe_gauge_label_column">
                                                <a name="357" type="action">
                                                    <field name="count_picking_late"/> Late
                                                </a>
                                            </div>
                                            <div class="oe_gauge_label_column">
                                                <a name="358" type="action">
                                                    <field name="count_picking_backorders"/> Backorders
                                                </a>
                                            </div>
                                        </div>
                                    </div>
 
                                </div>
                            </div>
                        </t>
                    </templates>
                </kanban>
</syntaxhighlight>
</syntaxhighlight>
El widget '''gauge''' és instal·lat en un mòdul a banda del base, al igual que el '''sparkline''' (spakline no funciona desde la versió 11, ara es pot utilitzar dashboard_graph.)
</div>


'''Forms dins de kanbans''':
'''Filtre per defecte'''


A partir de la versió 12 es pot introduir un form dins d'un kanban, encara que es recomana que siga simple. Aquest funciona si tenim activat el '''quick_create''' i preferiblement quan el kanban està agrupat per Many2one o altres.
El problema en la solució anterior és que lleva la possibilitat de veure el que no tenen aquest field a True i cal anar per un altre action a modificar-los. Si volem poder veure tots, podem crear un filtre en la vista search i en l'action dir que volem aquest filtre per defecte:
Observem, per exemple el kanban de la secció de tasques del mòdul de proyecte:
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
<kanban default_group_by="stage_id" class="o_kanban_small_column o_kanban_project_tasks" on_create="quick_create"
<!--  En la vista search -->
quick_create_view="project.quick_create_task_form" examples="project">
...
....
    <search>
</kanban>
        <filter name="player_partner" string="Is Player" domain="[('is_player','=',True)]" />
</syntaxhighlight>
    </search>
Com podem observar, té activat el '''quick_create''' i una referència al identificador extern d'una vista form en '''quick_create_view'''. Aquest és el contingut del form:
...
<syntaxhighlight lang="xml" style="font-family:monospace">
<!-- En l'action -->
<?xml version="1.0"?>
            <!--  <field name="domain"> [('is_player','=',True)]</field> -->
<form>
            <field name="domain"></field>
  <group>
            <field name="context">{'default_is_player': True, 'search_default_player_partner': 1}</field>
    <field name="name" string="Task Title"/>
    <field name="user_id" options="{'no_open': True,'no_create': True}"/>
  </group>
</form>
</syntaxhighlight>
</syntaxhighlight>


=== Vistes search ===
=== Herència en el controlador ===
 
L’herència en el controlador és un mecanisme conegut, ja que l’apliquem de forma inconscient quan ens veiem obligats a sobreescriure els mètodes de la capa ORM d’OpenObject en el disseny de molts mòduls.


Les vistes search tenen 3 tipus:
{{nota|'''Funció super()'''


* '''field''' que permeten buscar en un determinat camp.
El llenguatge Python recomana utilitzar la funció super() per invocar el mètode de la classe base quan s’està sobreescrivint en una classe derivada, en lloc d’utilitzar la sintaxi nomClasseBase.metode(self…).}}
* '''filter''' amb '''domain''' per filtrar per un valor predeterminat.
* '''filter''' amb '''group''' per agrupar per algun criteri.


Pel que fa a les search '''field''', sols cal indicar quins fields seran buscats.
L’efecte de l’herència en el controlador es manifesta únicament quan cal sobreescriure algun dels mètodes de l’objecte del qual es deriva i per a fer-ho adequadament cal tenir en compte que el mètode sobreescrit en l’objecte derivat:
<syntaxhighlight lang="xml" style="font-family:monospace">
<search>
    <field name="name"/>
    <field name="inventor_id"/>
</search>
</syntaxhighlight>
{{nota|Els fields han de ser guardats en la base de dades, encara que siguen de tipus '''computed'''}}
Les '''field''' poden tindre un '''domain''' per especificar quin tipus de búsqueda volem. Per exemple:
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="description" string="Name and description"
    filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
</syntaxhighlight>
Busca per ‘name’ i ‘description’ amb un domini que busca que es parega en “case-insensitive” (ilike) el que escriu l’usuari (self) amb el name o amb la descripció.


o:
*    A vegades vol substituir el mètode de l’objecte base sense aprofitar-ne cap funcionalitat: el mètode de l’objecte derivat no invoca el mètode sobreescrit.
<syntaxhighlight lang="xml" style="font-family:monospace">
*    A vegades vol aprofitar la funcionalitat del mètode de l’objecte base: el mètode de l’objecte derivat invoca el mètode sobreescrit.
<field name="cajones" string="Boxes or @" filter_domain="['|',('cajones','=',self),('arrobas','=',self)]"/>
</syntaxhighlight>
Busca per ''cajones o arrobas'' sempre que l'usuari pose el mateix número.


Les '''filter''' amb '''domain''' són per a predefinir filtres o búsquedes. Per exemple:
Exemples:
<syntaxhighlight lang="xml" style="font-family:monospace">
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<filter name="my_ideas" string="My Ideas" domain="[('inventor_id', '=', uid)]"/>
[http://www.odoo.yenthevg.com/override-create-functions-odoo/ Sobreescriure el mètode '''create''']:
<filter name="more_100" string="More than 100 boxes" domain="[('cajones','>',100)]"/>  
<syntaxhighlight lang="python" style="font-family:monospace">
<filter name="Today" string="Today" domain="[('date', '&gt;=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')),
class res_partner(models.Model):
                                            ('date', '&lt;=',datetime.datetime.now().strftime('%Y-%m-%d 23:23:59'))]"/>
    _inherit = 'res.partner'
</syntaxhighlight>
    passed_override_write_function = fields.Boolean(string='Has passed our super method')
 
{{nota|Els filtres sols poden comparar un field amb un valor específic. Així que si volem comparar dos fields cal fer una funció.}}
    @api.model
 
    def create(self, values):
Operadors per als domains:
        # Override the original create function for the res.partner model
<div  class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
        record = super(res_partner, self).create(values)
'like': [('input', 'like', 'open')] - Returns case sensitive (wildcards - '%open%') search.
 
        # Change the values of a variable in this super function
O/p: open, opensource, openerp, Odooopenerp
        record['passed_override_write_function'] = True
 
        print 'Passed this function. passed_override_write_function value: ' + str(record['passed_override_write_function'])
'not like': [('input', 'not like', 'open')] - Returns results not matched with case sensitive (wildcards - '%open%') search.
 
        # Return the record so that the changes are applied and everything is stored.
O/p: Openerp, Opensource, Open, Odoo, odoo, OdooOpenerp
return record
</syntaxhighlight>
</div>


'=like': [('name', '=like', 'open')] - Returns exact (= 'open') case sensitive search.
== El controlador ==


O/p: open
Part del controlador l'hem mencionat al parlar dels camps '''computed'''. No obstant, cal comentar les facilitats que proporciona Odoo per a no tindre que accedir dirèctament a la base de dades.


'ilike': [('name', 'ilike', 'open')] - Returns exact case insensitive (wildcards - '%open%') search.
La capa '''ORM''' d’Odoo facilita uns mètodes que s’encarreguen del mapatge entre els objectes Python i les taules de PostgreSQL. Així, disposem de mètodes per crear, modificar, eliminar i cercar registres a la base de dades.


O/p: Openerp, openerp, Opensource, opensource, Open, open, Odooopenerp, OdooOpenerp
En ocasions, pot ser necessari alterar l’acció automàtica de cerca – creació – modificació – eliminació facilitada per Odoo i haurem de sobreescriure els corresponents mètodes en les nostres classes.


'not ilike': [('name', 'not ilike', 'open')] - Returns results not matched with exact case insensitive (wildcards - '%open%') search.
Els programadors en el framework d'Odoo hem de conèixer els mètodes subministrats per la capa ORM i hem de dominar el disseny de mètodes per:


O/p: Odoo, odoo
*    Poder definir camps funcionals en el disseny del model.
*    Poder definir l’acció que cal executar en modificar el contingut d’un field d’una vista form (@api.onchange)
*    Poder alterar les accions automàtiques de cerca, creació, modificació i eliminació de recursos.


'=ilike': [('name', '=ilike', 'open')] - Returns exact (= 'open' or 'Open') case insensitive search.
Una darrera consideració a tenir en compte en l’escriptura de mètodes i funcions en Odoo és que els textos de missatges inclosos en mètodes i funcions, per poder ser traduïbles, han de ser introduïts amb la sintaxi _('text') i el fitxer .py ha de contenir from tools.translate import _ a la capçalera.  


O/p: Open, open
=== API de l'ORM ===


'=?':
{{nota|'''Interactuar en la terminal'''
$ odoo shell -d castillo -u containers
[https://asciinema.org/a/123126 Asciinema amb alguns exemples]
Observa cóm hem ficat el paràmetre '''shell'''. Les coses que se fan en la terminal no són persistents en la base de dades fins que no s'executa '''self.env.cr.commit()'''. Dins de la terminal podem obtindre ajuda dels mètodes d'Odoo amb help(), per exemple: help(tools.image)
Amb el següent exemple, podem arrancar odoo sense molestar a l'instància que està en marxa redefinint els ports:
$ odoo shell -c /path/to/odoo.conf --xmlrpc-port 8888 --longpolling-port 8899
Documentació: [https://medium.com/@RafnixG/explorando-odoo-a-fondo-c%C3%B3mo-trabajar-con-la-shell-de-la-cli-y-configurar-ipython-como-repl-8f7bd04a26d] [https://medium.com/@RafnixG/shell-de-odoo-domina-operaciones-avanzadas-integraci%C3%B3n-de-librer%C3%ADas-y-automatizaci%C3%B3n-de-tareas-2e85c7d81d34]
}}


name = 'odoo' parent_id = False [('name', 'like', name), ('parent_id', '=?', parent_id)] - Returns name domain result & True
Un mètode creat dins d'un model actua sobre tots els elements del model que estiguen actius en el moment de cridar al mètode. Si és un tree, seran molts i si és un form sols un. Però en qualsevol cas és una 'llista' d'elements i es diu '''recordset'''.


name = 'odoo' parent_id = 'openerp' [('name', 'like', name), ('parent_id', '=?', parent_id)] - Returns name domain result & parent_id domain result
Bàsicament la interacció amb els models en el controlador es fa amb els anomenats '''recordsets''' que són col·leccions d'objectes sobre un model. Si iterem dins dels recordset , obtenim els '''singletons''', que són objectes individuals de cada línia en la base de dades.
 
<syntaxhighlight lang="python" style="font-family:monospace">
'=?' is a short-circuit that makes the term TRUE if right is None or False, '=?' behaves like '=' in other cases
def do_operation(self):
 
    print self # => a.model(1, 2, 3, 4, 5)
'in': [('value1', 'in', ['value1', 'value2'])] - in operator will check the value1 is present or not in list of right term
    for record in self:
 
        print record # => a.model(1), then a.model(2), then a.model(3), ...
'not in': [('value1', 'not in', ['value2'])] - not in operator will check the value1 is not present in list of right term While these 'in' and 'not in' works with list/tuple of values, the latter '=' and '!=' works with string
 
'=': value = 10 [('value','=',value)] - term left side has 10 in db and term right our value 10 will match
 
'!=': value = 15 [('value','!=',value)] - term left side has 10 in db and term right our value 10 will not match
 
'child_of': parent_id = '1' #Agrolait 'child_of': [('partner_id', 'child_of', parent_id)] - return left and right list of partner_id for given parent_id
 
'<=', '<', '>', '>=': These operators are largely used in openerp for comparing dates - [('date', '>=', date_begin), ('date', '<=', date_end)]. You can use these operators to compare int or float also.
 
</div>
 
Els '''filter''' amb '''group''' agrupen per algun field:
 
<syntaxhighlight lang="xml" style="font-family:monospace">
<group string="Group By">
        <filter name="group_by_inventor" string="Inventor" context="{'group_by': 'inventor_id'}"/>
</group>
o:
</syntaxhighlight>
</syntaxhighlight>
<syntaxhighlight lang="xml" style="font-family:monospace">
Podem accedir a tots els fields d'un model sempre que estem en un singleton, no en un recordset:
  <filter name="group_by_matricula" string="Matricula" context="{'group_by': 'matricula'}"/>
<syntaxhighlight lang="python" style="font-family:monospace">
>>> record.name
Example Name
>>> record.company_id.name
Company Name
>>> record.name = "Bob"
</syntaxhighlight>
</syntaxhighlight>
Intentar llegir o escriure un field en un recordset donarà un error. Accedir a un '''many2one, one2many o many2many''' donarà un recordset.


Si agrupem per data, el grup és per defecte per cada mes, si volem agrupar per dia:
==== Set operations ====
<syntaxhighlight lang="xml" style="font-family:monospace">
Els recordsets es poden combinar amb operacions específiques que són les típiques dels conjunts:
<filter name="group_by_exit_day" string="Exit" context="{'group_by': 'exit_day:day'}"/>
* '''record in set''' retorna si el record està en el set
* '''set1 | set2''' Unió de sets
* '''set1 & set2''' Intersecció de sets
* '''set1 - set2''' Diferència de sets
 
Amés, un recordset no té elements repetits i permet accedir a recordsets dins d'ell. Per exemple:
<syntaxhighlight lang="python" style="font-family:monospace">
>>> record.students.classrooms
</syntaxhighlight>
</syntaxhighlight>
Dona la llista de totes les classes de tots els estudiants i sense repetir cap.


Si volem que un filtre estiga predefinit s'ha de posar en el context de '''l'action''':
==== Programació funcional en l'ORM ====
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="context">{'search_default_clients':1,"default_is_client": True}</field>
</syntaxhighlight>
En aquest exemple, filtra amb en '''search_default_XXXX''' que activa el filtre XXXX i, amés, fa que en els formularis tiguen un camp boolean a true.


=== Vistes Calendar ===
Python una serie de funcions que permeten iterar una llista i aplicar una funció als elements. Les més utilitzades són map(), filter(), reduce(), sort(), zip()... Odoo treballa en recordsets, no llistes, i té les seues funcions pròpies per a imitar aquestes:
Si el recurs un camp date o datetime.
Permet editar els recursos ordenats per temps.
L’exemple són els esdeveniments del mòdul de ventes.
* '''string''', per al títol de la vista
* '''date_start''', que ha de contenir el nom d’un camp datetime o date del model.
* '''date_delay''', que ha de contenir la llargada en hores de l’interval.
* '''date_stop''', Aquest atribut és ignorat si existeix l’atribut date_delay.
* '''day_length''', per indicar la durada en hores d’un dia. OpenObject utilitza aquest valor per calcular la data final a partir del valor de date_delay. Per defecte, el seu valor és 8 hores.
* '''color''', per indicar el camp del model utilitzat per distingir, amb colors, els recursos mostrats a la vista.
* '''mode''', per mostrar l’enfoc (dia/setmana/mes) amb el què s’obre la vista. Valors possibles: day, week, month. Per defecte, month.


<syntaxhighlight lang="xml" style="font-family:monospace">
* '''filtered()''' Filtra el recordset de manera que sols tinga els records que complixen una condició.
<record model="ir.ui.view" id="session_calendar_view">
<syntaxhighlight lang="python" style="font-family:monospace">
            <field name="name">session.calendar</field>
records.filtered(lambda r: r.company_id == user.company_id)
            <field name="model">openacademy.session</field>
records.filtered("partner_id.is_company")
            <field name="arch" type="xml">
</syntaxhighlight>
                <calendar string="Session Calendar" date_start="start_date"
* '''sorted()''' Ordena segons uns funció, se defineix una funció lambda (key) que indica que s'ordena per el camp name:
                          date_stop="end_date"
<syntaxhighlight lang="python" style="font-family:monospace">
                          color="instructor_id">
# sort records by name
                    <field name="name"/>
records.sorted(key=lambda r: r.name)
                </calendar>
records.sorted(key=lambda r: r.name, reverse=True)
            </field>
</syntaxhighlight>
        </record>
* '''mapped()''' Li aplica una funció a cada recordset i retorna un recordset amb els canvis demanats:
<syntaxhighlight lang="python" style="font-family:monospace">
# returns a list of summing two fields for each record in the set
records.mapped(lambda r: r.field1 + r.field2)
# returns a list of names
records.mapped('name')
# returns a recordset of partners
record.mapped('partner_id')
# returns the union of all partner banks, with duplicates removed
record.mapped('partner_id.bank_ids')
</syntaxhighlight>
</syntaxhighlight>


=== Vistes Graph ===
Aquestes funcions són útils per a fer tècniques de [https://docs.python.org/3.7/howto/functional.html programació funcional]


Pot contenir els següents atributs:
====Enviroment====
* '''string''', per al títol de la vista
* '''type''', per al tipus de gràfic. (no fa cas en Odoo 9)
* '''orientation'''


La definició dels elements fills de l’element arrel graph determina el contingut del gràfic:
L'anomenat enviroment o '''env''' guarda algunes dades contextuals interessants per a treballar amb l'ORM, com ara el cursor a la base de dades, l'usuari actual o el context (que guarda algunes metadades).  
* Primer camp: eix X (horitzontal). Obligatori.
* Segon camp: eix Y (vertical). Obligatori.
* Tercer camp: eix Z (tridimensionals). Optatiu.
 
A cadascun dels camps que determinen els eixos, se’ls pot aplicar els atributs següents:
 
* '''group=“True”''', a utilitzar en el segon o tercer camp, per indicar que cal agrupar tots els recursos d’igual valor per aquest camp. El primer camp sempre actua amb group=“True”. Per la resta de camps, s’aplica l’operador indicat a l’atribut operator.
* '''operator''', per indicar l’operador a utilitzar per calcular el valor en la resta dels camps, com a conseqüència de l’agrupació indicada a l’atribut group. Els operadors permesos són: + (suma), *(producte), **(exponent), min i max (menor i major valors de la llista de valors de l’agrupació). Per defecte, actua l’operador +.
 
<syntaxhighlight lang="xml" style="font-family:monospace">
        <record model="ir.ui.view" id="openacademy_session_graph_view">
            <field name="name">openacademy.session.graph</field>
            <field name="model">openacademy.session</field>
            <field name="arch" type="xml">
                <graph string="Participations by Courses">
                    <field name="course_id"/>
                    <field name="attendees_count" type="measure"/>
                </graph>
            </field>
        </record>


Tots els recordsets tenen un enviroment accesible amb env. Quant volem crear un recordset dins d'un altre, podem usar env:
<syntaxhighlight lang="python" style="font-family:monospace">
>>> self.env['res.partner']
res.partner
>>> self.env['res.partner'].search([['is_company', '=', True], ['customer', '=', True]])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
</syntaxhighlight>
</syntaxhighlight>


Si volem ficar-lo dins d'un form comun camp one2many, cal especificar un domain:
El primer cas crea un recordset buit però que fa referència a res.partner i es poden fer les funcions de l'ORM que necessitem.
<syntaxhighlight lang="xml" style="font-family:monospace">
  <field name="id"/> <!-- És necessari el camp id, pot ser invisible -->
  <field name="rounds" mode="tree,graph" domain="[('team','=', id)]" >
      <graph string="Points" type="line">
          <field name="round"/>
          <field name="point_v" />
      </graph>
</syntaxhighlight>


{{nota|Les vistes graph en Odoo són molt limitades, sols accepten un element en les X i necessiten que els camps estiguen guardats en la base de dades}}
=====Context=====


El type="line" sembla no funcionar en Odoo 9 , per tant es deu ficar en el context:
El context és un diccionari de python que conté dades útils per a totes les vistes i els mètodes. Les funcions d'Odoo reben el context i el consulten si cal. Context pot tindre de tot, però quasi sempre té al menys el user ID, l'idioma o la zona temporal. Quant Odoo va a renderitzar una vista XML, consulta el context per veure si ha d'aplicar algun paràmetre.
 
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
print(self.env.context)
<field name="context">{'graph_mode':'line'}</field>
</syntaxhighlight>
</syntaxhighlight>
Al llarg de tot aquest manual utilitzem sovint paràmetres del context. Aquests són els paràmetres que hem utilitzat en algun moment:
* active_id : self._context.get('active_id') es tracta de l'id de l'element del model que està en pantalla.
* active_ids : Llista de les id seleccionats en un tree.
* active_model : El model actual.
* default_<field> : En un action o en un one2many es pot assignar un valor per defecte a un field.
* search_default_<filter> : Per aplicar un filtre per defecte a la vista en un '''action'''.
* group_by : Dins d'un camp '''filter''' per a crear agrupacions en les vistes '''search'''.
* graph_mode : En les vistes '''graph''', aquest paràmetre canvia el '''type'''
* context.get : En les vistes es pot treure algunes dades del context per a mostrar condicionalment o per als ''domains''


== Els reports ==
El context va passant d'un mètode a un altre o a les vistes i, de vegades volem modificar-lo.


El nou motor de reports utilitza una combinació de '''QWeb, BootStrap i Wkhtmltopdf'''.
Imaginem que volem fer un botó que obriga un [[Odoo#Wizards|wizard]], però volem passar-li '''paràmetres''' al wizard. En els botons i fields relacionals es pot especificar un context:
 
<syntaxhighlight lang="xml" style="font-family:monospace">
{{nota|Pot ser que el wkhtmltopdf de la distribució no funcione. Cal anar a https://github.com/wkhtmltopdf/wkhtmltopdf/releases/ i descarregar el '''.deb''' de la versió estable més alta. S'instal·larà amb '''dpkg -i'''
<button name="%(reserves.act_w_clients_bookings)d" type="action" string="Select bookings" context="{'b_fs':bookings_fs}"/>
 
</syntaxhighlight>
Amb '''wkhtmltopdf -V''' podem comprovar si la versió correcta s'ha instal·lat. }}
Eixe action obre un wizard, que és un model transitori en el que podem definir un field amb els continguts del context:
 
<syntaxhighlight lang="python" style="font-family:monospace">
Un report consta de dos elements:
def _default_bookings(self):
* Un registre en la base de dades en el model: '''ir.actions.report.xml''' amb els paràmetres bàsics
        return self._context.get('b_fs')
* Una vista [https://www.odoo.com/documentation/8.0/reference/qweb.html Qweb] per al contingut.
bookings_fs = fields.Many2many('reserves.bookings',readonly=True, default=_default_bookings)
 
Per exemple, en el xml:
<syntaxhighlight lang="xml" style="font-family:monospace">
<report
        id="report_session"
        model="openacademy.session"
        string="Session Report"
        name="openacademy.report_session_view"
        file="openacademy.report_session"
        report_type="qweb-pdf" />
 
    <template id="report_session_view">
        <t t-call="report.html_container">
            <t t-foreach="docs" t-as="doc">
                <t t-call="report.external_layout">
                    <div class="page">
                        <h2 t-field="doc.name"/>
                        <p>From <span t-field="doc.start_date"/> to <span t-field="doc.end_date"/></p>
                        <h3>Attendees:</h3>
                        <ul>
                            <t t-foreach="doc.attendee_ids" t-as="attendee">
                                <li><span t-field="attendee.name"/></li>
                            </t>
                        </ul>
                    </div>
                </t>
            </t>
        </t>
    </template>
</syntaxhighlight>
</syntaxhighlight>
Aquest many2many tindrà els mateixos elements que el form que l'ha cridat. (Això és com el [[Odoo#Millores_en_les_vistes_form|'''default_''']] en els One2many, però fet a mà)


Els reports simplifiquen amb l'etiqueta '''report''' la creació d'un action de tipus report. Automàticament situen un botó dalt del tree o form per imprimir.
També es pot utilitzar aquesta manera d'enviar un recordset per un context per al '''domain''' d'un field Many2one o Many2many:
 
<syntaxhighlight lang="python" style="font-family:monospace">
Una mínima template que funciona:
def _domain_bookings(self):
<syntaxhighlight lang="xml" style="font-family:monospace">
        return [('id','=',self._context.get('b_fs').ids)]
<template id="report_invoice">
bookings_fs = fields.Many2many('reserves.bookings',readonly=True, domain=_default_bookings)
    <t t-call="report.html_container">
        <t t-foreach="docs" t-as="o">
            <t t-call="report.external_layout">
                <div class="page">
                    <h2>Report title</h2>
                    <p>This object's name is <span t-field="o.name"/></p>
                </div>
            </t>
        </t>
    </t>
</template>
</syntaxhighlight>
</syntaxhighlight>


Analitzem aquesta template:
En ocasions necessitem especificar valors per defecte i filtres per defecte en un '''action'''. Per exemple, quan implementem l'herència, volem que els nous registres que es facen en el nostre '''action''' tinguem un valor per defecte. En el següent exemple, en la primera línia és el que es sol fer en la [[#Her.C3.A8ncia_en_la_vista | Herència]] i en la segona estem especificant un [[#External_Ids | External ID]] amb '''ref()''' dins d'un eval.
* '''external_layout''': Afegeix la capçalera i el peu per defecte de Odoo.
<syntaxhighlight lang="python" style="font-family:monospace">
* Dins de '''<div class="page">''': Està el contingut del report.
        <field name="context">{'default_is_player': True, 'search_default_player_partner': 1}</field>
* '''id''': A de ser el mateix que el name del report.
        <field name="context" eval="{'default_partner_id':ref('base.main_partner'), 'company_hide':False, 'default_company_id':ref('base.main_company'), 'search_default_my_bank':1}"/>
* '''docs''': Llista d'objectes a imprimir. (Paregut a self)
 
Es poden afegir css locals o externs al report heredant el template e insertant el css:
<syntaxhighlight lang="xml" style="font-family:monospace">
<template id="report_saleorder_style" inherit_id="report.layout">
  <xpath expr="//style" position="after">
    <style type="text/css">
      .example-css-class {
        background-color: red;
      }
    </style>
  </xpath>
</template>
</syntaxhighlight>
</syntaxhighlight>


Per afegir una imatge de la base de dades:
El context és un diccionari inmutable (frozendict) que no pot ser alterat en funcions. no obstant, si volem modificar el context actual per enviar-lo a un action o cridar a una funció d'un model amb un altre context, es pot fer amb '''[https://www.odoo.com/documentation/11.0/reference/orm.html#odoo.models.Model.with_context with_context]''':
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
<span t-field="doc.logo" t-field-options="{&quot;widget&quot;: &quot;image&quot;, &quot;class&quot;: &quot;img-rounded&quot;}"/>
# current context is {'key1': True}
r2 = records.with_context({}, key2=True)
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}
</syntaxhighlight>
</syntaxhighlight>


'''Notes sobre QWeb'''
Si és precís modificar el context es pot fer:
<syntaxhighlight lang="python" style="font-family:monospace">
self.env.context = dict(self.env.context)
self.env.context.update({'key': 'val'})
</syntaxhighlight>
o
<syntaxhighlight lang="python" style="font-family:monospace">
self = self.with_context(get_sizes=True)
print self.env.context
</syntaxhighlight>
Però no funciona més enllà del recordset actual. És a dir, no modifica el context en el que s'ha cridat.


QWeb és el motor de plantilles de Odoo. Els elements són etiquetes XML que comencen per '''t-'''  
Si el que volem és passar el valor d'un field per context a un botó dins d'una 'subvista', podem utilitzar el paràmetre '''parent''', que funciona tant en en '''domain''', '''attr''', com en context. Ací tenim un exemple de tree dins d'un field amb botons que envíen per context coses del pare:
* t-field: Per mostrar el contingut d'un field
* t-if: Per fer condicionals. Per fer un condicional en funció de si un field està o no, sols cal ficar el field en questió dins del condicional.
<syntaxhighlight lang="xml" style="font-family:monospace">
<syntaxhighlight lang="xml" style="font-family:monospace">
  <t t-if="viatge.hotel">
<field name="movies" >
     <!-- ... -->
     <tree>
  </t>
        <field name="photo_small"/>
        <field name="name"/>
        <field name="score" widget='priority'/>
        <button name="book_it" string="Book it" type="object" context="{'b_client':parent.client,'b_day':parent.day}"/>
    </tree>
</syntaxhighlight>
</syntaxhighlight>
* t-foreach: Per fer bucles per els elements d'un one2many, per exemple. 


'''Depurar els reports'''
Podem passar el context per un action i el podem utilitzar en la vista, ja que tenim l'objecte '''context''' disponible en QWeb. Si, per exemple, volem retornar un action que cride a una vista i un field tinga un domain passat per context:


Because reports are standard web pages, they are available through a URL and output parameters can be manipulated through this URL, for instance the HTML version of the Invoice report is available through http://localhost:8069/report/html/account.report_invoice/1 (if account is installed) and the PDF version through http://localhost:8069/report/pdf/account.report_invoice/1.
<syntaxhighlight lang="python" style="font-family:monospace">
    return {
            'name': 'Travel wizard action',
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
            'context': dict(self._context, cities_available_context= (self.cities_available.city).ids),
        }
</syntaxhighlight>
<syntaxhighlight lang="xml" style="font-family:monospace">
          <field name="destiny"
                  domain = "[('id','in',context.get('cities_available_context',[]))]"
                  />
</syntaxhighlight>


Més informació https://www.odoo.com/documentation/8.0/reference/reports.html
==== Mètodes de l'ORM ====


== Herència ==
=====search()=====
 
A partir d'un '''domain''' de Odoo, proporciona un recordset amb tots els elements que coincideixen:
El framework OpenObject facilita el mecanisme de l’herència per tal que els programadors puguin adaptar mòduls existents i garantir a la vegada que les actualitzacions dels mòduls no destrossin les adequacions desenvolupades.
<syntaxhighlight lang="python" style="font-family:monospace">
 
>>> # searches the current model
L’herència es pot aplicar en els tres components del patró MVC:
>>> self.search([('is_company', '=', True), ('customer', '=', True)])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
>>> self.search([('is_company', '=', True)], limit=1).name
'Agrolait'
</syntaxhighlight>
{{nota| Es pot obtindre la quantitat d'elements amb el mètode '''search_count()'''}}
<syntaxhighlight lang="python" style="font-family:monospace">
Parameters
    args -- A search domain. Use an empty list to match all records.
    offset (int) -- number of results to ignore (default: none)
    limit (int) -- maximum number of records to return (default: all)
    order (str) -- sort string
    count (bool) -- if True, only counts and returns the number of matching records (default: False)
</syntaxhighlight>


*    En el model: possibilita ampliar les classes existents o dissenyar noves classes a partir de les existents.
=====create()=====
*    En la vista: possibilita modificar el comportament de vistes existents o dissenyar noves vistes.
Te dona un recordset a partir d'una definició de varis fields:
*    En el controlador: possibilita sobreescriure els mètodes existents o dissenyar-ne de nous.
<syntaxhighlight lang="python" style="font-family:monospace">
>>> self.create({'name': "New Name"})
res.partner(78)
</syntaxhighlight>




OpenObject proporciona tres mecanismes d’herència: l’herència de classe, l’herència per prototip i l’herència per delegació.
El mètode '''create''' s'utilitza sovint per a ser sobreescrit en herència per fer coses en el moment de la creació. Ací tenim un exemple en el que modifiquem el '''create''' d'un model per crear una instància associada amb una imatge predefinida:
 
<syntaxhighlight lang="python" style="font-family:monospace">
{|  class="wikitable" 
    @api.model
!    Mecanisme
    def create(self, values):
!    Característiques
        new_id = super(player, self).create(values)
!    Com es defineix
        print values
|- 
        name_player = new_id.name
|    <strong>De classe</strong>
        img = self.env['mmog.fortress'].search([('name','=','f1')])[0].icon
|    - Herència simple. <br/>
        self.env['mmog.fortress'].create({'name':name_player+"-fortress",'level':1,'soldiers':100,'population':10,'food':1000,'integrity':100,'id_player':new_id.id,'icon':img})
- La classe original queda substituïda per la nova classe. <br/>
        return new_id
- Afegeix noves funcionalitats (atributs i/o mètodes) a la classe original. <br/>
</syntaxhighlight>
- Les vistes definides sobre la classe original continuen funcionant. <br/>
 
- Permet sobreescriure mètodes de la classe original. <br/>
=====write()=====
- En PostgreSQL, continua mapada en la mateixa taula que la classe original, ampliada amb els nous atributs que pugui incorporar.
Escriu uns fields dins de tots els elements del recordset, no retorna res:
|    - S’utilitza l’atribut <code>_inherit</code> en la definició de la nova classe Python: <code>_inherit = obj</code><br/>
<syntaxhighlight lang="python" style="font-family:monospace">
- El nom de la nova classe ha de continuar sent el mateix que el de la classe original: <code>_name = obj</code>
self.write({'name': "Newer Name"})
|- 
</syntaxhighlight>
|    <strong>Per prototip</strong>
 
|    - Herència simple. <br/>
'''Escriure en un many2many''':
- Aprofita la definició de la classe original (com si fos un «prototipus»). <br/>
- La classe original continua existint. <br/>
- Afegeix noves funcionalitats (atributs i/o mètodes) a les aportades per la classe original. <br/>
- Les vistes definides sobre la classe original no existeixen (cal dissenyar-les de nou). <br/>
- Permet sobreescriure mètodes de la classe original. <br/>
- En PostgreSQL, queda mapada en una nova taula.  
|    - S’utilitza l’atribut <code>_inherit</code> en la definició de la nova classe Python: <code>_inherit = obj</code><br/>
- Cal indicar el nom de la nova classe: <code>_name = nou_nom</code>
|- 
|    <strong>Per delegació</strong>
|    - Herència simple o múltiple. <br/>
- La nova classe «delega» certs funcionaments a altres classes que incorpora a l’interior. <br/>
- Els recursos de la nova classe contenen un recurs de cada classe de la que deriven. <br/>
- Les classes base continuen existint. <br/>
- Afegeix les funcionalitats pròpies (atributs i/o mètodes) que correspongui. <br/>
- Les vistes definides sobre les classes bases no existeixen a la nova classe. <br/>
- En PostgreSQL, queda mapada en diferents taules: una taula per als atributs propis, mentre que els recursos de les classes derivades resideixen en les taules corresponents a les dites classes.  
|    - S’utilitza l’atribut <code>_inherits</code> en la definició de la nova classe Python: <code>_inherits = … </code><br/>
- Cal indicar el nom de la nova classe: <code>_name = nou_nom</code>
|}
[[Archivo:Inheritance methods.png]]


El fitxer __openerp__.py ha de contindre les dependències de la clase heretada.
La manera més senzilla és passar una llista d'ids. Però si ja existeixen elements abans, necessitem uns codis especials (vegeu [[Odoo#Expressions]]):


=== Herència en el Model ===
Per exemple:
 
<syntaxhighlight lang="python" style="font-family:monospace">
El disseny d’un objecte d’OpenObject heretat és paregut al disseny d’un objecte d’OpenObject no heretat; únicament hi ha dues diferències:
self.sessions = [(4,s.id)]
self.write({'sessions':[(4,s.id)]})
self.write({'sessions':[(6,0,[ref('vehicle_tag_leasing'),ref('fleet.vehicle_tag_compact'),ref('fleet.vehicle_tag_senior')] )]})
</syntaxhighlight>


*  Apareix l’atribut '''_inherit''' o '''_inherits''' per indicar l’objecte (herència simple) o els objectes (herència múltiple) dels quals deriva el nou objecte. La sintaxi a seguir és:
=====browse()=====
A partir d'una llista de ids, retorna un recordset.
<syntaxhighlight lang="python" style="font-family:monospace">
>>> self.browse([7, 18, 12])
res.partner(7, 18, 12)
</syntaxhighlight>


_inherit = 'nom.objecte.del.que.es.deriva'
_inherits = {'nom.objecte1':'nom_camp_FK1', ...}


*    En cas d’herència simple, el nom (atribut _name) de l’objecte derivat pot coincidir o no amb el nom de l’objecte pare. També és possible no indicar l’atribut _name, fet que indica que el nou objecte manté el nom de l’objecte pare.
=====exists()=====
Retorna si un record en concret encara està en la base de dades.
<syntaxhighlight lang="python" style="font-family:monospace">
if not record.exists():
    raise Exception("The record has been deleted")
o:
records.may_remove_some()
# only keep records which were not deleted
records = records.exists()
</syntaxhighlight>
En el segon exemple, refresca un recordset amb aquells que encara existixen.


L’herència simple (_inherit) amb atribut _name idèntic al de l’objecte pare, s’anomena herència de classe i en ella el nou objecte substitueix l’objecte pare, tot i que les vistes sobre l’objecte pare continuen funcionant. Aquest tipus d’herència, la més habitual, s’utilitza quan es vol afegir ''fields'' i/o modificar propietats de dades existents i/o modificar el funcionament d’alguns mètodes. En cas d’afegir dades, aquestes s’afegeixen a la taula de la base de dades en la qual estava mapat l’objecte pare.  
=====ref()=====
 
Retorna un singleton a partir d'un [[Odoo#External_Ids|'''External ID''']].  


<div style="border: 1px dotted #ddd; width:80%; margin:auto; padding:10px; background-color:#fefffe">
'''Exemple d'herència de classe'''
L’herència de classe la trobem en molts mòduls que afegeixen dades i mètodes a objectes ja existents, com per exemple, el mòdul comptabilitat (account) que afegix dades i mètodes a l’objecte res.partner. Fixem-nos en el contingut del mòdul account:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
    class res_partner(Model.model):
>>> env.ref('base.group_public')
    _name = 'res.partner'
res.groups(2)
    _inherit = 'res.partner'
    debit_limit = fields.float('Payable limit')
    ...
</syntaxhighlight>
</syntaxhighlight>
Podeu comprovar que la taula res_partner d’una empresa sense el mòdul account instal·lat no conté el camp debit_limit, que en canvi sí que hi apareix una vegada instal·lat el mòdul.


Odoo té molts mòduls que deriven de l’objecte res.partner per afegir-hi característiques i funcionalitats.
=====ensure_one()=====
</div>
S'asegura de que el record en concret siga un singleton.
<syntaxhighlight lang="python" style="font-family:monospace">
records.ensure_one()
# is equivalent to but clearer than:
assert len(records) == 1, "Expected singleton"
</syntaxhighlight>


L’herència simple (_inherit) amb atribut '''_name''' diferent al de l’objecte pare, s’anomena '''herència per prototip''' i en ella es crea un nou objecte que aglutina les dades i mètodes que tenia l’objecte del qual deriva, juntament amb les noves dades i mètodes que pugua incorporar el nou objecte. En aquest cas, sempre es crea una nova taula a la base de dades per mapar el nou objecte.  
=====unlink()=====
Esborra de la base de dades els elements del recordset actual.  


<div style="border: 1px dotted #ddd; width:80%; margin:auto; padding:10px; background-color:#fefffe">
Exemple de cóm sobreescriure el mètode unlink per a esborrar en cascada:
'''Exemple d'herència per prototip'''
L’herència per prototip és difícil de trobar en els mòduls que incorpora Odoo. Un exemple el tenim en el mòdul base_calendar en el qual podem observar el mòdul comptabilitat (account) que afegix dades i mètodes a l’objecte res.partner. Fixem-nos en el contingut del mòdul account:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
     class res_alarm(Model.model):
     def unlink(self):
    _name = 'res.alarm'
        for x in self:
    ...
            x.catid.unlink()
    class calendar_alarm(Model.model):
        return super(product_uom_class, self).unlink()
    _name = 'calendar.alarm'
    _inherit = 'res.alarm'
    ...
</syntaxhighlight>
</syntaxhighlight>
En una empresa que tingui el mòdul base_calendar instal·lat podeu comprovar l’existència de la taula res_alarm amb els camps definits a l’apartat _atributs de la classe res_alarm i la taula calendar_alarm amb camps idèntics als de la taula res_alarm més els camps definits a l’apartat _atributs de la classe calendar_alarm.
</div>
{{nota|L'herència per prototip és la tradicional en els llenguatges orientats a objectes, ja que crea una nova classe vinculada}}


L’herència múltiple (_inherits) s’anomena herència per delegació i sempre provoca la creació d’una nova taula a la base de dades. L’objecte derivat ha d’incloure, per cada derivació, un camp many2one apuntant l’objecte del qual deriva, amb la propietat '''ondelete='cascade''''. L’herència per delegació obliga que cada recurs de l’objecte derivat apunte a un recurs de cadascun dels objectes dels quals deriva i es pot donar el cas que hi hagi diversos recursos de l’objecte derivat que apunten a un mateix recurs per algun dels objectes dels quals deriva.
'''read()'''
Es tracta d'un mètode de baix nivell per llegir un field en concret dels records. És preferible emprar browse()
 
'''name_search(name='', args=None, operator='ilike', limit=100)''' → records
Search for records that have a display name matching the given name pattern when compared with the given operator, while also matching the optional search domain (args).


<syntaxhighlight lang="python" style="font-family:monospace">
This is used for example to provide suggestions based on a partial value for a relational field. Sometimes be seen as the inverse function of name_get(), but it is not guaranteed to be.
    class res_alarm(Model.model):
    _name = 'res.alarm'
    ...
    class calendar_alarm(Model.model):
    _name = 'calendar.alarm'
    _inherits = {'res.alarm':'alarm_id'}
    ...
</syntaxhighlight>


=== Herència en la vista ===
This method is equivalent to calling search() with a search domain based on display_name and then name_get() on the result of the search.


L’herència de classe possibilita continuar utilitzant les vistes definides sobre l’objecte pare, però en moltes ocasions interessa disposar d’una versió retocada. En aquest cas, és molt millor heretar de les vistes existents (per afegir, modificar o eliminar camps) que reemplaçar-les completament.  
'''ids'''
<syntaxhighlight lang="xml" style="font-family:monospace">
Llista dels ids del recordset actual.
<field name="inherit_id" ref="id_xml_vista_pare"/>
 
</syntaxhighlight>
'''sorted(key=None, reverse=False)'''
En cas que la vista id_xml_vista_pare estiga en un mòdul diferent del que estem dissenyant, cal afegir el nom del mòdul al davant:  
Retorna el recordset ordenat per un criteri.
<syntaxhighlight lang="xml" style="font-family:monospace">
 
<field name="inherit_id" ref="modul.id_xml_vista_pare"/>
'''name_get()'''
Retorna el nom que tindrà el record quant siga referenciat externament. És el valor per defecte del field '''display_name'''. Aquest mètode, per defecte, mostra el field '''name''' si està. Es pot sobreescriure per mostrar un altre camp o mescla d'ells.
 
Anem a sobreescriure el mètode [http://odootechnical.com/overriding-name_get-method-in-odoo-8/ name_get].
<syntaxhighlight lang="python" style="font-family:monospace">
 
    def name_get(self):
        res=[]
        for i in self:
            res.append((i.id,str(i.name)+", "+str(i.id_player.name)))
        return res
</syntaxhighlight>
</syntaxhighlight>
El motor d’herència d’OpenObject, en trobar una vista heretada, processa el contingut de l’element arch. Per cada fill d’aquest element que tingui algun atribut, OpenObject cerca a la vista pare una etiqueta amb atributs coincidents (excepte el de la posició) i, a continuació, combina els camps de la vista pare amb els de la vista heretada i estableix la posició de les noves etiquetes a partir dels següents valors:


*    inside (per defecte): els valors s’afegeixen “dins” de l’etiqueta.
'''copy()'''
*    after: afegeix el contingut després de l’etiqueta.
Crea una còpia del singleton i permet aportar nous valors per als fields de la copia.  
*    before: afegeix el contingut abans de l’etiqueta.
*    replace: reemplaça el contingut de l’etiqueta.
*    attributes: Modifica [https://www.odoo.com/es_ES/forum/ayuda-1/question/xpath-how-to-replace-attributes-only-and-not-the-full-field-38192 els atributs].  


'''Reemplaçar'''
En els fields '''One2many''' no es pot copiar per defecte, però es pot dir '''copy=True'''.
<syntaxhighlight lang="xml" style="font-family:monospace">
 
<field name="arch" type="xml">
=====onchange=====
  <field name="camp" position="replace">
Si volem que un valor siga modificat en temps real quant modifiquem el valor d'un altre field sense encara haver guardat, podem usar els mètodes '''on_change'''.
    <field name="nou_camp" ... />
 
  </field>
{{nota| Els camps '''computed''' ja tenen el seu propi onchange, per tant, no cal fer-lo}}
</field>
 
En onchange es modifica el valor d'un o més camps dirèctament i, si cal un filtre o un missatge, es fa en el return:
<syntaxhighlight lang="python" style="font-family:monospace">
return {
    'domain': {'other_id': [('partner_id', '=', partner_id)]},
    'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'},
}
</syntaxhighlight>
</syntaxhighlight>
'''Esborrar'''
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="arch" type="xml">
  <field name="camp" position="replace"/>
</field>
</syntaxhighlight>
'''Inserir nous camps'''
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="arch" type="xml">
    <field name="camp" position="before">
      <field name="nou_camp" .../>
    </field>
</field>


<field name="arch" type="xml" style="font-family:monospace">
Si el '''type''' és '''notification''' es mostrarà en una notificació, en un altre cas, en un dialog. (Odoo 13)
    <field name="camp" position="after">
 
      <field name="nou_camp" .../>
Exemples:
    </field>
 
</field>
<syntaxhighlight lang="python" style="font-family:monospace">
</syntaxhighlight>
# onchange handler
'''Fer combinacions'''
@api.onchange('amount', 'unit_price')
<syntaxhighlight lang="xml" style="font-family:monospace">
def _onchange_price(self):
<field name="arch"type="xml">
    # set auto-changing field
  <data>
    self.price = self.amount * self.unit_price
    <field name="camp1" position="after">
    # Can optionally return a warning and domains
      <field name="nou_camp1"/>
    return {
    </field>
        'warning': {
    <field name="camp2" position="replace"/>
            'title': "Something bad happened",
    <field name="camp3" position="before">
            'message': "It was very bad indeed",
         <field name="nou_camp3"/>
         }
    </field>
    }
  </data>
</field>
</syntaxhighlight>


Per definir la posició dels elements que afegim, podem utilitzar una expresió '''xpath''':
@api.onchange('seats', 'attendee_ids')
<syntaxhighlight lang="xml" style="font-family:monospace">
def _verify_valid_seats(self):
<xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before">
    if self.seats < 0:
</syntaxhighlight>
        return {
És posssible que necessitem una vista totalment nova de l'objecte heredat. Si fem un action normal en l'XML es veuran els que més prioritat tenen. Si volem especificar quina vista volem en concret hem d'utilitzar '''view_id''':
            'warning': {
<syntaxhighlight lang="xml" style="font-family:monospace">
                'title': "Incorrect 'seats' value",
<field name="view_id" ref="view_school_parent_form2"/>
                'message': "The number of available seats may not be negative",
</syntaxhighlight>
            },          }
Tal vegada cal especificar totes les vistes. En eixe cas, s'ha de guardar per separat en ir.actions.act_window.view:
    if self.seats < len(self.attendee_ids):
<syntaxhighlight lang="xml" style="font-family:monospace">
          return {
<record model="ir.actions.act_window" id="action_my_hr_employee_seq">
            'warning': {
    <field name="name">Angajati</field>
                'title': "Too many attendees",
    <field name="res_model">hr.employee</field>
                'message': "Increase seats or remove excess attendees",
    <field name="view_type">form</field>
            },
    <field name="view_mode">tree,form</field>
        }
</record>
 
 
@api.onchange('pais')
<record model="ir.actions.act_window.view" id="act_hr_employee_tree_view">
def _filter_empleat(self):                                           
    <field eval="1" name="sequence"/>
      return { 'domain': {'empleat': [('country','=',self.pais.id)]} }     
    <field name="view_mode">tree</field>
    <field name="view_id" ref="your_tree_view_id"/>
    <field name="act_window_id" ref="action_my_hr_employee_seq"/>
</record>


<record model="ir.actions.act_window.view" id="act_hr_employee_form_view">
# Exemple avançat en el que l'autor crea un domain amb una llista d'ids i un '''in''':
     <field eval="2" name="sequence"/>
@api.multi
     <field name="view_mode">form</field>
def onchange_partner_id(self, part):
     <field name="view_id" ref="your_form_view_id"/>
    res = super(SaleOrder, self).onchange_partner_id(part)
     <field name="act_window_id" ref="action_my_hr_employee_seq"/>
    domain = [('active', '=', True), ('sale_ok', '=', True)]
</record>
     if part:
        partner = self.env['res.partner'].browse(part)
        if partner and partner.sales_channel_id:
            domain.append(('sales_channel_ids', '=',
                          partner.sales_channel_id.id))
     product_ids = self.env['product.product'].search(domain)
     res.update(domain={
        'order_line.product_id': ['id', 'in', [rec.id for rec in product_ids]]
     })
    return res
</syntaxhighlight>
</syntaxhighlight>


Si es vol especificar una vista search es pot inclourer la etiqueta '''search_view_id''':
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="search_view_id" ref="cine.pos_order_line_search_view"/> 
</syntaxhighlight>
Exemple:


<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
{{nota|Si l'usuari s'equivoca introduint algunes dades, Odoo proporciona varies maneres d'evitar-lo:
* Constraints
* onchange amb missatge d'error i restablint els valors originals
* Sobreescriptura del mètode write o create per comprovar coses abans de guardar
}}
 
=====Cron Jobs=====
Cal crear un record en el model ir.cron, per exemple:
<syntaxhighlight lang="xml" style="font-family:monospace">
        <record model="ir.cron" forcecreate="True" id="game.cron_update">
            <field name="name">Game: Cron Update</field>
            <field name="model_id" ref="model_game_player"/>
            <field name="state">code</field>
            <field name="code">model.update_resources()</field>
            <field name="user_id" ref="base.user_root"/>
            <field name="interval_number">1</field>
            <field name="interval_type">minutes</field>
            <field name="numbercall">-1</field>
            <field name="activity_user_type">specific</field>
            <field name="doall" eval="False" />
        </record>
</syntaxhighlight>
I un mètode amb el @api.model i aquests arguments:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
class socios(models.Model):
    @api.model
    _inherit = 'res.partner'
    def update_resources(self):
    _name = 'res.partner'
        ...
    #name = fields.Char()
    camions = fields.One2many('cooperativa.camion','socio',string='Trucks')
    n_camiones = fields.Integer(compute='_n_camiones',string='Number of Trucks')
    arrobas = fields.Float(compute='_n_camiones',string='@')
    @api.depends('camions')
    def _n_camiones(self):
      for i in self:
        for j in i.camions:
          i.arrobas = i.arrobas + j.arrobas
          i.n_camiones = i.n_camiones + 1
</syntaxhighlight>
</syntaxhighlight>
<syntaxhighlight lang="xml" style="font-family:monospace">
  <record model="ir.ui.view" id="socio_form_view">
            <field name="name">socio</field>
            <field name="model">res.partner</field>
  <field name="inherit_id" ref="base.view_partner_form"/>
          <field name="arch" type="xml">
    <field name="website" position="after">
                            <field name="camions"/>
                            <field name="n_camiones"/>
                            <field name="arrobas"/>
    </field>


            </field>
'''ir.cron''' té un many2one amb '''ir.actions.server''' i, al ser creat, crea l'acció de servidor corresponent. 
        </record>
És important ficar en el manifest que depén de '''mail''', ja que és un mòdul preinstal·lat que hereta i afegeix camps a '''ir.actions.server'''.
</syntaxhighlight>
 
https://poncesoft.blogspot.com/2018/05/creacion-metodos-automatizados-en-odoo.html https://webkul.com/blog/creating-cron-server-action-odoo-11/ https://odoo-development.readthedocs.io/en/latest/odoo/models/ir.cron.html
 
=== Els Decoradors ===


<syntaxhighlight lang="xml" style="font-family:monospace">
Com es veu, abans de moltes funcions es fica @api.depends, @api.multi...  
    <!--Inherit quotations search view-->
    <record id="view_sale_order_inherit_search" model="ir.ui.view">
      <field name="name">sale.order.search.expand.filter</field>
      <field name="model">sale.order</field>
      <field name="inherit_id" ref="sale.sale_order_view_search_inherit_quotation"/>
      <field name="arch" type="xml">
        <xpath expr="//search" position="inside">
          <filter string="Total &lt; 1000" name="total_under_1000" domain="[('amount_total', '&lt;', 1000)]"/>
          <filter string="Total &gt;= 1000" name="total_above_1000" domain="[('amount_total', '&gt;=', 1000)]"/>
        </xpath>
      </field>
    </record>
</syntaxhighlight>
</div>


'''Domains'''
Els decoradors modifiquen la forma en la que és cridada la funció. Entre altres coses, modifiquen el contingut de '''self''', les vegades que se crida i quant se crida.


Si volem que el action heredat sols mostre els elements que volem, s'ha de ficar un domain en el action:
<syntaxhighlight lang="xml" style="font-family:monospace">
<field name="domain"> [('isplayer','=',True)]</field>
</syntaxhighlight>


=== Herència en el controlador ===


L’herència en el controlador és un mecanisme conegut, ja que l’apliquem de forma inconscient quan ens veiem obligats a sobreescriure els mètodes de la capa ORM d’OpenObject en el disseny de molts mòduls.
* '''@api.depends()''' Aquest decorador crida a la funció sempre que el camp del que depén siga modificat. Encara que el camp diga ''store=True''. Per defecte, '''self''' és un recordset, per tant, cal fer un for.
* '''@api.model''' S'utilitza per a funcions que afecten al model i no als recordsets.
* '''@api.constrains()''' S'utilitza per a comprovar les ''constrains''. Self és un recordset. Com que quasi sempre es crida en un form, funciona si utilitzem self directament. Però cal fer for, ja que pot ser cridat en un recordset quant modifiquem camps en grup.
* '''@api.onchange()''' S'executa cada vegada que modifiquem el field indicat en la vista. En aquest, com que es crida quant es modifica un form, sempre '''self''' serà un singleton. Però si fiquem un for no passa res.


{{nota|'''Funció super()'''
{{Nota | @api.multi i @api.one estan obsolets en el Odoo 13 i no es poden utilitzar mai. }}


El llenguatge Python recomana utilitzar la funció super() per invocar el mètode de la classe base quan s’està sobreescrivint en una classe derivada, en lloc d’utilitzar la sintaxi nomClasseBase.metode(self…).}}
[https://github.com/xxjcaxx/SGE-Odoo-2016-2017/tree/master/proves_decoradors Exemple de tots els decoradors: (Odoo 12)]


L’efecte de l’herència en el controlador es manifesta únicament quan cal sobreescriure algun dels mètodes de l’objecte del qual es deriva i per a fer-ho adequadament cal tenir en compte que el mètode sobreescrit en l’objecte derivat:


*    A vegades vol substituir el mètode de l’objecte base sense aprofitar-ne cap funcionalitat: el mètode de l’objecte derivat no invoca el mètode sobreescrit.
=== Càlculs en dates ===
*    A vegades vol aprofitar la funcionalitat del mètode de l’objecte base: el mètode de l’objecte derivat invoca el mètode sobreescrit.
Odoo gestiona les dates com a strings. Per una altra banda, python té el seu propi tipus de dades anomenat datetime, date i timedelta entre altres. Això pot provocar dificultats per a fer cálculs en dates. Odoo proporciona algunes ferramentes mínimes per facilitar aquesta tasca.  


Exemples:
Primer de tot, anem a importar datetime:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
[http://www.odoo.yenthevg.com/override-create-functions-odoo/ Sobreescriure el mètode '''create''']:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
class res_partner(models.Model):
from odoo import models, fields, api
    _inherit = 'res.partner'
from datetime import datetime, timedelta
    passed_override_write_function = fields.Boolean(string='Has passed our super method')
</syntaxhighlight>
El primer que necessitem saber és cóm transformar de date o datetime d’Odoo a python. En definitva, passar de string a datetime.
    @api.model
 
    def create(self, values):
Tenim un field datetime declarat de la següent manera:
        # Override the original create function for the res.partner model
<syntaxhighlight lang="python" style="font-family:monospace">
        record = super(res_partner, self).create(values)
start_date = fields.Datetime()
</syntaxhighlight>
        # Change the values of a variable in this super function
En la base de dades guardarà un string amb el format: '%Y-%m-%d %H:%M:%S'. Per tant, si volem transformar aquesta data en string a un objecte datetime.datetime tenim que ejecutar el constructor de la classe amb aquests paràmetres:
        record['passed_override_write_function'] = True
<syntaxhighlight lang="python" style="font-family:monospace">
        print 'Passed this function. passed_override_write_function value: ' + str(record['passed_override_write_function'])
fmt = '%Y-%m-%d %H:%M:%S'
data = datetime.strptime(self.start_date,fmt)
        # Return the record so that the changes are applied and everything is stored.
return record
</syntaxhighlight>
</syntaxhighlight>
</div>
És a dir, transforma un string en aquest format al tipus de dades datetime.datetime oficial de python.


== El controlador ==
Per no tindre que especificar el format cada vegada, Odoo dona una ferramenta més facil. La classe '''fields.Datetime''' té un mètode per generar un datetime.datetime de un string:
https://www.odoo.com/documentation/8.0/reference/orm.html
<syntaxhighlight lang="python" style="font-family:monospace">
 
data = fields.Datetime.from_string(self.start_date)
Part del controlador l'hem mencionat al parlar dels camps '''computed'''. No obstant, cal comentar les facilitats que proporciona OpenObject per a no tindre que accedir dirèctament a la base de dades.
</syntaxhighlight>
 
De la mateixa manera passa al contrari:
{{nota|Per encarar amb garanties el disseny de mètodes en OpenObject es pressuposa uns coneixements mínims de disseny de mètodes en Python.}}
<syntaxhighlight lang="python" style="font-family:monospace">
 
fmt = '%Y-%m-%d %H:%M:%S'
La capa '''ORM''' d’OpenObject facilita un seguit de mètodes que s’encarreguen del mapatge entre els objectes Python i les taules de PostgreSQL. Així, disposem de mètodes per crear, modificar, eliminar i cercar registres a la base de dades. Aquests mètodes són utilitzats de manera automàtica per OpenObject en l’execució dels diversos tipus de vista que OpenObject ens permet dissenyar.
self.start_date = data.strftime(fmt)
 
vs
En ocasions, pot ser necessari alterar l’acció automàtica de cerca – creació – modificació – eliminació facilitada per OpenObject i haurem de sobreescriure els corresponents mètodes en les nostres classes.
self.start_date = fields.Datetime.to_string(data)
</syntaxhighlight>


{{nota|Com exemple d’aquesta necessitat, podem considerar el cas de la gestió de comandes de venda (classe sale_order) dins el fitxer sale.py del mòdul sale d’OpenERP. Si hi fem una ullada, al final de la classe hi trobem el disseny dels mètodes unlink (eliminar), create (crear) i write(modificar). Cadascun d’ells executa un seguit de comprovacions i/o accions i, si tot és correcte, invoca la crida dels corresponents mètodes unlink, create i write de la capa ORM. Així, el mètode unlink (eliminació de comandes) comprova si les comandes a eliminar tenen l’estat draft o cancel (estats en els quals l’eliminació és permesa, segons la lògica de negoci) i si alguna de les comandes no és eliminable genera una excepció tot avortant l’eliminació; en canvi, si totes són eliminables, procedeix a l’eliminació tot invocant el mètode unlink subministrat per la capa ORM.}}
'''A continuació, anem a veure cóm incrementar una data en un temps:'''


Els programadors en el framework OpenObject hem de conèixer els mètodes subministrats per la capa ORM i hem de dominar el disseny de mètodes per:
En el format d’Odoo (fields.Datetime) no es pot, cal passar a datetime.datetime per sumar el temps i després tornar a passar a fields.Datetime.
 
Per sumar o restar temps a un datetime.datetime cal utilitzar una classe anomenada datetime.timedelta. Aquesta classe representa una duració o la diferència entre dues dates.  
*    Poder definir camps funcionals en el disseny del model.
Per exemple, aquest constructor representa molt bé les opcions que es poden ficar per crear un timedelta:
*    Poder definir l’acció que cal executar en modificar el contingut d’un field d’una vista form (atribut on_change del field)
*    Poder alterar les accions automàtiques de cerca, creació, modificació i eliminació de recursos.  
 
Una darrera consideració a tenir en compte en l’escriptura de mètodes i funcions en OpenObject és que els textos de missatges inclosos en mètodes i funcions, per poder ser traduïbles, han de ser introduïts amb la sintaxi _('text') i el fitxer .py ha de contenir from tools.translate import _ a la capçalera.  
 
=== API de l'ORM ===
 
 
{{nota|'''Interactuar en la terminal'''
$ /usr/bin/python /usr/bin/odoo.py shell --config /var/lib/odoo/.openerp_serverrc -d castillo -u containers
[https://asciinema.org/a/123126 Asciinema amb alguns exemples]
Observa cóm hem ficat el paràmetre '''shell'''. Les coses que se fan en la terminal no són persistents en la base de dades fins que no s'executa '''self.env.cr.commit()'''. Dins de la terminal podem obtindre ajuda dels mètodes d'Odoo amb help(), per exemple: help(tools.image)}}
 
Un mètode creat dins d'un model actua sobre tots els elements del model que estiguen actius en el moment de cridar al métode. Si és un tree, seran molts i si és un form sols un. Però en qualsevol cas és una 'llista' d'elements i es diu recordset.
 
Bàsicament la interacció amb els models en el controlador es fa amb els anomenats '''recordsets''' que són col·leccions d'objectes sobre un model. Si iterem dins dels recordset , obtenim els '''singletons''', que són objectes individuals de cada línia en la base de dades.
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
def do_operation(self):
un_any = timedelta(weeks=40, days=84, hours=23, minutes=50, seconds=600)  
    print self # => a.model(1, 2, 3, 4, 5)
    for record in self:
        print record # => a.model(1), then a.model(2), then a.model(3), ...
</syntaxhighlight>
</syntaxhighlight>
Podem accedir a tots els fields d'un model sempre que estem en un singleton, no en un recordset:
Aquest exemple d’Odoo mostra cóm afegir 3 dies a un field:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
>>> record.name
data=fields.Datetime.from_string(self.start_date)
Example Name
data=data+timedelta(hours=3)
>>> record.company_id.name
self.end_date=fields.Datetime.to_string(data)
Company Name
>>> record.name = "Bob"
</syntaxhighlight>
</syntaxhighlight>
Intentar llegir o escriure un field en un recordset donarà un error. Accedir a un '''many2one, one2many o many2many''' donarà un recordset.
O si es vol fer sols en mètodes python:
 
'''Set operations'''
Els recordsets es poden combinar amb operacions específiques que són les típiques dels conjunts:
* '''record in set''' retorna si el record està en el set
* '''set1 | set2''' Unió de sets
* '''set1 & set2''' Intersecció de sets
* '''set1 - set2''' Diferència de sets
* '''filtered()''' Filtra el recordset de manera que sols tinga els records que complixen una condició.
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
records.filtered(lambda r: r.company_id == user.company_id)
fmt = '%Y-%m-%d %H:%M:%S'
records.filtered("partner_id.is_company")
data = datetime.strptime(self.start_date,fmt)
data=data+timedelta(hours=3)
self.end_date=data.strftime(fmt)
</syntaxhighlight>
</syntaxhighlight>
* '''sorted()''' Ordena segons uns funció, se defineix una funció lambda (key) que indica que s'ordena per el camp name:
 
'''Ara anem a veure cóm calcular el temps que ha passat entre dues dates:'''
 
Solució amb '''relativedelta''':
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
# sort records by name
from datetime import datetime, timedelta
records.sorted(key=lambda r: r.name)
from dateutil.relativedelta import relativedelta
records.sorted(key=lambda r: r.name, reverse=True)
</syntaxhighlight>
* '''mapped()''' Li aplica una funció a cada recordset i retorna un recordset amb els canvis demanats:
<syntaxhighlight lang="python" style="font-family:monospace">
# returns a list of summing two fields for each record in the set
records.mapped(lambda r: r.field1 + r.field2)
# returns a list of names
records.mapped('name')
# returns a recordset of partners
record.mapped('partner_id')
# returns the union of all partner banks, with duplicates removed
record.mapped('partner_id.bank_ids')
</syntaxhighlight>


Aquestes funcions són útils per a fer tècniques de [https://docs.python.org/2.7/howto/functional.html programació funcional]
start=fields.Datetime.from_string(self.start_date)
end=fields.Datetime.from_string(self.end_date)


====Enviroment====
relative=relativedelta(start,end)
print r.years
print r.months
print r.days
print r
</syntaxhighlight>
El problema és que dona la data per separat. No sol ser molt útil per a Odoo on necessitem la diferència sols en dies, hores o minuts.


L'anomenat enviroment o '''env''' guarda algunes dades contextuals interessants per a treballar amb l'ORM, com ara el cursor a la base de dades, l'usuari actual o el context (que guarda algunes metadades).
Solució sols amb '''Datetime''':
 
Tots els recordsets tenen un enviroment accesible amb env. Quant volem crear un recordset dins d'un altre, podem usar env:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
>>> self.env['res.partner']
from datetime import datetime
res.partner
>>> self.env['res.partner'].search([['is_company', '=', True], ['customer', '=', True]])
start=fields.Datetime.from_string(self.start_date)
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
end=fields.Datetime.from_string(self.end_date)
print (end-start).days * 24 * 60
print (end-start).total_seconds()/60/60/24
</syntaxhighlight>
Solució amb '''Unix timestamp'''
<syntaxhighlight lang="python" style="font-family:monospace">
d1_ts = time.mktime(d1.timetuple())
d2_ts = time.mktime(d2.timetuple())
 
print int(d2_ts-d1_ts) / 60
</syntaxhighlight>
</syntaxhighlight>
La solució és la mateixa, sols és per si necessiteu algun càlcul intermedi que necessite la data en un Integer.


El primer cas crea un recordset buit però que fa referència a res.partner i es poden fer les funcions de l'ORM que necessitem.
El resultat de restar dos datetime és un timedelta. Podem demanar els dies i segons com en el relative delta, però amés té una funció per traure els segons totals i després fer els càlculs que necessitem.


=====Context=====
'''Consultar si una data és anterior a una altra:'''


El context és un diccionari de python que conté dades útils per a totes les vistes i els mètodes. Les funcions d'Odoo reben el context i el consulten si cal. Context pot tindre de tot, però quasi sempre té al menys el user ID, l'idioma o la zona temporal. Quant Odoo va a renderitzar una vista XML, consulta el context per veure si ha d'aplicar algun paràmetre.
Les dates en format Datetime o Date es poden comparar:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
print(self.env.context)
d3=fields.Datetime.from_string(self.d3)
d4=datetime.now()
if d3 < d4:
  print "La data és anterior"
</syntaxhighlight>
</syntaxhighlight>
Al llarg de tot aquest manual utilitzem sovint paràmetres del context. Aquests són els paràmetres que hem utilitzat en algun moment:
També es pot calcular si és del mateix dia, sols cal transformar de datetime a date:
* active_id : self._context.get('active_id') es tracta de l'id de l'element del model que està en pantalla.
<syntaxhighlight lang="python" style="font-family:monospace">
* active_ids : Llista de les id seleccionats en un tree.
d3=d3.date()
* active_model : El model actual.
d4=d4.date()
* default_<field> : En un action o en un one2many es pot assignar un valor per defecte a un field.
* search_default_<filter> : Per aplicar un filtre per defecte a la vista en un '''action'''.
* group_by : Dins d'un camp '''filter''' per a crear agrupacions en les vistes '''search'''.
* graph_mode : En les vistes '''graph''', aquest paràmetre canvia el '''type'''


El context va passant d'un mètode a un altre o a les vistes i, de vegades volem modificar-lo.
if d3 == d4 :
  ….
</syntaxhighlight>
Si volem saber si són del mateix més o any, es pot calcular la diferència i veure si en dies és major o menor de 30, per exemple. Però si volem major precisió, en aquest cas es recomana utilitar relativedelta.


Imaginem que volem fer un botó que obriga un [[Odoo#Wizards|wizard]], però volem passar-li '''paràmetres''' al wizard. En els botons i fields relacionals es pot especificar un context:
== Wizards ==
<syntaxhighlight lang="xml" style="font-family:monospace">
<button name="%(reserves.act_w_clients_bookings)d" type="action" string="Select bookings" context="{'b_fs':bookings_fs}"/>
</syntaxhighlight>
Eixe action obre un wizard, que és un model transitori en el que podem definir un field amb els continguts del context:
<syntaxhighlight lang="python" style="font-family:monospace">
def _default_bookings(self):
        return self._context.get('b_fs')
bookings_fs = fields.Many2many('reserves.bookings',readonly=True, default=_default_bookings)
</syntaxhighlight>
Aquest many2many tindrà els mateixos elements que el form que l'ha cridat. (Això és com el [[Odoo#Millores_en_les_vistes_form|'''default_''']] en els One2many, però fet a mà)


El context és un diccionari inmutable (frozendict) que no pot ser alterat en funcions. no obstant, si volem modificar el context actual per enviar-lo a un action o cridar a una funció d'un model amb un altre context, es pot fer amb '''[https://www.odoo.com/documentation/11.0/reference/orm.html#odoo.models.Model.with_context with_context]''':
Els wizards permeten fer un asistent interactiu per a que l'usuari complete una tasca. Com que no ha d'agafar les dades dirèctament en un formulari, si no que va ajundant-lo a completar-lo, no pot ser guardat en la base de dades fins al final.
<syntaxhighlight lang="python" style="font-family:monospace">
# current context is {'key1': True}
r2 = records.with_context({}, key2=True)
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}
</syntaxhighlight>


Si és precís modificar el context es pot fer:
Els wizards en Odoo són models que estenen la classe TransientModel en compte de Model. Aquesta classe és molt pareguda, però:
<syntaxhighlight lang="python" style="font-family:monospace">
self.env.context = dict(self.env.context)
self.env.context.update({'key': 'val'})
</syntaxhighlight>
o
<syntaxhighlight lang="python" style="font-family:monospace">
self = self.with_context(get_sizes=True)
print self.env.context
</syntaxhighlight>
Però no funciona més enllà del recordset actual. És a dir, no modifica el context en el que s'ha cridat.


Si el que volem és passar el valor d'un field per context a un botó dins d'una 'subvista', podem utilitzar el paràmetre '''parent''', que funciona tant en en '''domain''', '''attr''', com en context. Ací tenim un exemple de tree dins d'un field amb botons que envíen per context coses del pare:
* Les dades no són persistents, encara que es guarden temporalment en la base de dades.
<syntaxhighlight lang="xml" style="font-family:monospace">
* A partir de odoo 14 necessiten permisos
<field name="movies" >
* Els records dels wizards poden tindre referències Many2One amb el records dels models normals, però no al contrari.
    <tree>
        <field name="photo_small"/>
        <field name="name"/>
        <field name="score" widget='priority'/>
        <button name="book_it" string="Book it" type="object" context="{'b_client':parent.client,'b_day':parent.day}"/>
    </tree>
</syntaxhighlight>


==== Mètodes de l'ORM ====
Veure més:
[[Wizards en Odoo]]
 
== Client web ==
 
[[El client Web Odoo]] (Obsolet a partir de la versió 14)


=====search()=====
== Web Controllers ==
A partir d'un '''domain''' de Odoo, proporciona un recordset amb tots els elements que coincideixen:
<syntaxhighlight lang="python" style="font-family:monospace">
>>> # searches the current model
>>> self.search([('is_company', '=', True), ('customer', '=', True)])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
>>> self.search([('is_company', '=', True)], limit=1).name
'Agrolait'
</syntaxhighlight>
{{nota| Es pot obtindre la quantitat d'elements amb el mètode '''search_count()'''}}
<syntaxhighlight lang="python" style="font-family:monospace">
Parameters
    args -- A search domain. Use an empty list to match all records.
    offset (int) -- number of results to ignore (default: none)
    limit (int) -- maximum number of records to return (default: all)
    order (str) -- sort string
    count (bool) -- if True, only counts and returns the number of matching records (default: False)
</syntaxhighlight>


=====create()=====
[[Web Controller en Odoo]]
Te dona un recordset a partir d'una definició de varis fields:
<syntaxhighlight lang="python" style="font-family:monospace">
>>> self.create({'name': "New Name"})
res.partner(78)
</syntaxhighlight>


=====write()=====
== Pàgina web ==
Escriu uns fields dins de tots els elements del recordset, no retorna res:
 
<syntaxhighlight lang="python" style="font-family:monospace">
https://www.odoo.yenthevg.com/creating-webpages-controllers-odoo10/
self.write({'name': "Newer Name"})
http://learnopenerp.blogspot.com/2018/08/odoo-web-controller.html
</syntaxhighlight>


'''Escriure en un many2many''':
== Exemples ==


La manera més senzilla és passar una llista d'ids. Però si ja existeixen elements abans, necessitem uns codis especials (vegeu [[Odoo#Expressions]]):
[[Projecte Odoo complet]]


Per exemple:  
[https://www.youtube.com/watch?v=hwhhZcSEG1s Vídeo de Mòdul Odoo complet][https://github.com/tivisse/odoodays-2018 Codi del vídeo]
<syntaxhighlight lang="python" style="font-family:monospace">
self.sessions = [(4,s.id)]  
self.write({'sessions':[(4,s.id)]})
self.write({'sessions':[(6,0,[ref('vehicle_tag_leasing'),ref('fleet.vehicle_tag_compact'),ref('fleet.vehicle_tag_senior')] )]})
</syntaxhighlight>


=====browse()=====
[[Point Of Sale]]
A partir d'una llista de ids, retorna un recordset.
<syntaxhighlight lang="python" style="font-family:monospace">
>>> self.browse([7, 18, 12])
res.partner(7, 18, 12)
</syntaxhighlight>


== Misc. ==


=====exists()=====
* Si volem fer un print en colors, podem ficar un caracter de escape: \033[93m i \033[0m al final
Retorna si un record en concret encara està en la base de dades.  
* Traure la menor potència de 2 major o igual a un número: http://stackoverflow.com/a/14267557
<syntaxhighlight lang="python" style="font-family:monospace">
 
if not record.exists():
Distintes alertes:
    raise Exception("The record has been deleted")
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
o:
Odoo pot mostrar distintes alertes en funció del que necessitem. Totes estan en openerp.exceptions
records.may_remove_some()
# only keep records which were not deleted
records = records.exists()
</syntaxhighlight>
En el segon exemple, refresca un recordset amb aquells que encara existixen.


=====ref()=====
Si entrem en el mode shell del debug podem executar aquest comandament:
<pre class="code">
>>> help(openerp.exceptions)
</pre>
Una vegada dins podem detectar:
AccessDenied
DeferredException
QWebException
RedirectWarning
except_orm
        AccessError
        MissingError
        UserError
        ValidationError


Retorna un singleton a partir d'un [[Odoo#External_Ids|'''External ID''']].  
Normalment són utilitzats pel Odoo sense necessitat de que els cridem nosaltres. Però en ocasion pot ser útil.  


Per exemple, si volem mostrar un Warning perquè úsuari ha fet alguna cosa mal. (Normalment es fa un onchange que ja pot tornar el warning)
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
>>> env.ref('base.group_public')
from openerp import _
res.groups(2)
from openerp.exceptions import Warning
[...]
raise Warning(_('Alguna cosa ha fallat!'))
</syntaxhighlight>
</syntaxhighlight>


=====ensure_one()=====
O si volem Donar opcions a l'usuari amb RedirectWarning:
S'asegura de que el record en concret siga un singleton.
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
records.ensure_one()
action = self.env.ref('base.action_res_users')
# is equivalent to but clearer than:
msg = _("You cannot create a new user from here.\n To create new user please go to configuration panel.")
assert len(records) == 1, "Expected singleton"
raise openerp.exceptions.RedirectWarning(msg, action.id, _('Go to the configuration panel'))
</syntaxhighlight>
</syntaxhighlight>
En aquest exemple, per al missatge, utilitza la barra baixa '''_()''' per a obtindre la traducció en cas de que existisca. '''self.env.ref()''' retorna l'objecte referit amb una id externa. En aquest cas, un action.
En el cas de les Constrains també s'ha de llançar un Validation error.
</div>


=====unlink()=====
Funcions lambda:
Esborra de la base de dades els elements del recordset actual.  
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
En moltes ocasions, cal cridar a alguna funció de l'ORM o similar passant com a paràmetre una funció lambda. La raó és que si passem una variable, esta queda establerta en temps de càrrega i no es modifica. La funció sempre recalcula.  


Exemple de cóm sobreescriure el mètode unlink per a esborrar en cascada:
La sintaxi de la funció lambda és:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.multi
a = lambda x,y: x*y
    def unlink(self):
a(2,3)
        for x in self:
6
            x.catid.unlink()
        return super(product_uom_class, self).unlink()
</syntaxhighlight>
</syntaxhighlight>
On les primeres x,y són els arguments que rep la funció, després el que calcula.


'''read()'''
Cal recordar que les funcions lambda són de una sola línia de codi. Si volem alguna cosa més sofisticada hem de cridar a una funció normal.  
Es tracta d'un mètode de baix nivell per llegir un field en concret dels records. És preferible emprar browse()
</div>


'''name_search(name='', args=None, operator='ilike', limit=100)''' → records
Search for records that have a display name matching the given name pattern when compared with the given operator, while also matching the optional search domain (args).


This is used for example to provide suggestions based on a partial value for a relational field. Sometimes be seen as the inverse function of name_get(), but it is not guaranteed to be.


This method is equivalent to calling search() with a search domain based on display_name and then name_get() on the result of the search.
Imatges en Odoo:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Les imatges es guarden com un text en la base de dades. Per convertir un binari en text es fa en base64. Aquesta codificació funciona sense problemes en els navegadors web i no cal preocupar-se de cóm guardar les imatges en PostgreSQL.


'''ids'''
Per mostrar la imatge en formularis o llistes es pot ficar '''widget="image"''' en el ''field''. Si no fem res més, la imatge es veurà en la mida original. Podem navegar per altres formularis per veure cóm han fet per mostrar la imatge i aprofitar les classes CSS. També podem utilitzar la etiqueta '''width''' o '''style''' per especificar la mida exacta.
Llista dels ids del recordset actual.  


'''sorted(key=None, reverse=False)'''
No obstant, no és recomanable carregar les imatges en el seu tamany original. El millor és guardar la versió mitjana o menuda i mostrar eixa sols. Per aconseguir-ho, es pot fer un camp binary computed d'aquesta manera:
Retorna el recordset ordenat per un criteri.
<syntaxhighlight lang="python" style="font-family:monospace">
from odoo import models, fields, api, tools
[...]
    photo = fields.Binary()
    photo_small = fields.Binary(compute='_get_images',store=True)
    photo_medium = fields.Binary(compute='_get_images',store=True)


'''name_get()'''
    @api.one
Retorna el nom que tindrà el record quant siga referenciat externament. És el valor per defecte del field '''display_name'''. Aquest mètode, per defecte, mostra el field '''name''' si està. Es pot sobreescriure per mostrar un altre camp o mescla d'ells.
    @api.depends('photo')
    def _get_images(self):
        image = self.photo
        data = tools.image_get_resized_images(image)
        self.photo_small = data["image_small"]
        self.photo_medium = data["image_medium"]
</syntaxhighlight>


'''copy()'''
La ferramenta '''tools.image''' d'Odoo té funcions per a reescalar, retallar o millorar les imatges.  
Crea una còpia del singleton i permet aportar nous valors per als fields de la copia.  


En els fields '''One2many''' no es pot copiar per defecte, però es pot dir '''copy=True'''.
Per utilitzar una imatge dins d'un Kanban, es necessita la funció '''kanban_image()''':
 
<syntaxhighlight lang="xml" style="font-family:monospace">
=====onchange=====
<img t-att-src="kanban_image('reserves.hotels', 'photo_small', record.id.value)"/>
Si volem que un valor siga modificat en temps real quant modifiquem el valor d'un altre field sense encara haver guardat, podem usar els mètodes '''on_change'''.
</syntaxhighlight>
Prèviament, s'ha declarat el field ''photo_small'' i ''id'' al kanban.
</div>


{{nota| Els camps '''computed''' ja tenen el seu propi onchange, per tant, no cal fer-lo}}


A partir de Odoo 8, en onchange es modifica el valor d'un o més camps dirèctament i, si cal un filtre o un missatge, es fa en el return:
'''res.config.settings'''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Si volem que el nostre mòdul tinga configuració podem afegir-la com a un field més del model '''res.control.settings'''. Aquest ja s'encarrega de centralitzar opcions de configuració.
Per a que aparega en el menú de configuració també podem afegir-lo heretant en la vista:
<syntaxhighlight lang="python" style="font-family:monospace">
<syntaxhighlight lang="python" style="font-family:monospace">
return {
class config(models.TransientModel):
     'domain': {'other_id': [('partner_id', '=', partner_id)]},
     _inherit = 'res.config.settings'
    'warning': {'title': "Warning", 'message': "What is this?"},
    players = fields.Char(string='players',
}
                            config_parameter="expanse.players")
</syntaxhighlight>


Exemples:


<syntaxhighlight lang="python" style="font-family:monospace">
    def reset_universe(self):
# onchange handler
        print("reset",self)
@api.onchange('amount', 'unit_price')
</syntaxhighlight>
def _onchange_price(self):
<syntaxhighlight lang="xml" style="font-family:monospace">
    # set auto-changing field
<record id="res_config_settings_view_form_inherit" model="ir.ui.view">
    self.price = self.amount * self.unit_price
            <field name="name">res.config.settings.view.form.</field>
    # Can optionally return a warning and domains
            <field name="model">res.config.settings</field>
    return {
            <field name="priority" eval="25" />
        'warning': {
            <field name="inherit_id" ref="base.res_config_settings_view_form" />
            'title': "Something bad happened",
            <field name="arch" type="xml">
            'message': "It was very bad indeed",
                <xpath expr="//div[hasclass('settings')]" position="inside">
        }
                    <div class="app_settings_block" data-string="Expanse Settings" string="Expanse Settings" data-key="expanse">
    }
                        <div id="players">
                            <h2>Expanse</h2>
                            <button type="object" name="reset_universe" string="Reset Universe"  class="btn-primary"/>
                        </div>


@api.onchange('seats', 'attendee_ids')
                    </div>
def _verify_valid_seats(self):
                </xpath>
    if self.seats < 0:
        return {
            'warning': {
                'title': "Incorrect 'seats' value",
                'message': "The number of available seats may not be negative",
            },          }
    if self.seats < len(self.attendee_ids):
          return {
            'warning': {
                'title': "Too many attendees",
                'message': "Increase seats or remove excess attendees",
            },
        }


@api.onchange('pais')
             </field>
def _filter_empleat(self):                                           
         </record>
      return { 'domain': {'empleat': [('country','=',self.pais.id)]} }     
 
# Exemple avançat en el que l'autor crea un domain amb una llista d'ids i un '''in''':
@api.multi
def onchange_partner_id(self, part):
    res = super(SaleOrder, self).onchange_partner_id(part)
    domain = [('active', '=', True), ('sale_ok', '=', True)]
    if part:
        partner = self.env['res.partner'].browse(part)
        if partner and partner.sales_channel_id:
             domain.append(('sales_channel_ids', '=',
                          partner.sales_channel_id.id))
    product_ids = self.env['product.product'].search(domain)
    res.update(domain={
         'order_line.product_id': ['id', 'in', [rec.id for rec in product_ids]]
    })
    return res
</syntaxhighlight>
</syntaxhighlight>


Si en data-key posem el nom del mòdul, afegirà l'icona al menú de settings.


{{nota|Si l'usuari s'equivoca introduint algunes dades, Odoo proporciona varies maneres d'evitar-lo:
https://www.youtube.com/watch?v=MsVoYPQ4-J4
* Constraints
</div>
* onchange amb missatge d'error i restablint els valors originals
* Sobreescriptura del mètode write o create per comprovar coses abans de guardar
}}


=== Els Decoradors ===
== Enllaços ==
https://www.odoo.com/documentation/8.0/
https://www.odoo.com/documentation/9.0/


Com es veu, abans de moltes funcions es fica @api.depends, @api.multi...  
https://www.odoo.com/documentation/8.0/howtos/backend.html


Els decoradors modifiquen la forma en la que és cridada la funció. Entre altres coses, modifiquen el contingut de '''self''', les vegades que se crida i quant se crida.
Blogs: http://ludwiktrammer.github.io/ http://www.odoo.yenthevg.com/ https://sateliteguayana.wordpress.com/ https://poncesoft.blogspot.com/


* '''@api.multi''': En aquest cas, '''self''' conté un recordset amb tots les instàncies del model que estiguen visibles. Si és un tree seran totes les visibles i en un form sols la que està en eixe moment. En qualsevol cas, és recomanable fer un for que les recórrega. En els camps computed s'executa sempre que no siguen ''store=True''. També serveix per a botons.
Repositori dels exemples: https://github.com/xxjcaxx/sge20152016 https://github.com/xxjcaxx/SGE-Odoo-2016-2017
* '''@api.one''' Per a aquest decorador, '''self''' és un sol element. Si és cridat en un tree, la funció és cridada una vegada per a cada element. Pot simplificar la tasca en botons en els formularis. '''Es considera obsolet'''
* '''@api.depends()''' Aquest decorador crida a la funció sempre que el camp del que depén siga modificat. Encara que el camp diga ''store=True''. Per defecte, '''self''' és com en '''@api.multi'''.
* '''@api.model''' S'utilitza sobretot per a transformar peticions d'Openerp 7 a Odoo. Per defecte és com ''@api.multi''
* '''@api.constrains()''' S'utilitza per a comprovar les ''constrains''. Self és un recordset, com en ''@api.multi''. Com que quasi sempre es crida en un form, funciona si utilitzem self directament. Però cal fer for, ja que pot ser cridat en un recordset quant modifiquem camps en grup.
* '''@api.onchange()''' S'executa cada vegada que modifiquem el field indicat en la vista. En aquest, com que es crida quant es modifica un form, sempre '''self''' serà un singleton. Però si fiquem un for no passa res.


{{nota|En conclusió: El '''@api.one''' és l'únic on es recomana no utilitzar el for, ja que està fet per això. @api.one es considera obsolet i no es recomana utilitzar més, sempre cal utilitzar el multi i iterar.}}
https://www.youtube.com/watch?v=0GUxV85DDm4&feature=youtu.be&t=5h47m45s


[https://github.com/xxjcaxx/SGE-Odoo-2016-2017/tree/master/proves_decoradors Exemple de tots els decoradors:]
http://es.slideshare.net/openobject/presentation-of-the-new-openerp-api-raphael-collet-openerp
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
https://github.com/xxjcaxx/SGE-Odoo-2016-2017/tree/master/proves_decoradors
<syntaxhighlight lang="python" style="font-family:monospace">
# -*- coding: utf-8 -*-


from openerp import models, fields, api
http://fundamentos-de-desarrollo-en-odoo.readthedocs.org/es/latest/capitulos/comenzando-con-odoo.html
from openerp.exceptions import ValidationError


class proves_decoradors(models.Model):
https://www.odoo.com/es_ES/slides/slide/keynote-odoo-9-new-features-201
    _name = 'proves_decoradors.proves_decoradors'


    name = fields.Char()
https://media.readthedocs.org/pdf/odoo-development/latest/odoo-development.pdf
    value = fields.Integer()
    valuedepends = fields.Float(compute="_value_depends", store=False)
    valuemulti = fields.Float(compute="_value_multi", store=False)
    valueone = fields.Float(compute="_value_one", store=False)
    valuemodel = fields.Float(compute="_value_model", store=False)
    description = fields.Text()


    @api.depends('value')
http://webkul.com/blog/beginner-guide-odoo-clicommand-line-interface/
    def _value_depends(self):
        print "\033[91mSelf en @api.depends: \033[0m" + str(self)
        for record in self:
            record.valuedepends = float(record.value) / 100


    @api.multi
http://useopenerp.com/v8
    def _value_multi(self):
        print "\033[92mSelf en @api.multi:\033[0m " + str(self)
        for record in self:
            record.valuemulti = float(record.value) / 10


    @api.one
Podcast que parlen dels beneficis d'Odoo: http://www.ivoox.com/podcast-26-odoo-transformacion-digital-audios-mp3_rf_18433975_1.html
    def _value_one(self):
        print "\033[93mSelf en @api.one:\033[0m " + str(self)
        self.valueone = float(self.value) / 20


    @api.model
[https://www.youtube.com/channel/UC8gl7Ap_GZVbsKjri2GChkg Canal de youtube de SGE amb Odoo en castellà]
    def _value_model(self):
        print "\033[94mSelf en @api.model:\033[0m " + str(self)
        for record in self:
            record.valuemodel = float(record.value) / 30


    @api.constrains('value')
https://www.odoo.yenthevg.com/extend-selection-odoo-10/
    def _check_value(self):
        print "\033[95mSelf en @api.constrains:\033[0m " + str(self)
        for record in self:
            if record.value > 1000:
              raise ValidationError("Deuria ser menys que 1000 %s" % record.value)
 
    @api.onchange('value')
    def _onchange_value(self):
        print "\033[95mSelf en @api.onchange:\033[0m " + str(self)
        self.description = str(self.value)
</syntaxhighlight></div>
 
=== Casos resolts del controlador ===
 
1er Cas, el % de caixons:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Sobre l'exemple de la cooperativa, anem a suposar que sabem que tots els camions tenen un màxim de 1000 caixons de capacitat. Ja tenim una funció que comprova si l'usuari fica més de 1000. Però ara necessitem calcular el percentatge de caixons sobre el màxim que han carregat en un viatge.
 
Primer de tot, cal fer un camp '''computed''':
<syntaxhighlight lang="python" style="font-family:monospace">
    ...
    percent = fields.Float(compute='_get_percent',string='% Loaded', store=False)
    ...
</syntaxhighlight>
 
Com que el % depen del caixons, cal fer una funció amb el api '''depends''':
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.depends('cajas')
    def _get_percent(self):
      ...
</syntaxhighlight>
En l'exemple, '''self''', és un recordset que conté la llista de camions sobre els que es va a calcular el camp computed. Si volem veure sobre quins en concret, podem ficar un '''print''':
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.depends('cajas')
    def _get_percent(self):
        print str(self)
</syntaxhighlight>
El print traurà per la terminal on hem iniciat el Odoo en mode depuració. el valor de la variable '''self'''.
 
Si la funció es cridada en temps d'edició del formulari o des del tree, el contingut del self serà distint. En el form serà un recordset amb un sol singleton i el tree serà un recordset amb tots els visibles. En qualsevol cas, cal iterar sobre el recordset per traure cada un dels singletons o records per treballar individualment en ells.
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.depends('cajas')
    def _get_percent(self):
        for i in self:
            i.percent = i.cajas*100/1000
</syntaxhighlight>
Cada i de la interació és un singleton. Al assignar un valor a un field computed en cada singleton es queda accesible des de la vista, però no es guarda a la base de dades si no fica store=True.
 
</div>
 
2on cas, generador de partides:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
En l'exemple del joc es pot entendre que els jugadors tenen fortaleses (fortress) i es van fent atacs. Anem a fer que, quant es crea un jugador, automàticament es genere una fortalesa mínima per poder començar a jugar.
 
Si volem que tot siga automàtic, tenim que fer que la funció per generar la fortalesa es cride en el moment de creació del jugador. L'ORM té un mètod per crear que es diu '''create()''' i pot ser sobreescrit (override).
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.model
    def create(self, values):
        new_id = super(player, self).create(values)
        print values
        return new_id
</syntaxhighlight>
En este cas, el nou métod sols imprimeix per la terminal els valors que se li pasen.
 
Anem a fer que cree també una fortalesa:
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.model
    def create(self, values):
        new_id = super(player, self).create(values)
        print values
        name_player = new_id.name
        self.env['mmog.fortress'].create({'name':name_player+"-fortress",'level':1,'soldiers':100,'population':10,'food':1000,'integrity':100,'id_player':new_id.id})
        return new_id
</syntaxhighlight>
 
Com es pot veure, per crear un element d'un model distint sobre el que estem fent el mètode, cal cridar a env['_name del altre model']. Una vegada obtens accés a l'altre recordset, es pot crear un nou passant-li un diccionari amb els fields i valor que necessitem. New_id és una variable que guarda un objecte que representa el jugador nou. Per traure les seues dades es pot accedir amb un punt, com és el cas de '''new_id.id'''.
 
En aquest mètode falta una cosa important: el icon de la fortalesa. Com que és una imatge, és molt difícil generar-la. Per tant, es pot tindre un model especial que servisca de banc d'imatges amb un xml de dades en algunes imatges predefinides en base64. Després sols seria accedir a una d'elles. També poden tindre unes fortaleses sense jugador predefinides sobre les quals traure les imatges o inclús que servisquen de plantilla per totes les altres dades. Anem a fer la segona opció:
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.model
    def create(self, values):
        new_id = super(player, self).create(values)
        print values
        name_player = new_id.name
        img = self.env['mmog.fortress'].search([('name','=','f1')])[0].icon
        self.env['mmog.fortress'].create({'name':name_player+"-fortress",'level':1,'soldiers':100,'population':10,'food':1000,'integrity':100,'id_player':new_id.id,'icon':img})
        return new_id
</syntaxhighlight>
Prèviament he creat [https://github.com/xxjcaxx/sge20152016/blob/master/mmog/data_fortress.xml un xml] en el que guardava 3 fortress amb els noms 'f1','f2' i 'f3'. El mètode extrau el icon del primer record resultat de buscar per el nom f1.
</div>
 
3er Cas, les batalles:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
En primer lloc, cal fer un record dins d'un model especial que serveix per a fer ''cron jobs'':
<syntaxhighlight lang="xml" style="font-family:monospace">
  <record forcecreate="True" id="cron_atacs" model="ir.cron">
                <field name="name">Cron Attack</field>
                <field eval="True" name="active" />
                <field name="user_id" ref="base.user_root" />
                <field name="interval_number">1</field>
                <field name="interval_type">minutes</field>
                <field name="numbercall">-1</field>
                <field eval="False" name="doall" />
                <field eval="'mmog.attack'" name="model" />
                <field eval="'update_progress'" name="function" />
  </record>
</syntaxhighlight>
Aquest cron job crida cada 1 minut al mètode update_progress del model mmog.attack.
El codi del update_progres:
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.model
    def update_progress(self):
        att = self.search([('finished','=',False)])                                       
        DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"                                             
        att.write({'last_update': datetime.datetime.now()})                               
        print att
        for i in att:
          end=datetime.datetime.strptime(i.arrival_date,DATETIME_FORMAT)                 
          if end < datetime.datetime.now():
              diflevel = i.fortress_attacking.level/i.fortress_defender.level                                                                                 
              i.write({'progress':10})
              for k in range(0,i.soldiers_sent):
                  if i.fortress_defender.soldiers > 0 and randint(0,100) < 30:
                    i.fortress_defender.write({'soldiers': i.fortress_defender.soldiers - 1})
                    i.write({'defender_soldiers_killed': i.defender_soldiers_killed+1}) 
              for k in range(0,i.fortress_defender.soldiers):                             
                  if i.soldiers_sent > 0 and randint(0,100) < 30:                         
                    i.write({'soldiers_sent': i.soldiers_sent - 1})                     
                    i.write({'attacker_soldiers_killed': i.attacker_soldiers_killed+1}) 
              i.write({'progress':min(i.progress+1,100)})                                 
              if i.soldiers_sent == 0 or i.fortress_defender.soldiers == 0:               
                i.write({'finished':True,'progress':100})                               
                i.fortress_attacking.write({'soldiers': i.fortress_attacking.soldiers+i.soldiers_sent})
</syntaxhighlight>
Es fica el decorador @api.model per transformar una cridada segons l'api antic en l'api nou.
Primer busca els atacs no finalitzats i sobre ells calcula el temps d'arribada. Si ha arribat, calcula soldat per soldat atacant i defensor la batalla.
Si un dels dos equips acaba en 0 soldats s'acaba la batalla i els atacants tornen a la base.
Falten coses com calcular el resultat en funció del nivell de cada jugador, però no són rellevants per a l'explicació del model de Odoo.
 
Cal observar els distints search i write.
</div>
 
4t Cas, els noms del les fortaleses (El tema del name_get):
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Anem a sobreescriure el mètode [http://odootechnical.com/overriding-name_get-method-in-odoo-8/ name_get].
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.multi
    def name_get(self):
        res=[]
        for i in self:
            res.append((i.id,str(i.name)+", "+str(i.id_player.name)))
        return res
</syntaxhighlight>
Observem que hem creat una llista buida i anem plenant la llista amb la combinació del id i el nom, que està compost pel nom i el nom del jugador al que pertany.
</div>
 
== Wizards ==
 
Els wizards permeten fer un asistent interactiu per a que l'usuari complete una tasca. Com que no ha d'agafar les dades dirèctament en un formulari, si no que va ajundant-lo a completar-lo, no pot ser guardat en la base de dades fins al final.
 
Els wizards en Odoo són models que estenen la classe '''TransientModel''' en compte de ''Model''. Aquesta classe és molt pareguda, però:
* Les dades no són persistents, encara que es guarden temporalment en la base de dades.
* No necessiten permisos explícits.
* Els records dels wizards poden tindre referències Many2One amb el records dels models normals, però no al contrari.
 
<syntaxhighlight lang="python" style="font-family:monospace">
class wizard(models.TransientModel):
    _name = 'mmog.wizard'
    def _default_attacker(self):
        return self.env['mmog.fortress'].browse(self._context.get('active_id')) # El context conté, entre altre coses,
                                                                                #el active_id del model que està obert.
    fortress_attacker = fields.Many2one('mmog.fortress',default=_default_attacker)
    fortress_target = fields.Many2one('mmog.fortress')
    soldiers_sent = fields.Integer(default=1)
 
    @api.multi
    def launch(self):
      if self.fortress_attacker.soldiers >= self.soldiers_sent:
          self.env['mmog.attack'].create({'fortress_attacking':self.fortress_attacker.id,
                                          'fortress_defender':self.fortress_target.id,
                                          'data':fields.datetime.now(),'soldiers_sent':self.soldiers_sent})
      return {}
</syntaxhighlight>
En el python cal observar la classe de la que hereta, el default, que extrau el '''active_id''' del form que a llançat el wizard i el mètode que és cridat pel botó de la vista.
 
<syntaxhighlight lang="xml" style="font-family:monospace">
        <record model="ir.ui.view" id="wizard_mmog_fortress_view">
            <field name="name">wizard.mmog.fortress</field>
            <field name="model">mmog.wizard</field>
            <field name="arch" type="xml">
                <form string="Select fortress">
                    <group>
                        <field name="fortress_attacker"/>
                        <field name="fortress_target"/>
                        <field name="soldiers_sent"/>
                    </group>
                    <footer>
                        <button name="launch" type="object"
                                string="Launch" class="oe_highlight"/>
                        or
                        <button special="cancel" string="Cancel"/>
                    </footer>
 
                </form>
            </field>
        </record>
 
        <act_window id="launch_mmog_fortress_wizard"
                    name="Launch attack"
                    src_model="mmog.fortress"
                    res_model="mmog.wizard"
                    view_mode="form"
                    target="new"
                    key2="client_action_multi"/>
</syntaxhighlight>
En la vista, tenim creat un form normal amb dos botons. Un d'ells és especial per a cancel·lar el wizard. L'altre crida al mètode. També s'ha creat un action indicant el '''src_model''' sobre el que treballa i el model del wizard que utilitza. Els action que criden a wizard tenen l'atribut '''target''' a '''new''' per a que llance una finestra emergent.
 
{{nota| L'etiqueta '''act_window''' és un alies de record '''model ir.actions.act_window''' on es poden posar tots els atributs de l'acció no com a fields sino com a atributs de l'etiqueta.}}
 
{{nota| '''src_model''' és el model on es pot llançar el wizard. Amb això només ja apareix en el menú superior d'accions. Però podem fer un botó que el cride de forma més intuitiva. }}
 
<syntaxhighlight lang="xml" style="font-family:monospace">
<button name="%(launch_mmog_fortress_wizard)d" type="action" string="Launch attack" class="oe_highlight" />
</syntaxhighlight>
Si volem, podem ficar un botó que cride al action del wizard. Observem la sintaxi del name, que és igual sempre que el button siga de tipus action, ja que és l'anomenat '''XML id'''.
 
Els wizards poden tindre, al igual que els forms, estats i formen assistents: ✎
 
En aquest exemple anem a fer una espècie de workflow. Per començar, cal crear un camp '''state''' amb varis valors possibles:
<syntaxhighlight lang="python" style="font-family:monospace">
      state = fields.Selection([
        ('pelis', "Movie Selection"),
        ('dia', "Day Selection"),                                                                       
      ], default='pelis')
                   
      @api.multi     
      def action_pelis(self):
        self.state = 'pelis'
        return { "type": "ir.actions.do_nothing", }                                                     
                               
      @api.multi                       
      def action_dia(self):             
        self.state = 'dia'                     
        return { "type": "ir.actions.do_nothing", }
</syntaxhighlight>
 
I uns botons que van fent que passe d'un estar a un altre:
<syntaxhighlight lang="xml" style="font-family:monospace">
                    <header>
                        <button name="action_pelis" type="object"
                                string="Reset to movie selection"
                                states="dia"/>
                        <button name="action_dia" type="object"
                                string="Select dia" states="pelis"
                                class="oe_highlight"/>
                        <field name="state" widget="statusbar"/>
                    </header>
 
                    <group states="pelis">
                        <field name="cine"/>
                        <field name="pelicules"/>
                    </group>
                    <group states="dia">
                        <field name="dia"/>
                    </group>
</syntaxhighlight>
Després es pot fer que el formulari tinga un aspecte diferent depèn del valor de '''state'''
 
Els wizards poden tornar a recarregar la vista des de la que són cridats:
<syntaxhighlight lang="python" style="font-family:monospace">
return {
    'name': 'Reserves',
    'view_type': 'form',
    'view_mode': 'form',  # Pot ser form, tree, kanban...
    'res_model': 'wizards.reserves', # El model de destí
    'res_id': reserva.id,      # El id concret per obrir el form
  # 'view_id': self.ref('wizards.reserves_form') # Opcional si hi ha més d'una vista posible.
    'context': self._context,  # El context es pot ampliar per afegir opcions
    'type': 'ir.actions.act_window',
    'target': 'current',  # Si ho fem en current, canvia la finestra actual.
}
</syntaxhighlight>
L'exemple anterior és la manera llarga i completa de cridar a una vista en concret, però si sols necessitem refrescar la vista cridada, podem afegir:
<syntaxhighlight lang="python" style="font-family:monospace">
return {
    'type': 'ir.actions.client',
    'tag': 'reload',
}
</syntaxhighlight>
 
''' Exemple Complet de Wizards '''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
El codi complet de l'exemple està a: [https://github.com/xxjcaxx/sge18-19/tree/master/wizards]
Els wizards, generalment, necessiten una vista i un action que la cride:
<syntaxhighlight lang="xml" style="font-family:monospace">
      <record model="ir.ui.view" id="wizards.w_reserves">
            <field name="name">wizard reserves</field>
            <field name="model">wizards.w_reserves</field>
            <field name="arch" type="xml">
    <form>
            <header>
                        <field name="state" widget="statusbar"/>
                    </header>
                    <group>
    <h4>  <field name="teatre"/></h4>
    </group>
    <group states="obra">
    <field name="obra"/>
    </group>
    <group states = "actuacio">
    <field name="actuacio"/>
    </group>
    <group states="butaca,fin">
                        <field name="butaca"/>
                    </group>
                    <footer>
    <button states="fin" name="reserva" type="object"
    string="Reserva" class="oe_highlight"/>
                        or
                        <button special="cancel" string="Cancel"/>
                    </footer>
                </form>
            </field>
        </record>
        <act_window id="wizards.w_reserves_action"
                    name="Crear reserves"
                    src_model="wizards.teatres"
                    res_model="wizards.w_reserves"
                    view_mode="form"
                    target="new"
                    key2="client_action_multi"/>
</syntaxhighlight>
Observem, en especial, el header amb el camp '''state''' i els groups amd '''states''' per ser mostrats condicionalment. Això permetrà crear un assistent.
 
Per a que funcione la vista, és necessari el model i el codi del controlador del wizard:
<syntaxhighlight lang="python" style="font-family:monospace">
class w_reserves(models.TransientModel):  # La classe és transientModel
    _name = 'wizards.w_reserves'
    def _default_teatre(self):                   
        return self.env['wizards.teatres'].browse(self._context.get('active_id'))
        # El context conté, entre altre coses, el active_id del model que està obert.
    teatre = fields.Many2one('wizards.teatres',default=_default_teatre)
    obra = fields.Many2one('wizards.obres')
    actuacio = fields.Many2one('wizards.actuacions',required=True)
    butaca = fields.Many2one('wizards.butaques',required=True)
    state = fields.Selection([    # El camp state és per a crear l'assistent.
        ('teatre', "Teatre Selection"),
        ('obra', "Obra Selection"),                                           
        ('actuacio', "Actuacio Selection"),
        ('butaca', "butaca Selection"),
        ('fin', "Fin"),
        ], default='teatre')
    @api.onchange('teatre') 
    # Tots aquests onchange serveixen per ajudar a
    # seleccionar les coses a l'usuari amb filtres
    def _oc_teatre(self):
        if len(self.teatre) > 0:
        actuacions = self.env['wizards.actuacions'].search([('teatre','=',self.teatre.id)])
        print(actuacions)
        obres = actuacions.mapped('obra')
        print(obres)
        self.state='obra'   
        # Canviem el state per a donar continuitat a l'assistent.
        return { 'domain': {'obra': [('id', 'in', obres.ids)]},}   
        # Modifiquem el filtre del següent field.
 
    @api.onchange('obra')
    def _oc_obra(self):
        if len(self.obra) > 0:
          actuacions = self.env['wizards.actuacions'].search([('teatre','=',self.teatre.id),('obra','=',self.obra.id)])
 
          self.state='actuacio'
          return { 'domain': {'actuacio': [('id', 'in', actuacions.ids)]},}
 
 
    @api.onchange('actuacio')
    def _oc_actuacio(self):
        if len(self.actuacio) > 0:
          print('butaques ******************************************')
          butaques = self.env['wizards.butaques'].search([('teatre','=',self.actuacio.teatre.id)])
          b_reservades = self.actuacio.reserves.mapped('butaca')
          print(b_reservades)
          b_disponibles = butaques - b_reservades   
          # Despres d'obtindre totes les butaques li llevem les reservades.
          print(b_disponibles)
 
          self.state='butaca'
          return { 'domain': {'butaca': [('id', 'in', b_disponibles.ids)]},}
 
    @api.onchange('butaca')
    def _oc_butaca(self):
        if len(self.butaca) > 0:
            self.state='fin'
 
    @api.multi
    def reserva(self):
        reserva = self.env['wizards.reserves'].create({
              'actuacio':self.actuacio.id,
              'butaca':self.butaca.id,
              'name':str(self.actuacio.name)+" - "+str(self.butaca.name)
              })
        return {   
        # Aquest return crea un action que, al ser cridat pel client,
        # obri el formulari amb la reserva creada.
    'name': 'Reserves',
    'view_type': 'form',
    'view_mode': 'form',
    'res_model': 'wizards.reserves',
    'res_id': reserva.id,
    'context': self._context,
    'type': 'ir.actions.act_window',
    'target': 'current',
                }
</syntaxhighlight>
 
Ara, al formulari del teatre, li afegim un botó per obrir el wizard:
<syntaxhighlight lang="xml" style="font-family:monospace">
<button name="%(wizards.w_reserves_action)d" string="Crear Reserva" type="action"/>
</syntaxhighlight>
</div>
 
== Client web ==
{{nota|Secció en construcció}}
En la secció de la vista i de l'herència en la vista hem pogut modificar la forma en que Odoo mostra o gestiona la base de dades amb el client web. La gestió de la vista es limita a crear trees, forms, kanbans... Els creadors d'Odoo recomanen utilitzar aquestes vistes sempre que es puga. No obstant, de vegades volem fer alguna cosa més personalitzada. Si volem personalitzar a baix nivell l'aparença i funcionament del client web, deguem entendre bé cóm funciona.
 
Les pàgines web més simples són estàtiques. Però en el moment que necessitem accedir a una base de dades, necessitem un llenguatge de programació de servidor que obtinga les dades i les envie al navegador web. Fins a Odoo 6, el backend creava html complet i l'enviava al client. Després van entendre que això sobrecarrega al servidor amb aspectes més relacionats amb la vista.
 
El odoo actual carrega un client complet a la web, que es comunica amb missatges breus i concrets amb el servidor. Missatges en JSON que sols tenen dades o ordres a executar. Cada vegada que refresquem el navegador web, està enviant-se un programa de client complet, però quan entrem a un menú, sols s'envia un missatge JSON demanant unes dades i es rep un altre amb una llista de les dades a mostrar pel client. Opcionalment, s'envía el xml de la vista, el qual serà interpretat pel client per mostrar correctament les dades.
 
El client web va creant i destruint elements de la interfície contínuament. Aquests elements són, entre altre coses, Widgets.
 
Si volem saber modificar a baix nivell el client web, necessitem saber prou de Javascript, de JQuery, BootStrap i altres, amés de HTML5.
 
En Odoo, es proporcionen tres clients web diferenciats, però que, internament, funcionen amb el mateix framework. Aquest són el '''web client''' que és el backend on es treballa en les dades, el '''website''' que és la pàgina web pública i el '''point of sale''', que és per al punts de venda.
 
=== Arquitectura del client web ===
[[Archivo:Mvc client.png|312px|thumb|Arquitectura MVC]]
El '''WebClient''' d'Odoo es construeix amb mòduls, de la mateixa manera que els mòduls per al 'servidor'. Sols que en els mòduls per ampliar el client web es modifiquen altres arxius més que els típics dels models de python o els xml de la vista.
 
L'arquitectura és '''MVC''' internament. És a dir, Odoo té un model (ORM sobre PostgresSQL), un controlador (Mètodes de Python) i una vista (El client Web definit en el servidor amb XML) i el propi client web també té un model (Les peticions Ajax al servidor amb JSON i la interpretació d'aquestes), un controlador (Funcions Javascript per a gestionar aquestes dades) i una vista (El renderitzat dels elements web al navegador). Com que Javascript és un llenguatge que deixa fer de tot, no tenim perquè respectar aquesta arquitectura, però els nostres mòduls han de poder ser mantinguts i cal que aprofiten al màxim els recursos que el client web ja ens proporciona, per tant, intentarem programar el menys possible i aprofitar tot el que ja té el client.
 
Els mòduls principal del client web depenen del mòdul '''web''', que proporciona el nucli del client web. Els altres complementen a aquest. Hi ha mòduls que específicament es diuen, per exemple, '''web_kanban''', que amplien la web. Però en realitat qualsevol mòdul ho pot fer. El mòdul web conté tot el HTML i Javascript necessari per a que els altres mòduls de la web funcionen.
 
Per modificar el client web cal proporcional HTML, xml, CSS, Javascript i imatges. Això ha d'estar en el directori '''static''' del mòdul:
 
* static/src/js : the JavaScript files
* static/src/css : the CSS files
* static/src/xml : the HTML template files
* static/img : pictures used in templates or CSS
* static/libs : JS libraries needed by the module
 
El server no manipula aquesta informació, però la processa en certa manera i l'envia a client.
 
{{nota|Com que els CSS i JS no són processades pel servidor, no cal reiniciar el servidor per veure els canvis, sols refrescar el navegador. Això no sempre funciona, ja que el servidor pot ser que no processe els assets o que la cau del navegador no actualitze el JS o el XML.}}
 
=== Enviant el client al navegador ===
 
Cada vegada que refresquem, s'envia el client sencer. Això vol dir molts CSS, moltes línies de Javascript de molt fitxers distints i molt de HTML i XML. Per evitar saturar la xarxa, el servidor fa una compressió de totes eixes dades de la següent manera:
* Tots els CSS i Javascript són concatenats en un sol fitxer. La concatenació s'ordena per dependències entre mòduls.
* El Javascript és minimitzat llevant espais i refactoritzant les variables per noms més curts.
* Una web HTML molt simple sols amb l'enllaç als CSS i Javascript és enviada al client.
* Tot es comprimeix en gzip pel server per reduir l'enviament. El navegador és capaç de descomprimir.
 
Tot això fa difícil de fer debug amb el client. Per això es recomana ficar '''?debug=1''' a la URL per demanar que no minimitze. 
 
=== Els Assets ===
 
El client d'Odoo és molt complex i necessita tindre Javascript, HTML i CSS de molts fitxers distints. Gestionar això permetent que qualsevol puga fer un mòdul per afegir més implica una gestió d'aquests enllaços més automàtica. Per això han creat principalment tres '''bundles''' en XML que no són més que una col·lecció de links a Javascript o CSS. Aquests tenen l'estructura d'un Template QWeb i són:
* '''web.assets_common''': amb les coses comuns.
* '''web.assets_backend''': Amb les coses específiques del Backend.
* '''web.assets_frontend''': Amb les coses de la web pública.
 
Si volem afegir fitxers a un asset, sols cal heretar el XML com fem en l'[[Odoo#Her.C3.A8ncia_en_la_vista|Herència en la vista]]:
<syntaxhighlight lang="xml">
<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend">
    <xpath expr="//script[last()]" position="after">
        <link rel="stylesheet" href="/helpdesk/static/src/less/helpdesk.less"/>
        <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script>
    </xpath>
</template>
</syntaxhighlight>
Observem que afegeix coses al Asset del Backend, concretament al final.
{{nota|Aquesta és la manera general d'afegir funcionalitats o estils. Però tal vegada el nostre widget no necessita ser carregat sempre i estem afegint una càrrega constant a la xarxa. Per això pot ser interessant afegir la llibreria sols quan es crea el widget en temps d'execució. Odoo proporciona formes de carrega llibreries i CSS de forma dinàmica.}}
 
==== Afegir CSS al nostre mòdul ====
 
Abans d'entrar en la creació de Widgets, pot ser interessant observar cóm els '''bundles''' es poden ampliar d'una forma simple per modificar o afegir CSS.
 
El primer és crear el css en '''/<modul>/static/src/css/<modul>.css'''. En el nostre cas, sols fem un per a fer la lletra mès menuda:
<syntaxhighlight lang="css">
.reserves_tree { font-size:0.8em;}
</syntaxhighlight>
Després creem un template per afegir el CSS al bundle '''assets_backend''':
<syntaxhighlight lang="xml">
<template id="assets_backend" name="reserves assets" inherit_id="web.assets_backend">
    <xpath expr="//script[last()]" position="after">
    <link rel="stylesheet" href="/reserves/static/src/css/reserves.css"/>
    </xpath>
</template>
</syntaxhighlight>
I per últim, sols cal utilitzar la classe css:
<syntaxhighlight lang="xml">
<field name="bookings" limit="10" class="reserves_tree">
</syntaxhighlight>
 
=== Arquitectura dels mòduls en Javascript ===
 
Javascript no té una manera definida de fer mòduls. Per tant, cada programador utilitza un patró de disseny diferent. En Odoo han optat per utilitzar una única variable global anomenada '''odoo''' que conté una referència a cada funció de cada mòdul web. Per tant, per definir una funció deguem observar aquest exemple:
<syntaxhighlight lang="javascript">
// in file a.js
odoo.define('module.A', function (require) { 
    "use strict";
    var A = ...;
    return A;
});
 
// in file b.js
odoo.define('module.B', function (require) {
    "use strict";
    var A = require('module.A');
    var B = ...; // something that involves A
    return B;
});
</syntaxhighlight>
 
El mètode '''odoo.define''' accepta tres arguments:
* '''moduleName''': El nom del mòdul. Es recomana seguir la mateixa sintaxi que en els models de la programació en python.
* '''dependencies''': (opcional) Es tracta d'una llista d'strings amb els noms d'altres mòduls dels que depen.
* '''function''': L'últim argument és una funció que defineix el mòdul i que retorna la classe o un array de les classes definides.
Per tant, un mòdul de client web en Odoo és el resultat de la funció define() de la classe global Odoo, la qual necessita el nom del mòdul, depedències i una funció que retorne una variable o un diccionari de variables. Aquestes variables són les classes que exporta el mòdul.
 
=== Classes Javascript en Odoo ===
 
En Javascript no hi ha una manera estàndard tampoc de crear classes, però proporciona mecanismes per simular l'efecte. Odoo utilitza la tècnica de [https://johnresig.com/blog/simple-javascript-inheritance/ John Resig]. Cridanta al mètode '''extend()''' d'una classe.
<syntaxhighlight lang="javascript">
var Class = require('web.Class');
 
var Animal = Class.extend({
    init: function () {
        this.x = 0;
        this.hunger = 0;
    },
    move: function () {
        this.x = this.x + 1;
        this.hunger = this.hunger + 1;
    },
    eat: function () {
        this.hunger = 0;
    },
});
</syntaxhighlight>
El mètode extend() agafa un diccionari amb una llista de funcions i atributs. Podem crear tants atributs com funcions necessitem, podem sobreescriure atributs i mètodes de la classe pare i cridar a mètodes de la classe pare amb '''this._super.apply(this, arguments);'''
 
'''Més sobre classes en Javascript/Odoo:'''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Per fer herència:
<syntaxhighlight lang="javascript">
var Animal = require('web.Animal');
 
var Dog = Animal.extend({
    move: function () {
        this.bark();
        this._super.apply(this, arguments);
    },
    bark: function () {
        console.log('woof');
    },
});
 
var dog = new Dog();
dog.move()
</syntaxhighlight>
També es pot mesclar l'herencia de varies classes:
<syntaxhighlight lang="javascript">
var Animal = require('web.Animal');
var DanceMixin = {
    dance: function () {
        console.log('dancing...');
    },
};
 
var Hamster = Animal.extend(DanceMixin, {
    sleep: function () {
        console.log('sleeping');
    },
});
</syntaxhighlight>
Una altra cosa que es pot fer és ampliar una classe existent amb '''include'''
<syntaxhighlight lang="javascript">
var Hamster = require('web.Hamster');
 
Hamster.include({
    sleep: function () {
        this._super.apply(this, arguments);
        console.log('zzzz');
    },
});
</syntaxhighlight>
</div>
 
[https://www.odoo.com/documentation/12.0/reference/javascript_reference.html Manual Oficial Javascript Reference]
[https://github.com/gdeb/technical-training technical training]
[https://www.youtube.com/watch?v=u-6aLi1oqcw Video Odoo JS Framework (2017)]
[https://www.youtube.com/watch?v=e3YOpQBJL_A Video JS Framework (2018)]
 
=== Widgets personalitzats ===
[[Archivo:Odoocomunicacionscomponentesjs.png|400px|thumb|Esquema recomanat per comunicar widgets entre sí. ([https://www.odoo.com/es_ES/slides/slide/the-odoo-js-framework-569])]]
{{nota|A partir d'Odoo 9, el client web ha canviat substancialment. Aixì que no podem fiar-nos de tutorial basats en aquest. De fet, el propi manual oficial d'Odoo no està actualitzat i no funciona. }}
 
El Widget és la manera que té Odoo de mostrar les dades i gestionar-les de forma estàndard en tota la interfície.
 
La classe Widget és una de les més importants en el framework Javascript d'Odoo i està definida en el mòdul '''web.Widget''' concretament en '''widget.js'''. Els widgets tenen algunes capacitats interessants:
* Es poden establir relacions pare/fill entre els widgets per mesclar i afegir funcionalitats.
* Tenen un '''cicle de vida''' que permet, per exemple, destruir els widgets fills quan es destrueix el pare.
* Es renderitzen amb QWeb. La classe widget té una funció anomenada '''renderElement''' que crea el html i l'inserta en el lloc indicat de la web. Aquesta funció primer consulta el '''template''', que és un atribut de la classe i està definit en un xml amb el llenguatge QWeb. Si no hi ha template, dins del codi javascript de la funció '''start:''', per exemple, es pot insertar html amb el llenguatge de JQuery.
* Tenen funcions per interactuar amb l'exterior. Per exemple, quan modifiquem el valor d'un '''widget field''', aquest informa cap a dalt de la modificació i aquesta és enregistrada per ser enviada a la base dades si cal.
 
{{nota|Els desenvolupadors d'Odoo han fet que els widgets no siguen els que gestionen les seues dades. Per obtindrer-les, el model de la vista demanarà les dades a la base de dades i el controlador demanarà a la vista que renderitze el widget amb el valor obtingut de la base de dades. Per escriure en la base de dades, el widget sols envia un trigger que és arreplegat pels pares cap a dalt fins que arriba al controlador que li demana al model que envíe les dades al servidor.}}
 
Quan es recarrega la web sencera, es demana a '''/web/webclient/qweb''' i aquest descarrega totes les '''templates''' que necessita. Aquesta és una de les descàrregues més pesades (~200Kb) ja que conté quasi totes les plantilles de tota la interfície. De vegades no necessitem aquesta plantilla més que en un lloc molt concret i no volem saturar la xarxa més. Aleshores podem demanar la descàrrega en temps de inicialització del widget amb '''xmlDependencies''':
<syntaxhighlight lang="javascript">
    template: 'some.template',
    xmlDependencies: ['/myaddon/path/to/my/file.xml'],
</syntaxhighlight>
 
==== Widgets Fields ====
 
Quasi tot el que es mostra en la interfície web està format per Widgets. Per tant, hi ha de moltes maneres. Alguns són elements bàsics de la interfície. Altres contenen informació de la base de dades, altres són elements interactius entre altres widgets... Com que hi ha tanta varietat, la classe Widget s'ha ampliat amb herència per a simplificar la programació. Per exemple, els widgets que són '''fields''' hereten tots d'una classe filla de widget anomenada '''AbstractField'''. Si volem fer un Widget per a mostrar un field d'un manera diferent, hem de heretar de AbstractField. És més, probablement podem heretar d'un widget més concret, per exemple '''FieldChar'''.
 
Aquest missatge està en el .js que defineix el '''AbstractField''' i explica les particularitats dels Widgets que són per a Fields:
<pre style="font-size:0.7em;">
/**
* This is the basic field widget used by all the views to render a field in a view.
* These field widgets are mostly common to all views, in particular form and list
* views.
*
* The responsabilities of a field widget are mainly:
* - render a visual representation of the current value of a field
* - that representation is either in 'readonly' or in 'edit' mode
* - notify the rest of the system when the field has been changed by
*  the user (in edit mode)
*
* Notes
* - the widget is not supposed to be able to switch between modes.  If another
*  mode is required, the view will take care of instantiating another widget.
* - notify the system when its value has changed and its mode is changed to 'readonly'
* - notify the system when some action has to be taken, such as opening a record
* - the Field widget should not, ever, under any circumstance, be aware of
*  its parent.  The way it communicates changes with the rest of the system is by
*  triggering events (with trigger_up).  These events bubble up and are interpreted
*  by the most appropriate parent.
*
* Also, in some cases, it may not be practical to have the same widget for all
* views. In that situation, you can have a 'view specific widget'.  Just register
* the widget in the registry prefixed by the view type and a dot.  So, for example,
* a form specific many2one widget should be registered as 'form.many2one'.
*
* @module web.AbstractField
*/
</pre>
 
Com diu el propi comentari, els widgets field no són responsables de les seues dades. Quan un usuari les modifica, aquests informen cap a dalt ('''_setValue'''). Els fields tampoc són responsables de carregar les dades. Els widgets tenen una funció '''init:''' que rep un paràmetre anomenat '''record''' que és un objecte que representa el record obtingut pel client web. Aquest record té, entre altres coses, els valors de cada field. La funció '''init:''' de '''AbstractField''' guarda el seu valor en '''this.value'''.
 
Els Widget tenen un cicle de vida en el que s'executen una serie de funcions:
* '''init''': On es crea el widget, aquesta funció agafa les dades i crea l'estructura del widget. Cal dir que les dades ja estan en el client. El widget agafa el paràmetre '''record''' que rep el init de '''AbstractField''' i selecciona les dades que corresponen al seu field. Aquesta funció és síncrona, és a dir, no es pot utilitzar per demanar coses al servidor.
* '''willStart''': Funció '''asíncrona''' cridada abans d'insertar en el DOM. Si es necessita demanar alguna cosa al servidor, ha de ser en aquest moment.
* '''start''': On s'inicia el widget. Ací podem afegir contingut o modificar l'aspecte.
* '''render''': El client web executa aquesta funció per mostrar o actualitzar el widget.
* '''destroy''': Quan és eliminat pel client web. Aquesta funció pot eliminar el widgets fills o fer alguna cosa abans de ser eliminat.
 
Tots els widgets tenen una variable anomenada '''$el''' o símplement '''el''' que conté l'element del DOM en format objecte JQuery on comença el widget. Per defecte, a falta de una plantilla, és un '''<div>''' buit.
 
La millor manera de saber fer fields widgets és mirar exemples. Els següents exemples tenen comentaris per explicar qué està passant:
 
'''Exemple de Widget field simple: Widget comptador:'''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
<syntaxhighlight lang="javascript">
console.log('Creacio del widget');
odoo.define('model.module', function(require) {
    "use strict";
var FieldInteger = require('web.basic_fields').FieldInteger;
/* web.basic_fields defineix la majoría dels fields
* no relacionals. Podem veure els que té vejent el final
* del fitxer /web/static/src/js/fields/basic_fields.js
* */
var contador = FieldInteger.extend({
    //template: 'contador_template', en aquest cas no fa falta template
    events: _.extend({},FieldInteger.prototype.events, {
        'click': '_onClick',
    }),
    _renderReadonly: function () {
        this._super.apply(this,arguments);
        // render en  mode sols lectura; _renderEdit
    },
    start: function() { 
        return this._super.apply(this,arguments);
    },
    init: function() { //inizialització amb valors
        this._super.apply(this,arguments);
    // console.log(arguments)
    // arguments d'init AbstractField:
    // init: function (parent, name, record, options)
    },
    _onClick: function () {
this.value++;
    /**
      *Fragment del mètode _setValue de AbstractField:
      *
    * this method is called by the widget, to change its value and to notify
    * the outside world of its new state.  This method also validates the new
    * value.  Note that this method does not rerender the widget, it should be
    * handled by the widget itself, if necessary.
    *
    * @private
    * @param {any} value
    * @param {Object} [options]
    * @param {boolean} [options.doNotSetDirty=false] if true, the basic model
    *  will not consider that this field is dirty, even though it was changed.
    *  Please do not use this flag unless you really need it.  Our only use
    *  case is currently the pad widget, which does a _setValue in the
    *  renderEdit method.
    * @param {boolean} [options.notifyChange=true] if false, the basic model
    *  will not notify and not trigger the onchange, even though it was changed.
    * @param {boolean} [options.forceChange=false] if true, the change event will be
    *  triggered even if the new value is the same as the old one
    * @returns {Deferred}
    */
this._setValue(this._formatValue(this.value),{forceChange:true});
    // Si no fem el forceChange no actualitza el field.
    // El _formatValue transforma string en Integer si fora el cas.
this._render(); // Cal actualitzar el valor
    },
});
var fieldRegistry = require('web.field_registry');
fieldRegistry.add('contador', contador); // Son cal fer widget="contador" en un field Integer
return contador;
});
</syntaxhighlight>
</div>
'''Exemple de Widget field complex: Widget galeria:'''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Aquest widget fa ús del [[Odoo#RPC|RPC]] per carregar en temps de renderitzat unes imatges. En aquest cas no està seguint les recomanacions de Odoo que diuen que el field no deuria gestionar les seues dades. Per fer millor el que fa el field el recomanable és fer una vista. No obstant, per a estudiar és molt interessant i planteja la dificultat afegida d'un field ''x2many''.
<syntaxhighlight lang="javascript">
/*
Aquest widget mostra una galeria de fotos sempre que tinga un field binary anomenat 'photo_small'.
La galeria no és editable ni interactiva.
*/
console.log('Creacio del widget galeria');
odoo.define('cine.galeria', function(require) {
    "use strict";
var AbstractField = require('web.AbstractField');
/* Ens basem en la classe abstracta
* del fitxer /web/static/src/js/fields/abstract_field.js
* */
var core = require('web.core');
var qweb = core.qweb; // Necessari per cridar al render
var utils = require('web.utils'); // per a la imatge
var photo = 'photo_small';  // El nom que té el field de la foto per defecte.
 
var galeria = AbstractField.extend({
    className: 'o_field_m2m_galeria', // classe CSS
    supportedFieldTypes: ['many2many','many2one'], // Suporta M2m i M2o
    galeria_template: 'galeria_template',
/*
*  template: Definició de la plantilla Qweb
*  Recordem que tots els templates estan en el client
*  perquè els demana amb web/webclient/qweb
*
*  En aquest cas utilitzem galeria_template perquè no volem que l'utilitze
*  dirèctament, sino cridar al qweb.render amb paràmetre.
*/
  fieldsToFetch: {  // Els fields que va a demanar el widget del model.
  // Sols demana els que diu aquesta llista. Es pot observar en el debug del navegador.
  // https://gitlab.merchise.org/merchise/odoo/commit/eafa14d3bc16e7212000d0c9c30a3ed922395574?view=inline
        display_name: {type: 'char'},
      // [photo]: {type: 'binary'},
  /*
    * Aquesta línia està comentada perquè l'interpreta abans de ser carregat el field.
    * Per tant, no pot fer ús del atribut 'image_field' de la vista i sempre utilitza el valor inicial
    * de la variable photo. fieldsTofech és interpretat per data_manager.js al carregar la vista sencera, no el widget.
    */
    },
    placeholder: "/web/static/src/img/placeholder.png", // Imatge en cas de no tindre imatge
    willStart: function(){  // Aquesta funció és asíncrona, per tant, pot servir per carregar dades des del servidor.
   
        var self = this;  // Com que anem a cridar a funcions, el this serà diferent dins i cal fer una variable independent.
 
        var res = this._rpc({         
model: this.value.model,  // El model demanat per el field
                method: 'read',        // Demana el mètode python read
                args: [this.value.res_ids, [photo,'display_name']],  // En aquest cas, enviem com a arguments els ids demanats i el nom dels fields demanats.
                context: this.record.getContext(),  // El context
                }).then(function (result) {     
                if (result.length === 0) {
                    console.log('no trobat');
                }
  var i;
            for(let i of result) {
var url = self.placeholder; // En cas de no tindre url
if (i[photo]) {
url = 'data:image/png;base64,' + i[photo];
    }
    i.url= url;
}
self.record.dataLoaded = { elements: result, readonly: self.mode === "readonly"}; // El render espera aquest objecte
});
    return res;  // res és un 'promise' de jquery, ja que segurament el rpc no acaba abans que la funció.
                // La funció que el cride ha de fer un $.when per esperar a que acabe la 'promise' i les dades estiguen carregades.
   
    },
    start: function() {
var p = this.$el.append('<p>Widget Galeria</p>');
    // ^ línia sols per provar cóm es poden afegir coses al widget en start
    // (no es veurà, ja que sols funciona amb el render per defecte)
        return $.when(p, this._super.apply(this,arguments)); // $.when espera a l'inserció
    },
    init: function(parent, name, record, options) { //inizialització amb valors
        photo = record.fieldsInfo[options.viewType][name].image_field //La manera d'extraure el valor d'un atribut
                                                              // En el field de la vista
        this._super.apply(this,arguments);
    },
   
    _LoadGaleria: function(){
    console.log('Load Galeria');
    },
 
/*
*La següent funció modifica els datos que s'envien al render afegint el base64 al raw de la imatge.
Com que no ha carregat la imatge en fieldsTofetch, cal fer un _rpc per a carregar-la en el moment del render.
Aquesta, no és la millor solució i per això està comentada, perquè carrega les dades cada vegada que es renderitza.
La solució correcta és fer-ho en el willStart que ja actua de forma asíncrona.
* */
    _getRenderGaleriaContext: function () {
        // var elements = this.value ? _.pluck(this.value.data, 'data') : [];
    // _.pluck() és una funció de underscore.js una biblioteca javascript que també
    // utilitza Odoo. pluck és l'equivalent a mapped() en python.
    // En aquest cas, de la llista sols volem un array amb la clau data de cadascun.
        /*var self = this;  // Com que anem a cridar a funcions, el this serà diferent dins i cal fer una variable independent.
        var res = this._rpc({         
model: this.value.model,  // El model demanat per el field
                method: 'read',        // Demana el mètode python read
                args: [this.value.res_ids, [photo,'display_name']],  // En aquest cas, enviem com a arguments els ids demanats i el nom dels fields demanats.
                context: this.record.getContext(),  // El context
                }).then(function (result) {     
                if (result.length === 0) {
                    console.log('no trobat');
                }
  var i;
            for(let i of result) {
var url = self.placeholder; // En cas de no tindre url
if (i[photo]) {
url = 'data:image/png;base64,' + i[photo];
    }
    i.url= url;
}
self.record.dataLoaded = { elements: result, readonly: self.mode === "readonly"}; // El render espera aquest objecte
});
    return res;  // res és un 'promise' de jquery, ja que segurament el rpc no acaba abans que la funció.
                // La funció que el cride ha de fer un $.when per esperar a que acabe la 'promise' i les dades estiguen carregades. */
    },
 
    _renderReadonly: function () {
        this._renderGaleria();
    },
    _renderEdit: function () {
        this._renderGaleria();
    },
    _renderGaleria: function () {
    var self = this;
            $.when(this._getRenderGaleriaContext()).done(function(){
            //this.$el.html(qweb.render(this.tag_template, this._getRenderTagsContext()));
            self.$el.html(qweb.render(self.galeria_template, self.record.dataLoaded));
    });
    /*
    *qweb.render() és una funció que accepta una template i un context en el que estan les
    variables que en template necessita. En aquest cas enviem elements i l'opcio de readonly
    * */
    },
});
var fieldRegistry = require('web.field_registry');
fieldRegistry.add('m2m_galeria', galeria); // Son cal fer widget="m2m_galeria" en un field m2m o o2m
return galeria;
});
 
</syntaxhighlight>
</div>
 
==== RPC ====
 
Observem el mètode '''_fetchRecord()''' de '''/web/static/src/js/views/basic/basic_model.js'''.
<syntaxhighlight lang="javascript">
    _fetchRecord: function (record, options) {
        var self = this;
        options = options || {}; 
        var fieldNames = options.fieldNames || record.getFieldNames(options);
        fieldNames = _.uniq(fieldNames.concat(['display_name']));  // Als fields afegir Display_name, que sempre fa falta
        return this._rpc({  // El mètode _rpc
                model: record.model,  // El model demanat
                method: 'read',        // Demana el mètode de l'ORM read, pot ser qualsevol de l'ORM o del model.
                args: [[record.res_id], fieldNames],  // En aquest cas, enviem com a arguments
                                                      //el id demanat i el nom dels fields demanats.
                context: _.extend({}, record.getContext(), {bin_size: true}),  // El context
            })
            .then(function (result) {     
                if (result.length === 0) {
                    return $.Deferred().reject();
                }
                result = result[0];
                record.data = _.extend({}, record.data, result);
            })
            .then(function () {
                self._parseServerData(fieldNames, record, record.data);  //transforma les dades per al javascript
            })
            .then(function () { 
                return $.when(  // Com que és una vista, ha de demanar tots els records dels fields x2Many i demés.
                    self._fetchX2Manys(record, options),
                    self._fetchReferences(record, options)
                ).then(function () {
                    return self._postprocess(record, options);
                });
            });
    },
</syntaxhighlight>
 
'''_rpc''' és una funció que ejecuta un '''service''' Ajax. Odoo incorpora el concepte de '''service''' per centralitzar la comunicació entre elements del programa. El que retorna és un objecte '''promise''' com els de '''JQuery'''. Això perment utilitzar la funció '''$.when''' i '''$.then''' per esperar a que es carregue.
 
https://stackoverflow.com/questions/45049996/how-can-i-create-a-simple-widget-in-odoo10
 
https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines
 
[https://www.youtube.com/watch?v=H-iFhOh1tOE video A Single Page] [https://github.com/dbo-odoo/odoo-js-demo Codi del video]
 
=== Vistes Personalitzades ===
 
[https://www.youtube.com/watch?v=SIoljYJhTqk Create a View (2018)]
 
== Pàgina web ==
 
https://www.odoo.yenthevg.com/creating-webpages-controllers-odoo10/
 
== Exemples ==
 
[[Projecte Odoo complet]]
 
[https://www.youtube.com/watch?v=hwhhZcSEG1s Vídeo de Mòdul Odoo complet][https://github.com/tivisse/odoodays-2018 Codi del vídeo]
 
'''El conservatori'''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Els models:
<syntaxhighlight lang="python" style="font-family:monospace">
from openerp import models, fields, api
 
class music(models.Model):
    _name = 'conservatori.music'
 
    name = fields.Char()
    instrument = fields.Char()
    grup = fields.Many2one('conservatori.grup')
    reforc = fields.Many2many('conservatori.grup','m2mrefoc','reforcos','reforc')
    grupantic = fields.Many2many('conservatori.grup','m2mantic','musicantic','grupantic')
    phone = fields.Char(related='grup.director.phone', store=False)
 
class grup(models.Model):
    _name = 'conservatori.grup'
    name = fields.Char()
    director = fields.Many2one('res.partner')
    titulars = fields.One2many('conservatori.music','grup')
    reforcos = fields.Many2many('conservatori.music','m2mrefoc','reforc','reforcos')
    musicantic = fields.Many2many('conservatori.music','m2mantic','grupantic','musicantic')
 
</syntaxhighlight>
Les vistes i menús:
<syntaxhighlight lang="python" style="font-family:monospace">
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
    <data>
 
<record model="ir.ui.view" id="music_form_view">
            <field name="name">conservatori.music</field>
            <field name="model">conservatori.music</field>
            <field name="arch" type="xml">
                <form string="Music Form">
                    <sheet>
                        <group>
                            <field name="name"/>
                            <field name="grup"/>
                            <field name="reforc"/>
    <field name="grupantic"/>                      
    <field name="phone"/>
  </group>
                    </sheet>
                </form>
            </field>
        </record>
<record model="ir.ui.view" id="grup_form_view">
            <field name="name">conservatori.grup</field>
            <field name="model">conservatori.grup</field>
            <field name="arch" type="xml">
                <form string="Group Form">
                    <sheet>
                        <group>
                            <field name="name"/>
                            <field name="director"/>
                            <field name="titulars"/>
    <field name="reforcos"/>
    <field name="musicantic"/>                   
    </group>
                    </sheet>
                </form>
            </field>
        </record>
 
        <record model="ir.actions.act_window" id="music_list_action">
            <field name="name">Musics</field>
            <field name="res_model">conservatori.music</field>
            <field name="view_type">form</field>
            <field name="view_mode">tree,form</field>
            <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first music
                </p>
            </field>
        </record>
        <record model="ir.actions.act_window" id="grup_list_action">
            <field name="name">Group</field>
            <field name="res_model">conservatori.grup</field>
            <field name="view_type">form</field>
            <field name="view_mode">tree,form</field>
            <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first grup
                </p>
            </field>
        </record>
 
        <!-- top level menu: no parent -->
        <menuitem id="main_cons_menu" name="Conservatory"/>
        <!-- A first level in the left side menu is needed
            before using action= attribute -->
        <menuitem id="cons_menu" name="Configuration"
                  parent="main_cons_menu"/>
        <!-- the following menuitem should appear *after*
            its parent openacademy_menu and *after* its
            action course_list_action -->
        <menuitem id="music_menu" name="Musics" parent="cons_menu"
                  action="music_list_action"/>
        <!-- Full id location:
            action="openacademy.course_list_action"
            It is not required when it is the same module -->
        <menuitem id="grup_menu" name="Group" parent="cons_menu"
                  action="grup_list_action"/>
    </data>
</openerp>
</syntaxhighlight>
</div>
 
'''La cooperativa'''
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Els models:
<syntaxhighlight lang="python" style="font-family:monospace">
# -*- coding: utf-8 -*-
 
from openerp import models, fields, api
from openerp.exceptions import ValidationError
 
class camion(models.Model):
    _name = 'cooperativa.camion'
 
    name = fields.Char(string='Identificator')
    matricula = fields.Char(string='Plate')
    socio = fields.Many2one('cooperativa.socio',string='Partner')
    cajas = fields.Integer(default=500,string='Boxes')
    arrobas = fields.Float(compute='_get_peso',string='@')
    kilos = fields.Float(compute='_get_peso',string='Kg')
    @api.depends('cajas')
    def _get_peso(self):
      for i in self:
          i.arrobas = i.cajas * 1.5
          i.kilos = i.arrobas * 13.5
 
    @api.constrains('cajas')
    def _check_caixons(self):
      for record in self:
        if record.cajas > 1000:
            raise ValidationError("Too many Boxes: %s" % record.cajas)
 
 
class socios(models.Model):
    _name = 'cooperativa.socio'
    name = fields.Char()
    camions = fields.One2many('cooperativa.camion','socio',string='Trucks')
    n_camiones = fields.Integer(compute='_n_camiones',string='Number of Trucks')
    arrobas = fields.Float(compute='_n_camiones',string='@')
    @api.depends('camions')
    def _n_camiones(self):
      for i in self:
        for j in i.camions:
          i.arrobas = i.arrobas + j.arrobas
          i.n_camiones = i.n_camiones + 1
</syntaxhighlight>
Les vistes i menús:
<syntaxhighlight lang="python" style="font-family:monospace">
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
  <record model="ir.ui.view" id="camion_form_view">
            <field name="name">camion</field>
            <field name="model">cooperativa.camion</field>
            <field name="arch" type="xml">
                <form string="Camion Form">
                    <sheet>
                        <group>
                            <field name="name"/>
                            <field name="matricula"/>
                            <field name="socio"/>
                            <field name="cajas"/>
                            <field name="arrobas"/>
                            <field name="kilos"/>
                        </group>
                    </sheet>
                </form>
            </field>
        </record>
  <record model="ir.ui.view" id="socio_form_view">
            <field name="name">socio</field>
            <field name="model">cooperativa.socio</field>
            <field name="arch" type="xml">
                <form string="Socio Form">
                    <sheet>
                        <group>
                            <field name="name"/>
                            <field name="camions"/>
                            <field name="n_camiones"/>
                            <field name="arrobas"/>
                        </group>
                    </sheet>
                </form>
            </field>
        </record>
        <record model="ir.ui.view" id="view_camions_search">
            <field name="name">coop.camions.search</field>
            <field name="model">coop.camions</field>
            <field name="arch" type="xml">
            <search>
              <field name="name"/>
              <field name="matricula"/>
              <filter name="buits" string="Empty trucks" domain="[('caixons', '=', 0)]"/>
              <filter name="group_by_matricula" string="Matricula" context="{'group_by': 'matricula'}"/>
            </search>
            </field>
          </record>
 
<record model="ir.actions.act_window" id="camion_list_action">
            <field name="name">Trucks</field>
            <field name="res_model">cooperativa.camion</field>
            <field name="view_type">form</field>
            <field name="view_mode">tree,form</field>
            <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first truck
                </p>
            </field>
        </record>
        <record model="ir.actions.act_window" id="socis_list_action">
            <field name="name">Socios</field>
            <field name="res_model">cooperativa.socio</field>
            <field name="view_type">form</field>
            <field name="view_mode">tree,form</field>
            <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first socio
                </p>
            </field>
        </record>
        <menuitem id="main_cooperativa_menu" name="Cooperativa"/>
        <menuitem id="cooperativa_menu" name="Cooperativa"
                  parent="main_cooperativa_menu"/>
        <menuitem id="camion_menu" name="Trucks" parent="cooperativa_menu"
                  action="camion_list_action"/>
        <menuitem id="socio_menu" name="Partner" parent="cooperativa_menu"
                  action="socis_list_action"/>
    </data>
</openerp>
 
</syntaxhighlight>
</div>
 
'''El joc '''(per veure millores dins de la vista):
 
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Els models:
<syntaxhighlight lang="python" style="font-family:monospace">
# -*- coding: utf-8 -*-
 
from openerp import models, fields, api
 
class player(models.Model):
    _name = 'mmog.player'
    name = fields.Char()
    active = fields.Boolean()
    points = fields.Integer()
    money = fields.Integer()
    gold = fields.Integer()
    coal = fields.Integer()
    stones = fields.Integer()
    fortress = fields.One2many('mmog.fortress','id_player')
    avatar = fields.Binary()
 
class fortress(models.Model):
    _name = 'mmog.fortress'
    name = fields.Char()
    id_player = fields.Many2one('mmog.player')
    level = fields.Integer()
    state = fields.Char()
    soldiers = fields.Integer()
    population = fields.Integer()
    food = fields.Integer()
    integrity = fields.Float()
    x = fields.Integer()
    y = fields.Integer()
    icon = fields.Binary()
 
class attack(models.Model):
    _name = 'mmog.attack'
    name = fields.Char()
    fortress_attacking = fields.Many2one('mmog.fortress')
    fortress_defender = fields.Many2one('mmog.fortress')
    data = fields.Datetime()
    soldiers_sent = fields.Integer()
    attacker_soldiers_killed = fields.Integer()
    defender_soldiers_killed = fields.Integer()
    progress = fields.Float()
</syntaxhighlight>
El XML:
<syntaxhighlight lang="xml" style="font-family:monospace">
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
  <data>
    <record model="ir.ui.view" id="player_kanban_view">
      <field name="name">player.kanban</field>
      <field name="model">mmog.player</field>
      <field name="arch" type="xml">
        <kanban quick_create="true">
          <field name="points"/>
          <templates>
            <t t-name="kanban-box">
              <div  t-attf-class="oe_kanban_color_{{kanban_getcolor(record.points.raw_value)}}
                oe_kanban_global_click_edit oe_semantic_html_override
                oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
                <a type="open">
                  <img class="oe_kanban_image"
                    t-att-src="kanban_image('mmog.player', 'avatar', record.id.value)" />
                  </a>
                  <div t-attf-class="oe_kanban_content">
                    <h4>
                      <a type="edit">
                        <field name="name"></field>
                      </a>
                    </h4>
                    <ul>
                      <li>Points: <field name="points"  widget="percentpie"/></li>
                      <li>Money: <field name="money"/></li>
                      <li>Gold: <field name="gold"/></li>
                      <li>Coal: <field name="coal"/></li>
                      <li>Stones: <field name="stones"/></li>
                      <li>Fortresses: <field name="fortress" widget="many2many"/></li>
                    </ul>
                  </div>
                </div>
              </t>
            </templates>
          </kanban>
        </field>
      </record>
      <record model="ir.ui.view" id="fortress_kanban_view">
        <field name="name">fortress.kanban</field>
        <field name="model">mmog.fortress</field>
        <field name="arch" type="xml">
          <kanban quick_create="true" default_group_by="id_player" default_order="level">
            <field name="id_player"/>
            <templates>
              <t t-name="kanban-box">
                <div  t-attf-class="oe_kanban_color_{{kanban_getcolor(record.id_player.raw_value)}}
                  oe_kanban_global_click_edit oe_semantic_html_override
                  oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
                  <a type="open">
                    <img class="oe_kanban_image"
                      t-att-src="kanban_image('mmog.fortress', 'icon', record.id.value)" />
                    </a>
                    <div t-attf-class="oe_kanban_content">
                      <h4>
                        <a type="edit">
                          <field name="name"></field>
                        </a>
                      </h4>
                      <ul>
                        <li>Integrity:
                          <field name="integrity" widget="gauge" style="width:150px; height: 110px;" options="{'levelcolors': ['#a9d70b', '#f9c802', '#ff0000'], 'action_jump': '357'}">Integrity</field>
                        </li>
                      </ul>
                    </div>
                  </div>
                </t>
              </templates>
            </kanban>
          </field>
        </record>
       
        <record model="ir.ui.view" id="player_search_view">
          <field name="name">player.search</field>
          <field name="model">mmog.player</field>
          <field name="arch" type="xml">
            <search string="Player search">
              <field name="name"/>
              <field name="points"/>
              <field name="fortress"/>
              <filter name="0_points" string="0 Points" domain="[('points','=',0)]"/
            </search>
          </field>
        </record>
        <record model="ir.ui.view" id="fortress_search_view">
          <field name="name">fortress.search</field>
          <field name="model">mmog.fortress</field>
          <field name="arch" type="xml">
            <search string="Fortress search">
              <field name="id_player"/>
              <field name="name"/>
              <field name="integrity"/>
              <field name="level"/>
              <filter name="less_50" string="less than 50% of integrity" domain="[('integrity','&lt;',50)]"/>
              <group string="Group By:">
                <filter name="player" string="Player" context="{'group_by':'id_player'}"/>
                <filter name="level" string="Level" context="{'group_by':'level'}"/>
              </group>
            </search>
          </field>
        </record>
        <record model="ir.ui.view" id="attack_search_view">
          <field name="name">attack.search</field>
          <field name="model">mmog.attack</field>
          <field name="arch" type="xml">
            <search string="attack search">
             
              <filter name="progress" string="Not Finished" domain="[('progress','&lt;',100)]"/>
              <group string="Group By:">
                <filter name="data" string="Data" context="{'group_by':'data'}"/>
                <filter name="fortress_attacking" string="Attacking" context="{'group_by':'fortress_attacking'}"/>
                <filter name="fortress_defender" string="Defending" context="{'group_by':'fortress_defender'}"/>
              </group> </search>
            </field>
          </record>
 
          <record model="ir.ui.view" id="player_tree_view">
            <field name="name">player.tree</field>
            <field name="model">mmog.player</field>
            <field name="arch" type="xml">
              <tree string="Player tree">
                <field name="name"/>
                <field name="points" widget="percentpie"/>
                <field name="money"/>
                <field name="gold"/>
                <field name="coal"/>
                <field name="stones"/>
                <field name="fortress"/>
              </tree>
            </field>
          </record>
          <record model="ir.ui.view" id="fortress_tree_view">
            <field name="name">fortress.tree</field>
            <field name="model">mmog.fortress</field>
            <field name="arch" type="xml">
              <tree string="Fortress tree">
                <field name="id_player"/>
                <field name="name"/>
                <field name="integrity" widget="progressbar"/>
                <field name="x"/><field name="y"/>
                <field name="level"/>
                <field name="food"/>
                <field name="soldiers"/>
                <field name="population"/>
              </tree>
            </field>
          </record>
          <record model="ir.ui.view" id="attack_tree_view">
            <field name="name">attack.tree</field>
            <field name="model">mmog.attack</field>
            <field name="arch" type="xml">
              <tree string="attack tree">
                <field name="data"/>
                <field name="progress" widget="progressbar"/>
                <field name="fortress_attacking"/>
                <field name="soldiers_sent"/>
                <field name="attacker_soldiers_killed"/>
                <field name="fortress_defender"/>
                <field name="defender_soldiers_killed"/>
              </tree>
            </field>
          </record>
          <record model="ir.ui.view" id="player_form_view">
            <field name="name">player.form</field>
            <field name="model">mmog.player</field>
            <field name="arch" type="xml">
              <form string="Player Form">
                <sheet>
                  <notebook>
                    <page string="Player data">
                      <field name="avatar" widget="image" class="oe_left oe_avatar"/>
                      <group string="Personal Data">
                        <field name="name"/>
                        <field name="points" widget="integer"/>
                        <field name="active"/>
                      </group>
                    </page>
                    <page string="Resources">
                      <group string="Stock">
                        <field name="money"/>
                        <field name="gold"/>
                        <field name="coal"/>
                        <field name="stones"/>
                      </group>
                      <group string="Buidings">
                        <field name="fortress" mode="kanban,tree">
                          <kanban>
                            <!--list of field to be loaded -->
                            <field name="name" />
                            <field name="icon" />
                           
                            <templates>
                              <t t-name="kanban-box">
                                <div class="oe_product_vignette">
                                  <a type="open">
                                    <img class="oe_kanban_image" style="width:50px; height:auto;"
                                      t-att-src="kanban_image('mmog.fortress', 'icon', record.id.value)" />
                                    </a>
                                    <div class="oe_product_desc">
                                      <h4>
                                        <a type="edit">
                                          <field name="name"></field>
                                        </a>
                                      </h4>
                                     
                                    </div>
                                  </div>
                                </t>
                              </templates>
                            </kanban>
                          </field>
                        </group>
                      </page>
                    </notebook>
                  </sheet>
                </form>
              </field>
            </record>
            <record model="ir.ui.view" id="fortress_form_view">
              <field name="name">fortress.form</field>
              <field name="model">mmog.fortress</field>
              <field name="arch" type="xml">
                <form string="Fortress Form">
                  <sheet>
                    <notebook>
                      <page string="Fortress data">
                        <group><field name="id_player"/></group>
                        <group string="Main Data">
                          <field name="icon" widget="image" class="oe_left oe_avatar"/>
                          <field name="name"/>
                          <field name="integrity"/>
                          <field name="x"/><field name="y"/>
                          <field name="state"/>
                        </group>
                      </page>
                      <page string="Resources">
                        <group string="Stock">
                          <field name="level"/>
                          <field name="food"/>
                        </group>
                        <group string="People">
                          <field name="soldiers"/>
                          <field name="population"/>
                        </group>
                      </page>
                    </notebook>
                  </sheet>
                </form>
              </field>
            </record>
            <record model="ir.ui.view" id="attack_form_view">
              <field name="name">attack.form</field>
              <field name="model">mmog.attack</field>
              <field name="arch" type="xml">
                <form string="attack Form">
                  <sheet>
                    <notebook>
                      <page string="Attack data">
                        <group string="Status">
                          <field name="data"/>
                          <field name="progress"/>
                        </group>
                        <group string="Attacking Fortress">
                          <field name="fortress_attacking"/>
                          <field name="soldiers_sent"/>
                          <field name="attacker_soldiers_killed"/>
                        </group>
                        <group string="Defender Fortress">
                          <field name="fortress_defender"/>
                          <field name="defender_soldiers_killed"/>
                        </group>
                      </page>
                    </notebook>
                  </sheet>
                </form>
              </field>
            </record>
 
            <record model="ir.actions.act_window" id="players_list_action">
              <field name="name">Players</field>
              <field name="res_model">mmog.player</field>
              <field name="view_type">form</field>
              <field name="view_mode">tree,form,kanban</field>
              <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first player
                </p>
              </field>
            </record>
            <record model="ir.actions.act_window" id="fortresss_list_action">
              <field name="name">Fortress</field>
              <field name="res_model">mmog.fortress</field>
              <field name="view_type">form</field>
              <field name="view_mode">tree,form,kanban</field>
              <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first fortress
                </p>
              </field>
            </record>
            <record model="ir.actions.act_window" id="attacks_list_action">
              <field name="name">Attacks</field>
              <field name="res_model">mmog.attack</field>
              <field name="view_type">form</field>
              <field name="view_mode">tree,form,calendar</field>
              <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first attack
                </p>
              </field>
            </record>
           
            <menuitem id="main_mmog_menu" name="Mmog"/>
            <menuitem id="mmog_menu" name="Mmog Config"
              parent="main_mmog_menu"/>
              <menuitem id="players_menu" name="Players" parent="mmog_menu"
                action="players_list_action"/>
                <menuitem id="fortress_menu" name="Fortress" parent="mmog_menu"
                  action="fortresss_list_action"/>
                  <menuitem id="attack_menu" name="Attack" parent="mmog_menu"
                    action="attacks_list_action"/>
                  </data>
                </openerp>
 
</syntaxhighlight>
</div>
 
== Misc. ==
 
* Si volem fer un print en colors, podem ficar un caracter de escape: \033[93m i \033[0m al final
* Traure la menor potència de 2 major o igual a un número: http://stackoverflow.com/a/14267557
 
Cron Jobs
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Cal crear un record en el model ir.cron, per exemple:
<syntaxhighlight lang="xml" style="font-family:monospace">
    <record forcecreate="True" id="revision_due_invoices_v1" model="ir.cron">
          <field name="name">Revision de Facturas Vencidas</field>
          <field eval="True" name="active" />
          <field name="user_id" ref="base.user_root" />
          <field name="interval_number">24</field>
          <field name="interval_type">hours</field>
          <field name="numbercall">-1</field>
          <field ref="model_account_invoice" name="model_id" />
            <field name="state">code</field>
          <field name="code">model.revision_due_invoices()</field>
          <field eval="False" name="doall"/>
          <field name="function">True</field>
        </record>
</syntaxhighlight>
I un mètode amb el @api.model i aquests arguments:
<syntaxhighlight lang="python" style="font-family:monospace">
    @api.model
    def revision_due_invoices(self, id=None):
        print "### Revisando las Facturas Vencidas"
        ...
</syntaxhighlight>
https://poncesoft.blogspot.com/2018/05/creacion-metodos-automatizados-en-odoo.html https://webkul.com/blog/creating-cron-server-action-odoo-11/ https://odoo-development.readthedocs.io/en/latest/odoo/models/ir.cron.html
</div>
 
Distintes alertes:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Odoo pot mostrar distintes alertes en funció del que necessitem. Totes estan en openerp.exceptions
 
Si entrem en el mode shell del debug podem executar aquest comandament:
<pre class="code">
>>> help(openerp.exceptions)
</pre>
Una vegada dins podem detectar:
AccessDenied
DeferredException
QWebException
RedirectWarning
except_orm
        AccessError
        MissingError
        UserError
        ValidationError
 
Normalment són utilitzats pel Odoo sense necessitat de que els cridem nosaltres. Però en ocasion pot ser útil.
 
Per exemple, si volem mostrar un Warning perquè úsuari ha fet alguna cosa mal. (Normalment es fa un onchange que ja pot tornar el warning)
<syntaxhighlight lang="python" style="font-family:monospace">
from openerp import _
from openerp.exceptions import Warning
[...]
raise Warning(_('Alguna cosa ha fallat!'))
</syntaxhighlight>
 
O si volem Donar opcions a l'usuari amb RedirectWarning:
<syntaxhighlight lang="python" style="font-family:monospace">
action = self.env.ref('base.action_res_users')
msg = _("You cannot create a new user from here.\n To create new user please go to configuration panel.")
raise openerp.exceptions.RedirectWarning(msg, action.id, _('Go to the configuration panel'))
</syntaxhighlight>
En aquest exemple, per al missatge, utilitza la barra baixa '''_()''' per a obtindre la traducció en cas de que existisca. '''self.env.ref()''' retorna l'objecte referit amb una id externa. En aquest cas, un action.
 
En el cas de les Constrains també s'ha de llançar un Validation error.
</div>
 
Funcions lambda:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
En moltes ocasions, cal cridar a alguna funció de l'ORM o similar passant com a paràmetre una funció lambda. La raó és que si passem una variable, esta queda establerta en temps de càrrega i no es modifica. La funció sempre recalcula.
 
La sintaxi de la funció lambda és:
<syntaxhighlight lang="python" style="font-family:monospace">
a = lambda x,y: x*y
a(2,3)
6
</syntaxhighlight>
On les primeres x,y són els arguments que rep la funció, després el que calcula.
 
Cal recordar que les funcions lambda són de una sola línia de codi. Si volem alguna cosa més sofisticada hem de cridar a una funció normal.
</div>
 
Càlculs en dates:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Odoo gestiona les dates com a strings. Per una altra banda, python té el seu propi tipus de dades anomenat datetime, date i timedelta entre altres. Això pot provocar dificultats per a fer cálculs en dates. Odoo proporciona algunes ferramentes mínimes per facilitar aquesta tasca.
 
Primer de tot, anem a importar datetime:
<syntaxhighlight lang="python" style="font-family:monospace">
from odoo import models, fields, api
from datetime import datetime, timedelta
</syntaxhighlight>
El primer que necessitem saber és cóm transformar de date o datetime d’Odoo a python. En definitva, passar de string a datetime.
 
Tenim un field datetime declarat de la següent manera:
<syntaxhighlight lang="python" style="font-family:monospace">
start_date = fields.Datetime()
</syntaxhighlight>
En la base de dades guardarà un string amb el format: '%Y-%m-%d %H:%M:%S'. Per tant, si volem transformar aquesta data en string a un objecte datetime.datetime tenim que ejecutar el constructor de la classe amb aquests paràmetres:
<syntaxhighlight lang="python" style="font-family:monospace">
fmt = '%Y-%m-%d %H:%M:%S'
data = datetime.strptime(self.start_date,fmt)
</syntaxhighlight>
És a dir, transforma un string en aquest format al tipus de dades datetime.datetime oficial de python.
 
Per no tindre que especificar el format cada vegada, Odoo dona una ferramenta més facil. La classe '''fields.Datetime''' té un mètode per generar un datetime.datetime de un string:
<syntaxhighlight lang="python" style="font-family:monospace">
data = fields.Datetime.from_string(self.start_date)
</syntaxhighlight>
De la mateixa manera passa al contrari:
<syntaxhighlight lang="python" style="font-family:monospace">
fmt = '%Y-%m-%d %H:%M:%S'
self.start_date = data.strftime(fmt)
vs
self.start_date = fields.Datetime.to_string(data)
</syntaxhighlight>
 
'''A continuació, anem a veure cóm incrementar una data en un temps:'''
 
En el format d’Odoo (fields.Datetime) no es pot, cal passar a datetime.datetime per sumar el temps i després tornar a passar a fields.Datetime.
Per sumar o restar temps a un datetime.datetime cal utilitzar una classe anomenada datetime.timedelta. Aquesta classe representa una duració o la diferència entre dues dates.
Per exemple, aquest constructor representa molt bé les opcions que es poden ficar per crear un timedelta:
<syntaxhighlight lang="python" style="font-family:monospace">
un_any = timedelta(weeks=40, days=84, hours=23, minutes=50, seconds=600)
</syntaxhighlight>
Aquest exemple d’Odoo mostra cóm afegir 3 dies a un field:
<syntaxhighlight lang="python" style="font-family:monospace">
data=fields.Datetime.from_string(self.start_date)
data=data+timedelta(hours=3)
self.end_date=fields.Datetime.to_string(data)
</syntaxhighlight>
O si es vol fer sols en mètodes python:
<syntaxhighlight lang="python" style="font-family:monospace">
fmt = '%Y-%m-%d %H:%M:%S'
data = datetime.strptime(self.start_date,fmt)
data=data+timedelta(hours=3)
self.end_date=data.strftime(fmt)
</syntaxhighlight>
 
'''Ara anem a veure cóm calcular el temps que ha passat entre dues dates:'''
 
Solució amb '''relativedelta''':
<syntaxhighlight lang="python" style="font-family:monospace">
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
 
start=fields.Datetime.from_string(self.start_date)
end=fields.Datetime.from_string(self.end_date)
 
relative=relativedelta(start,end)
print r.years
print r.months
print r.days
print r
</syntaxhighlight>
El problema és que dona la data per separat. No sol ser molt útil per a Odoo on necessitem la diferència sols en dies, hores o minuts.
 
Solució sols amb '''Datetime''':
<syntaxhighlight lang="python" style="font-family:monospace">
from datetime import datetime
start=fields.Datetime.from_string(self.start_date)
end=fields.Datetime.from_string(self.end_date)
print (end-start).days * 24 * 60
print (end-start).total_seconds()/60/60/24
</syntaxhighlight>
Solució amb '''Unix timestamp'''
<syntaxhighlight lang="python" style="font-family:monospace">
d1_ts = time.mktime(d1.timetuple())
d2_ts = time.mktime(d2.timetuple())
 
print int(d2_ts-d1_ts) / 60
</syntaxhighlight>
La solució és la mateixa, sols és per si necessiteu algun càlcul intermedi que necessite la data en un Integer.
 
 
El resultat de restar dos datetime és un timedelta. Podem demanar els dies i segons com en el relative delta, però amés té una funció per traure els segons totals i després fer els càlculs que necessitem.
 
'''Consultar si una data és anterior a una altra:'''
 
Les dates en format Datetime o Date es poden comparar:
<syntaxhighlight lang="python" style="font-family:monospace">
d3=fields.Datetime.from_string(self.d3)
d4=datetime.now()
if d3 < d4:
  print "La data és anterior"
</syntaxhighlight>
També es pot calcular si és del mateix dia, sols cal transformar de datetime a date:
<syntaxhighlight lang="python" style="font-family:monospace">
d3=d3.date()
d4=d4.date()
 
if d3 == d4 :
  ….
</syntaxhighlight>
Si volem saber si són del mateix més o any, es pot calcular la diferència i veure si en dies és major o menor de 30, per exemple. Però si volem major precisió, en aquest cas es recomana utilitar relativedelta.
 
</div>
 
Imatges en Odoo:
<div class="toccolours mw-collapsible mw-collapsed" style="overflow: hidden;">
Les imatges es guarden com un text en la base de dades. Per convertir un binari en text es fa en base64. Aquesta codificació funciona sense problemes en els navegadors web i no cal preocupar-se de cóm guardar les imatges en PostgreSQL.
 
Per mostrar la imatge en formularis o llistes es pot ficar '''widget="image"''' en el ''field''. Si no fem res més, la imatge es veurà en la mida original. Podem navegar per altres formularis per veure cóm han fet per mostrar la imatge i aprofitar les classes CSS. També podem utilitzar la etiqueta '''width''' o '''style''' per especificar la mida exacta. 
 
No obstant, no és recomanable carregar les imatges en el seu tamany original. El millor és guardar la versió mitjana o menuda i mostrar eixa sols. Per aconseguir-ho, es pot fer un camp binary computed d'aquesta manera:
<syntaxhighlight lang="python" style="font-family:monospace">
from odoo import models, fields, api, tools
[...]
    photo = fields.Binary()
    photo_small = fields.Binary(compute='_get_images',store=True)
    photo_medium = fields.Binary(compute='_get_images',store=True)
 
    @api.one
    @api.depends('photo')
    def _get_images(self):
        image = self.photo
        data = tools.image_get_resized_images(image)
        self.photo_small = data["image_small"]
        self.photo_medium = data["image_medium"]
</syntaxhighlight>
 
La ferramenta '''tools.image''' d'Odoo té funcions per a reescalar, retallar o millorar les imatges.
 
Per utilitzar una imatge dins d'un Kanban, es necessita la funció '''kanban_image()''':
<syntaxhighlight lang="xml" style="font-family:monospace">
<img t-att-src="kanban_image('reserves.hotels', 'photo_small', record.id.value)"/>
</syntaxhighlight>
Prèviament, s'ha declarat el field ''photo_small'' i ''id'' al kanban.
</div>
 
== Enllaços ==
https://www.odoo.com/documentation/8.0/
https://www.odoo.com/documentation/9.0/
 
https://www.odoo.com/documentation/8.0/howtos/backend.html
 
Blogs: http://ludwiktrammer.github.io/ http://www.odoo.yenthevg.com/ https://sateliteguayana.wordpress.com/ https://poncesoft.blogspot.com/
 
Repositori dels exemples: https://github.com/xxjcaxx/sge20152016 https://github.com/xxjcaxx/SGE-Odoo-2016-2017
 
https://www.youtube.com/watch?v=0GUxV85DDm4&feature=youtu.be&t=5h47m45s


http://es.slideshare.net/openobject/presentation-of-the-new-openerp-api-raphael-collet-openerp
[https://apuntesfpinformatica.es/sistemas-de-gestion-empresarial/ Apunts d'altres professors recopilats]


http://fundamentos-de-desarrollo-en-odoo.readthedocs.org/es/latest/capitulos/comenzando-con-odoo.html
https://naglis.me/post/odoo-13-changelog/  https://www.odoo.com/es_ES/forum/ayuda-1/question/odoo-13-features-and-odoo-14-expected-features-148369#answer-148370


https://www.odoo.com/es_ES/slides/slide/keynote-odoo-9-new-features-201
https://medium.com/@manuelcalerosolis


https://media.readthedocs.org/pdf/odoo-development/latest/odoo-development.pdf
https://www.youtube.com/playlist?list=PLeJtXzTubzj-tbQ94heWeQFB0twGd0vvN


http://webkul.com/blog/beginner-guide-odoo-clicommand-line-interface/
https://vimeo.com/channels/m10dam/page:1
 
http://useopenerp.com/v8
 
Podcast que parlen dels beneficis d'Odoo: http://www.ivoox.com/podcast-26-odoo-transformacion-digital-audios-mp3_rf_18433975_1.html
 
https://www.odoo.yenthevg.com/extend-selection-odoo-10/

Revisión actual - 16:17 9 nov 2023

Articles relacionats: Instal·lar Odoo, Accions i menús en Odoo, Millores en la vista en Odoo, Odoo reports, Wizards en Odoo, El client Web Odoo, Web Controller en Odoo

El servidor Odoo

Aquesta secció està detallada en l'articul Instal·lar Odoo

Arquitectura

El framework d'Odoo facilita diversos components que permeten construir l’aplicació:

Arquitectura MVC
Cliente Servidor Odoo
Arquitectura per capes
  • La capa ORM (Object Relational Mapping) entre els objectes Python i la base de dades PostgreSQL. El dissenyador-programador no efectua el disseny de la base de dades; únicament dissenya classes, per les quals la capa ORM d’Odoo n’efectuarà el mapat sobre el SGBD PostgreSQL.
  • Una arquitectura MVC (model-vista-controlador) en la qual el model resideix en les dades de les classes dissenyades amb Python, la vista resideix en els formularis, llistes, calendaris, gràfics… definits en fitxers XML i el controlador resideix en els mètodes definits en les classes que proporcionen la lògica de negoci.
  • Odoo és un ERP amb una arquitectura de Tenencia Múltiple. És A dir, té una base de dades i un servidor comú per a tots els clients. El contrari sería tindre un servidor o base de dades per client o virtualitzar.
  • Dissenyadors d’informes.
  • Facilitats de traducció de l’aplicació a diversos idiomes.

El servidor Odoo proporciona un accés a la base de dades emb ORM. El servidor necessita tindre instal·lats mòduls, ja que comença buit.

Per altra banda, el client es comunica amb el servidor en XML-RPC, els clients web per JSON-RPC. El client sols té que mostrar el que dona el servidor o demanar correctament les dades. Per tant, un client pot ser molt simple i fer-se en qualsevol llenguatge de programació. Odoo proporciona un client web encara que es pot fer un client en qualsevol plataforma.

Les dades estan guardades en una base de dades relacional. Gràcies a l'ORM, no cal fer consultes SQL directament. ORM proporciona una serie de mètodes per a treballar de manera més ràpida i segura. En compte de parlar de taules es parla de models. Aquest són 'mapejats' per l'ORM en taules. No obstant, un model té més que dades en una taula. Un model es comporta con un objecte al tindre camps funcionals, restriccions i camps relacionals que deixen la normalització de la base de dades en mans d'Odoo.

L'accés del client a les dades es fa fent ús d'un servici. Aquest pot ser WSGI. WSGI és una solució estàndard per a fer servidors i clients HTTP en Python. En el cas d'Odoo, aquest té el OpenERP Web Project, que és el servidor web.

Un altre concepte dins d'Odoo són els Business Objects quasi tot en Odoo és un Business Object, és persistent gràcies a ORM i es troba estructurat en el directori /modules.

Odoo proporciona els anomenats Wizards, que es comporten com un assistent per introduir dades d'una manera més fàcil per a l'usuari.

El client web és fàcil de desenvolupar gràcies al Widgets o Window GaDGETS. Aquests proporcionen un comportament i visualització correctes per a cada tipus de dades. Per exemple: si el camp és per definir una data, mostrarà un calendari. Alguns tenen diferents visualitzacions en funció del tipus de vista i se'n poden definir Widgets personalitzats.

Tal com es pot observar, són molts els components d’OpenObject a conèixer per poder adequar l’Odoo a les necessitats de l’organització, en cas que les funcionalitats que aporta l’Odoo, tot i ser moltes, no siguin suficients.

La base de dades d'Odoo

En l’Odoo no hi ha un disseny explícit de la base de dades, sinó que la base de dades d’una empresa d’Odoo és el resultat del mapatge del disseny de classes de l’ERP cap el SGBD PostgreSQL que és el que proporciona la persistència necessària per als objectes. Això és el ORM.

En conseqüència, l’Odoo no facilita cap disseny entitat-relació sobre la base de dades d’una empresa ni tampoc cap diagrama del model relacional.

Si sorgeix la necessitat de detectar la taula o les taules on resideix determinada informació, és perquè es coneix l’existència d’aquesta informació gestionada des de l’ERP i, per tant, es coneix algun formulari de l’ERP a través del qual s’introdueix la informació.

L’Odoo possibilita, mitjançant el clients web recuperar el nom de la classe Python que defineix la informació que s’introdueix a través d’un formulari i el nom de la dada membre de la classe corresponent a cada camp del formulari. Aquesta informació permet arribar a la taula i columna afectades, tenint en compte dues qüestions:

  • El nom de les classes Python d’Odoo sempre són en minúscula (s’utilitza el guió baix per fer llegible els mots compostos) i segueix la nomenclatura nom_del_modul.nom1.nom2.nom3… en la qual s’utilitza el punt per indicar un cert nivell de jerarquia. Cada classe Python d’Odoo és mapada en una taula de PostgreSQL amb moltes possibilitats que el seu nom coincideixi amb el nom de la classe, tot substituint els punts per guions baixos.
  • Els noms dels atributs d’una classe Python sempre són en minúscula (s’utilitza el guió baix per fer llegible els mots compostos). Cada dada membre d’una classe Python d’Odoo que sigui persistent (una classe pot tenir dades membres calculades no persistents) és mapat com un atribut en la corresponent taula de PostgreSQL amb el mateix nom.
Per exemple: La classe Python sale.order d’Odoo està pensada per descriure la capçalera de les comandes de venda i la corresponent taula a PostgreSQL és sale_order.

D’aquesta manera, coneixent el nom de la classe i el nom de la dada membre, és molt possible conèixer el nom de la taula i de la columna corresponents. Es pot configurar el client web per tal que informe del nom de la classe i de la dada membre en situar el ratolí damunt les etiquetes dels camps dels formularis.

Els mòduls

Tant el servidor com els clients són mòduls. Tots estàn guardats en una base de dades. Tot els que es puga fer per modificar Odoo es fa en mòduls.

Composició d'un mòdul

Els mòduls d'Odoo amplien o modifiquen parts de Model-Vista-Controlador. D'aquesta manera, un mòdul pot tindre:

  • Objectes de negoci: Són la part del model, estan definits en classes de Python segons una sintaxy pròpia de l'ORM d'Odoo.
  • Fitxers de dades: Són fitxers XML que poden definir dades, vistes o configuracions.
  • Controladors web: Gestionen les peticions dels navegadors web.
  • Dades estàtiques: Imatges, CSS, o javascript utilitzats per l'interficie web. És necessari que les dades estátiques es guarden en el directori static. Per exemple, l'icona del mòdul va en static/description/icon.png

Estructura de fitxers d'un mòdul

03 module gen view.png

  • Tots el mòduls estan en un directori definit en l'opció --addons-path o el fitxer de configuració. Poden ser més d'un directori.
  • Un mòdul de python es declara en un fitxer de manifest que dona informació sobre el mòdul, el que fa el mòduls dels que depen i cóm s'ha d'instal·lar o actualitzar. [1]
  • Un mòdul és un paquet de Python que necessita un __init__.py per a instanciar tots els fitxers python.

Creació de mòduls

Per ajudar al programador, Odoo conté un comandament per crear mòduls buits. Aquest crea l'estructura de fitxers necessaria per començar a treballar:

 $ odoo scaffold <module name> <where to put it>

Més al voltant d'Scaffold:

Manual oficial Scaffolding

El parametre scaffold pot tindre la opció -t per indicar el directori del template. Aquest està fet utilitzant jinja2, que és un motor de plantilles per a python.

Els templates estan en el directori d'instal·lació d'odoo al directori cli. En el nostre cas: cli/templates/ dins del directori d'instal·lació d'Odoo.

Podem fer un template copiant el directori default o theme i modificant els fitxers. Això pot ser útil si sempre fem mòduls amb la mateixa plantilla. Per exemple per ficar el nostre logo, copyright o demés.

ORM

Odoo mapeja els seus objectes en una base de dades amb ORM, evitant al programador la majoria de consultes SQL. D'aquesta manera el desenvolupament dels mòduls és molt ràpid i evitem errades de programació.

Els models són creats com classes de python que extenen la classe models.Model que conté els camps i mètodes útils per a fer anar l'ORM.

Els models, al heretar de models.Model, necessiten donar valor a algunes variables, com ara _name

Odoo considera que un model és la referència a una o més taules en la base de dades. Un model no és una fila en la taula, és tota la taula.

En programació, el Model és una manera de relacionar el programa amb la base de dades. És de més alt nivell que les consultes directes en quant a base de dades i que les clases i objectes respecte a la programació orientada a objectes. El model junta en un únic concepte les estructures de dades, les restriccions d'integritat i les opcions de manipulació de les dades.

Els models en Odoo poden heretar de models.Model i ser els normals mapejats i permanents en la base de dades. També poden ser models.TransientModel i són iguals, sols que no tenen permanència definitiva en la base de dades. Aquest són els recomanables per a fer wizards. També poden ser models.AbstractModel per a definir models abstractes per a després heretar.

Inspeccionar el models

Per veure els models existents, es pot accedir a la base de dades postgreSQL o mirar en Configuración > Estructura de la base de datos > Modelos dins del mode desenvolupador.

Cal destacar el camp modules on diu els mòduls instal·lats on es defineix o hereta el model observat.

Fields

Les "columnes" del model són els fields. Aquests poden ser de dades normals com Integer, Float, Boolean, date, char... o especials como many2one, one2many, related...

Hi ha uns fields reservats:

  • id (Id) the unique identifier for a record in its model
  • create_date (Datetime) creation date of the record
  • create_uid (Many2one) user who created the record
  • write_date (Datetime) last modification date of the record
  • write_uid (Many2one) user who last modified the record

Hi ha altres fields que podem declarar i tenen propietats especials. Aquests són els més importants:

  • name És el field utiltizat per fer l'Identificador Extern o quan es fa referència en els many2one en la vista.
  • active que diu si el record és actiu. Permet ocultar productes que ja no es necessiten, per exemple.
  • sequence Permet definir l'ordre del records a mostrar en una llista.
  • state És de tipus selection i permet crear un cicle de vida d'un model. Amés es pot representar en el <head> d'un form amb un widget statusbar i els fields de les vistes poden ocultar-se en funció d'un camp state ficant l'atribut states="".

Els fields es declaren amb un constructor:

from openerp import models, fields

class LessMinimalModel(models.Model):
    _name = 'test.model2'

    name = fields.Char()

Tenen uns atributs comuns:

  • string (unicode, per defecte: El nom del field) L'etiqueta que veuran els usuaris en la vista.
  • required (bool, per defecte: False) Si és True, el camp no es por deixar buit.
  • help (unicode, per defecte: ) En els formularis proporciona ajuda a l'usuari per plenar el camp.
  • index (bool, per defecte: False) Demana a Odoo fer que siga el índex de la base de dades. En altre cas, el ORM crea un camp id.

I algunes, sobretot les especials, tenen atributs particulars.

Exemple complet:

class AModel(models.Model):

    _name = 'a_name'

    name = fields.Char(
        string="Name",                   # Optional label of the field
        compute="_compute_name_custom",  # Transform the fields in computed fields
        store=True,                      # If computed it will store the result
        select=True,                     # Force index on field
        readonly=True,                   # Field will be readonly in views
        inverse="_write_name"            # On update trigger
        required=True,                   # Mandatory field
        translate=True,                  # Translation enable
        help='blabla',                   # Help tooltip text
        company_dependent=True,          # Transform columns to ir.property
        search='_search_function',        # Custom search function mainly used with compute
        copy =True                       # Si es pot copiar amb el mètode copy() 
    )

   # The string key is not mandatory
   # by default it wil use the property name Capitalized

   name = fields.Char()  #  Valid definition

Si volem valors per defecte, es poden indicar com un atribut del field.

 name = fields.Char(default='Alberto')
 # o:
 name = fields.Char(default=a_fun)
 #...
 def a_fun(self):
   return self.do_something()

Veure: Valors per defecte

Fields normals

Aquests són els fields per a dades normals que proporciona Odoo:

  • Integer
  • Char
  • Text
  • Date : Mostra un calendari en la vista.
  • Datetime
  • Float
  • Boolean
  • Html : Guarda un text, però es representa de manera especial en el client.
  • Binary : Per guardar, per exemple, imatges. Utilitza codificació base64 al enviar els fitxers al client. En realitat les guarda en /var/lib/odoo/.local/share/Odoo/filestore i la ruta als fitxers la diu la taula ir_attachment junt amb el id, nom del field i el model.
  • Image (Odoo13) : En el cas d'imatges, accepta els atributs max_width i max_height on es pot dir en píxel que ha de redimensionar la imatge a eixa mida màxima.
  • Selection : Mostra un select amb les opcions indicades.
     type = fields.Selection([('1','Basic'),('2','Intermediate'),('3','Completed')])
     aselection = fields.Selection(selection='a_function_name') # Es pot cridar a una funció que defineix les opcions.

Fields Relacionals

Les relacions entre els models (en definitiva, entre les taules de la base de dades) també les simplifica l'ORM. D'aquesta maneram les relacions 1 a molts es fan en el Odoo anomena Many2one i les relacions Mols a Molts es fan el el Many2Many. Les relacions molts a molts, en una base de dades relacional, impliquen una tercera taula en mitg, però en Odoo no tenim que preocupar-nos d'aquestes coses si no volem, el mapat dels objectes el detectarà i farà les taules, claus i restriccions d'integritat necessaries. Anem a repasar un a un aquests camps:

Reference

Una referència arbitrària a un model i un camp. [2]

 
 aref = fields.Reference([('model_name', 'String')])
 aref = fields.Reference(selection=[('model_name', 'String')])
 aref = fields.Reference(selection='a_function_name')

# Fragment de test_new_api:
    reference = fields.Reference(string='Related Document', selection='_reference_models')
    @api.model
    def _reference_models(self):
        models = self.env['ir.model'].search([('state', '!=', 'manual')])
        return [(model.model, model.name)
                for model in models
                if not model.model.startswith('ir.')]

Els fields reference no són molt utilitzats, ja que normalment les relacions entre models són sempre les mateixes.

Many2one

Relació amb un altre model

 arel_id = fields.Many2one('res.users')
 arel_id = fields.Many2one(comodel_name='res.users')
 an_other_rel_id = fields.Many2one(comodel_name='res.partner', delegate=True)

En aquest cas:

----------            -----------
| Pais   |  one       |  Ciutat | 
---------- -----      -----------
| * id   |     |      | * id    |
| * name |     |  many| * name  |
----------     |------| * pais  |
                      -----------

El codi resultant sería:

class ciutat(models.Model):
    _name = 'mon.ciutat'
    pais = fields.Many2one("mon.pais", string='Pais', ondelete='restrict')

delegate està en True per a fer que els fields del model apuntat siguen accessibles des del model actual.

Accepta també context i domain com en la vista. D'aquesta manera queda disponible per a totes les possibles vistes.

Un altre argument addicional és ondelete que permet definir el comportament al esborrar l'element referenciat a set null, restrict o cascade.

ondelete cascade esborra els fills a nivel de PostgreSQL, però no elimina en External Id, això es fa en unlink(), però no executa unlink() dels fills. Per tant, si volem que s'eliminen per complet, cal heretar el unlink del pare i afegir la cridada al dels fills. Mirar l'exemple

One2many

Inversa del Many2one. Necessita de la existència d'un Many2one en l'altre:

 arel_ids = fields.One2many('res.users', 'arel_id')
 arel_ids = fields.One2many(comodel_name='res.users', inverse_name='arel_id')

Un One2many funciona perquè hi ha un many2one en l'altre model. D'aquesta manera, sempre has de especificar el nom del model i el nom del camp Many2one del model que fa referencia a l'actual, com es pot veure en l'exemple.

En l'exemple anterior, quedaria com:

class pais(models.Model):
    _name = 'mon.pais'
    ciutats = fields.One2many('mon.ciutat', 'pais', string='Ciutats')
És important entendre que el One2many no implica dades addicionals en la base de dades i sempre és calculat com un select en la base de dades on el id del model actual coincidisca amb el Many2one (clau aliena) de l'altre model. Això fa que no tinga sentit fer One2many computed o ficar un domain per restringit els que es poden afegir.
Els One2many poden tindre domain per no mostrar els que no compleixen una condició, això no significa que no existeixi aquesta relació.

Many2many

Relació molts a molts.

 arel_ids = fields.Many2many('res.users')
 arel_ids = fields.Many2many(comodel_name='res.users', # El model en el que es relaciona
                            relation='table_name', # (opcional) el nom del la taula en mig
                            column1='col_name', # (opcional) el nom en la taula en mig de la columna d'aquest model
                            column2='other_col_name')  # (opcional) el nom de la columna de l'altre model.

El primer exemple sol funcionar directament, però si volem tindre més d'una relació Many2many entre els dos mateixos models, cal utilitzar la sintaxi completa on especifiquem el nom de la relació i el nom de les columnes que identifiquem els dos models. Pensem que una relació Many2many implica una taula en mig i estem especificant les seues claus alienes.

També és precís especificar la taula en mig si es fa una relació Many2many al propi model.
Un Many2many implica una taula en mig. Si volem afegir atributs a aquesta relació, cal crear explícitament el model del mig. El many2many pot ser computed i en el còmput es pot ordenar o filtrar. Un Many2many computed no crea la taula en mig.

Related

Un camp d'un altre model, necessita una relació Many2one. Encara que estiga Store=True, Odoo 8.0 l'actualitza correctament. D'aquesta manera es poden aprofitar les funcionalitats de guardar, com ara les búsquedes o referències en funcions. En termes de bases de dades, un camp related trenca la tercera forma normal. Això sol ser problemàtic, però Odoo té mecanismes per a que no passe res. De totes maneres, si ens preocupa això, amb store=False no guarda res en la taula.

participant_nick = fields.Char(string='Nick name',
                               store=True,
                               related='partner_id.name'

Un camp related pot ser de qualsevol tipus. Per exemple, many2one:

sala = fields.Many2one('cine.sala',related='sessio.sala',store=True,readonly=True)
Un camp related pot ser perillós, ja que si es modifica, pot modificar l'original. Per tant, quasi sempre és necessari afegir readonly, com en l'exemple anterior.

Many2oneReference

Un Many2one on es guardar també el model al qual fa referència amb el atribut: model_field.

One2one

Els camps One2one no existeixen en Odoo. Però si volem aquesta funcionalitat podem utilitzar varies tècniques:

  • Fer dos camps Many2many i restringir amb constrains que sols pot existir una relació. Problemes:
    • En la vista no podem ficar un widget com en el Many2one i és complicat evitar relacions creuades.
    • Es pot fer un limit en la vista, però es continuarà comportant com un Many2many.
  • Fer dos Many2one i restringit amb contrains o sql constrains que sols pot existir una relació mútua. (Cal sobreescriure els mètodes create i write per a que es cree l'associació automàticament). Problemes:
    • Si sobreescribim el write de els dos, es pot produir una cridada recursiva sense fi i és molt complicat aconseguir que no tingam referències creuades.
  • Fer un Many2one i en l'altre model un Many2one computed que busque en els del primer model. Per poder editar en els dos cal fer una funció inversa per al camp computed. Aquesta és una de les opcions més elegants. Exemple:
class orderline(models.Model):
    _name = 'sale.order.line'
    _inherit = 'sale.order.line'
    booking = fields.Many2one('reserves.bookings')
      
    _sql_constraints = [
    ('booking_uniq', 'unique(booking)', 'There is another order line for this booking'),
    ]

class bookings(models.Model):
    _name = 'reserves.bookings'

    name = fields.Char()
    order_line = fields.Many2one('sale.order.line',compute='_get_order_line',inverse='_set_order_line')

    @api.multi
    def _get_order_line(self):
        for b in self:
            b.order_line=self.env['sale.order.line'].search([('booking.id','=',b.id)]).id

    @api.one
    def _set_order_line(self):
        o = self.order_line.id
        self.env['sale.order.line'].search([('id','=',o)]).write({'booking':self.id})
  • Fer un Many2one i un One2many i restringir el màxim del One2many ( [3] ). Problemes:
    • Els mateixos que en els dos many2manys. És més simple restringir les relacions creuades.
  • Fer una herència múltiple. [4]. Problemes:
    • Esta és, en teoría, la forma més oficial de fer-ho, però obliga a crear sempre la relació i els models en un ordre determinat.

Filtres (Domains)

En ocasions és necessari afegir un filtre en el codi python per fer que un camp relacional no puga tindre certes referències. El comportament del domain és diferent depen del tipus de field.

  • Domain en Many2one: Filtra els elements del model referenciat que poden ser elegits per al field:
parent = fields.Many2one('game.resource', domain="[('template', '=', True)]")
  • Domain en Many2many: La llista d'elements a triar es filtra segons el domain:
characters_attack = fields.Many2many('game.character',
                                      relation='characters_attack', 
                                      domain="[('id', 'in', characters_attack_available)]")
  • Domain en One2many: Al ser una relació que depen d'altre Many2one, no es pot filtrar, si fiquem un domain, sols deixarà de mostrar els que no compleixen el domain, però no deien d'existir:
raws = fields.One2many('game.raws','clan', domain= lambda s: [('quantity','>',0)])

Observem com hem fet un domain amb lambda, és a dir, aquest domain crida a una funció lambda al ser aplicat.

Millores_en_la_vista_en_Odoo#Operadors_per_als_domains:

Fields Computed

Moltes vegades volem que el contingut d'un camp siga calculat en el moment en que anem a veure-lo. Tots els tipus de fields poden ser computed. Anem a veure alguns exemples:

   taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')   # Aquest camp no es guarda a la base de dades 
                                                                #i sempre es recalcula quan executem un action que el mostra

   @api.depends('seats', 'attendee_ids')  # El decorador @api.depends() indica que es cridarà a la funció 
                                          # sempre que es modifiquen els camps seats i attendee_ids. 
                                          #Si no el posem, es recalcula sols al recarregar el action.
   def _taken_seats(self):          
      for r in self:  # El for recorre self, que és un recordset amb tots els elements del model mostrats 
                      # per la vista, si és un tree, seran tots els visibles i si és un form, serà un singleton.
          if not r.seats: # r és un singleton i es pot accedir als fields com a variables de l'objecte.      
              r.taken_seats = 0.0 
          else:
              r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats

En aquest exemple es veu cóm el camp float taken seats es calcula en una funció privada _taken_seats. És interessant observar el for perquè recorre totes les instancies a les que fa referència el model. Aquesta funció sols s'executarà una vegada encara que tinga que calcular tots els elements d'una llista. Per això, la propia funció és la que té que iterar els elements de self. self és un recordset, per tant, és com una llista en la que cada element és un registre del model. Si el computed és cridat al entrar a un formulari, el recordset tindrà sols un element, però si el camp computed es veu en una llista (tree), pot ser que siguen més d'un registre. És important recordar fer el for record in self: encara que pensem que el camp computed sols l'utilitzarem en un formulari.

Un altre exemple:

class ComputedModel(models.Model):
    _name = 'test.computed'

    name = fields.Char(compute='_compute_name')
    value = fields.Integer()

    @api.depends('value')
    def _compute_name(self):
        for record in self:
            self.name = "Record with value %s" % self.value

En aquest exemple, és el nom el que és calcular a partir del value.

Exemples de computed de tots els tipus de fields:

# -*- coding: utf-8 -*-

from openerp import models, fields, api, tools
from datetime import date, datetime

class proves_computed(models.Model):
     _name = 'proves_computed.proves_computed'

     name = fields.Char()
     value = fields.Integer()
     image = fields.Binary(String="Image original")
     computedfloat = fields.Float(compute="_value_pc", store=True)
     computedchar = fields.Char(compute="_value_pc", store=False)
     medium_image = fields.Binary(compute="_redimensionar", store=True)
     small_image = fields.Binary(compute="_redimensionar", store=True)
     computedm2o = fields.Many2one('res.partner',compute="_value_pc", store=False)
     computedm2m = fields.Many2many(comodel_name='product.template',compute="_value_pc", store=False)
     computeddate = fields.Date(compute="_value_pc", store=False)
     computeddatetime = fields.Datetime(compute="_value_pc", store=False)
     
     description = fields.Text()

     @api.depends('value')
     def _value_pc(self):
      for r in self:
        r.computedfloat = float(r.value) / 100 
        r.computedchar = "("+str(r.value)+")"
        r.computedm2o = self.env['res.partner'].search([('id','=',r.value)]).id # Many2one espera un id, que és un camp Integer. 
        print '\033[93m'+str(self.env['product.product'].search([('id','>',r.value)]).ids)+'\033[0m'
        r.computedm2m = self.env['product.template'].search([('id','>',r.value)]).ids #Many2many espera un array d'ids o un recordset. 
        # El codi comentat a continuació fa el mateix, per si volem fer altres coses dins del for.
        #ids = []
        #for t in self.env['product.template'].search([('id','>',r.value)]):
        # ids.append(t.id)
        #r.computedm2m = ids
 
        #r.computeddate = date.today() # Aquest depen de Python
        r.computeddate = fields.date.today() # Recomanem aquest, ja que és propi de la classe fields d'Odoo
        #r.computeddate = datetime.now()
        r.computeddatetime = fields.datetime.now()
        

     @api.depends('image')
     def _redimensionar(self):
       for r in self:
         image_original = r.image
         if image_original:
            images = tools.image_get_resized_images(image_original)
            r.medium_image = images['image_medium']                        
            r.small_image = images['image_small']                
         else:
            r.medium_image = ""                        
            r.small_image = ""

(Codi complet)

En l'apartat del controlador s'expliquem més detalls de les funcions en python-odoo.

Buscar i escriure en camps computed

Amb el api.depends podem fer que camps calculats puguen ser buscats o referenciats des d'uns altres models, ja que podem dir que sí se guarden en la base de dades. Si es guarda en la base de dades no es recalcula fins que no canvia el contingut del field del que depèn. Però si el camp calculat no depèn de valors estàtics d'altres fields i/o necessitem que sempre es calcule, no tenim moltes opcions elegants. Una d'elles pot ser fer dos camps, un calculat store=False i altre no i fer un write en la funció. L'altra possibilitat és fer una funció pública que puga ser cridada des d'un altre model. La més elegant que no sempre funciona és utilitzar l'opció search i assignar-li una funció que ha de retornar un domini de búsqueda. El problema és que no accepta molta complexitat, ja que suposa una cerca per tota la base de dades i pot ser molt ineficient.

Per defecte no es pot escriure en un camp computed. No té massa sentit la majoria dels casos, ja que és un camp que depèn d'altres. Però pot ser que, de vegades volem escriure el resultat i que modifique el camp origen. Imaginem, per exemple, que sabem el preu final i volem que calcule el preu sense IVA. Per fer-ho, la millor manera és crear una funció i fer que estiga en l'opció inverse.

Exemple:

 preu = fields.Float('Price',compute="_get_price",search='_search_price',inverse='_set_price')

 @api.depends('pelicula','descompte')
      def _get_price(self):
        for r in self:
          price = r.pelicula.preu
          price = price - (price*r.descompte/100)
          r.preu = price

      def _search_price(self,operator,value): # De moment aquest search sols és per a ==
       preus = self.search([]).mapped(lambda e: [e.id , e.pelicula.preu - (e.pelicula.preu*e.descompte/100)]) # Un bon exemple de mapped en lambda
       print preus
       p = [ num[0] for num in preus if num[1] == value]  # condició if en una llista python sense fer un for (list comprehension)
       # també es pot provar en un filter() de python
       print p
       # p és una llista de les id que ja compleixen la condició, per tant sols cal fer que la id estiga en la llista.
       return [('id','in',p)]

      def _set_price(self):
       self.pelicula.preu = self.preu  # Açò és un exemple, però està mal, ja que modifiques el preu de la peli en totes les sessions

Documentació oficial: https://www.odoo.com/documentation/12.0/reference/orm.html#computed-fields

Valors per defecte

En Odoo és molt fàcil fer valors per defecte, ja que és un argument més en el constructor dels fields:

name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
start_date = fields.Date(default=fields.Date.today())
active = fields.Boolean(default=True)
def compute_default_value(self):
    return self.get_value()
a_field = fields.Char(default=compute_default_value)

Si volem, per exemple, ficar la data del moment de crear, no podem fer això:

start_date = fields.Date(default=fields.Date.today())  # MAL

Perquè calcula la data del moment d'actualitzar el mòdul, no el de crear l'element en el model. Cal fer:

start_date = fields.Date(default=lambda self: fields.Date.today())  # CORRECTE

o

start_date = fields.Datetime(default=lambda self: fields.Datetime.now()) # CORRECTE

El valor per defecte no pot dependre d'un field que està creant-se en eixe moment. En eixe cas es pot utilitzar un on_change.

Veure també La part de valors per defecte en un One2Many

En cas de tindre molts valors per defecte o que depenen del context, es pot utilitzar la funció default_get que ja tenen els models.

    @api.model
    def default_get(self, default_fields):
        result = super(SelectSalePrice, self).default_get(default_fields)
        if self._context.get('default_picking_id') is not None:
            result['picking_id'] = self._context.get('default_picking_id')
        return result

El que fa aquesta funció és un poc avançat de moment, ja que fa ús del context i l'herencia per afegir un valor per defecte al diccionari que retorna aquesta funció en la classe Model

Restriccions (constrains)

Els objectes poden incorporar, de forma opcional, restriccions d’integritat, addicionals a les de la base de dades. Odoo valida aquestes restriccions en les modificacions de dades i, en cas de violació, mostra una pantalla d’error.

from odoo.exceptions import ValidationError

@api.constrains('age')
def _check_something(self):
    for record in self:
        if record.age > 20:
            raise ValidationError("Your record is too old: %s" % record.age)
    # all records passed the test, don't return anything

En ocasions, quan tenim clar cóm faríem aquesta restricció en SQL, tal vegada ens resulte més interessant fer una restricció de la base de dades amb una sql constraint. Aquestes es defineixen amb 3 strings (name, sql_definition, message). Per exemple:

_sql_constraints = [
    ('name_uniq', 'unique(name)', 'Custom Warning Message'),
    ('contact_uniq', 'unique(contact)', 'Custom Warning Message')
]

En aquest cas és una restricció d'unicitat, la qual és més simple que fer una búsqueda en python.

Fitxers de dades

Quan fem un mòdul d'Odoo, es poden definir dades que es guardaran en la base de dades. Aquestes dades poden ser necessàries per al funcionament del mòdul, de demostració o inclús part de la vista.

Alguns mòduls sols estan per clavar dades en Odoo

Tots els fitxers de dades són en XML i tenen una estructura com esta:

<odoo>
        <record model="{model name}" id="{record identifier}">
            <field name="{a field name}">{a value}</field>
        </record>
<odoo>

Dins de les etiquetes odoo (o les etiquetes openerp i data en versions anteriors) poden trobar una etiqueta record per cada fila en la taula que volem introduir. Cal especificar el model i el id. El id és un identificador extern, que no te perquè coincidir amb la clau primària que l'ORM utilitzarà després. Cada field tindrà un nom i un valor.

External Ids

Tots els records de la base de dades tenen un identificador únic en la seua taula, el id. És un número auto incremental assignat per la base de dades. No obstant, si volem fer referència a ell en fitxers de dades o altres llocs, no sempre tenim perquè saber el id. La solució d'odoo són els External Identifiers. Això és una taula que relaciona cada id de cada taula en un nom. Es tracta del model ir.model.data. Per trobar-los cal anar a:

Settings > Technical > Sequences & identifiers > External Indentifiers

Ahí dins trobem la columna Complete ID.

Per trobar les id al fer fitxers de demostració o de dades podem anar al menú, però eixes ids canvien d'una instal·lació a un altra. Per tant, cal utilitzar les external id. Per aconseguir-lo podem obrir el mode desenvolupador i obrir el menú View Metadata.

En les dades de demo, els external Ids s'utilitzen per no utilitzar les id, que poden canviar al ser auto incrementals. Per a que funcione cal utilitzar l'atribut ref:

<field name="product_id" ref="product.product1"/>
Es recomana fer l'atribut id en el record, encara que no sobreescriu el id real, serveix per declarar el External Id i és més fàcil després fer referència a ell.

Veure també la funció ref() de l'ORM

Expressions

De vegades volem que els fields es calculen cada vegada que s'actualitza el mòdul. Això es pot fer en l'atribut eval que avalua una expressió de Python.

<field name="date" eval="(datetime.now()+timedelta(-1)).strftime('%Y-%m-%d')"/>
<field name="product_id" eval="ref('product.product1')"/> # Equivalent a l'exemple anterior
<field name="price" eval="ref('product.product1').price"/>
<field name="avatar" model="school.template" eval="obj().env.ref('school.template_student1').image" ></field>  # Com que utilitza obj() necessita model="...

Per al x2many, es pot utilitzar el eval per assignar una llista d'elements.

<field name="tag_ids" eval="[(6,0,[ref('vehicle_tag_leasing'),ref('fleet.vehicle_tag_compact'),
                                   ref('fleet.vehicle_tag_senior')] )]" />

Observem que hem passat una tripleta amb un 6, un 0 i una llista de refs. Les tripletes poden ser:

  • (0,_ ,{'field': value}): Crea un nou registre i el vincula
  • (1,id,{'field': value}): Actualtiza els valors en un registre ja vinculats
  • (2,id,_): Desvincula i elimina el registre
  • (3,id,_): Desvincula però no elimina el registre de la relació.
  • (4,id,_): Vincula un registre ja existent
  • (5,_,_): Desvincula pero no elimina tots els registres vinculats
  • (6,_,[ids]): Reemplaça la llista de registres vinculats.

Esborrar

Amb l'etiqueta delete es pot especificar els elements a esborrar amb el external ID o amb un search:

<delete model="cine.sessio" id="sessio_cine1_1"></delete>
Si volem que sempre s'actualitzen les dades de demo (per exemple la data) podem esborrar i tornar a crear en el mateix fitxer de demo.
Si falla l'actualització amb dades de demo, és possible que Odoo 12 deshabilite la possibilitat de tornar-les a instal·lar. Això és el field demo de ir.module.module que és readonly, per tant, cal modificar-lo a ma en la base de dades:
En postgresql:
update ir_module_module set demo = 't' where name='school';

Accions i menús

Si vols conèixer en més detall cóm funcionen les accions en Odoo, llig l'article Accions i menús en Odoo.

El client web de Odoo conté uns menús dalt i a l'esquerra. Aquests menús, al ser accionats mostren altres menús i les pantalles del programa. Quant pulsem en un menú, canvia la pantalla perquè hem fet una acció.

Una acció bàsicament té:

  • type: El tipus d'acció que és i cóm l'acció és interpretada. Quan la definim en el XML, el type no cal especificar-lo, ja que ho indica el model en que es guarda.
  • name: El nom, que pot ser mostrat en la pantalla o no. Es recomana que siga llegible per els humans.

Les accions i els menús es declaren en fitxers de dades en XML o dirèctament si una funció retorna un diccionari que la defineix. Les accions poden ser cridades de tres maneres:

  • Fent clic en un menú.
  • Fent clic en botons de les vistes (han d'estar connectats amb accions).
  • Com accions contextuals en els objectes.

D'aquesta manera, el client web pot saber quina acció ha d'executar si rep alguna d'aquestes coses:

  • false: Indica que s'ha de tancar el diàleg actual.
  • Una string: Amb l'etiqueta de l'acció de client a executar.
  • Un número: Amb el ID o external ID de l'acció a trobar a la base de dades.
  • Un diccionari: Amb la definició de l'acció, aquesta no està ni en XML ni en la base de dades. En general, és la manera de cridar a un action al finalitzar una funció.

Accions tipus window

Les accions window són un record més (ir.actions.act_window). No obstant, els menús que les criden, tenen una manera més ràpida de ser declarats amb una etiqueta menuitem:

<record model="ir.actions.act_window" id="action_list_ideas">
    <field name="name">Ideas</field>
    <field name="res_model">idea.idea</field>
    <field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_ideas" parent="menu_root" name="Ideas" sequence="10"
          action="action_list_ideas"/>
Les accions han de ser declarades al XML abans que els menús que les accionen.
A partir d'Odoo 12, cal donar permisos explícitament als usuaris per veure els menús.

Exemple:

<?xml version="1.0" encoding="UTF-8"?>
<openerp>
    <data>
        <!-- window action -->
        <!--
            The following tag is an action definition for a "window action",
            that is an action opening a view or a set of views
        -->
        <record model="ir.actions.act_window" id="course_list_action">
            <field name="name">Courses</field>
            <field name="res_model">openacademy.course</field>
            <field name="view_type">form</field>
            <field name="view_mode">tree,form</field>
            <field name="help" type="html">
                <p class="oe_view_nocontent_create">Create the first course
                </p>
            </field>
        </record>

        <!-- top level menu: no parent -->
        <menuitem id="main_openacademy_menu" name="Open Academy"/>
        <!-- A first level in the left side menu is needed
             before using action= attribute -->
        <menuitem id="openacademy_menu" name="Open Academy"
                  parent="main_openacademy_menu"/>
        <!-- the following menuitem should appear *after*
             its parent openacademy_menu and *after* its
             action course_list_action -->
        <menuitem id="courses_menu" name="Courses" parent="openacademy_menu"
                  action="course_list_action"/>
        <!-- Full id location:
             action="openacademy.course_list_action"
             It is not required when it is the same module -->
    </data>
</openerp>

Sols el tercer nivell de menús pot tindre associada un action. El primer és el menú de dalt i el segon no es 'clicable'.

El que hem vist en esta secció és la definició d'una acció en un XML com a part de la vista, però una acció no és més que una forma còmoda d'escriure moltes coses que farà el client en javascript per demanar alguna cosa al servidor. Els actions separen i simplifiquen el desenvolupament de la interfície d'usuari que és el client web. Un menú o botó en html acciona una funció javascript que en principi no sap el que fer. Aquesta demana que es carregue la definició del seu action. Una vegada carregada la definició, queda clar tot el que ha de demanar (les vistes, context, dominis, vistes search, lloc on carregar-ho tot...) aleshores demana les vistes i amb ajuda de les vistes i els fields, demana els records que són les dades a mostrar. Per tant, un action és la definició sense programar javascript de coses que ha de fer el javascript. Odoo permet declarar actions com a resposta de funcions. Aquestes actions no estan en la base de dades, però són enviades igualment al client i el client fa en elles el mateix que en un action que ell ha demanat. Un exemple d'això són els actions que retornen els botons dels wizards. De fet, podem fer que un botó torne un action i, per tant, obrir una vista diferent.

Si vols conèixer en més detall cóm funcionen les accions en Odoo, llig l'article Accions i menús en Odoo.

La vista

Per saber més sobre les vistes i cómo millorar-les, consulta l'article de Millores en la vista en Odoo.

Les vistes són la manera en la que es representen els models. En cas de que no declarem les vistes, es poden referenciar per el seu tipus i Odoo generarà una vista de llista o formulari estandar per poder vorer els registres de cada model. No obstant, quasi sempre volem personalitzar les vistes i en aquest cas, es poden referenciar per un identificador.

Les vistes tenen una prioritat i, si no s'especifica el identificador de la que volem mostrar, es mostrarà la que més prioritat tinga.

<record model="ir.ui.view" id="view_id">
    <field name="name">view.name</field>
    <field name="model">object_name</field>
    <field name="priority" eval="16"/>
    <field name="arch" type="xml">
        <!-- view content: <form>, <tree>, <graph>, ... -->
    </field>
</record>
Les vistes es guarden en el model ir.ui.view. Tots els elements de interficie tenen en el seu nom ir.ui (Information Repository, User Interface). Els menús a ir.ui.menu o les accions a ir.actions.window

Exemple de vista form:

  <record model="ir.ui.view" id="course_form_view">
            <field name="name">course.form</field>
            <field name="model">openacademy.course</field>
            <field name="arch" type="xml">
                <form string="Course Form">
                    <sheet>
                        <group>
                            <field name="name"/>
                            <field name="description"/>
                        </group>
                    </sheet>
                </form>
            </field>
        </record>

Encara que Odoo ja proporciona un tree i un form per defecte, la vista cal millorar-la quasi sempre. Totes les vistes tenen fields que poden tindre widgets diferents. En les vistes form, podem adaptar molt l'aspecte amb grups de fields, pestanyes, camps ocults condicionalment...

Per saber més sobre les vistes i cómo millorar-les, consulta l'article de Millores en la vista en Odoo.

Els reports

Odoo reports

Herència

El framework d'Odoo facilita el mecanisme de l’herència per tal que els programadors puguin adaptar mòduls existents i garantir a la vegada que les actualitzacions dels mòduls no destrossin les adequacions desenvolupades.

L’herència es pot aplicar en els tres components del patró MVC:

  • En el model: possibilita ampliar les classes existents o dissenyar noves classes a partir de les existents.
  • En la vista: possibilita modificar el comportament de vistes existents o dissenyar noves vistes.
  • En el controlador: possibilita sobreescriure els mètodes existents o dissenyar-ne de nous.


OpenObject proporciona tres mecanismes d’herència: l’herència de classe, l’herència per prototip i l’herència per delegació.

Mecanisme Característiques Com es defineix
De classe - Herència simple.

- La classe original queda substituïda per la nova classe.
- Afegeix noves funcionalitats (atributs i/o mètodes) a la classe original.
- Les vistes definides sobre la classe original continuen funcionant.
- Permet sobreescriure mètodes de la classe original.
- En PostgreSQL, continua mapada en la mateixa taula que la classe original, ampliada amb els nous atributs que pugui incorporar.

- S’utilitza l’atribut _inherit en la definició de la nova classe Python: _inherit = obj

- El nom de la nova classe ha de continuar sent el mateix que el de la classe original: _name = obj

Per prototip - Herència simple.

- Aprofita la definició de la classe original (com si fos un «prototipus»).
- La classe original continua existint.
- Afegeix noves funcionalitats (atributs i/o mètodes) a les aportades per la classe original.
- Les vistes definides sobre la classe original no existeixen (cal dissenyar-les de nou).
- Permet sobreescriure mètodes de la classe original.
- En PostgreSQL, queda mapada en una nova taula.

- S’utilitza l’atribut _inherit en la definició de la nova classe Python: _inherit = obj

- Cal indicar el nom de la nova classe: _name = nou_nom

Per delegació - Herència simple o múltiple.

- La nova classe «delega» certs funcionaments a altres classes que incorpora a l’interior.
- Els recursos de la nova classe contenen un recurs de cada classe de la que deriven.
- Les classes base continuen existint.
- Afegeix les funcionalitats pròpies (atributs i/o mètodes) que correspongui.
- Les vistes definides sobre les classes bases no existeixen a la nova classe.
- En PostgreSQL, queda mapada en diferents taules: una taula per als atributs propis, mentre que els recursos de les classes derivades resideixen en les taules corresponents a les dites classes.

- S’utilitza l’atribut _inherits en la definició de la nova classe Python: _inherits = …

- Cal indicar el nom de la nova classe: _name = nou_nom

Inheritance methods.png

El fitxer __openerp__.py ha de contindre les dependències de la clase heretada.

Herència en el Model

El disseny d’un objecte d’OpenObject heretat és paregut al disseny d’un objecte d’OpenObject no heretat; únicament hi ha dues diferències:

  • Apareix l’atribut _inherit o _inherits per indicar l’objecte (herència simple) o els objectes (herència múltiple) dels quals deriva el nou objecte. La sintaxi a seguir és:
_inherit = 'nom.objecte.del.que.es.deriva'
_inherits = {'nom.objecte1':'nom_camp_FK1', ...}
  • En cas d’herència simple, el nom (atribut _name) de l’objecte derivat pot coincidir o no amb el nom de l’objecte pare. També és possible no indicar l’atribut _name, fet que indica que el nou objecte manté el nom de l’objecte pare.

L’herència simple (_inherit) amb atribut _name idèntic al de l’objecte pare, s’anomena herència de classe i en ella el nou objecte substitueix l’objecte pare, tot i que les vistes sobre l’objecte pare continuen funcionant. Aquest tipus d’herència, la més habitual, s’utilitza quan es vol afegir fields i/o modificar propietats de dades existents i/o modificar el funcionament d’alguns mètodes. En cas d’afegir dades, aquestes s’afegeixen a la taula de la base de dades en la qual estava mapat l’objecte pare.

Exemple d'herència de classe L’herència de classe la trobem en molts mòduls que afegeixen dades i mètodes a objectes ja existents, com per exemple, el mòdul comptabilitat (account) que afegix dades i mètodes a l’objecte res.partner. Fixem-nos en el contingut del mòdul account:

    class res_partner(Model.model):
    _name = 'res.partner'
    _inherit = 'res.partner'
    debit_limit = fields.float('Payable limit')
    ...

Podeu comprovar que la taula res_partner d’una empresa sense el mòdul account instal·lat no conté el camp debit_limit, que en canvi sí que hi apareix una vegada instal·lat el mòdul.

Odoo té molts mòduls que deriven de l’objecte res.partner per afegir-hi característiques i funcionalitats.

L’herència simple (_inherit) amb atribut _name diferent al de l’objecte pare, s’anomena herència per prototip i en ella es crea un nou objecte que aglutina les dades i mètodes que tenia l’objecte del qual deriva, juntament amb les noves dades i mètodes que pugua incorporar el nou objecte. En aquest cas, sempre es crea una nova taula a la base de dades per mapar el nou objecte.

Exemple d'herència per prototip L’herència per prototip és difícil de trobar en els mòduls que incorpora Odoo. Un exemple el tenim en el mòdul base_calendar en el qual podem observar el mòdul comptabilitat (account) que afegix dades i mètodes a l’objecte res.partner. Fixem-nos en el contingut del mòdul account:

    class res_alarm(Model.model):
    _name = 'res.alarm'
    ...
    class calendar_alarm(Model.model):
    _name = 'calendar.alarm'
    _inherit = 'res.alarm'
    ...

En una empresa que tingui el mòdul base_calendar instal·lat podeu comprovar l’existència de la taula res_alarm amb els camps definits a l’apartat _atributs de la classe res_alarm i la taula calendar_alarm amb camps idèntics als de la taula res_alarm més els camps definits a l’apartat _atributs de la classe calendar_alarm.

L'herència per prototip és la tradicional en els llenguatges orientats a objectes, ja que crea una nova classe vinculada

L’herència múltiple (_inherits) s’anomena herència per delegació i sempre provoca la creació d’una nova taula a la base de dades. L’objecte derivat ha d’incloure, per cada derivació, un camp many2one apuntant l’objecte del qual deriva, amb la propietat ondelete='cascade'. L’herència per delegació obliga que cada recurs de l’objecte derivat apunte a un recurs de cadascun dels objectes dels quals deriva i es pot donar el cas que hi hagi diversos recursos de l’objecte derivat que apunten a un mateix recurs per algun dels objectes dels quals deriva.

    class res_alarm(Model.model):
    _name = 'res.alarm'
    ...
    class calendar_alarm(Model.model):
    _name = 'calendar.alarm'
    _inherits = {'res.alarm':'alarm_id'}
    ...

Herència en la vista

L’herència de classe possibilita continuar utilitzant les vistes definides sobre l’objecte pare, però en moltes ocasions interessa disposar d’una versió retocada. En aquest cas, és molt millor heretar de les vistes existents (per afegir, modificar o eliminar camps) que reemplaçar-les completament.

 <field name="inherit_id" ref="id_xml_vista_pare"/>

En cas que la vista id_xml_vista_pare estiga en un mòdul diferent del que estem dissenyant, cal afegir el nom del mòdul al davant:

 <field name="inherit_id" ref="modul.id_xml_vista_pare"/>

El motor d’herència d’OpenObject, en trobar una vista heretada, processa el contingut de l’element arch. Per cada fill d’aquest element que tingui algun atribut, OpenObject cerca a la vista pare una etiqueta amb atributs coincidents (excepte el de la posició) i, a continuació, combina els camps de la vista pare amb els de la vista heretada i estableix la posició de les noves etiquetes a partir dels següents valors:

  • inside (per defecte): els valors s’afegeixen “dins” de l’etiqueta.
  • after: afegeix el contingut després de l’etiqueta.
  • before: afegeix el contingut abans de l’etiqueta.
  • replace: reemplaça el contingut de l’etiqueta.
  • attributes: Modifica els atributs.

Reemplaçar

 <field name="arch" type="xml">
   <field name="camp" position="replace">
     <field name="nou_camp" ... />
   </field>
 </field>

Esborrar

 <field name="arch" type="xml">
   <field name="camp" position="replace"/>
 </field>

Inserir nous camps

 <field name="arch" type="xml">
    <field name="camp" position="before">
       <field name="nou_camp" .../>
    </field>
 </field>

 <field name="arch" type="xml" style="font-family:monospace">
    <field name="camp" position="after">
       <field name="nou_camp" .../>
    </field>
 </field>

Fer combinacions

 <field name="arch"type="xml">
   <data>
     <field name="camp1" position="after">
       <field name="nou_camp1"/>
     </field>
     <field name="camp2" position="replace"/>
     <field name="camp3" position="before">
        <field name="nou_camp3"/>
     </field>
   </data>
 </field>

Per definir la posició dels elements que afegim, podem utilitzar una expresió xpath:

 <xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before">
 <xpath expr="//form/*" position="before">
  <header>
    <field name="status" widget="statusbar"/>
  </header>
 </xpath>

És posssible que necessitem una vista totalment nova de l'objecte heredat. Si fem un action normal en l'XML es veuran els que més prioritat tenen. Si volem especificar quina vista volem en concret hem d'utilitzar view_ids, observem aquest exemple:

        <record model="ir.actions.act_window" id="terraform.player_action_window">
            <field name="name">Players</field>
            <field name="res_model">res.partner</field>
            <field name="view_mode">tree,form,kanban</field>
            <field name="domain"> [('is_player','=',True)]</field>
            <field name="context">{'default_is_player': True}</field>
            <field name="view_ids" eval="[(5, 0, 0),
            (0, 0, {'view_mode': 'tree', 'view_id': ref('terraform.player_tree')}),
            (0, 0, {'view_mode': 'form', 'view_id': ref('terraform.player_form')}),]" />
        </record>

En (0,0,{registre_a_crear}) li diguem que a eixe Many2many hi ha que afegir un nou registre amb eixes dades en concret. El que necessita és el view_mode i el view_id, com en els records anteriors.

Si es vol especificar una vista search es pot inclourer la etiqueta search_view_id:

 <field name="search_view_id" ref="cine.pos_order_line_search_view"/>

Exemple:

class socios(models.Model):
     _inherit = 'res.partner'
     _name = 'res.partner'
     #name = fields.Char()
     camions = fields.One2many('cooperativa.camion','socio',string='Trucks')
     n_camiones = fields.Integer(compute='_n_camiones',string='Number of Trucks')
     arrobas = fields.Float(compute='_n_camiones',string='@')
     @api.depends('camions')
     def _n_camiones(self):
       for i in self:
         for j in i.camions:
           i.arrobas = i.arrobas + j.arrobas
           i.n_camiones = i.n_camiones + 1
  <record model="ir.ui.view" id="socio_form_view">
            <field name="name">socio</field>
            <field name="model">res.partner</field>
	   <field name="inherit_id" ref="base.view_partner_form"/> 
           <field name="arch" type="xml">
    <field name="website" position="after">
                            <field name="camions"/>
                            <field name="n_camiones"/>
                            <field name="arrobas"/>
    </field>

            </field>
        </record>
    <!--Inherit quotations search view-->
    <record id="view_sale_order_inherit_search" model="ir.ui.view">
      <field name="name">sale.order.search.expand.filter</field>
      <field name="model">sale.order</field>
      <field name="inherit_id" ref="sale.sale_order_view_search_inherit_quotation"/>
      <field name="arch" type="xml">
        <xpath expr="//search" position="inside">
          <filter string="Total &lt; 1000" name="total_under_1000" domain="[('amount_total', '&lt;', 1000)]"/>
          <filter string="Total &gt;= 1000" name="total_above_1000" domain="[('amount_total', '&gt;=', 1000)]"/>
        </xpath>
      </field>
    </record>

Domains

Si volem que el action heredat sols mostre els elements que volem, s'ha de ficar un domain en el action:

<field name="domain"> [('isplayer','=',True)]</field>

Amés, es pot dir que, per defecte, quan es crea un nou registre a través d'aquest action, tinga el field a True:

<field name="context">{'default_is_player': True}</field>

Filtre per defecte

El problema en la solució anterior és que lleva la possibilitat de veure el que no tenen aquest field a True i cal anar per un altre action a modificar-los. Si volem poder veure tots, podem crear un filtre en la vista search i en l'action dir que volem aquest filtre per defecte:

<!--   En la vista search -->
...
    <search>
        <filter name="player_partner" string="Is Player" domain="[('is_player','=',True)]" />
    </search>
...
<!-- En l'action -->
            <!--  <field name="domain"> [('is_player','=',True)]</field> -->
            <field name="domain"></field>
            <field name="context">{'default_is_player': True, 'search_default_player_partner': 1}</field>

Herència en el controlador

L’herència en el controlador és un mecanisme conegut, ja que l’apliquem de forma inconscient quan ens veiem obligats a sobreescriure els mètodes de la capa ORM d’OpenObject en el disseny de molts mòduls.

Funció super() El llenguatge Python recomana utilitzar la funció super() per invocar el mètode de la classe base quan s’està sobreescrivint en una classe derivada, en lloc d’utilitzar la sintaxi nomClasseBase.metode(self…).

L’efecte de l’herència en el controlador es manifesta únicament quan cal sobreescriure algun dels mètodes de l’objecte del qual es deriva i per a fer-ho adequadament cal tenir en compte que el mètode sobreescrit en l’objecte derivat:

  • A vegades vol substituir el mètode de l’objecte base sense aprofitar-ne cap funcionalitat: el mètode de l’objecte derivat no invoca el mètode sobreescrit.
  • A vegades vol aprofitar la funcionalitat del mètode de l’objecte base: el mètode de l’objecte derivat invoca el mètode sobreescrit.

Exemples:

Sobreescriure el mètode create:

class res_partner(models.Model):
    _inherit = 'res.partner'
    passed_override_write_function = fields.Boolean(string='Has passed our super method')
 
    @api.model
    def create(self, values):
        # Override the original create function for the res.partner model
        record = super(res_partner, self).create(values)
 
        # Change the values of a variable in this super function
        record['passed_override_write_function'] = True
        print 'Passed this function. passed_override_write_function value: ' + str(record['passed_override_write_function'])
 
        # Return the record so that the changes are applied and everything is stored.
	return record

El controlador

Part del controlador l'hem mencionat al parlar dels camps computed. No obstant, cal comentar les facilitats que proporciona Odoo per a no tindre que accedir dirèctament a la base de dades.

La capa ORM d’Odoo facilita uns mètodes que s’encarreguen del mapatge entre els objectes Python i les taules de PostgreSQL. Així, disposem de mètodes per crear, modificar, eliminar i cercar registres a la base de dades.

En ocasions, pot ser necessari alterar l’acció automàtica de cerca – creació – modificació – eliminació facilitada per Odoo i haurem de sobreescriure els corresponents mètodes en les nostres classes.

Els programadors en el framework d'Odoo hem de conèixer els mètodes subministrats per la capa ORM i hem de dominar el disseny de mètodes per:

  • Poder definir camps funcionals en el disseny del model.
  • Poder definir l’acció que cal executar en modificar el contingut d’un field d’una vista form (@api.onchange)
  • Poder alterar les accions automàtiques de cerca, creació, modificació i eliminació de recursos.

Una darrera consideració a tenir en compte en l’escriptura de mètodes i funcions en Odoo és que els textos de missatges inclosos en mètodes i funcions, per poder ser traduïbles, han de ser introduïts amb la sintaxi _('text') i el fitxer .py ha de contenir from tools.translate import _ a la capçalera.

API de l'ORM

Interactuar en la terminal
$ odoo shell -d castillo -u containers
Asciinema amb alguns exemples

Observa cóm hem ficat el paràmetre shell. Les coses que se fan en la terminal no són persistents en la base de dades fins que no s'executa self.env.cr.commit(). Dins de la terminal podem obtindre ajuda dels mètodes d'Odoo amb help(), per exemple: help(tools.image) Amb el següent exemple, podem arrancar odoo sense molestar a l'instància que està en marxa redefinint els ports:

$ odoo shell -c /path/to/odoo.conf --xmlrpc-port 8888 --longpolling-port 8899

Documentació: [5] [6]

Un mètode creat dins d'un model actua sobre tots els elements del model que estiguen actius en el moment de cridar al mètode. Si és un tree, seran molts i si és un form sols un. Però en qualsevol cas és una 'llista' d'elements i es diu recordset.

Bàsicament la interacció amb els models en el controlador es fa amb els anomenats recordsets que són col·leccions d'objectes sobre un model. Si iterem dins dels recordset , obtenim els singletons, que són objectes individuals de cada línia en la base de dades.

def do_operation(self):
    print self # => a.model(1, 2, 3, 4, 5)
    for record in self:
        print record # => a.model(1), then a.model(2), then a.model(3), ...

Podem accedir a tots els fields d'un model sempre que estem en un singleton, no en un recordset:

>>> record.name
Example Name
>>> record.company_id.name
Company Name
>>> record.name = "Bob"

Intentar llegir o escriure un field en un recordset donarà un error. Accedir a un many2one, one2many o many2many donarà un recordset.

Set operations

Els recordsets es poden combinar amb operacions específiques que són les típiques dels conjunts:

  • record in set retorna si el record està en el set
  • set1 | set2 Unió de sets
  • set1 & set2 Intersecció de sets
  • set1 - set2 Diferència de sets

Amés, un recordset no té elements repetits i permet accedir a recordsets dins d'ell. Per exemple:

>>> record.students.classrooms

Dona la llista de totes les classes de tots els estudiants i sense repetir cap.

Programació funcional en l'ORM

Python té una serie de funcions que permeten iterar una llista i aplicar una funció als elements. Les més utilitzades són map(), filter(), reduce(), sort(), zip()... Odoo treballa en recordsets, no llistes, i té les seues funcions pròpies per a imitar aquestes:

  • filtered() Filtra el recordset de manera que sols tinga els records que complixen una condició.
records.filtered(lambda r: r.company_id == user.company_id)
records.filtered("partner_id.is_company")
  • sorted() Ordena segons uns funció, se defineix una funció lambda (key) que indica que s'ordena per el camp name:
# sort records by name
records.sorted(key=lambda r: r.name)
records.sorted(key=lambda r: r.name, reverse=True)
  • mapped() Li aplica una funció a cada recordset i retorna un recordset amb els canvis demanats:
# returns a list of summing two fields for each record in the set
records.mapped(lambda r: r.field1 + r.field2)
# returns a list of names
records.mapped('name')
# returns a recordset of partners
record.mapped('partner_id')
# returns the union of all partner banks, with duplicates removed
record.mapped('partner_id.bank_ids')

Aquestes funcions són útils per a fer tècniques de programació funcional

Enviroment

L'anomenat enviroment o env guarda algunes dades contextuals interessants per a treballar amb l'ORM, com ara el cursor a la base de dades, l'usuari actual o el context (que guarda algunes metadades).

Tots els recordsets tenen un enviroment accesible amb env. Quant volem crear un recordset dins d'un altre, podem usar env:

>>> self.env['res.partner']
res.partner
>>> self.env['res.partner'].search([['is_company', '=', True], ['customer', '=', True]])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)

El primer cas crea un recordset buit però que fa referència a res.partner i es poden fer les funcions de l'ORM que necessitem.

Context

El context és un diccionari de python que conté dades útils per a totes les vistes i els mètodes. Les funcions d'Odoo reben el context i el consulten si cal. Context pot tindre de tot, però quasi sempre té al menys el user ID, l'idioma o la zona temporal. Quant Odoo va a renderitzar una vista XML, consulta el context per veure si ha d'aplicar algun paràmetre.

print(self.env.context)

Al llarg de tot aquest manual utilitzem sovint paràmetres del context. Aquests són els paràmetres que hem utilitzat en algun moment:

  • active_id : self._context.get('active_id') es tracta de l'id de l'element del model que està en pantalla.
  • active_ids : Llista de les id seleccionats en un tree.
  • active_model : El model actual.
  • default_<field> : En un action o en un one2many es pot assignar un valor per defecte a un field.
  • search_default_<filter> : Per aplicar un filtre per defecte a la vista en un action.
  • group_by : Dins d'un camp filter per a crear agrupacions en les vistes search.
  • graph_mode : En les vistes graph, aquest paràmetre canvia el type
  • context.get : En les vistes es pot treure algunes dades del context per a mostrar condicionalment o per als domains

El context va passant d'un mètode a un altre o a les vistes i, de vegades volem modificar-lo.

Imaginem que volem fer un botó que obriga un wizard, però volem passar-li paràmetres al wizard. En els botons i fields relacionals es pot especificar un context:

<button name="%(reserves.act_w_clients_bookings)d" type="action" string="Select bookings" context="{'b_fs':bookings_fs}"/>

Eixe action obre un wizard, que és un model transitori en el que podem definir un field amb els continguts del context:

def _default_bookings(self):
         return self._context.get('b_fs')
bookings_fs = fields.Many2many('reserves.bookings',readonly=True, default=_default_bookings)

Aquest many2many tindrà els mateixos elements que el form que l'ha cridat. (Això és com el default_ en els One2many, però fet a mà)

També es pot utilitzar aquesta manera d'enviar un recordset per un context per al domain d'un field Many2one o Many2many:

def _domain_bookings(self):
         return [('id','=',self._context.get('b_fs').ids)]
bookings_fs = fields.Many2many('reserves.bookings',readonly=True, domain=_default_bookings)

En ocasions necessitem especificar valors per defecte i filtres per defecte en un action. Per exemple, quan implementem l'herència, volem que els nous registres que es facen en el nostre action tinguem un valor per defecte. En el següent exemple, en la primera línia és el que es sol fer en la Herència i en la segona estem especificant un External ID amb ref() dins d'un eval.

        <field name="context">{'default_is_player': True, 'search_default_player_partner': 1}</field>
        <field name="context" eval="{'default_partner_id':ref('base.main_partner'), 'company_hide':False, 'default_company_id':ref('base.main_company'), 'search_default_my_bank':1}"/>

El context és un diccionari inmutable (frozendict) que no pot ser alterat en funcions. no obstant, si volem modificar el context actual per enviar-lo a un action o cridar a una funció d'un model amb un altre context, es pot fer amb with_context:

# current context is {'key1': True}
r2 = records.with_context({}, key2=True)
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}

Si és precís modificar el context es pot fer:

 self.env.context = dict(self.env.context)
 self.env.context.update({'key': 'val'})

o

 self = self.with_context(get_sizes=True)
 print self.env.context

Però no funciona més enllà del recordset actual. És a dir, no modifica el context en el que s'ha cridat.

Si el que volem és passar el valor d'un field per context a un botó dins d'una 'subvista', podem utilitzar el paràmetre parent, que funciona tant en en domain, attr, com en context. Ací tenim un exemple de tree dins d'un field amb botons que envíen per context coses del pare:

 <field name="movies" >
    <tree>
        <field name="photo_small"/>
        <field name="name"/>
        <field name="score" widget='priority'/>
        <button name="book_it" string="Book it" type="object" context="{'b_client':parent.client,'b_day':parent.day}"/>
     </tree>

Podem passar el context per un action i el podem utilitzar en la vista, ja que tenim l'objecte context disponible en QWeb. Si, per exemple, volem retornar un action que cride a una vista i un field tinga un domain passat per context:

     return {
            'name': 'Travel wizard action',
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
            'context': dict(self._context, cities_available_context= (self.cities_available.city).ids),
        }
           <field name="destiny"
                  domain = "[('id','in',context.get('cities_available_context',[]))]"
                  />

Mètodes de l'ORM

search()

A partir d'un domain de Odoo, proporciona un recordset amb tots els elements que coincideixen:

>>> # searches the current model
>>> self.search([('is_company', '=', True), ('customer', '=', True)])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)
>>> self.search([('is_company', '=', True)], limit=1).name
'Agrolait'
Es pot obtindre la quantitat d'elements amb el mètode search_count()
Parameters
    args -- A search domain. Use an empty list to match all records.
    offset (int) -- number of results to ignore (default: none)
    limit (int) -- maximum number of records to return (default: all)
    order (str) -- sort string
    count (bool) -- if True, only counts and returns the number of matching records (default: False)
create()

Te dona un recordset a partir d'una definició de varis fields:

>>> self.create({'name': "New Name"})
res.partner(78)


El mètode create s'utilitza sovint per a ser sobreescrit en herència per fer coses en el moment de la creació. Ací tenim un exemple en el que modifiquem el create d'un model per crear una instància associada amb una imatge predefinida:

     @api.model
     def create(self, values):
        new_id = super(player, self).create(values)
        print values
        name_player = new_id.name
        img = self.env['mmog.fortress'].search([('name','=','f1')])[0].icon
        self.env['mmog.fortress'].create({'name':name_player+"-fortress",'level':1,'soldiers':100,'population':10,'food':1000,'integrity':100,'id_player':new_id.id,'icon':img})
        return new_id
write()

Escriu uns fields dins de tots els elements del recordset, no retorna res:

self.write({'name': "Newer Name"})

Escriure en un many2many:

La manera més senzilla és passar una llista d'ids. Però si ja existeixen elements abans, necessitem uns codis especials (vegeu Odoo#Expressions):

Per exemple:

 self.sessions = [(4,s.id)] 
 self.write({'sessions':[(4,s.id)]})
 self.write({'sessions':[(6,0,[ref('vehicle_tag_leasing'),ref('fleet.vehicle_tag_compact'),ref('fleet.vehicle_tag_senior')] )]})
browse()

A partir d'una llista de ids, retorna un recordset.

>>> self.browse([7, 18, 12])
res.partner(7, 18, 12)


exists()

Retorna si un record en concret encara està en la base de dades.

if not record.exists():
    raise Exception("The record has been deleted")
o:
records.may_remove_some()
# only keep records which were not deleted
records = records.exists()

En el segon exemple, refresca un recordset amb aquells que encara existixen.

ref()

Retorna un singleton a partir d'un External ID.

>>> env.ref('base.group_public')
res.groups(2)
ensure_one()

S'asegura de que el record en concret siga un singleton.

records.ensure_one()
# is equivalent to but clearer than:
assert len(records) == 1, "Expected singleton"
unlink()

Esborra de la base de dades els elements del recordset actual.

Exemple de cóm sobreescriure el mètode unlink per a esborrar en cascada:

    def unlink(self):
        for x in self:
            x.catid.unlink()
        return super(product_uom_class, self).unlink()

read() Es tracta d'un mètode de baix nivell per llegir un field en concret dels records. És preferible emprar browse()

name_search(name=, args=None, operator='ilike', limit=100) → records Search for records that have a display name matching the given name pattern when compared with the given operator, while also matching the optional search domain (args).

This is used for example to provide suggestions based on a partial value for a relational field. Sometimes be seen as the inverse function of name_get(), but it is not guaranteed to be.

This method is equivalent to calling search() with a search domain based on display_name and then name_get() on the result of the search.

ids Llista dels ids del recordset actual.

sorted(key=None, reverse=False) Retorna el recordset ordenat per un criteri.

name_get() Retorna el nom que tindrà el record quant siga referenciat externament. És el valor per defecte del field display_name. Aquest mètode, per defecte, mostra el field name si està. Es pot sobreescriure per mostrar un altre camp o mescla d'ells.

Anem a sobreescriure el mètode name_get.

  
     def name_get(self):
        res=[]
        for i in self:
            res.append((i.id,str(i.name)+", "+str(i.id_player.name)))
        return res

copy() Crea una còpia del singleton i permet aportar nous valors per als fields de la copia.

En els fields One2many no es pot copiar per defecte, però es pot dir copy=True.

onchange

Si volem que un valor siga modificat en temps real quant modifiquem el valor d'un altre field sense encara haver guardat, podem usar els mètodes on_change.

Els camps computed ja tenen el seu propi onchange, per tant, no cal fer-lo

En onchange es modifica el valor d'un o més camps dirèctament i, si cal un filtre o un missatge, es fa en el return:

return {
    'domain': {'other_id': [('partner_id', '=', partner_id)]},
    'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'},
}

Si el type és notification es mostrarà en una notificació, en un altre cas, en un dialog. (Odoo 13)

Exemples:

# onchange handler
@api.onchange('amount', 'unit_price')
def _onchange_price(self):
    # set auto-changing field
    self.price = self.amount * self.unit_price
    # Can optionally return a warning and domains
    return {
        'warning': {
            'title': "Something bad happened",
            'message': "It was very bad indeed",
        }
    }

@api.onchange('seats', 'attendee_ids')
def _verify_valid_seats(self):
     if self.seats < 0:
         return {
             'warning': {
                 'title': "Incorrect 'seats' value",
                 'message': "The number of available seats may not be negative",
             },          }
     if self.seats < len(self.attendee_ids):
          return {
             'warning': {
                 'title': "Too many attendees",
                 'message': "Increase seats or remove excess attendees",
             },
         }

@api.onchange('pais')
def _filter_empleat(self):                                             
      return { 'domain': {'empleat': [('country','=',self.pais.id)]} }      

# Exemple avançat en el que l'autor crea un domain amb una llista d'ids i un '''in''':
@api.multi
def onchange_partner_id(self, part):
    res = super(SaleOrder, self).onchange_partner_id(part)
    domain = [('active', '=', True), ('sale_ok', '=', True)]
    if part:
        partner = self.env['res.partner'].browse(part)
        if partner and partner.sales_channel_id:
            domain.append(('sales_channel_ids', '=',
                           partner.sales_channel_id.id))
    product_ids = self.env['product.product'].search(domain)
    res.update(domain={
        'order_line.product_id': ['id', 'in', [rec.id for rec in product_ids]]
    })
    return res


Si l'usuari s'equivoca introduint algunes dades, Odoo proporciona varies maneres d'evitar-lo:
  • Constraints
  • onchange amb missatge d'error i restablint els valors originals
  • Sobreescriptura del mètode write o create per comprovar coses abans de guardar
Cron Jobs

Cal crear un record en el model ir.cron, per exemple:

        <record model="ir.cron" forcecreate="True" id="game.cron_update">
            <field name="name">Game: Cron Update</field>
            <field name="model_id" ref="model_game_player"/>
            <field name="state">code</field>
            <field name="code">model.update_resources()</field>
            <field name="user_id" ref="base.user_root"/>
            <field name="interval_number">1</field>
            <field name="interval_type">minutes</field>
            <field name="numbercall">-1</field>
            <field name="activity_user_type">specific</field>
            <field name="doall" eval="False" />
        </record>

I un mètode amb el @api.model i aquests arguments:

    @api.model
    def update_resources(self):
        ...

ir.cron té un many2one amb ir.actions.server i, al ser creat, crea l'acció de servidor corresponent. És important ficar en el manifest que depén de mail, ja que és un mòdul preinstal·lat que hereta i afegeix camps a ir.actions.server.

https://poncesoft.blogspot.com/2018/05/creacion-metodos-automatizados-en-odoo.html https://webkul.com/blog/creating-cron-server-action-odoo-11/ https://odoo-development.readthedocs.io/en/latest/odoo/models/ir.cron.html

Els Decoradors

Com es veu, abans de moltes funcions es fica @api.depends, @api.multi...

Els decoradors modifiquen la forma en la que és cridada la funció. Entre altres coses, modifiquen el contingut de self, les vegades que se crida i quant se crida.


  • @api.depends() Aquest decorador crida a la funció sempre que el camp del que depén siga modificat. Encara que el camp diga store=True. Per defecte, self és un recordset, per tant, cal fer un for.
  • @api.model S'utilitza per a funcions que afecten al model i no als recordsets.
  • @api.constrains() S'utilitza per a comprovar les constrains. Self és un recordset. Com que quasi sempre es crida en un form, funciona si utilitzem self directament. Però cal fer for, ja que pot ser cridat en un recordset quant modifiquem camps en grup.
  • @api.onchange() S'executa cada vegada que modifiquem el field indicat en la vista. En aquest, com que es crida quant es modifica un form, sempre self serà un singleton. Però si fiquem un for no passa res.
@api.multi i @api.one estan obsolets en el Odoo 13 i no es poden utilitzar mai.

Exemple de tots els decoradors: (Odoo 12)


Càlculs en dates

Odoo gestiona les dates com a strings. Per una altra banda, python té el seu propi tipus de dades anomenat datetime, date i timedelta entre altres. Això pot provocar dificultats per a fer cálculs en dates. Odoo proporciona algunes ferramentes mínimes per facilitar aquesta tasca.

Primer de tot, anem a importar datetime:

from odoo import models, fields, api
from datetime import datetime, timedelta

El primer que necessitem saber és cóm transformar de date o datetime d’Odoo a python. En definitva, passar de string a datetime.

Tenim un field datetime declarat de la següent manera:

start_date = fields.Datetime()

En la base de dades guardarà un string amb el format: '%Y-%m-%d %H:%M:%S'. Per tant, si volem transformar aquesta data en string a un objecte datetime.datetime tenim que ejecutar el constructor de la classe amb aquests paràmetres:

fmt = '%Y-%m-%d %H:%M:%S'
data = datetime.strptime(self.start_date,fmt)

És a dir, transforma un string en aquest format al tipus de dades datetime.datetime oficial de python.

Per no tindre que especificar el format cada vegada, Odoo dona una ferramenta més facil. La classe fields.Datetime té un mètode per generar un datetime.datetime de un string:

data = fields.Datetime.from_string(self.start_date)

De la mateixa manera passa al contrari:

fmt = '%Y-%m-%d %H:%M:%S'
self.start_date = data.strftime(fmt)
vs
self.start_date = fields.Datetime.to_string(data)

A continuació, anem a veure cóm incrementar una data en un temps:

En el format d’Odoo (fields.Datetime) no es pot, cal passar a datetime.datetime per sumar el temps i després tornar a passar a fields.Datetime. Per sumar o restar temps a un datetime.datetime cal utilitzar una classe anomenada datetime.timedelta. Aquesta classe representa una duració o la diferència entre dues dates. Per exemple, aquest constructor representa molt bé les opcions que es poden ficar per crear un timedelta:

un_any = timedelta(weeks=40, days=84, hours=23, minutes=50, seconds=600)

Aquest exemple d’Odoo mostra cóm afegir 3 dies a un field:

data=fields.Datetime.from_string(self.start_date)
data=data+timedelta(hours=3)
self.end_date=fields.Datetime.to_string(data)

O si es vol fer sols en mètodes python:

fmt = '%Y-%m-%d %H:%M:%S'
data = datetime.strptime(self.start_date,fmt)
data=data+timedelta(hours=3)
self.end_date=data.strftime(fmt)

Ara anem a veure cóm calcular el temps que ha passat entre dues dates:

Solució amb relativedelta:

from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

start=fields.Datetime.from_string(self.start_date)
end=fields.Datetime.from_string(self.end_date)

relative=relativedelta(start,end)
print r.years
print r.months
print r.days
print r

El problema és que dona la data per separat. No sol ser molt útil per a Odoo on necessitem la diferència sols en dies, hores o minuts.

Solució sols amb Datetime:

from datetime import datetime
 
start=fields.Datetime.from_string(self.start_date)
end=fields.Datetime.from_string(self.end_date)
 
print (end-start).days * 24 * 60
print (end-start).total_seconds()/60/60/24

Solució amb Unix timestamp

d1_ts = time.mktime(d1.timetuple())
d2_ts = time.mktime(d2.timetuple())

print int(d2_ts-d1_ts) / 60

La solució és la mateixa, sols és per si necessiteu algun càlcul intermedi que necessite la data en un Integer.


El resultat de restar dos datetime és un timedelta. Podem demanar els dies i segons com en el relative delta, però amés té una funció per traure els segons totals i després fer els càlculs que necessitem.

Consultar si una data és anterior a una altra:

Les dates en format Datetime o Date es poden comparar:

d3=fields.Datetime.from_string(self.d3)
d4=datetime.now()
if d3 < d4:
   print "La data és anterior"

També es pot calcular si és del mateix dia, sols cal transformar de datetime a date:

d3=d3.date()
d4=d4.date()

if d3 == d4 :
   .

Si volem saber si són del mateix més o any, es pot calcular la diferència i veure si en dies és major o menor de 30, per exemple. Però si volem major precisió, en aquest cas es recomana utilitar relativedelta.

Wizards

Els wizards permeten fer un asistent interactiu per a que l'usuari complete una tasca. Com que no ha d'agafar les dades dirèctament en un formulari, si no que va ajundant-lo a completar-lo, no pot ser guardat en la base de dades fins al final.

Els wizards en Odoo són models que estenen la classe TransientModel en compte de Model. Aquesta classe és molt pareguda, però:

  • Les dades no són persistents, encara que es guarden temporalment en la base de dades.
  • A partir de odoo 14 necessiten permisos
  • Els records dels wizards poden tindre referències Many2One amb el records dels models normals, però no al contrari.

Veure més: Wizards en Odoo

Client web

El client Web Odoo (Obsolet a partir de la versió 14)

Web Controllers

Web Controller en Odoo

Pàgina web

https://www.odoo.yenthevg.com/creating-webpages-controllers-odoo10/ http://learnopenerp.blogspot.com/2018/08/odoo-web-controller.html

Exemples

Projecte Odoo complet

Vídeo de Mòdul Odoo completCodi del vídeo

Point Of Sale

Misc.

  • Si volem fer un print en colors, podem ficar un caracter de escape: \033[93m i \033[0m al final
  • Traure la menor potència de 2 major o igual a un número: http://stackoverflow.com/a/14267557

Distintes alertes:

Odoo pot mostrar distintes alertes en funció del que necessitem. Totes estan en openerp.exceptions

Si entrem en el mode shell del debug podem executar aquest comandament:

>>> help(openerp.exceptions)

Una vegada dins podem detectar:

AccessDenied
DeferredException
QWebException
RedirectWarning
except_orm
        AccessError
        MissingError
        UserError
        ValidationError

Normalment són utilitzats pel Odoo sense necessitat de que els cridem nosaltres. Però en ocasion pot ser útil.

Per exemple, si volem mostrar un Warning perquè úsuari ha fet alguna cosa mal. (Normalment es fa un onchange que ja pot tornar el warning)

from openerp import _
from openerp.exceptions import Warning
[...]
raise Warning(_('Alguna cosa ha fallat!'))

O si volem Donar opcions a l'usuari amb RedirectWarning:

 action = self.env.ref('base.action_res_users')
 msg = _("You cannot create a new user from here.\n To create new user please go to configuration panel.")
 raise openerp.exceptions.RedirectWarning(msg, action.id, _('Go to the configuration panel'))

En aquest exemple, per al missatge, utilitza la barra baixa _() per a obtindre la traducció en cas de que existisca. self.env.ref() retorna l'objecte referit amb una id externa. En aquest cas, un action.

En el cas de les Constrains també s'ha de llançar un Validation error.

Funcions lambda:

En moltes ocasions, cal cridar a alguna funció de l'ORM o similar passant com a paràmetre una funció lambda. La raó és que si passem una variable, esta queda establerta en temps de càrrega i no es modifica. La funció sempre recalcula.

La sintaxi de la funció lambda és:

a = lambda x,y: x*y
a(2,3)
6

On les primeres x,y són els arguments que rep la funció, després el que calcula.

Cal recordar que les funcions lambda són de una sola línia de codi. Si volem alguna cosa més sofisticada hem de cridar a una funció normal.


Imatges en Odoo:

Les imatges es guarden com un text en la base de dades. Per convertir un binari en text es fa en base64. Aquesta codificació funciona sense problemes en els navegadors web i no cal preocupar-se de cóm guardar les imatges en PostgreSQL.

Per mostrar la imatge en formularis o llistes es pot ficar widget="image" en el field. Si no fem res més, la imatge es veurà en la mida original. Podem navegar per altres formularis per veure cóm han fet per mostrar la imatge i aprofitar les classes CSS. També podem utilitzar la etiqueta width o style per especificar la mida exacta.

No obstant, no és recomanable carregar les imatges en el seu tamany original. El millor és guardar la versió mitjana o menuda i mostrar eixa sols. Per aconseguir-ho, es pot fer un camp binary computed d'aquesta manera:

from odoo import models, fields, api, tools
[...]
    photo = fields.Binary()
    photo_small = fields.Binary(compute='_get_images',store=True)
    photo_medium = fields.Binary(compute='_get_images',store=True)

    @api.one
    @api.depends('photo')
    def _get_images(self):
        image = self.photo
        data = tools.image_get_resized_images(image)
        self.photo_small = data["image_small"]
        self.photo_medium = data["image_medium"]

La ferramenta tools.image d'Odoo té funcions per a reescalar, retallar o millorar les imatges.

Per utilitzar una imatge dins d'un Kanban, es necessita la funció kanban_image():

<img t-att-src="kanban_image('reserves.hotels', 'photo_small', record.id.value)"/>

Prèviament, s'ha declarat el field photo_small i id al kanban.


res.config.settings

Si volem que el nostre mòdul tinga configuració podem afegir-la com a un field més del model res.control.settings. Aquest ja s'encarrega de centralitzar opcions de configuració. Per a que aparega en el menú de configuració també podem afegir-lo heretant en la vista:

class config(models.TransientModel):
    _inherit = 'res.config.settings'
    players = fields.Char(string='players',
                             config_parameter="expanse.players")


    def reset_universe(self):
        print("reset",self)
 <record id="res_config_settings_view_form_inherit" model="ir.ui.view">
            <field name="name">res.config.settings.view.form.</field>
            <field name="model">res.config.settings</field>
            <field name="priority" eval="25" />
            <field name="inherit_id" ref="base.res_config_settings_view_form" />
            <field name="arch" type="xml">
                <xpath expr="//div[hasclass('settings')]" position="inside">
                    <div class="app_settings_block" data-string="Expanse Settings" string="Expanse Settings" data-key="expanse">
                        <div id="players">
                            <h2>Expanse</h2>
                            <button type="object" name="reset_universe" string="Reset Universe"  class="btn-primary"/>
                        </div>

                    </div>
                </xpath>

            </field>
        </record>

Si en data-key posem el nom del mòdul, afegirà l'icona al menú de settings.

https://www.youtube.com/watch?v=MsVoYPQ4-J4

Enllaços

https://www.odoo.com/documentation/8.0/ https://www.odoo.com/documentation/9.0/

https://www.odoo.com/documentation/8.0/howtos/backend.html

Blogs: http://ludwiktrammer.github.io/ http://www.odoo.yenthevg.com/ https://sateliteguayana.wordpress.com/ https://poncesoft.blogspot.com/

Repositori dels exemples: https://github.com/xxjcaxx/sge20152016 https://github.com/xxjcaxx/SGE-Odoo-2016-2017

https://www.youtube.com/watch?v=0GUxV85DDm4&feature=youtu.be&t=5h47m45s

http://es.slideshare.net/openobject/presentation-of-the-new-openerp-api-raphael-collet-openerp

http://fundamentos-de-desarrollo-en-odoo.readthedocs.org/es/latest/capitulos/comenzando-con-odoo.html

https://www.odoo.com/es_ES/slides/slide/keynote-odoo-9-new-features-201

https://media.readthedocs.org/pdf/odoo-development/latest/odoo-development.pdf

http://webkul.com/blog/beginner-guide-odoo-clicommand-line-interface/

http://useopenerp.com/v8

Podcast que parlen dels beneficis d'Odoo: http://www.ivoox.com/podcast-26-odoo-transformacion-digital-audios-mp3_rf_18433975_1.html

Canal de youtube de SGE amb Odoo en castellà

https://www.odoo.yenthevg.com/extend-selection-odoo-10/

Apunts d'altres professors recopilats

https://naglis.me/post/odoo-13-changelog/ https://www.odoo.com/es_ES/forum/ayuda-1/question/odoo-13-features-and-odoo-14-expected-features-148369#answer-148370

https://medium.com/@manuelcalerosolis

https://www.youtube.com/playlist?list=PLeJtXzTubzj-tbQ94heWeQFB0twGd0vvN

https://vimeo.com/channels/m10dam/page:1