Usare l'ORM

Fino a questo punto abbiamo utilizzato l’ORM di Odoo senza scendere nel dettaglio su come funziona. Ora andiamo a vedere quali sono i suoi principali componenti

Decoratori

Abbiamo notato che a molti metodi dei modelli viene applicato un decoratore come @api.multi. Questi decorati hanno lo scopo di istruire il backend su come gestire i metodi rispetto alle API esposte.

Il decoratore @api.model invece si utilizza per decorare metodi statici in cui il self non fa rifermento a nessuna entità in particolare. Per coerenza self farà sempre riferimento a un oggetto di tipo recordset ma il suo contenuto diventa irrilevante. Questo tipo di metodi non possono essere richiamati dalle API e quindi non possono essere usati sui bottoni nell’interfaccia utente.

Altri decoratori con scopi più specifici:

  • @api.depends(campo1, campo2, …) è utilizzato dai computed fields per identificare le dipendenze in base a cui scatenare il ricalcolo del valore del campo.
  • @api.constraints(campo1, campo2, …) è utilizzato per la validazione, la funzione viene invocata ogni volta che si cerca di modificare il valore del campo.
  • @api.onchange(campo1, campo2, …) è richiamata ogni volta che nella UI vengono modificati i campi elencati nel decoratore. è utile per aggiornare al volo i form con valori dipendendi da altri campi.

La shell di odoo

Lanciando il seguente comando, odoo ci presenta un linea di comando interattiva dove possiamo accedere a tutto l’ambiente di odoo. E' molto comoda per effettuare dei test:

$ docker compose run odoo shell

In [1]: self
Out[1]: res.users(1,)

In [2]: self._name
Out[2]: 'res.users'

In [3]: self.name
Out[3]: 'Administrator'

Recordset

I metodi principali utilizzabili nei modelli sono quelli disponibili sulla documentazione ufficiale

Interrogare i modelli

Attraverso la variabile self possiamo accedere solamente ai metodi del modello che stiamo attualmente utilizzando. Ma ogni modello ha un variabile env, accesssibile tramite self.env che ci permette di avere un riferimento a qualsiasi modello installato sul sistema. Per esempio self.env[‘res.parner’] restituisce un riferimento al modello dei Partner permettendoci quindi di utilizzare metodi come search o browse su quel determinato set di dati.

Il metodo search() accetta come paramentro un domain e restituisce un recordset contenente le righe che rispettano le condizioni del dominio. Passando un domain vuoto ([]) si ottengono tutte le righe presenti. Gli altri paramentri accettati da search() sono:

  • order e' la stringa utilizzata nella clausula ORDER BY della query SQL
  • limit il numero massimo di elementi da restituire nella query
  • offset ingnora i primi n risultati. Combinato con limit serve a gestire la paginazione degli elementi

A volte serve solo contare gli elemneti in un determinato domain, in quei casi è possibile utilizzare la funzione search_count() che accetta gli stessi parametri della search() ma restituisce un intero (e ha un impatto infinitesimale in termini di performance rispetto alla classica search())

Il metodo browse() prende una lista di ID oppure un singolo ID e ritorna un recordset contenente i record trovati. E' molto più performante della search ma richiede la conoscenza degli ID interessati.

Alcuni Esempi:

In [2]: self.env['res.partner'].search([('name','ilike','ad')])
Out[2]: res.partner(3,)

In [4]: p = self.env['res.partner'].browse(3)
In [5]: p.name
Out[5]: 'Administrator'

Operazioni sui recordset

I Recordset supportano diverse operazioni. Possiamo per esempio controllare se un elemento è contenuto in un recordset oppure no.

Considerando x un singleton e test_recordset un insieme di elementi possiamo scrivere

  • x in test_recordset
  • x not in test_recordset

Sono inoltre dissponibili le seguenti proprietà:

  • test_recordset.ids restituisce una lista di ID relativa agli elementi contenuti
  • test_recordset.ensure_one() controlla che il recordset sia un singleton altrimenti lancia un eccezione di tipo ValueError
  • test_recordset.filtered(func) ritorna un recordset filtrato secondo la funzione func
  • test_recordset.mapped(func) ritorna una lista di valori mappati secondo la funzione func
  • test_recordset.sorted(func) ritorna un recordset ordinato secondo la funzione func

Manipolazione dei recordset

Per aggiungere, togliere o sostituire elementi dai recorset ci sono una serie di operatori che ci possono aiutare. I recordset di per sè sono immutabili ma attraverso questi operatori è possibile generare nuovi recordset modificati partendo da quelli esistenti

Gli operatori di manipolazione sono:

  • rs1 | rs2 restituisce l'unione dei due recordset, il risultato conterrà tutti gli elementi di entrambi gli insiemi (senza doppioni)
  • rs1 + rs2 restituisce la somma dei due recordset, il risultato conterrà la concatenazione degli elementi di entrambi gli insiemi (possono quindi esserci dei doppioni)
  • rs1 & rs2 restituisce l'intersezione dei due recordset, il risultato conterrà solo gli elementi presenti in entrambi gli insiemi
  • rs1 - rs2 restituisce la differenza dei due recordset, il risultato conterrà gli elementi presenti in rs1 ma non presenti in rs2

È inoltre possibile accedere agli elementi dei recordset attverso gli operatori di list Python, queste sono quindi espressioni valide:

  • rs[0] il primo elemento del recordset
  • rs[-1] l’ultimo elemento del recordset
  • rs[1:] restituisce una copia del recordset senza il primo elemento

Altri operatori:

  • rs_ids |= element_id aggiunge element_id al recordset rs_ids se non presente
  • rs_ids -= element_id rimuove element_id al recordset rs_ids se presente

Query SQL

E' sempre possibile accedere al databse direttametne eseguendo query SQL personalizzate. Nella varibile self.env.cr è disponibile un cursore legato all’attuale connessione al db che possiamo utilizzare proprio a questo scopo.

Per effetturare una query utilizziamo il metodo execute successitamente dobbiamo invocare un’altra funzione per ottenerne gli eventuali risultati:

  • fetchall() restituisce una lista di tuple rappresentanti le righe
  • dictfetchall() restituisce una lista di dizionari rappresentanti le righe con il nome della colonna utilizzato come chiave
In [1]: self.env.cr.execute("SELECT id, login FROM res_users WHERE login='%s'" % 'admin')

In [2]: self.env.cr.dictfetchall()
Out[2]: [{'id': 1, 'login': 'admin'}]

Campi Relazionali

I campi relazionali possono essere utilizzati con la notazione puntata, esempio:

In [14]: self
Out[14]: res.users(1,)

In [15]: self.company_id
Out[15]: res.company(1,)

In [16]: self.company_id.name
Out[16]: 'My Company'

In [17]: self.company_id.currency_id
Out[17]: res.currency(1,)

In [18]: self.company_id.currency_id.name
Out[18]: 'EUR'

Quando dobbiamo scrivere un campo Many2one dobbiamo ricordarci di passare solo l’id dell’oggetto e non il singleton corrispondente, quindi e' corretto

# CORRETTO
In [17]: self.write({'user_id': self.env.user.id})

ma darebbe invece errore

# SBAGLIATO
In [17]: self.write({'user_id': self.env.user})

Quando invece dobbiamo scrivere i campi Many2many oppure One2many esiste una sintassi specifica per farlo. Il valore che dobbiamo passare nella write è una tupla contenente tre valori i cui possibili valori sono:

  • (0, _, values) aggiungi un nuovo record creato dal dizionario values
  • (1, id, values) aggiorna gli elementi correlati esistenti con i valori passati nel dizionario values (non può essere usato con il metodo create())
  • (2, id, _) rimuovi la relazione con l’oggetto idenficato da id e poi cancella l’oggetto rimosso dal db
  • (3, id, _) rimuovi la relazione con l’oggetto idenficato da id e ma non rimuovere l’oggetto rimosso dal db (solo Many2many e non durante la create())
  • (4, id, _) aggiungi l’oggetto esistente con id = id (solo Many2many)
  • (5, _, _) rimuovi tutti gli oggetti dalla relazione (solo Many2many)
  • (6, _, ids) sostituisci tutti gli elementi dalla relazione con quelli identificati dagli id ids (equivalente di chimare prima il comando 5 e poi il 4 su tutti gli ids)