Odoo

De castillowiki
Saltar a: navegación, buscar

El servidor Odoo

Aquesta secció està molt més 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:

$ odoo.py

Pot ser que estiga configurat el servici en l'arranc del sistema operatiu. No obstant, si volem depurar, cal parar el servici i 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:

$ /usr/bin/python /usr/bin/odoo.py --config /etc/odoo/openerp-server.conf --logfile /var/log/odoo/odoo-server.log

Per reiniciar-lo de manera manual:

 $ sudo systemctl restart odoo

El servici Odoo proporciona accés als clients via RPC, Odoo proporciona per defecte un client web.

Arquitectura

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

Arquitectura MVC
Cliente Servidor OpenERP/Odoo
  • 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.
  • Un sistema de fluxos de treball o workflows.
  • 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 plateforma.

Les dades estan guardades en una base de dades relacional. Gràcies a l'ORM, no cal fer consultes SQL dirèctament. 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àndar 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.

Figura 1.1 Activar el debug mode

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.

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.py 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: /usr/lib/python2.7/dist-packages/openerp/cli/templates/

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

És en aquest punt on més diferència trobem amb OpenERP 7, per tant, cal tindre cura de no fer cas totalment de tutorials o ajuda per al 7 o anteriors

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.

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
    )
 
   # 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
  • Selection : Mostra un select amb les opcions indicades.
 
     type = fields.Selection([('1','Basic'),('2','Intermediate'),('3','Completed')])
     aselection = fields.Selection(selection='a_function_name')

Fields Relacionals

  • 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.')]
  • 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)

L'opció delegate: delegate: set it to True to make fields of the target model accessible from the current model (corresponds to _inherits)

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.

  • Many2many : Relació molts a molts.
 arel_ids = fields.Many2many('res.users')
 arel_ids = fields.Many2many(comodel_name='res.users',
                            relation='table_name',
                            column1='col_name',
                            column2='other_col_name')

El primer exemple sol funcionar dirèctamen, però si volem tindre més d'una relació Many2many, 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.

  • 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.
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.

Fields Computed

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

   taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
 
   @api.depends('seats', 'attendee_ids')
   def _taken_seats(self):
      for r in self:
          if not r.seats:
              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.

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. 
        # 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. Però si el camp calculat no depèn de valors estàtics d'altres fields i 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ó: https://www.odoo.com/documentation/8.0/reference/orm.html#computed-fields

Valors per defecte

En Odoo 8.0 é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())

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())

o

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

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

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

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 openerp.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

Fitxers de dades

En Odoo, es poden definir dades que es guardaran en models de l'ORM sobre la base de dades. Aquestes dades poden ser 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, el id. És un número seqüencial 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. Per a que funcione cal utilitzar l'atribut ref:

<field name="product_id" ref="product.product1"/>
Es recomana fer el field 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.

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"/>

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.

Accions i menús

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. No obstant, els menús, 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 abans que els menús que les accionen

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'.

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 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>

Exemple:

  <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>

Vorer més

Vistes Millorades

Encara que Odoo ja proporciona un tree i un form per defecte, la vista cal millorar-la quasi sempre.

Millores en les vistes tree

En les vistes tree es pot modificar el color en funció del contingut d'un field:

<tree string="Idea Categories" colors="blue:state=='draft';red:state=='trashed'">
    <field name="name"/>
    <field name="state"/>
</tree>

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

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"

<tree string="Session 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>

Millores en les vistes form

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.

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.

Per facilitar la gestió, un form pot tindre pestanyes temàtiques. Es fa en <notebook> <page string="titol">

Es pot separar els grups amb <separator string="Description for Quotations"/>

Alguns One2Many donen una vista tree que no es adequada, per això es pot modificar el tree per defecte:

<field name="subscriptions" colspan="4" mode=”tree”>
   <tree>...</tree>
</field>

Per mostrar en un One2many un tree i un kanban, cal especifiar el widget:

  <field name="gallery" mode="kanban,tree" widget="one2many_list">
    <tree>...</tree>
    <kanban>...</kanban>
   </field>

Valors per defecte en un one2many

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 context: Dins del field one2many que estem fent fiquem aquest codi:

context="{'default_<camp many2one>':active_id}"

O este exemple per a dins d'un action:

<field name="context">{"default_doctor": True}</field>
Aquesta sintaxi funciona per a passar per context valors per defecte a un form cridat amb un action. Pot ser en Many2one, butons o menús

Domains en Many2ones

Els camps Many2one es poden filtrar, per exemple:

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


Widgets

Alguns camps, com ara les imatges, es poden mostrar utilitzant un widget distint que el per defecte:

<field name="image" widget="image" class="oe_left oe_avatar"/>
<field name="taken_seats" widget="progressbar"/>
<field name="country_id" widget="selection"/>
<field name="state" widget="statusbar"/>

Llista de widgets d'Odoo disponibles per a camps dins de forms:

instance.web.form.widgets = new instance.web.Registry({
    'char' : 'instance.web.form.FieldChar',
    'id' : 'instance.web.form.FieldID',
    'email' : 'instance.web.form.FieldEmail',
    'url' : 'instance.web.form.FieldUrl',
    'text' : 'instance.web.form.FieldText',
    'html' : 'instance.web.form.FieldTextHtml',
    'char_domain': 'instance.web.form.FieldCharDomain',
    'date' : 'instance.web.form.FieldDate',
    'datetime' : 'instance.web.form.FieldDatetime',
    'selection' : 'instance.web.form.FieldSelection',
    'radio' : 'instance.web.form.FieldRadio',
    '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',
});

Tret de: https://github.com/odoo/odoo/blob/8.0/addons/web/static/src/js/view_form.js#L6355

buttons: Podem introduir un botó en el form:

 <button name="update_progress" type="object" string="update" class="oe_highlight" />

El name ha de ser igual que la funció a la que crida. 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:

 <button name="%(launch_mmog_fortress_wizard)d" type="action" string="Launch attack" class="oe_highlight" />

Els buttons es poden posar per el form, encara que es reconama en el header: ✎


Ocultar condicionalment un field

Es pot ocultar un field si algunes condicions no es cumpleixen. Per exemple:

<field name="boyfriend_name" attrs="{'invisible':[('married', '!=', False)]}" />
<field name="boyfriend_name" attrs="{'invisible':[('married', '!=', 'selection_key')]}" />

Tambés es pot ocultar i mostrar sols en el mode edició o lectura:

<field name="partit" class="oe_edit_only"/>
<field name="equip" class="oe_read_only"/>

O mostrar si un camp anomenat state té un determinat valor:

 <group states="dia"><field name="dia"/></group>

Editar condicionalment un field

En attrs també es pot afegir readonly

<field name="name2" attrs="{'readonly': [('condition', '=', False)]}"/>

Aquests exemples combinen tots els attrs:

<field name="name" attrs="{'invisible': [('condition1', '=', False)], 'required': [('condition2', '=', True)], 'readonly': [('condition3','=',True)]}" />
 
<field name="suma" attrs="{'readonly':[('valor','=','calculat')], 'invisible': ['|',('servici','in',['Reparacions','Manteniment']),('client','=','Pepe')]}" />

Workflows

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

<field name="state" widget="statusbar" statusbar_visible="draft,sent,progress,invoiced,done" />
<button name="action_draft" type="object" string="Reset to draft" states="confirmed,done"/>

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.

<record model="ir.ui.view" id="socio_kanban_view">
            <field name="name">cooperativa.socio</field>
            <field name="model">cooperativa.socio</field>
            <field name="arch" type="xml">
                <kanban>
                    <!--list of field to be loaded -->
                    <field name="name" />
                    <field name="foto" />
                    <field name="arrobas"/>
 
                    <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>
        </record>

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.

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.

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ó.

Si ja volem fer un kanban més avançat, tenim aquestes opcions:

  • 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:

      <record model="ir.ui.view" id="music_kanban_view">
            <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>

I ara el kanban del magatzem que és realment potent:

<kanban class="oe_background_grey" create="0">
                    <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>

El widget gauge és instal·lat en un mòdul a banda del base, al igual que el sparkline

Vistes search

Les vistes search tenen 3 tipus:

  • field que permeten buscar en un determinat camp.
  • 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.

<search>
    <field name="name"/>
    <field name="inventor_id"/>
</search>
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:

<field name="description" string="Name and description"
    filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>

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:

<field name="cajones" string="Boxes or @" filter_domain="['|',('cajones','=',self),('arrobas','=',self)]"/>

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:

<filter name="my_ideas" string="My Ideas" domain="[('inventor_id', '=', uid)]"/>
<filter name="more_100" string="More than 100 boxes" domain="[('cajones','>',100)]"/> 
<filter name="Today" string="Today" domain="[('date', '&gt;=', datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')),('date', '&lt;=',datetime.datetime.now().strftime('%Y-%m-%d 23:23:59'))]"/>

Operadors per als domains:

'like': [('input', 'like', 'open')] - Returns case sensitive (wildcards - '%open%') search.

O/p: open, opensource, openerp, Odooopenerp

'not like': [('input', 'not like', 'open')] - Returns results not matched with case sensitive (wildcards - '%open%') search.

O/p: Openerp, Opensource, Open, Odoo, odoo, OdooOpenerp

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

O/p: open

'ilike': [('name', 'ilike', 'open')] - Returns exact case insensitive (wildcards - '%open%') search.

O/p: Openerp, openerp, Opensource, opensource, Open, open, Odooopenerp, OdooOpenerp

'not ilike': [('name', 'not ilike', 'open')] - Returns results not matched with exact case insensitive (wildcards - '%open%') search.

O/p: Odoo, odoo

'=ilike': [('name', '=ilike', 'open')] - Returns exact (= 'open' or 'Open') case insensitive search.

O/p: Open, open

'=?':

name = 'odoo' parent_id = False [('name', 'like', name), ('parent_id', '=?', parent_id)] - Returns name domain result & True

name = 'odoo' parent_id = 'openerp' [('name', 'like', name), ('parent_id', '=?', parent_id)] - Returns name domain result & parent_id domain result

'=?' is a short-circuit that makes the term TRUE if right is None or False, '=?' behaves like '=' in other cases

'in': [('value1', 'in', ['value1', 'value2'])] - in operator will check the value1 is present or not in list of right term

'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.

Els filter amb group agrupen per algun field:

<group string="Group By">
        <filter name="group_by_inventor" string="Inventor" context="{'group_by': 'inventor_id'}"/>
</group>
o:
  <filter name="group_by_matricula" string="Matricula" context="{'group_by': 'matricula'}"/>

Si volem que un filtre estiga predefinit s'ha de posar en el context de l'action:

<field name="context">{'search_default_clients':1,"default_is_client": True}</field>

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

Si el recurs té 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.
 <record model="ir.ui.view" id="session_calendar_view">
            <field name="name">session.calendar</field>
            <field name="model">openacademy.session</field>
            <field name="arch" type="xml">
                <calendar string="Session Calendar" date_start="start_date"
                          date_stop="end_date"
                          color="instructor_id">
                    <field name="name"/>
                </calendar>
            </field>
        </record>

Vistes Graph

Pot contenir els següents atributs:

  • 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:

  • 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 +.
        <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>

Si volem ficar-lo dins d'un form comun camp one2many, cal especificar un domain:

   <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>
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

El type="line" sembla no funcionar en Odoo 9 , per tant es deu ficar en el context:

<field name="context">{'graph_mode':'line'}</field>

Els reports

Odoo 8.0 ve amb un nou motor de reports. Ja en el 7.0, utilitzàvem un mòdul, el Webkit Report Engine per ampliar les possibilitats dels reports per defecte. El 8.0 incorpora de manera nativa aquestes millores i algunes més.

El nou motor de reports utilitza una combinació de QWeb, BootStrap i Wkhtmltopdf.

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 Amb wkhtmltopdf -V podem comprovar si la versió correcta s'ha instal·lat.

Un report consta de dos elements:

  • Un registre en la base de dades en el model: ir.actions.report.xml amb els paràmetres bàsics
  • Una vista Qweb per al contingut.

Per exemple, en el xml:

<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>

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.

Una mínima template que funciona:

<template id="report_invoice">
    <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>

Analitzem aquesta template:

  • external_layout: Afegeix la capçalera i el peu per defecte de Odoo.
  • Dins de
    : Està el contingut del report.
  • id: A de ser el mateix que el name del report.
  • 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:

<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>

Per afegir una imatge de la base de dades:

<span t-field="doc.logo" t-field-options="{&quot;widget&quot;: &quot;image&quot;, &quot;class&quot;: &quot;img-rounded&quot;}"/>

Notes sobre QWeb

QWeb és el motor de plantilles de Odoo. Els elements són etiquetes XML que comencen per t-

  • 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.
  <t t-if="viatge.hotel">
    <!-- ... -->
  </t>
  • t-foreach: Per fer bucles per els elements d'un one2many, per exemple.

Depurar els reports

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.

Més informació https://www.odoo.com/documentation/8.0/reference/reports.html

Herència

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.

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 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.

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">

É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:

<field name="view_id" ref="view_school_parent_form2"/>

Tal vegada cal especificar totes les vistes. En eixe cas, s'ha de guardar per separat en ir.actions.act_window.view:

<record model="ir.actions.act_window" id="action_my_hr_employee_seq">
    <field name="name">Angajati</field>
    <field name="res_model">hr.employee</field>
    <field name="view_type">form</field>
    <field name="view_mode">tree,form</field>
</record>
 
<record model="ir.actions.act_window.view" id="act_hr_employee_tree_view">
    <field eval="1" name="sequence"/>
    <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">
    <field eval="2" name="sequence"/>
    <field name="view_mode">form</field>
    <field name="view_id" ref="your_form_view_id"/>
    <field name="act_window_id" ref="action_my_hr_employee_seq"/>
</record>

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>

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

https://www.odoo.com/documentation/8.0/reference/orm.html

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.

Per encarar amb garanties el disseny de mètodes en OpenObject es pressuposa uns coneixements mínims de disseny de mètodes en Python.

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.

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.

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.

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:

  • 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 (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

Interactuar en la terminal
$ /usr/bin/python /usr/bin/odoo.py shell --config /var/lib/odoo/.openerp_serverrc -d castillo -u containers
Asciinema amb alguns exemples
Observa cóm hem ficat el paràmetre shell

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
  • 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')


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

El context va passant d'un mètode a un altre o a les vistes i, de vegades volem modificar-lo. Ací tenim un exemple de creació d'un objecte amb un altre context.

order_obj = self.env['sale.order'].with_context({
            key: val
            for key, val in context.iteritems()
            if not (isinstance(key, basestring) and key.startswith('default_'))
        })

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)
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()
>>> 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:

    @api.multi
    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.

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

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:

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

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

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.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.
  • @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.
  • @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.
En conclusió: El @api.one és l'únic on es recomana no utilitzar el for, ja que està fet per això.

Exemple de tots els decoradors:

https://github.com/xxjcaxx/SGE-Odoo-2016-2017/tree/master/proves_decoradors

# -*- coding: utf-8 -*-
 
from openerp import models, fields, api
from openerp.exceptions import ValidationError
 
class proves_decoradors(models.Model):
     _name = 'proves_decoradors.proves_decoradors'
 
     name = fields.Char()
     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')
     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
     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
     def _value_one(self):
         print "\033[93mSelf en @api.one:\033[0m " + str(self)
         self.valueone = float(self.value) / 20
 
     @api.model
     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')
     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)

Casos resolts del controlador

1er Cas, el % de caixons:

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:

     ...
     percent = fields.Float(compute='_get_percent',string='% Loaded', store=False)
     ...

Com que el % depen del caixons, cal fer una funció amb el api depends:

     @api.depends('cajas')
     def _get_percent(self):
       ...

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:

     @api.depends('cajas')
     def _get_percent(self):
         print str(self)

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.

     @api.depends('cajas')
     def _get_percent(self):
         for i in self:
             i.percent = i.cajas*100/1000

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.

2on cas, generador de partides:

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).

     @api.model
     def create(self, values):
        new_id = super(player, self).create(values)
        print values
        return new_id

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:

     @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

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ó:

     @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

Prèviament he creat 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.

3er Cas, les batalles:

En primer lloc, cal fer un record dins d'un model especial que serveix per a fer cron jobs:

   <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>

Aquest cron job crida cada 1 minut al mètode update_progress del model mmog.attack. El codi del update_progres:

     @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})

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.

4t Cas, els noms del les fortaleses (El tema del name_get):

Anem a sobreescriure el mètode name_get.

     @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

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.

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.
class wizard(models.TransientModel):
     _name = 'mmog.wizard'
 
     def _default_attacker(self):
         return self.env['mmog.fortress'].browse(self._context.get('active_id'))
 
     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 {}

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.

        <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"/>

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.

 <button name="%(launch_mmog_fortress_wizard)d" type="action" string="Launch attack" class="oe_highlight" />

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:

      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", }

I uns botons que van fent que passe d'un estar a un altre:

                    <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>

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:

ir_model_data = self.env['ir.model.data']
view_id = ir_model_data.get_object_reference('module_name', 'view_name')[1]
 
return {
    'name': 'view name',
    'view_type': 'form',
    'view_mode': 'kanban,tree,form',
    'res_model': 'your.model.to.reload',
    'view_id': view_id,
    'context': self._context,
    'type': 'ir.actions.act_window',
    'target': 'current',
}

Exemples

El conservatori

Els models:

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')

Les vistes i menús:

<?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>

La cooperativa

Els models:

# -*- 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

Les vistes i menús:

<?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>

El joc (per veure millores dins de la vista):

Els models:

# -*- 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()

El XML:

<?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>

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

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

        <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>

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

 @api.model
     def update_progress(self,cr,uid,ids=None,context=None):
          print self

El @api.model és per a transformar la cridada feta en el api antic als métodes moderns. En altre cas, cal definir la funció com es feia en OpenERP 7.0

Exemples del 7.0 no completament funcionals en el 8.0: http://www.mindissoftware.com/2014/11/21/Odoo-Cron-Job/ https://github.com/Yenthe666/Odoo_Samples/tree/master/scheduler_demo

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.

Càlculs en dates:

# Per calcular diferències de dies, mesos i anys:
 
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
 
date_format = '%Y-%m-%d'
 
joining_date = '2013-08-23'
current_date = (datetime.today()).strftime(date_format) # Les dos dates en format string
 
d1 = datetime.strptime(joining_date, date_format).date()
d2 = datetime.strptime(current_date, date_format).date() # Se transformen a format datetime
r = relativedelta(d2,d1) # Calcula la diferencia en format relativedelta
 
print r.years
print r.months
print r.days
# Relativedelta conté la diferència de cada temps per separat. Si la diferència és, per exemple, de 32 dies, r.days dirà 1 o 2, no 32
 
# Per a calcular la diferència de minuts:
 
from datetime import datetime
 
fmt = '%Y-%m-%d %H:%M:%S'
d1 = datetime.strptime('2010-01-01 17:31:22', fmt)
d2 = datetime.strptime('2010-01-03 17:31:22', fmt)
 
print (d2-d1).days * 24 * 60
print (d2-d1).total_seconds()/60/60/24
 
 
# The above doesn't work in cases where the dates don't have the same exact time.
 
original problem:
 
from datetime import datetime
 
fmt = '%Y-%m-%d %H:%M:%S'
d1 = datetime.strptime('2010-01-01 17:31:22', fmt)
d2 = datetime.strptime('2010-01-03 17:31:22', fmt)
 
daysDiff = (d2-d1).days
print daysDiff
> 2
 
# convert days to minutes
minutesDiff = daysDiff * 24 * 60
 
print minutesDiff
> 2880
d2-d1 gives you a datetime.timedelta and when you use days it will only show you the days in the timedelta.
 
In this case it works fine, but if you would have the following.
 
from datetime import datetime
 
fmt = '%Y-%m-%d %H:%M:%S'
d1 = datetime.strptime('2010-01-01 16:31:22', fmt)
d2 = datetime.strptime('2010-01-03 20:15:14', fmt)
 
daysDiff = (d2-d1).days
print daysDiff
> 2
 
# convert days to minutes
minutesDiff = daysDiff * 24 * 60
 
print minutesDiff
> 2880  # that is wrong
It would have still given you the same answer since it still returns 2 for days, it ignores the hour, min and second from the timedelta.
 
A better approach would be to convert the dates to a common format and then do the calculation.
 
The easiest way to do this is to convert them to unix timestamps.
 
Here is the code to do that.
 
from datetime import datetime
import time
 
fmt = '%Y-%m-%d %H:%M:%S'
d1 = datetime.strptime('2010-01-01 17:31:22', fmt)
d2 = datetime.strptime('2010-01-03 20:15:14', fmt)
 
# convert to unix timestamp
d1_ts = time.mktime(d1.timetuple())
d2_ts = time.mktime(d2.timetuple())
 
# they are now in seconds, subtract and then divide by 60 to get minutes.
print int(d2_ts-d1_ts) / 60
> 3043  # much better

http://www.hitechnologia.com/forum/odoo-forum-1/question/odoo-openerp-how-to-calculate-difference-between-two-dates-283

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/

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
Obtenido de «http://castilloinformatica.com/wiki/index.php?title=Odoo&oldid=3210»