Akcje, dekoratory, self i env

ORM Odoo implementuje typowe funkcje spotykane w tego typu bibliotekach jak odczyt (read), zapis (write), utworzenie (create). Akcje w obrębie modelu wykorzystują kursor (cr), identyfikator użytkownika (uid), oraz kontekst (context). Należałoby więc zawsze gdy korzystamy z odczytu pisać coś w rodzaju:

read(cr, uid, lista_identyfikatorow, lista_pol, context=context)

Aby uniknąć przekazywania w parametrach danych nie związanych z logiką programu, API definiuje dekoratory, które przekazują te dane wprost do metody z użyciem zmiennej env. Env nie zawiera zatem jedynie listy modeli.

Atrybuty i metody env:

  • env.cr kursor bazy danych
  • env.uid ID użytkownika dla aktualnej sesji
  • env.user rekord aktualnego (zalogowanego) użytkownika
  • env.context słownik wartości opisujących kontekst obliczeń (ustawiany programowo)
  • env.with_context(key=value,...) zmiana kontekst
  • env.sudo(user) wykonanie operacji z uprawnieniami innego użytkownika.
  • env.ref() funkcja zwracająca rekord dla podanych parametrów

Użycie dekoratora zdefiniowanego przez api:

@api.dekorator
def metoda(self, parametry):
....

jest równoważne do:

def metoda(self, cr, uid, parametry, context=context):
....

jeśli metoda działa na zbiorze rekordów - zamiast przekazywać listę ich identyfikatorów za pośrednictwem parametru (ids), zmienna self staje się zbiorem rekordów (recordset - zob. https://www.odoo.com/documentation/online/reference/orm.html). Udekorowana funkcja jest w takim wypadku równoważna do:

def metoda(self, cr, uid, ids, parametry, context=context):
  ....

Więcej szczegółów: https://www.slideshare.net/openobject/odoo-from-v7-to-v8-the-new-api

Model zbudowany z użyciem api Odoo jest bardzo prosty w użyciu:

env = Env(cr, uid, context)         # cr, uid, context wbudowane w env
model = env[MODEL]                  # instancja modelu MODEL
recs = model.search(DOMAIN)         # wyszukanie rekordów (search zwraca recordset 
                                    # dla warunków z DOMAIN)
for rec in recs:                    # iterate over the records
  print rec.name
recs.write(VALUES)                  # zapis wartości VALUES.

MODEL oznacza tu nazwę modelu, DOMAIN - listę warunków (krotek w rodzaju ('pole', '=', wartosc)), a VALUES zbiór wartości (na przykład {'pole1': wartosc1, 'pole2':wartosc2}).

Poza pojęciem domeny (DOMAIN - w przybliżeniu odpowiednik where w SQL) spotykamy pojęcie filtru - czyli definicji wyboru rekordów opartej na domenie.

Pierwsza z powyższych operacji (env = ...) nie występuje w aplikacjach, gdyż o zawartość env dba framework. Jeśli w programie chcemy zmienić środowisko (env), posługujemy się metodami withenv oraz with_context (zob. https://www.odoo.com/documentation/8.0/reference/orm.html).

Do zapisu nie musimy używać funkcji write - wystarczy pod zmieną reprezentującą pole podstawić nową wartość. Na przykład:

self.name = "nowa nazwa"

Przykład praktyczny: jak odczytać listę osób zapisanych na wydarzenie (moduł event)?

lista = self.env['event.registration'].search([
 ('state', '!=', 'cancel'),
 ('partner_id', '>', 0),
 ('event_ticket_id', '>', 0),
 ('event_id', '=', event_id),
 ])

Odpowiada to zapytaniu do bazy danych:

select * 
from event_registration 
where partner_id>0 and state<>'cancel' and event_ticket_id>0 and event_id=:id;

Lista dekoratorów

  • @api.multi
  • @api.one
  • @api.model
  • @api.depends
  • @api.returns
  • @api.onchange
  • @api.constrains
  • @api.noguess

Zobacz: http://odoo-new-api-guide-line.readthedocs.org/en/latest/decorator.html

@api.multi

Metoda api.multi pozwala na przetwarzane informacji z modelu bez ich zmiany. Przykład:

  @api.multi
  def get_names(self):
    names = []
    for rec in self:
      names.append(rec.name)
    return ', '.join(names)

@api.one

Jeśli model obejmuje tylko jeden rekord, zamiast @api.multi można - choć nie jest to zalecane - stosować @api.one.

  @api.one
  def get_name(self):
    return self.name # self w tym miejscu znaczy, że mamy tylko jeden rekord!

Wywołanie:

partner.get_name()

Jeśli model zawiera więcej rekordów - pojawia się wyjątek.

Zamiast @api.one zaleca się stosowanie @api.multi oraz metodę self.ensure_one().

@api.multi
def get_name(self):
  self.ensure_one()
  return self.name # self w

Testujemy:

class  Partner2(models.Model):
  _inherit = 'res.partner'
  _name = 'Partner'

  def __init__(self, name):
    self.name = name

  @api.one
  def get_name1(self):
    return self.name

  @api.multi
  def get_name2(self):
    self.ensure_one()
    return self.name 

  @api.multi
  def get_names(self):
    names = []
    for rec in self:
      names.append(rec.name)
    return ', '.join(names)

@api.model

Dekorator używany zawsze gdy chcemy wykorzystać mechanizmy self/env zamiast przekazywać dane przez parametry (a nie ma innego dekoratora). Na przykład dla funkcji ustawiającej wartość domyślną (aktualnego użytkownika):

created_id = fields.Many2one('res.users', default=_default_created_user,  string='Created by')
@api.model
def _default_created_user(self):
  uid = self._uid
  return uid

@api.returns

Tak udekorowana funkcja zwraca zbiór rekordów (RecordSet, w starym API automatycznie zamieni na listę identyfikatorów).

Przykład:

@api.returns('self')
def _get_return(self):
       return self

@api.onchange

Wiąże funkcję z obsługą zdarzenia onchange na formularzu (XML). Bez dekoratora taka funkcja może wyglądać następująco:

    def on_change_cena0(self,cena,upust):
      total = cena - upust
      res = {
              'value': {
              'wartosc': total
              }
            }
      return res

Aby ją wywołać - na formularzu musimy zdefiniować pola:

 <field name="wartosc" />
 <field name="cena" on_change="on_change_cena0(cena,upust)"/>
 <field name="upust" on_change="on_change_cena0(cena,upust)"/>

Użycie dekoratora pokazuje niesamowite możliwości w ukrywaniu szczegółów implementacyjnych (dla programisty zostaje czysta logika biznesowa). Funkcja z dekoratorem:

    @api.onchange('cena','upust')
    def on_change_cena(self):
      self.wartosc = self.cena - self.upust

Nie jest w tym przypadku potrzebne definiowanie własności on_change na formularzu!!! Muszą tylko zgadzać się identyfikatory. Oczywiście dekorator może być deklarowany dla jednego lub więcej.

Jeśli coś nam nie pasuje - możemy po staremu zgłosić komunikat (podkreślenie to wywołanie translatora):

    @api.onchange('cena','upust')
    def on_change_cena(self):
      if self.cena<self.upust:
        return {
        'warning': {
           'title': _('Warning!'),
           'message': _('Price must be > discount'),
           },
        }
      self.wartosc = self.cena - self.upust

W przypadku pól powiązanych (Many2one) można też w taki sposób sterować filtrami wyboru (domain). Zobacz przykład: https://stackoverflow.com/questions/45690736/how-to-return-domain-on-onchange

@api.depends

Dekorator używany w polach obliczeniowych - przekazuje wartości pól na podstawie których wylicza się wartość. Więcej informacji na ten temat znajdziesz w następnym rozdziale.

@api.constrains

Odoo umożliwia korzystanie z więzów integralności bazy danych. Pokazuje to poniższy przykład (zaczerpnięty z podręcznika "Odoo 10 Developement Essentials").

class TodoTask(models.Model):
  _sql_constraints = [
   ('todo_task_name_uniq',
    'UNIQUE (name, active)',
    'Task title must be unique!')]

Jednak możemy definiować dodatkowe ograniczenia programowo

Na przykład:

    @api.multi
    @api.constrains('cena')
    def _on_cena_nieujemna(self):
#        self.ensure_one()
      for item in self:
        if item.cena<0:
#          _logger.warning('Cena = (%s).' % self.cena)
          raise Warning(_('Error! Price must be >0.'))

Zamiast Warning można użyć ValidationError, Warning (from odoo.exceptions import ValidationError, Warning).

Uwaga! Te ograniczenia są sprawdzane dopiero w momencie zapisu! Nie tak jak w przypadku depends, czy onchange - gdzie odbywa się sprawdzanie w momencie edycji.