Dispensa 2.0

Gestione del magazzino di casa

Come tenere sotto controllo le scorte delle spese domestiche

Oggi giorno è diventata comune la pratica di acquistare online la spesa settimanale, cercando di fare una buona scorta e limitare al minimo indispensabile l'ingresso negli affollati negozi reali.

Per questo motivo è bene dotarsi di qualche sistema per cercare di gestire queste "giacenze", evitando il troppo o il troppo poco, o peggio che il fantomatico lievito manchi proprio quando avete deciso di fare la pizza...

E' inoltre comune la pratica di acquistare settimanalmente un determinato articolo per abitudine, senza rendersi realmente conto di averne già una quantità industriale in casa...

La modalità più comune per poter decidere cosa acquistare e cosa no, è normalmente l'osservazione, perchè è chiaro che a casa non abbiamo un centro commerciale con migliaia di prodotti da tenere in magazzino, per cui ci facciamo bastare la classica lista dove annotare di volta in volta il prodotto esaurito.

Un piccolo upgrade a questa procedura "manuale" potrebbe essere quello di dotarsi di liste della spesa elettroniche, come Bring, che è anche integrabile con Alexa, per cui diventa un gioco aggiungere al volo il prodotto mancante nella lista della spesa.

In questo articolo però voglio trattare di una implementazione interessante e a poco costo per trasformare la vostra dispensa di casa in un piccolo magazzino da gestire con strumenti che si vedono tipicamente nei negozi 😊


Il tutto si basa nell'inserire in un database i prodotti che avete nella dispensa di casa attraverso la scansione del relativo codice a barre normalmente già presente sui prodotti in commercio

Il sistema permetterà di tracciare sia il prodotto acquistato che si aggiunge al magazzino che quello in uscita per essere consumato in modo che le relative quantità nel nostro database restino sempre aggiornate e reali. 

In questo modo è chiaro che nel momento in cui servirà effettuare una nuova spesa, si potrà contare su una lista aggiornata per fare degli acquisti più oculati.  

A tutti gli effetti è una procedura semplice. Il programma deve: 

  • aver censito inizialmente i prodotti che già avete in casa o quelli nuovi che aggiungerete per creare una anagrafica dei prodotti.
  • aggiornare in più o in meno la quantità di un prodotto quando si fa la spesa e quando lo si preleva dalla dispensa  

Indice

Architettura

Vediamo qual è l'architettura di massima:

  • praticamente tutti i prodotti in commercio hanno un codice a barre, questo permette di definire univocamente un prodotto e di tracciarlo nel tempo
  • questi codici possono essere letti molto velocemente con i classici barcode readerPer questo progetto ne ho acquistato uno economico da circa 30€ su Amazon, dotato di connettività wifi e interfaccia USB che si può collegare ad un PC o direttamente al server dove vedremo è installato il db. 
  • A tutti gli effetti il reader si comporta come uno scanner che legge e invia, come fosse una tastiera molto veloce, i dati letti al PC. Il programma in ascolto è scritto in Python.
       Giro dei link molto interessanti se si vuole approfondire questo                           linguaggio: 

  • I dati sono memorizzati in un DB PostgreSQL installato su un piccolo server Raspberry PI 4.0 


Installazione dei componenti

Vediamo come si installano i vari componenti.

Barcode reader

La configurazione del barcode reader è in realtà la cosa più semplice del progetto. Basta staccare il piccolo ricevitore USB che arriva inserito nel reader e collegarlo al PC dove è attivo il programma.

A questo punto saremo in grado di scansionare un codice a barre di esempio e inviarlo a qualsiasi programma di scrittura del PC per provarlo (es. notepad).

Database

Come dicevo il database scelto è PostgreSQL. E' un open source più che sufficiente per essere utilizzato in progetti di questo tipo.

Supporta anche il backup online per il salvataggio dei dati. Vuol dire che non c'è la necessità di spegnere il database per farne una copia di salvataggio.

Per l'installazione del DB, è utile dotarsi di un piccolo server in modo che venga condiviso e rappresentare l'unica vista dei dati.
 
Ho usato un Raspberry Pi 4 che in realtà non è dedicato a questo progetto, perchè normalmente usato iso desktop da mia figlia per la DAD collegato ad una TV.

Quindi se già avete a casa un dispositivo simile, tanto meglio, non ci sarà la necessità di un acquisto dedicato.
 


Il sistema operativo del Raspberry Pi Raspbian si basa sulla distribuzione di Linux Debian, per cui per installare PostgreSQL occorre dare dei comandi "manuali" come riportato ad esempio qui.

Al termine dell'installazione, si potrà proseguire con la configurazione del database con l'utenza standard postgres:

pi@raspberrypi:~ $ sudo su postgres


Ho utilizzato per semplicità l'utenza di default postgres presente sia nel database che su linux. In questo modo questa riceverà tutte le autorizzazioni necessarie per accedere al database e creare gli oggetti sempre nel database standard postgres




Configurazione DB per accesso remoto

Il database installato sul server Raspberry va configurato in modo da ricevere comandi da remoto.

Questo è necessario perchè alla fine il programma sarà eseguito su qualsiasi PC appartenenti alla LAN di casa.

Di default il DB PostgreSQL accetta solo connessioni locali al server dove è installato.

Per abilitare altri indirizzi IP a connettersi remotamente, occorrerà modificare queste impostazioni di base.

Quindi primo step è individuare dove è posizionato il file di configurazione dell'autenticazione dei client attraverso questo comando eseguibile sempre tramite interfaccia psql:

postgres=# SHOW hba_file;
              hba_file
-------------------------------------
 /etc/postgresql/11/main/pg_hba.conf
 
Tramite editor (es. vi) modificare questa riga inserendo la sottorete client di casa:

Originale:
#host    all             all             127.0.0.1/32            md5

Modificata:
host    all             all             192.168.1.0/24          md5

La seconda modifica necessaria è relativa al parametro listen_addresses presente nel file di configurazione del db, rintracciabile sempre utilizzando psql:

postgres=# SHOW config_file;
               config_file
-----------------------------------------
 /etc/postgresql/11/main/postgresql.conf

Originale:
#listen_addresses = 'localhost'         # what IP address(es) to listen on;

Modificata:
listen_addresses = '*'                  # what IP address(es) to listen on;

A questo punto il DB è in grado di ricevere i comandi di select, update, ecc anche da client remoti.

Creazione tabella prodotti

A questo punto è possibile con il tool psql impartire comandi all'interno del DB:

postgres@raspberrypi:/home/pi$ psql

e creare quindi la tabella dei prodotti products con questa istruzione:

postgres=# create table products (barcode CHAR(13) PRIMARY KEY, product VARCHAR NOT NULL, category VARCHAR, quantity INT);

La tabella creata in questo modo avrà quattro campi:
  • barcode che costituirà la chiave primaria, ovvero conterrà un elenco di valori univoci (appunto i codici a barre). I dati saranno di tipo CHAR con dimensione massima 13 (i barcode dei prodotti in commercio non superano questa lunghezza); 
  • product rappresenta il nome del prodotto in questione di tipo VARCHAR in modo da adattarsi automaticamente al numero di caratteri con cui un prodotto può essere chiamato;
  • category rappresenta la categoria di prodotto sempre VARCHAR;
  • quantity ovvero la quantità relativa. E' un numero, quindi va bene la tipologia INT.
Le righe della tabella, ovvero i singoli record, vengono inseriti, aggiornati o letti tramite i comandi SQL di insert, update e select

Queste istruzioni saranno recepite dal programma che, come vedremo dopo, si strutturerà in tre pulsanti principali che indirizzeranno le scritture nel database:




Il primo step sarà quello di inserire la riga corrispondente al nuovo prodotto da memorizzare nel DB.
Il corrispondente record sarà costituito quindi dal nuovo codice a barre (univoco), il nome del prodotto, la categoria e la quantità, inizialmente uguale a zero perchè in questa fase stiamo solo definendo l'anagrafica del prodotto.

A questo punto, la riga potrà essere aggiornata in funzione dell'operazione a magazzino da effettuare, ovvero tramite il comando di update si potrà variarne la quantità in funzione del numero di prodotti aggiunti o prelevati dal magazzino.

Il programma selezionerà (select), con le modalità descritte nei paragrafi successivi, la quantità attuale del prodotto relativo al corrispondente barcode e incrementerà/decrementerà della quantità desiderata (default: 1) tramite il comando di update.

Una volta ottenuta la nostra tabella popolata e mantenuta tramite il programma, è possibile effettuare anche aggiornamenti diretti in tabella per correggere eventuali anomalie piuttosto che effettuare delle query su di essa per avere sempre a disposizione la situazione aggiornata del nostro magazzino casalingo.

Ad esempio da un PC Windows, dopo aver attivato ssh sul Raspberry Pi e installato putty, si può accedere con l'utility psql al database.


Esempio:

Per selezionare tutti i prodotti con quantità limitata (per esempio inferiore a 2):

postgres=# select * from products where quantity <'2' order by category;


A questo punto si può affinare la query per verificare che ci siano pochi prodotti all'interno di una determinata categoria (es.: biscotti):

postgres=# select * from products where category='biscotti' order by quantity;


In questo caso l'analisi estesa all'intera categoria ha quindi indicato una situazione diversa circa le rimanenze effettive. Da questa vista non risulterebbe urgente acquistare ulteriori biscotti.

Programma

Occorre innanzitutto installare Python ed eseguire il codice allegato. Anche in questo caso esistono diversi siti che specificano come installare Python per cui non mi dilungherò in merito. 
Conviene installare Python sia sul Raspberry PI che sui PC Windows dove si vuole lanciare l'interfaccia grafica.



E' comodo eseguire il programma su un portatile nella stanza "magazzino" dove c'è la nostra dispensa, nel momento in cui occorre inserire i prodotti acquistati nel DB. 

Può essere invece eseguito direttamente sul Raspberry PI quando quotidianamente preleviamo un prodotto dalla dispensa, procediamo alla lettura del relativo barcode e lo portiamo in cucina o nella stanza dove sarà consumato.
A tal proposito è utile settare il Raspberry in modo che non vada in stand-by e sia sempre pronto a recepire la scansione dei prodotti che preleviamo in settimana dalla dispensa. Ad esempio qui è spiegato come disabilitare lo Screen Blanking attivo di default su Raspberry. 

 
Una volta eseguito il programma, apparirà la finestra principale:


Pulsante Nuovo prodotto


Cliccando su pulsante "Nuovo prodotto", compare la maschera per definire un prodotto non ancora presente nel magazzino.

Questa azione va effettuata solo una volta per singolo prodotto/barcode.




Il cursore si posizionerà automaticamente sul campo vuoto del Barcode. Questo fa sì che non servirà la testiera o il mouse per imputare il barcode tramite il reader.


La scansione del barcode sarà riportata all'interno della finestra nel campo Barcode.
I campi Nome prodotto e Categoria andranno riportati manualmente con la tastiera.




Il pulsante Pulisci serve per ripulire tutti i campi della finestra.

Il pulsante Salva effettuerà l'inserimento nel database dei dati inseriti. Il sistema gestisce eventuali errori di inserimento (es. "chiave duplicata" nel caso si stia tentando di inserire un barcode già esistente).

Al termine del salvataggio verrà mostrata una finestra con il riepilogo dell'attività:



Pulsante Inserisci

Tramite la finestra corrispondente al pulsante "inserisci", c'è la possibilità di incrementare la quantità di prodotto del nostro database.

Per esempio sempre nel caso del tonno appena definito, con il reader si potrà scansionare il barcode dalla scatola del prodotto, variandone eventualmente la quantità.

Il default per la quantità è sempre 1. Questo consente di dedicarsi alla scansione, senza pensare a digitare con la tastiera, rendendo il tutto molto più pratico nell'utilizzo.


Inoltre non è necessario cliccare sul pulsante Salva, perchè è attiva una funzionalità di salvataggio automatico dopo 6 secondi (eventualmente modificabile).

In questo modo si potrà passare al prodotto successivo utilizzando solamente il barcode reader, ad esempio perchè si è da poco fatta la spesa e serve scansionare un gran numero di prodotti in successione.



Notare che la finestra fornisce la quantità totale del corrispondente prodotto nel database (in questo caso 1).

Qualora il prodotto inserito non fosse stato mai censito prima, viene visualizzato il relativo avviso:






Pulsante Preleva

Molto simile al precedente, questa volta la finestra ci consente di prendere un prodotto dalla dispensa per poterlo consumare, aggiornando automaticamente la quantità residua nel database.

Per esempio,  applicandolo al caso precedente del tonno che ora invece preleviamo dal magazzino


Poichè in questo caso avevamo un'unica confezione di tonno, la rimanenza a magazzino sarà nulla. 

Per questo motivo, un corrispondente messaggio di avviso viene visualizzato: 






Programma principale

Dettagli funzionamento

Vediamo le linee guida utilizzate per il coding:
  • Interfaccia grafica a finestre, in modo da essere semplice e intuitivo per i membri della famiglia che saranno gli utilizzatori finali  
  • Pulsanti per scelta delle operazioni di "magazzino" principali:
    • pulsante INSERISCI: si apre la finestra per imputare il barcode relativo al prodotto acquistato e che entra a far parte del magazzino. Il programma dovrà sommare il numero di nuovi prodotti alla quantità già presente a sistema. 
    • pulsante PRELEVA: si apre la finestra per imputare il barcode relativo al prodotto che si intende prendere dal magazzino per consumarlo. Il programma dovrà detrarre il numero di prodotti prelevati dalla quantità già presente a sistema.   
    • pulsante NUOVO PRODOTTO: si apre la finestra per definire i prodotti del nostro magazzino in termini di codice a barre, nome prodotto e categoria. La categoria è un attributo che possiamo usare a piacimento per raggruppare la tipologia di prodotti e consentirne selezioni appropriate. Spesso infatti ci interessa sapere quanta pasta (categoria) abbiamo piuttosto del tipo di pasta. 
  • programmazione ad oggetti. Consente di evitare il codice ridondante, favorirne il riuso, tramite classi e metodi specializzati sulle funzionalità specifiche. Nel nostro contesto la programmazione ad oggetti è stata usata per queste funzioni:
    • Prodotti nel DB (classe). Il metodo sviluppato è quello necessario per implementare l'accesso al DB (selezione, inserimento, aggiornamento)
    • Modulo generico per inserimento dati (classe). Il relativo metodo fa in modo che questo si trasformi in maniera appropriata nel modulo per inserimento, prelievo della merce e definizione del nuovo prodotto.
    • Finestra pop-up per avvisi di qualunque genere (classe). Il metodo fa in modo che l'avviso sia trattato come messaggio di errore, avviso, successo.  
Per la parte grafica si è utilizzato il modulo standard Tkinter di Python che consente molto bene l'implementazione delle finestre, dei pulsanti e degli altri oggetti di una finestra chiamati widget

Inoltre questo modulo è cross piattaforma ed è l'ideale come nel mio caso in cui voglio far funzionare il programma allo stesso modo sia su un PC window che su un server Raspberry Pi con Raspbian.  

Vediamo più nel dettaglio come si compone il programma. Di seguito è un ordinamento logico per descrivere la sequenza delle operazioni che non rispecchia l'ordine effettivo del codice.

Ho inserito inoltre dei #commenti aggiuntivi rispetto a quelli esistenti nel programma.

Finestra principale




Ecco come viene creata la finestra principale:

# Creo una nuova finestra dal titolo "Gestione magazzino"
window = tk.Tk()        # crea la finestra con la label di default (tk)
window.title("Gestione magazzino") # viene assegnato il titolo alla finestra

E' utile organizzare la finestra in frame che serviranno come contenitori per i successivi widget. 
In questo caso associamo alla finestra un unico frame:

# Creo un nuovo frame per i pulsanti principali
frm_pulsanti_princ = tk.Frame()

Ora è il momento di "impacchettare" il frame all'interno della finestra:

# Impacchetta il frame dentro la finestra
frm_pulsanti_princ.pack(padx=100, pady=100) #con il padding il frame viene centrato nella finestra (100 pixel di spaziatura orizzontale e verticale)

Creiamo ora i tre pulsanti INSERISCI, PRELEVA e NUOVO PRODOTTO:

# Crea il pulsante "Inserisci" e lo impacchetta  
# sul lato sinistro del frame `frm_pulsanti_princ`

btn_inserisci = tk.Button(
    master=frm_pulsanti_princ,
    text="Inserisci",
    width=10,
    height=1,
    bg="white", #sfondo bianco
    fg="black"#testo nero
    font=("Verdana", 32),
    command=lambda: inserisci(), #premendo il pulsante viene eseguita la funzione inserisci()
    relief=tk.RAISED #il pulsante risulta in rilievo quando non è premuto
    )
btn_inserisci.pack(side=tk.LEFT, padx=5) #5 pixel di spaziatura dal bordo sinistro. Ovvero è il primo pulsante da sinistra a destra.

# Crea il pulsante "Preleva" e lo impacchetta
# sul lato sinistro del frame `frm_pulsanti_princ`

btn_preleva = tk.Button(
    master=frm_pulsanti_princ,
    text="Preleva",
    width=10,
    height=1,
    bg="white",
    fg="black",
    font=("Verdana", 32),
    command=lambda: preleva(),
    relief=tk.RAISED
    ) 
btn_preleva.pack(side=tk.LEFT, padx=20) #pulsante un po' più a destra del precedente

# Crea il pulsante "Nuovo prodotto" e lo impacchetta
# sul lato destro del frame `frm_pulsanti_princ`

btn_nuovop = tk.Button(
    master=frm_pulsanti_princ,
    text="Nuovo prodotto",
    width=16,
    height=1,
    bg="white",
    fg="black",
    font=("Verdana", 20),
    relief=tk.RAISED,
    command=lambda: nuovo_prod()
    ) 
btn_nuovop.pack(side=tk.RIGHT, padx=50, ipady = 10)

Funzioni Lambda 

Come si può vedere ho utilizzato in questo caso funzioni lambda che vengono eseguite all'occorrenza e quindi più flessibili (la funzione "normale" avrebbe la necessità di essere subito letta dall'interprete python e potrebbe avere un comportamento anomalo nel caso tutte le condizioni non fossero soddisfatte prima del richiamo effettivo).

Di seguito le funzioni lambda richiamate:

def nuovo_prod():

    btn_nuovop["relief"]=tk.SUNKEN #dà l'effetto di un pulsante schiacciato
    btn_inserisci["relief"]=tk.RAISED #dà l'effetto di un pulsante sollevato
    btn_preleva["relief"]=tk.RAISED
    btn_nuovop["state"]="disabled" # il pulsante viene disabilitato subito in modo da non poter essere schiacciato nuovamente
    btn_inserisci["state"]="normal" # il pulsante continua ad essere attivo e "schiacciabile"
    btn_preleva["state"]="normal" # il pulsante continua ad essere attivo e "schiacciabile"
    Modulo_generico.modulo(Modulo_generico("Nuovo prodotto")) # richiama la classe Modulo_generico spiegata di seguito  
    
def inserisci():

    btn_nuovop["relief"]=tk.RAISED
    btn_inserisci["relief"]=tk.SUNKEN
    btn_preleva["relief"]=tk.RAISED
    btn_nuovop["state"]="normal"
    btn_inserisci["state"]="disabled"
    btn_preleva["state"]="normal"
    Modulo_generico.modulo(Modulo_generico("Inserisci"))

def preleva():

    btn_nuovop["relief"]=tk.RAISED
    btn_inserisci["relief"]=tk.RAISED
    btn_preleva["relief"]=tk.SUNKEN
    btn_nuovop["state"]="normal"
    btn_inserisci["state"]="normal"
    btn_preleva["state"]="disabled"
    Modulo_generico.modulo(Modulo_generico("Preleva"))


Come visto, ciascun pulsante richiama un modulo generico "istanziato" sulla funzione specifica. Per questo risulta comodo usare una classe generica che consente di sviluppare il relativo codice una sola volta:

Modulo generico per imputazione

La logica è implementata nel metodo corrispondente (Modulo_generico.modulo). L'istanza è inizializzata passando l'attributo titolo:

class Modulo_generico:
    def __init__(self, titolo):
        self.titolo = titolo

    def modulo(self):

Di seguito una rappresentazione grafica del modulo. 











Quello in alto è la vista grafica dell'istanza della classe Modulo generico corrispondente al pulsante Nuovo Prodotto.
I moduli Inserisci e Preleva avranno in comune con il modulo precedente sicuramente il Barcode, ma necessiteranno di una nuova label e entry corrispondenti alla quantità che vogliamo inserire/prelevare:







La finestra sarà posizionata in alto a sinistra

winpro = tk.Toplevel() # questo comando è FONDAMENTALE. Fa sì che la finestra sia sempre in foreground (davanti alle altre) pronta a ricevere i dati dal barcode reader
winpro.title(self.titolo) #titolo è l'unico attributo passato alla classe
winpro.geometry('+1+0')

Viene creato il frame per ospitare le widget per label (etichette) e le entry (dove poter imputare i dati):

# Crea un nuovo frame `frm_modulo` per contenere le widget 
# Label e Entry per imputare le descrizioni dei prodotti (barcode, nome prodotto, categoria).
frm_modulo = tk.Frame(master=winpro, relief=tk.SUNKEN, borderwidth=3)
# Impacchetta il frame dentro la finestra
frm_modulo.pack()

Ora iniziamo a definire i widget:

# Crea le Label e Entry widget per "Barcode"
lbl_barcode = tk.Label(master=frm_modulo, # è semplicemente l'etichetta con la scritta "Barcode ="
                       text="Barcode: ",
                       bg="white",
                       fg="black",
                       font=("Verdana", 18)
                       )
ent_barcode = tk.Entry(master=frm_modulo, # crea lo spazio "digitabile" dopo poter imputare il barcode
                       bg="white",
                       fg="black",
                       font=("Verdana", 18),
                       width=25)
# Si usa il grid geometry manager per definirne le posizioni 
# Il widget Label va nella prima colonna, Entry nella seconda
# entrambi nella prima riga della griglia
lbl_barcode.grid(row=0, column=0, sticky="e") #l'etichetta viene posizionata più a destra possibile: "e" vuol dire "east" 
ent_barcode.focus_set() #il cursore lampeggia dentro il campo del barcode. Questo comando è FONDAMENTALE per il nostro programma in quanto lascia sempre in primo piano la entry dove scrivere il barcode senza richiedere intervento umano per schiacciare sulla finestra
ent_barcode.grid(row=0, column=1)

Allo stesso modo viene definito il widget per la quantità:


# Crea le Label e Entry widget per "Quantità"
        lbl_quantita = tk.Label(master=frm_modulo,
                               text="Quantità: ",
                               bg="white",
                               fg="black",
                               font=("Verdana", 18)
                               )
        ent_quantita = tk.Entry(master=frm_modulo,
                               bg="white",
                               fg="black",
                               font=("Verdana", 18),
                               width=25)
        ent_quantita.insert(tk.END, 1) #inserisce il valore di default pari a '1'

# Si usa il grid geometry manager per definirne le posizioni 
# Il widget Label va nella prima colonna, Entry nella seconda
# entrambi nella seconda riga della griglia

# notare il posizionamento in riga 1 colonne 0 e 1 di label e entry per quantità. Questo sarà sovrascritto dal modulo successivo ("Nuovo prodotto") che non avrà bisogno della quantità (quantità = 0 in quel caso):
        lbl_quantita.grid(row=1, column=0, sticky="e") #l'etichetta viene posizionata più a destra possibile
        ent_quantita.grid(row=1, column=1) 

Di seguito vediamo cosa accade per il modulo "Nuovo prodotto" che avrà bisogno di due righe nuove nella finestra, mentre non avrà bisogno della quantità:

if self.titolo == "Nuovo prodotto":
        
            # Crea le Label e Entry widget per "Nome prodotto"
            lbl_prodotto = tk.Label(master=frm_modulo,
                                    text="Nome prodotto: ",
                                    bg="white",
                                    fg="black",
                                    font=("Verdana", 18)
                                    )
            ent_prodotto = tk.Entry(master=frm_modulo,
                                    bg="white",
                                    fg="black",
                                    font=("Verdana", 18),
                                    width=25)
            # i relativi widget vengono posizionati sulla seconda riga
            lbl_prodotto.grid(row=1, column=0, sticky="e")  #in questo modo la relativa label sovrascrive quella della quantità
            ent_prodotto.grid(row=1, column=1) #in questo modo la relativa entry sovrascrive quella della quantità

            # Crea le Label e Entry widget per "Categoria"
            lbl_categoria = tk.Label(master=frm_modulo,
                                     text="Categoria: ",
                                     bg="white",
                                     fg="black",
                                     font=("Verdana", 18)
                                     )
            ent_categoria = tk.Entry(master=frm_modulo,
                                     bg="white",
                                     fg="black",
                                     font=("Verdana", 18),
                                     width=25)
            # i relativi widget vengono posizionati sulla terza riga
            lbl_categoria.grid(row=2, column=0, sticky="e")
            ent_categoria.grid(row=2, column=1)
        
Qui invece creiamo il secondo frame, quello più in basso che contiene i pulsanti salva e pulisci.

        # Crea `frm_pulsanti` per contenere 
        # i pulsanti Salva e Pulisci. Questo frame riempie
        # l'intera finestra orizzontalmente con
        # 5 pixels di padding interno orizzontale e verticale.
        frm_pulsanti = tk.Frame(master=winpro, relief=tk.RAISED, borderwidth=3)
        frm_pulsanti.pack(fill=tk.X, ipadx=5, ipady=5) #con fill la finestra diventa responsiva orizzontalmente (X) 

        # Crea il pulsante "Salva" e lo impacchetta  
        # a destra del frame `frm_pulsanti`
        btn_salva = tk.Button(master=frm_pulsanti,
                              text="Salva",
                              bg="white",
                              fg="black",
                              font=("Verdana", 18),
                              command=salva)
        btn_salva.pack(side=tk.RIGHT, padx=10, ipadx=10)

        # Crea il pulsante "Pulisci" e lo impacchetta
        # sul lato destro del frame `frm_pulsanti`
        btn_pulisci = tk.Button(master=frm_pulsanti,
                                text="Pulisci",
                                bg="white",
                                fg="black",
                                font=("Verdana", 18),
                                command=pulisci) 
        btn_pulisci.pack(side=tk.RIGHT, ipadx=10)

Funzioni salva e pulisci

Come si può vedere all'interno di quest'ultimo blocco, vengono richiamate le funzioni salva e pulisci. Queste funzioni vengono definite nella parte iniziale del metodo modulo(self)
Iniziamo a vedere la funzione pulisci:

def pulisci():
            print("pulito") #è solo un log
            ent_barcode.delete(0, tk.END)
            if self.titolo =="Inserisci" or self.titolo == "Preleva":
                ent_quantita.delete(0, tk.END) #cancella il testo in entry dalla posizione iniziale fino alla posizione finale
                ent_quantita.insert(tk.END, 1) #reinserisce il valore di default pari a '1'
                
            if self.titolo=="Nuovo prodotto":
                ent_prodotto.delete(0, tk.END)
                ent_categoria.delete(0, tk.END)
                
            ent_barcode.focus_set() #il cursore ritorna a lampeggiare dentro il campo del barcode.


La funzione salva è più complessa perchè al suo interno richiama altre due classi, quella per aggiornamento/lettura del DB e quella per la generazione di finestre di avviso.

Per ora diciamo che il metodo DB.command per accesso al DB accetta questi attributi:

  • barcode, prodotto, categoria, quantita che sono quelli corrispondenti a ciascuna riga del DB
  • command, ovvero il tipo di accesso che si vuole fare al db (selezione, aggiornamento o inserimento nuova riga)

Per quanto riguarda invece la classe winavv, il metodo winres si occupa di stampare a video il messaggio di avviso passato. Il metodo accetta in ingresso anche altri valori con cui si possono customizzare alcune impostazioni della finestra.

Vedremo in seguito ulteriori dettagli su queste due classi. Per ora torniamo alla descrizione della funzione salva():

def salva():
            print("salvato")
            barcode = ent_barcode.get() # preleva il contenuto dal campo barcode della finestra
            if self.titolo=="Nuovo prodotto":
                prodotto = ent_prodotto.get()# preleva il contenuto dal campo prodotto
                categoria = ent_categoria.get() # preleva il contenuto dal campo categoria
                quantita = 0 # imposta il valore della quantità a zero in quanto ci basta solo la definizione del prodotto
                command = "ins" #parametro per inserire (ins) i valori precedenti nel DB nel caso di Nuovo prodotto
            if self.titolo =="Inserisci" or self.titolo == "Preleva":
                command="sel" # in questo caso devo poter selezionare la quantita attuale di un prodotto per aggiungere / togliere quanto necessario
                delta=int(ent_quantita.get()) #il valore immesso nel campo quantità (default 1) convertito in intero
                try:
                    (prodotto, categoria, quantita) = DB.command(DB(barcode,"", "", "", command)) #risultato della selezione per il barcode passato
                    print(prodotto)                   
                        
                except (Exception) as error:
                    print(error)
                    return(error)
                

                if self.titolo=="Inserisci":
                    quantita=quantita + delta #quantità aggiornata
                    command="upd"
                    
                if self.titolo=="Preleva":
                    quantita=quantita - delta #quantità aggiornata
                    print(quantita)
                    if quantita==0:
                        avviso="Attenzione! Ultimo prodotto {}. Inserirlo in lista della spesa!".format(prodotto)
                        print(avviso)
                        winavv.winres(winavv("AVVISO", avviso, "red", "white", 50, 10000)) #viene stampato a video che si tratta dell'ultimo prodotto. 10000 sta per 10 secondi (poi sparisce)
                    command="upd"
            if prodotto != "null": #null è ritornato dal metodo di accesso al DB nel caso di prodotto inesistente
                print(prodotto)                
                DB.command(DB(barcode, prodotto, categoria, quantita, command)) #aggiornamento del DB con le nuove quantità
            pulisci() #viene richiamata la funzione per pulire il modulo di inserimento

Autosave

Nella parte finale del metodo Modulo della classe Modulo_generico, è richiamata questa semplice ma importante funzione.

In pratica vogliamo che la finestra, sebbene abbia il pulsante salva eseguibile manualmente, abbia la possibilità di autosalvare il contenuto dei campi.

Questo perchè vogliamo che basti la scansione del barcode per far acquisire il prodotto preso dal magazzino, soprattutto in settimana quando si è di corsa e non si ha tempo di utilizzare il mouse o la tastiera...

        def autosave():
            
            
            if self.titolo =="Inserisci" or self.titolo == "Preleva":
                #print("autosave")
                ent_barcode.focus_set() 
                barcode = ent_barcode.get()
                if barcode != "": #la funzione salva() si attiva solo quando è imputato qualcosa
                    print("barcode {} salvato automaticamente".format(barcode)) #questo è un log
                    salva()           
            
            winpro.after(6000, lambda: autosave())  #ogni 6 secondi la funzione viene eseguita (loop)
            
            
        ##############        
        #autosave()
        winpro.after(6000, lambda: autosave()) #dopo 6 secondi la funzione lambda autosave() è eseguita        
        ##############




Finestre di avviso

Vediamo più nel dettaglio la classe winavv per l'esecuzione delle finestre di avviso.

La classe accetta in input le caratteristiche essenziali che intendevo pilotare (titolo della finestra, testo del messaggio, colore di sfondo e del carattere, padding verticale, scadenza): 

class winavv:
    
    def __init__(self, titolo, testo, bg,fg,pady,wafter):
        self.titolo=titolo
        self.testo=testo
        self.bg=bg
        self.fg=fg
        self.pady=pady
        self.wafter=wafter

    def winres(self):
        win_avv = tk.Tk()        # crea la finestra con la label di default (tk)
        win_avv.title(self.titolo)
        win_avv.geometry('+150+450')
        frm_avv = tk.Frame(master=win_avv, relief=tk.RAISED, borderwidth=3)
        lbl_avv = tk.Label(master=frm_avv,
                           text="{}".format(self.testo),
                           bg=self.bg,
                           fg=self.fg,
                           font=("Verdana", 18)
                           )
        lbl_avv.grid(row=0, column=0, sticky="e") #l'etichetta viene posizionata più a destra possibile
        # Impacchetta il frame dentro la finestra
        frm_avv.pack(fill=tk.X, padx=50, pady=self.pady) #con fill la finestra diventa responsiva orizzontalmente (X)
        win_avv.after(self.wafter, lambda: win_avv.destroy()) # Chiude la finestra dopo 3 secondi

Gestione delle modifiche al database

Vediamo ora nello specifico la classe DB(). Prerequisito è l'installazione del modulo psycopg2, ovvero dell'adapter PostgreSQL di Python di cui esistono diverse spiegazioni in rete, sia per Windows che per linux.

Sebbene il DB sia installato sul server Raspberry, questo potrà essere acceduto da qualsiasi PC nella rete famigliare. Basta specificare il parametro host.  

A questo punto il programma Python potrà essere eseguito dove si vuole e il DB risulterà aggiornato ugualmente.

class DB():

    def __init__(self, barcode, prodotto, categoria, quantita, command):
        self.barcode = barcode
        self.prodotto = prodotto
        self.categoria = categoria
        self.quantita = quantita
        self.command = command
 
    def command(self):
        try:
            conn = psycopg2.connect(
                host = "raspberrypi.lan", # inserisci l'indirizzo del server Raspberry Pi dove è attivo il DB (meglio usare un nome DNS piuttosto dell'indirizzo IP che nella rete di casa potrebbe essere dinamico e cambiare al riavvio
                database = "postgres",
                user = "postgres",
                password = "<inserisci la password specifica>")
            cur = conn.cursor()
            if self.command=="ins":
                print("ins")
                command_text = "insert into products values ('{}','{}','{}','0');".format(self.barcode, self.prodotto, self.categoria)
            if self.command=="upd":
                print("upd")
                command_text = "update products set quantity = '{}' where barcode = '{}';".format(self.quantita, self.barcode)
            if self.command=="sel":
                command_text = "select * from products where barcode = '{}';".format(self.barcode)
            cur.execute(command_text)
            print(command_text)
            if self.command=="sel":
                record = cur.fetchone()
                print(record)
                if record == None: #è il caso della selezione per aggiornare un prodotto non esistente                    
                    avviso="Attenzione! Prodotto {} non esistente. Usa il modulo 'Nuovo prodotto' per definirlo".format(self.prodotto)
                    print("avviso")
                    winavv.winres(winavv("AVVISO", avviso, "yellow", "blue", 50, 10000))
                    return["null", "null", 0]
                prodotto = record[1]
                categoria = record[2]
                quantita=int(record[3])
                return[prodotto, categoria, quantita]
            conn.commit()
            avviso="Attività sul prodotto '{}' eseguita con successo! Quantità totale: {}".format(self.prodotto, self.quantita)
            winavv.winres(winavv("OK", avviso, "white", "black", 100, 3000))
            
        except (Exception, psycopg2.DatabaseError) as error:
            print(error)
            # Creo una nuova finestra dal titolo "errore"
            
            winavv.winres(winavv("ERRORE", error, "white", "black", 100, 10000))

        finally:
            if conn is not None:
                conn.close()

Backup del Database

Questa parte è facoltativa, ma fortemente raccomandata per avere la possibilità di ripristinare il database ad uno stato precedente, nel caso ci sia stato un problema HW o (soprattutto) un problema logico (es. qualcuno per errore ha cancellato dei dati nella tabella).  

Normalmente affinchè la copia di backup sia utilizzabile, occorre spegnere il DB prima di effettuarla.
Come dicevo PostgreSQL permette di fare anche il cosiddetto backup online, ovvero senza necessità di spegnimento.
Inoltre in linux esiste l'utility crontab che permette di schedularlo periodicamente all'orario voluto.
Come unità target per il backup, meglio usare un hard disk esterno al Rasperry Pi collegato con interfaccia USB. 

Occorre impostare il db in modo che sia abilitata la registrazione delle modifiche. Tecnicamente questo equivale ad abilitare il WAL (Write-Ahead Log) in modo che il DB effettui il log di tutte le modifiche.

Il backup effettuato più tutti i file dei log generati saranno necessari per riportare il db allo stato pre failure.

In PostgreSQL versione 11, occorre impostare nel file di configurazione i parametri wal_level e archive_mode come visibile di seguito:








Inoltre è fondamentale salvare i log del database in archive file, ovvero individuare una directory dove effettuare questo salvataggio tramite il parametro archive_command


Ovviamente il path indicato va sostituito e adattato alla vostra situazione.

Il modo più semplice per fare il backup è tramite il tool pg_basebackup

pg_basebackup -D /media/pi/Elements/postgres/backup/backupdir -F t -X s -Z 9 -P -v

Le opzioni usate sono:

-D directory di destinazione
-F t formato del file tar
-X s metodo stream (durante il backup vengono anche salvati gli archive log con un processo parallelo)
-Z 9 compressione gzip 9 (il livello di compressione dei corrispondenti file gzip varia da 0 a 9)
-P abilita l'invio delle informazioni di progress (stima dello stato di completamento del backup)
-v abilita il verbose (ulteriori informazioni visibili nel log del backup)

Ulteriori informazioni sono disponibili nella documentazione ufficiale.

In crontab il backup è schedulato settimanalmente:




Contenuto dello script bck_exe:

#!/bin/bash
pg_basebackup -D /media/pi/Elements/postgres/backup/backupdir/$(date +"%d-%m-%Y") -F t -X s -Z 9 -P -v

Viene dunque creata una sottodirectory di destinazione del backup dinamicamente con all'interno la data del backup:



Contenuto della directory dell'ultimo backup:



Conclusioni

Tutto quello riportato qui è stato effettivamente implementato. 

Ovviamente l'obiettivo principale è che continui ad essere utilizzato, ovvero che sia semplice e non complicato.

Sicuramente ci sono aspetti da migliorare, però devo dire che è diventato ora naturale passare davanti alla tv a scansionare il barcode prima di portare il prodotto in cucina...

Ogni tanto servirà fare un inventario per "martellare" qualche quantità non allineatissima al reale, però devo dire che è una comodità con due select capire cosa serve comprare nella prossima spesa... 






  




Commenti

Posta un commento

Post più popolari