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: