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 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.
A questo punto si può affinare la query per verificare che ci siano pochi prodotti all'interno di una determinata categoria (es.: biscotti):
Poichè in questo caso avevamo un'unica confezione di tonno, la rimanenza a magazzino sarà nulla.
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
- Installazione e spiegazione dei componenti
- Programma principale - dettagli di funzionamento
- Linee guida
- Finestra principale
- Funzioni lambda
- Modulo imputazione
- Finestre di avviso
- Gestione modifiche al database
- Backup del database
- Conclusioni
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 reader. Per 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.
- Video NiktorTheNat (infarinatura di base),
- Real Python (spiega bene i concetti del modulo Tkinter utilizzato),
- Programmare in Python (ottimo per spiegare la programmazione ad oggetti)
- 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.
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.
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
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:
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.
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...
Complimenti
RispondiEliminaGrazie!
Elimina