.. _tutorial_fatturazione/invoice_2: 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. .. raw:: html
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. .. raw:: html
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. .. raw:: html
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. .. raw:: html
.. raw:: html
**Allegati:** - `fattura `_ - `th_fattura_riga `_ - `fattura_riga `_