Lezione 6: Colonne calcolate e Trigger¶
In questa lezione vedremo come aggiungere un po” di semplice business logic alla nostra applicazione.
Prima di tutto implementeremo un metodo server che al variare di quantità e prodotto all’interno di una riga fattura calcoli il prezzo totale ed il valore di iva della riga.
In seguito vedremo i metodi di trigger, definibili su una table e li utilizzeremo per effettuare il calcolo dei totali della fattura.
Calcolo del totale di riga¶
Nella lezione precedente abbiamo visto come creare una nuova riga fattura e come riempire i campi prodotto_id
e quantita
. Di un prodotto però conosciamo il suo prezzo unitario e quindi dovremmo mostrarlo nella riga fattura. Inoltre dovremmo moltiplicarlo per la quantità così da calcolare il totale della riga.
Sebbene tutti questi calcoli siano fattibili anche lato client è per certi versi più semplice sviluppare questa logica in Python, lato server.
Andiamo quindi in th_fattura_riga
e nella ViewFromFattura
definiamo il metodo th_remoteRowController
che si occuperà di calcolare i totali.
th_remoteRowController¶
Il metodo th_remoteRowController
che andiamo a definire nella classe ViewFromFattura
è quello che, lato server, effettua il calcolo del totale. Dato che esso viene invocato direttamente dal client è necessario premettere il decoratore @public_method
. Questo per motivi di sicurezza, infatti i metodi rpc delle pagine Genropy, non devono poter essere invocati dal client a meno che siano dichiarati esplicitamente come pubblici tramite questo decoratore.
@public_method
def th_remoteRowController(self, row=None, field=None, **kwargs):
th_remoteRowController
riceve come parametri la row ovvero, la riga corrente con il suo contenuto corrente, e il field ovvero, il nome del campo che ha fatto scattare la chiamata.
Affinchè il metodo venga chiamato ogni volta che si modifica il valore di prodotto_id
o di quantita
è necessario cambiare i parametri di queste celle da edit=True
a edit=dict(remoteRowController=True)
.
def th_struct(self, struct):
r = struct.view().rows()
r.fieldcell('prodotto_id', edit=dict(remoteRowController=True, validate_notnull=True))
r.fieldcell('quantita', edit=dict(remoteRowController=True))
r.fieldcell('prezzo_unitario')
Cogliamo l’occasione per ricordare che il parametro edit
della cella se messo a True si limita a dichiarare la cella editabile. Se invece è un dizionario di attributi consente di aggiungere altri parametri relativi al widget di inserimento che compare quando si va in modalità modifica, come ad esempio le validazioni. In questo caso il parametro remoteRowController
fa sì che il metodo remoto venga invocato ogni volta che l’utente termina di modificare il valore della cella.
Calcolo del prezzo totale¶
@public_method
def th_remoteRowController(self, row=None, field=None, **kwargs):
if not row['quantita']:
row['quantita'] = 1
if field == 'prodotto_id':
prezzo_unitario = self.db.table('fatt.prodotto').readColumns(
columns='$prezzo_unitario',
pkey=row['prodotto_id'])
row['prezzo_unitario'] = prezzo_unitario
row['prezzo_totale'] = row['quantita'] * row['prezzo_unitario']
return row
Implementiamo ora il calcolo che deve essere svolto dal metodo th_remoteRowController
. Nel caso il campo variato sia prodotto_id
, andiamo a leggere il prezzo unitario del nuovo prodotto.
Per farlo usiamo il metodo readColumns
per leggere dalla tabella prodotto, la colonna prezzo_unitario
corrispondente al record che ha come primary key l’id del prodotto appena inserito nella cella. Quindi pkey=row['prodotto_id']
.
Notiamo che la colonna viene richiesta come $prezzo_unitario. Vedremo in altre parti la sintassi per l’accesso al database. Per il momento diciamo solo che per accedere ai nomi delle colonne della tabella corrente è necessario prefissarli col simbolo $.
Andando al browser possiamo ora modificare le righe e verificare che il totale viene calcolato automaticamente.
Calcolo IVA¶
Torniamo ora al metodo th_remoteRowController
per aggiungere alla nostra elaborazione di riga il calcolo dell’IVA.
Modifichiamo la lettura con readColumns
della tabella prodotto aggiungendo nelle colonne richieste la colonna @tipo_iva_codice.aliquota
.
In Genropy è possibile accedere a qualunque valore in tabelle collegate a qualunque livello semplicemente richiedendole con il loro path relazionale. In questo caso @tipo_iva_codice
ci porta sulla tabella tipo_iva
e qui prendiamo la colonna aliquota
. Genropy si prende carico di effettuare le join necessarie e ci evita possibili errori sql.
@public_method
def th_remoteRowController(self, row=None, field=None, **kwargs):
if not row['quantita']:
row['quantita'] = 1
if field == 'prodotto_id':
prezzo_unitario, aliquota_iva = self.db.table('fatt.prodotto').readColumns(
columns='$prezzo_unitario,@tipo_iva_codice.aliquota',
pkey=row['prodotto_id'])
row['prezzo_unitario'] = prezzo_unitario
row['aliquota_iva'] = aliquota_iva
row['prezzo_totale'] = row['quantita'] * row['prezzo_unitario']
row['iva'] = row['aliquota_iva'] * row['prezzo_totale'] / 100
return row
Una volta letta l’aliquota_iva andremo a completare la riga calcolando il valore dell’iva corrispondente al prezzo totale.
I trigger di table¶
Al momento del salvataggio della fattura desideriamo che i campi totale_imponibile
, totale_iva
e totale_fattura
vengano aggiornati automaticamente.
Per fare questo introduciamo il concetto di trigger. In ogni tabella possiamo definire dei trigger sugli eventi di insert, update e delete. In particolare sono disponibili:
trigger_onInserting
trigger_onInserted
trigger_onUpdating
trigger_onUpdated
trigger_onDeleting
trigger_onDeleted
La differenza tra i trigger che terminano con “ing” e quelli che terminano con “ed” sta nel momento di chiamata: quelli in “ing” vengono chiamati prima di eseguire l’operazione mentre quelli in ed, dopo l’operazione. Useremo i trigger in ing per cambiare dei valori nel record in corso mentre useremo i trigger in ed per effettuare azioni conseguenti in altre tabelle. In ogni caso tutti i trigger vengono ovviamente chiamati prima del commit finale che conclude la transazione di scrittura sul database.
Calcolo totali¶
Desideriamo che ogni volta che viene aggiunta, cancellata o modificata una riga fattura, vengano ricalcolati i totali della fattura.
Andiamo quindi a modificare il modulo fattura_riga.py
aggiungendo i metodi trigger_onInserted
, trigger_onUpdated
, trigger_onDeleted
che richiameranno il metodo ricalcolaTotali
, che andremo a definire nel modulo fattura.py
.
def trigger_onInserted(self, record=None):
self.db.table('fatt.fattura').ricalcolaTotali(record['fattura_id'])
def trigger_onUpdated(self, record=None, old_record=None):
self.db.table('fatt.fattura').ricalcolaTotali(record['fattura_id'])
def trigger_onDeleted(self,record=None):
if self.currentTrigger.parent:
return
self.aggiornaFattura(record)
A tale chiamata passeremo il valore del campo fattura_id
contenuto nel record di fattura_riga
che è stato inserito/modificato/cancellato.
Noterete che nella trigger_onDeleted
è stata aggiunta una condizione che impedisce l’aggiornamento della fattura. Tale condizione è stata aggiunta contemplando il caso in cui la riga_fattura venga cancellata come conseguenza della cancellazione della fattura stessa. Ricordiamo infatti che nel model abbiamo messo come attributo della relazione tra fattura_riga e fattura il parametro onDelete='cascade'
. In questo caso sarebbe sbagliato, oltre che inutile provare ad aggiornare una fattura mentre viene cancellata. Il test verifica quindi che il trigger corrente non sia stato attivato come conseguenza di un’altra cancellazione, valutando la proprietà self.currentTrigger.parent
.
Andiamo quindi a modificare il file di model della table fattura implementando il metodo ricalcolaTotali
.
Per aggiornare i campi dei totali dovremo:
Leggere il record della fattura bloccando l’accesso ad altri utenti
Leggere tutte le righe della fattura e sommare i valori
Aggiornare il record fattura con i totali calcolati
recordToUpdate¶
Per leggere il record facendo un lock sulla risorsa esistono diverse modalità in Genropy. Qui useremo il metodo recordToUpdate
della classe Table
, il quale utilizzando un context manager provvede ad accedere il record in modalità modifica per poi effettuare la scrittura di update sul database, all’uscita del blocco with
.
def ricalcolaTotali(self, fattura_id=None):
with self.recordToUpdate(fattura_id) as record:
totale_imponibile, totale_iva = self.db.table('fatt.fattura_riga'
).readColumns(columns="""SUM($prezzo_totale) AS totale_imponibile,
SUM($iva) AS totale_iva""",
where='$fattura_id=:f_id', f_id=fattura_id)
record['totale_imponibile'] = totale_imponibile
record['totale_iva'] = totale_iva
record['totale_fattura'] = record['totale_imponibile'] + record['totale_iva']
La riga:
with self.recordToUpdate(fattura_id) as record:
ci mette a disposizione il record di fattura locked da aggiornare. Utilizziamo nuovamente l’istruzione readColumns
, questa volta sulla table fattura_righe e chiediamo le colonne SUM($prezzo_totale)
e SUM($iva)
. Notiamo quindi che possiamo chiedere non solo le colonne ma anche usare funzioni SQL sulle stesse. Possiamo usare anche la clausola as anche se in questo caso, usando una readColumns, potrebbe essere omessa. Nella clausola where specifichiamo che desideriamo solo le righe che abbiano fattura_id uguale al fattura_id corrispondente alla fattura che stiamo aggiornando.
Possiamo quindi tornare a vedere con il browser il risultato delle nostre modifiche, verificando che il calcolo dei totali si aggiorni al salvataggio della fattura.
Allegati: