832 lines
46 KiB
Python
832 lines
46 KiB
Python
# Tkinter
|
||
from tkinter import IntVar, Checkbutton, LabelFrame, PhotoImage, Scrollbar, Listbox, Entry, Button, Label, Frame, Tk, Toplevel
|
||
from tkinter.ttk import Combobox, Separator
|
||
from tkinter.messagebox import showerror, showinfo, showwarning, askyesno
|
||
from tkinter.filedialog import askopenfile
|
||
# Regex
|
||
from re import sub
|
||
# Date
|
||
from datetime import date
|
||
|
||
# Import des fichiers pour gérer la base de donnée
|
||
from users import Utilisateurs
|
||
from stock import Stock
|
||
|
||
class GesMag:
|
||
"""Programme de Gestion d'une caise de magasin."""
|
||
def __init__(self, presentation: bool = False) -> None:
|
||
"""Instancie quelques variables pour plus de clareté."""
|
||
Utilisateurs().creationTable(presentation) # on créer la table utilisateurs si elle n'existe pas déjà
|
||
Stock().creationTable(presentation) # on créer la table du stock si elle n'existe pas déjà
|
||
|
||
self.nomApp = "GesMag" # nom de l'application
|
||
self.parent = Tk() # fenêtre affiché à l'utilisateur
|
||
self.parent.resizable(False, False) # empêche la fenêtre d'être redimensionnée
|
||
self.f = Frame(self.parent) # `Frame` "principale" affiché à l'écran
|
||
self.tableau = Frame() # `Frame` qui va afficher le tableau des éléments présents dans le stock
|
||
self.imagesStock = [] # liste qui va contenir nos images pour l'affichage du stock
|
||
self.dossierImage = PhotoImage(file = "img/dossier.gif") # image pour l'icone de selection
|
||
self.panierAffichage = Frame() # `Frame` qui va afficher le panier
|
||
self.panier = [] # liste des éléments "dans le panier"
|
||
|
||
def demarrer(self) -> None:
|
||
"""Lance le programme GesMag."""
|
||
self.font = ("Comfortaa", 14) # police par défaut
|
||
|
||
self._interfaceConnexion() # on créer la variable `self.f` qui est la frame a affiché
|
||
self.f.grid() # on affiche la frame
|
||
|
||
self.parent.mainloop() # on affiche la fenêtre
|
||
|
||
def motDePasseCorrect(self, motDPasse: str) -> tuple:
|
||
"""Détermine si un mot de passe suit la politique du programme ou non."""
|
||
if len(motDPasse) == 0: # si le champs est vide
|
||
return (False, "Mot de passe incorrect.")
|
||
if len(motDPasse) < 8: # si le mot de passe est plus petit que 8 caractères
|
||
return (False, "Un mot de passe doit faire 8 caractères minimum.")
|
||
"""
|
||
- Pour le regex, la fonction `sub` élimine tout ce qui est donné en fonction
|
||
du pattern renseigné, alors si la fonction `sub` renvoie pas exactement
|
||
la même chaîne de charactère alors c'est qu'il y avait un charactère interdit.
|
||
- J'utilises pas `match` parce que je suis plus à l'aise avec `sub`.
|
||
"""
|
||
if not sub(r"[A-Z]", '', motDPasse) != motDPasse:
|
||
return (False, "Un mot de passe doit au moins contenir une lettre majuscule.")
|
||
if not sub(r"[a-z]", '', motDPasse) != motDPasse:
|
||
return (False, "Un mot de passe doit au moins contenir une lettre minuscule.")
|
||
if not sub(r" *?[^\w\s]+", '', motDPasse) != motDPasse:
|
||
return (False, "Un mot de passe doit au moins contenir un caractère spécial.")
|
||
|
||
return (True,) # si aucun des tests précédents n'est valide, alors le mot de passe est valide
|
||
|
||
def utilisateurCorrect(self, utilisateur: str) -> tuple:
|
||
"""Détermine si un nom d'utilisateur suit la politique du programme ou non."""
|
||
"""
|
||
Pour le nom d'utilisateur on vérifie si le champs n'est pas vide
|
||
et si il y a bien que des lettres et des chiffres.
|
||
"""
|
||
if len(utilisateur) == 0:
|
||
return (False, "Utilisateur incorrect.")
|
||
if sub(r" *?[^\w\s]+", '', utilisateur) != utilisateur:
|
||
return (False, "Un nom d'utilisateur ne doit pas contenir de caractère spécial.")
|
||
return (True,)
|
||
|
||
def nomCorrect(self, nom: str) -> bool:
|
||
"""Détermine si un nom suit la politique du programme ou non."""
|
||
if len(nom) == 0:
|
||
return False
|
||
if sub(r" *?[^\w\s]+", '', nom) != nom: # pas de caractères spéciaux dans un nom
|
||
return False
|
||
return True
|
||
|
||
def prenomCorrect(self, prenom: str) -> bool:
|
||
"""Détermine si un prénom suit la politique du programme ou non."""
|
||
if len(prenom) == 0:
|
||
return False
|
||
if sub(r" *?[^\w\s]+", '', prenom) != prenom: # pas de caractères spéciaux dans un prénom
|
||
return False
|
||
return True
|
||
|
||
def naissanceCorrect(self, naissance: str) -> bool:
|
||
"""Détermine si une date de naissance suit la politique du programme ou non."""
|
||
if len(naissance) == 0:
|
||
return False
|
||
# lien pour mieux comprendre ce qui se passe : https://www.debuggex.com/r/hSD-6BfSqDD1It5Z
|
||
if sub(r"[0-9]{4}\/(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])", '', naissance) != '':
|
||
return False
|
||
return True
|
||
|
||
def adresseCorrect(self, adresse: str) -> bool:
|
||
"""Détermine si une adresse suit la politique du programme ou non."""
|
||
if len(adresse) == 0:
|
||
return False
|
||
return True
|
||
|
||
def postalCorrect(self, code: str) -> bool:
|
||
"""Détermine si un code postal suit la politique du programme ou non."""
|
||
if len(code) == 0:
|
||
return False
|
||
if sub(r"\d{5}", '', code) != '':
|
||
return False
|
||
return True
|
||
|
||
def connexion(self, utilisateur: str, motDePasse: str):
|
||
"""Gère la connexion aux différentes interfaces de l'application."""
|
||
"""
|
||
Vérification nom d'utilisateur / mot de passe correctement entré
|
||
avec leurs fonctions respectives.
|
||
"""
|
||
pseudoOk = self.utilisateurCorrect(utilisateur)
|
||
if not pseudoOk[0]:
|
||
showerror("Erreur", pseudoOk[1])
|
||
return
|
||
mdpOk = self.motDePasseCorrect(motDePasse)
|
||
if not mdpOk[0]:
|
||
showerror("Erreur", mdpOk[1])
|
||
return
|
||
|
||
# Redirection vers la bonne interface
|
||
utilisateurBaseDeDonnee = Utilisateurs().verificationIdentifiants(utilisateur, motDePasse)
|
||
if utilisateurBaseDeDonnee[0] > 0:
|
||
if utilisateurBaseDeDonnee[1] == 0: # si le métier est "Manager"
|
||
self._interfaceManager(utilisateurBaseDeDonnee[0])
|
||
elif utilisateurBaseDeDonnee[1] == 1: # si le métier est "Caissier"
|
||
self._interfaceCaissier(utilisateurBaseDeDonnee[0])
|
||
else:
|
||
showerror("Erreur", "Une erreur est survenue : métier inconnue.")
|
||
else:
|
||
showerror("Erreur", "Utilisateur ou mot de passe incorrect.")
|
||
|
||
def dimensionsFenetre(self, fenetre, nouveauX: int, nouveauY: int):
|
||
"""Permet de changer les dimensions de la fenêtre parent et la place au centre de l'écran."""
|
||
largeur = fenetre.winfo_screenwidth()
|
||
hauteur = fenetre.winfo_screenheight()
|
||
|
||
x = (largeur // 2) - (nouveauX // 2)
|
||
y = (hauteur // 2) - (nouveauY // 2)
|
||
|
||
fenetre.geometry(f"{nouveauX}x{nouveauY}+{x}+{y}")
|
||
|
||
def _interfaceConnexion(self):
|
||
"""Affiche l'interface de connexion."""
|
||
# Paramètres de la fenêtre
|
||
self.dimensionsFenetre(self.parent, 400, 600)
|
||
self.parent.title(f"Fenêtre de connexion – {self.nomApp}")
|
||
|
||
# Suppresssion de la dernière Frame
|
||
self.f.destroy()
|
||
# Instanciation d'une nouvelle Frame, on va donc ajouter tout nos widgets à cet Frame
|
||
self.f = Frame(self.parent)
|
||
self.f.grid()
|
||
|
||
# Affichage des labels et boutons
|
||
tentativeDeConnexion = lambda _ = None: self.connexion(utilisateur.get(), motDpasse.get()) # lambda pour envoyer les informations entrés dans le formulaire
|
||
ecart = 80 # écart pour avoir un affichage centré
|
||
Label(self.f).grid(row=0, pady=50) # utilisé pour du padding (meilleur affichage)
|
||
|
||
Label(self.f, text="Utilisateur", font=self.font).grid(column=0, row=1, columnspan=2, padx=ecart - 20, pady=20, sticky='w')
|
||
utilisateur = Entry(self.f, font=self.font, width=18)
|
||
utilisateur.grid(column=1, row=2, columnspan=2, padx=ecart)
|
||
|
||
Label(self.f, text="Mot de passe", font=self.font).grid(column=0, row=3, columnspan=2, padx=ecart - 20, pady=20, sticky='w')
|
||
motDpasse = Entry(self.f, font=self.font, show='⁎', width=18)
|
||
motDpasse.grid(column=1, row=4, columnspan=2, padx=ecart)
|
||
motDpasse.bind("<Return>", tentativeDeConnexion)
|
||
|
||
def __afficherMDP(self):
|
||
"""Permet de gérer l'affichage du mot de passe dans le champs sur la page de connexion."""
|
||
if self.mdpVisible == False: # si mot de passe caché, alors on l'affiche
|
||
self.mdpVisible = True
|
||
motDpasse.config(show='')
|
||
bouttonAffichageMDP.config(font=("Arial", 10, "overstrike"))
|
||
|
||
else: # inversement
|
||
self.mdpVisible = False
|
||
motDpasse.config(show='⁎')
|
||
bouttonAffichageMDP.config(font=("Arial", 10))
|
||
|
||
bouttonAffichageMDP = Button(self.f, text='👁', command=lambda: __afficherMDP(self))
|
||
bouttonAffichageMDP.grid(column=2, row=4, columnspan=2)
|
||
self.mdpVisible = False
|
||
|
||
bouton = Button(self.f, text="Se connecter", font=self.font, command=tentativeDeConnexion)
|
||
bouton.grid(column=0, row=5, columnspan=3, padx=ecart, pady=20)
|
||
bouton.bind("<Return>", tentativeDeConnexion)
|
||
|
||
Button(self.f, text="Quitter", font=self.font, command=quit).grid(column=0, row=6, columnspan=4, pady=20)
|
||
|
||
self._interfaceCaissier(1) # DEBUG: affiche directement l'interface du Caissier
|
||
|
||
def _interfaceCaissier(self, id: int):
|
||
"""Affiche l'interface du caissier."""
|
||
caissier = Utilisateurs().recuperationUtilisateur(id=id)
|
||
self.parent.title(f"Caissier {caissier['nom']} {caissier['prenom']} – {self.nomApp}")
|
||
self.dimensionsFenetre(self.parent, 1100, 710)
|
||
|
||
# Suppresssion de la dernière Frame
|
||
self.f.destroy()
|
||
# Instanciation d'une nouvelle Frame, on va donc ajouter tout nos widgets à cet Frame
|
||
self.f = Frame(self.parent)
|
||
self.f.grid()
|
||
|
||
Label(self.f, text="Interface Caissier", font=(self.font[0], 20)).grid(column=0, row=0) # titre de l'interface
|
||
|
||
# Partie affichage du Stock
|
||
stock = LabelFrame(self.f, text="Stock")
|
||
stock.grid(column=0, row=1, sticky='n', padx=5)
|
||
|
||
# Variables pour les filtres du tableau
|
||
stockDisponibleVerif = IntVar(stock) # controle si on affiche que les éléments en stocks ou non
|
||
# Cache un certain type de produit
|
||
fruitsLegumesVerif = IntVar(stock)
|
||
boulangerieVerif = IntVar(stock)
|
||
boucheriePoissonnerieVerif = IntVar(stock)
|
||
entretienVerif = IntVar(stock)
|
||
|
||
def __formatPrix(prix: str) -> str:
|
||
return f"{float(prix):.2f} €".replace('.', ',')
|
||
|
||
def __affichageTableau(page: int = 1):
|
||
"""Fonction qui va actualiser le tableau avec une page donnée (par défaut affiche la première page)."""
|
||
# On supprime et refais la frame qui va stocker notre tableau
|
||
self.tableau.destroy()
|
||
self.tableau = Frame(stock)
|
||
self.tableau.grid(column=0, row=1, columnspan=7)
|
||
|
||
# Filtre pour le tableau
|
||
filtres = Frame(stock) # Morceau qui va contenir nos checkbutton
|
||
ecartFiltre = 10 # écart entre les champs des filtres
|
||
Label(filtres, text="Filtre", font=self.font).grid(column=0, row=0) # titre
|
||
Checkbutton(filtres, text="Stock disponible\nuniquement", variable=stockDisponibleVerif, command=__affichageTableau).grid(sticky='w', pady=ecartFiltre)
|
||
Checkbutton(filtres, text="Cacher les\nfruits & légumes", variable=fruitsLegumesVerif, command=__affichageTableau).grid(sticky='w', pady=ecartFiltre)
|
||
Checkbutton(filtres, text="Cacher les produits de\nla boulangerie", variable=boulangerieVerif, command=__affichageTableau).grid(sticky='w', pady=ecartFiltre)
|
||
Checkbutton(filtres, text="Cacher les produits de\nla boucherie\net poissonnerie", variable=boucheriePoissonnerieVerif, command=__affichageTableau).grid(sticky='w')
|
||
Checkbutton(filtres, text="Cacher les produits\nd'entretien", variable=entretienVerif, command=__affichageTableau).grid(sticky='w', pady=ecartFiltre)
|
||
filtres.grid(column=7, row=1, sticky='w')
|
||
|
||
stockListe = Stock().listeStocks() # stock récupéré de la base de données
|
||
|
||
def ___miseAJourPanier(element: dict, action: bool):
|
||
"""
|
||
Permet d'ajouter ou de retirer des éléments au panier
|
||
-> Action
|
||
-> Vrai : Ajout
|
||
-> Faux : Retire
|
||
"""
|
||
# On compte combien de fois l'élément est présent dans le panier
|
||
nombreDeFoisPresentDansLePanier = 0
|
||
index = None
|
||
for idx, elementDansLePanier in enumerate(self.panier):
|
||
if elementDansLePanier[0] == element:
|
||
index = idx # On met à jour l'index
|
||
nombreDeFoisPresentDansLePanier = elementDansLePanier[1]
|
||
break # on peut quitter la boucle car on a trouvé notre élément
|
||
|
||
# On vérifie que on peut encore l'ajouter/retirer
|
||
if nombreDeFoisPresentDansLePanier == 0 and not action: # pop-up seulement si on veut retirer un élément pas présent
|
||
showerror("Erreur", "Impossible de retirer cet élément au panier.\nNon présent dans le panier.")
|
||
return
|
||
if nombreDeFoisPresentDansLePanier >= element["quantite"] and action: # pop-up seulement si on veut en rajouter
|
||
showerror("Erreur", "Impossible de rajouter cet élément au panier.\nLimite excédée.")
|
||
return
|
||
|
||
if index != None: # on retire l'ancienne valeur du panier si déjà présente dans le panier
|
||
self.panier.pop(index)
|
||
else: # sinon on définie un index pour pouvoir ajouté la nouvelle valeur à la fin de la liste
|
||
index = len(self.panier)
|
||
|
||
# On change la valeur dans le panier
|
||
if action: # si on ajoute
|
||
nombreDeFoisPresentDansLePanier += 1
|
||
else: # si on retire
|
||
nombreDeFoisPresentDansLePanier -= 1
|
||
|
||
# On rajoute l'élément avec sa nouvelle quantité seulement s'il y en a
|
||
if nombreDeFoisPresentDansLePanier > 0:
|
||
self.panier.insert(index, (element, nombreDeFoisPresentDansLePanier))
|
||
|
||
___affichagePanier() # Met-à-jour le panier
|
||
|
||
for i in range(0, len(stockListe)): # on retire les éléments plus présent dans la liste
|
||
if stockDisponibleVerif.get() == 1 and stockListe[i]["quantite"] < 1:
|
||
stockListe[i] = None
|
||
elif fruitsLegumesVerif.get() == 1 and stockListe[i]["type"] == "fruits legumes":
|
||
stockListe[i] = None
|
||
elif boulangerieVerif.get() == 1 and stockListe[i]["type"] == "boulangerie":
|
||
stockListe[i] = None
|
||
elif boucheriePoissonnerieVerif.get() == 1 and stockListe[i]["type"] == "boucherie poissonnerie":
|
||
stockListe[i] = None
|
||
elif entretienVerif.get() == 1 and stockListe[i]["type"] == "entretien":
|
||
stockListe[i] = None
|
||
|
||
# Supprime toutes les valeurs `None` de la liste
|
||
stockListe = list(filter(None, stockListe))
|
||
|
||
ecart = 10 # écart entre les champs
|
||
|
||
elementsParPage = 10 # on définit combien d'élément une page peut afficher au maximum
|
||
pageMax = -(-len(stockListe) // elementsParPage) # on définit combien de page il y au maximum
|
||
|
||
if pageMax <= 1:
|
||
page = 1 # on force la page à être à 1 si il n'y a qu'une page, peut importe l'argument donnée à la fonction
|
||
|
||
limiteIndex = elementsParPage * page # on définit une limite pour ne pas afficher plus d'éléments qu'il n'en faut par page
|
||
if len(stockListe) > 0: # si stock non vide
|
||
# Définition des colonnes
|
||
Label(self.tableau, text="ID").grid(column=0, row=0, padx=ecart)
|
||
Label(self.tableau, text="Image").grid(column=1, row=0, padx=ecart)
|
||
Label(self.tableau, text="Type").grid(column=2, row=0, padx=ecart)
|
||
Label(self.tableau, text="Nom").grid(column=3, row=0, padx=ecart)
|
||
Label(self.tableau, text="Quantité").grid(column=4, row=0, padx=ecart)
|
||
Label(self.tableau, text="Prix unité").grid(column=5, row=0, padx=ecart)
|
||
Label(self.tableau, text="Action").grid(column=6, row=0, padx=ecart)
|
||
Separator(self.tableau).grid(column=0, row=0, columnspan=7, sticky="sew")
|
||
Separator(self.tableau).grid(column=0, row=0, columnspan=7, sticky="new")
|
||
for j in range(0, 8):
|
||
Separator(self.tableau, orient='vertical').grid(column=j, row=0, columnspan=2, sticky="nsw")
|
||
|
||
curseur = limiteIndex - elementsParPage # on commence à partir du curseur
|
||
i = 1 # on commence à 1 car il y a déjà le nom des colonnes en position 0
|
||
self.imagesStock = [] # on vide la liste si elle contient déjà des images
|
||
for element in stockListe[curseur:limiteIndex]: # on ignore les éléments avant le curseur et après la limite
|
||
Label(self.tableau, text=element["id"]).grid(column=0, row=i, padx=ecart)
|
||
|
||
"""
|
||
L'idée est que on a une liste `images` qui permet de stocker toutes nos images
|
||
(c'est une limitation de tkinter que de garder nos images en mémoire)
|
||
Une fois ajouté à la liste, on l'affiche dans notre Label
|
||
"""
|
||
if Stock().fichierExiste(element["image_url"]): # si l'image existe, utilisation de la fonction de `db.py`
|
||
self.imagesStock.append(PhotoImage(file = element["image_url"]))
|
||
else: # si l'image n'existe pas
|
||
self.imagesStock.append(PhotoImage(file = "img/defaut.gif")) # image par défaut
|
||
Label(self.tableau, image=self.imagesStock[i - 1]).grid(column=1, row=i, padx=ecart)
|
||
|
||
Label(self.tableau, text=element["type"].capitalize()).grid(column=2, row=i, padx=ecart)
|
||
Label(self.tableau, text=element["nom"].capitalize()).grid(column=3, row=i, padx=ecart)
|
||
Label(self.tableau, text=element["quantite"]).grid(column=4, row=i, padx=ecart)
|
||
Label(self.tableau, text=__formatPrix(element["prix"])).grid(column=5, row=i, padx=ecart)
|
||
# boutons d'actions pour le panier
|
||
Button(self.tableau, text='+', font=("Arial", 7, "bold"), command=lambda e = element: ___miseAJourPanier(e, True)).grid(column=6, row=i, sticky='n', padx=ecart)
|
||
Button(self.tableau, text='−', font=("Arial", 7, "bold"), command=lambda e = element: ___miseAJourPanier(e, False)).grid(column=6, row=i, sticky='s', pady=2)
|
||
for j in range(0, 8):
|
||
Separator(self.tableau, orient='vertical').grid(column=j, row=i, columnspan=2, sticky="nsw")
|
||
Separator(self.tableau).grid(column=j, row=i, columnspan=2, sticky="sew")
|
||
curseur += 1
|
||
i += 1
|
||
|
||
# Information sur la page actuelle
|
||
Label(self.tableau, text=f"Page {page}/{pageMax}").grid(column=2, row=i, columnspan=3)
|
||
|
||
# Boutons
|
||
precedent = Button(self.tableau, text="Page précédente", command=lambda: __affichageTableau(page - 1))
|
||
precedent.grid(column=0, row=i, columnspan=2, sticky='w', padx=ecart, pady=ecart)
|
||
suivant = Button(self.tableau, text="Page suivante", command=lambda: __affichageTableau(page + 1))
|
||
suivant.grid(column=5, row=i, columnspan=2, sticky='e', padx=ecart)
|
||
if page == 1: # si on est a la première page on désactive le boutton précédent
|
||
precedent.config(state="disabled")
|
||
if page == pageMax: # si on est a la dernière page on désactive le boutton suivant
|
||
suivant.config(state="disabled")
|
||
else:
|
||
Label(self.tableau, text="Il n'y a rien en stock\nEssayez de réduire les critères dans le filtre.").grid(column=0, row=0, columnspan=7)
|
||
|
||
__affichageTableau() # affichage du tableau
|
||
|
||
# Partie affichage du ticket de caisse
|
||
ecart = 10
|
||
ticket = LabelFrame(self.f, text="Ticket de caisse")
|
||
ticket.grid(column=1, row=1, sticky='n', padx=5)
|
||
|
||
Label(ticket, text=f"Date de vente : {date.today().strftime('%Y/%m/%d')}").grid(column=0, row=0, pady=ecart)
|
||
|
||
def ___affichagePanier():
|
||
"""Affiche le panier."""
|
||
self.panierAffichage.destroy()
|
||
self.panierAffichage = Frame(ticket)
|
||
self.panierAffichage.grid(column=0, row=1, pady=ecart)
|
||
elementsAchetes = Label(self.panierAffichage)
|
||
elementsAchetes.grid(column=0)
|
||
prixTotal = 0
|
||
compteurElements = 0
|
||
for element in self.panier:
|
||
Label(self.panierAffichage, text=f"[{element[0]['id']}] - {element[1]}x {element[0]['nom'].capitalize()}").grid(column=0)
|
||
prixTotal += (element[0]["prix"] * element[1]) # ajout du prix
|
||
compteurElements += element[1]
|
||
|
||
elementsAchetes.config(text=f"Éléments achetés ({compteurElements}) :")
|
||
|
||
Label(self.panierAffichage, text=f"Prix total : {__formatPrix(prixTotal)}").grid(column=0, pady=ecart)
|
||
|
||
___affichagePanier()
|
||
|
||
Button(ticket, text="Valider le\nticket de caisse", font=self.font).grid(column=0, pady=ecart)
|
||
|
||
#.grid(column=1, row=1, sticky='s')
|
||
|
||
# Partie ajout élément au stock
|
||
def __ajouterElementStock():
|
||
"""Ouvre une fenêtre qui permet d'ajouter un nouvel élément à la base de donnée."""
|
||
"""
|
||
L'enfant (`TopLevel`) dépend de la `Frame` et non du parent (`Tk`)
|
||
pour éviter de resté ouverte meme lorsque le caissier se déconnecte.
|
||
"""
|
||
enfant = Toplevel(self.f)
|
||
enfant.title(f"Ajouter un élément au stock – {self.nomApp}")
|
||
|
||
def ___verification():
|
||
"""Vérifie si les champs renseignées sont valides."""
|
||
"""
|
||
La variable `ok` sert à savoir si la vérification est passée
|
||
si elle vaut `True` alors tout est bon,
|
||
Par contre si elle vaut `False` alors il y a eu une erreur.
|
||
Les valeurs `Entry` qui ne sont pas passés seront dans
|
||
la liste `mauvaisChamps`.
|
||
"""
|
||
ok = True
|
||
mauvaisChamps = []
|
||
# vérification pour l'image, on utilise la fonction du fichier `db.py`
|
||
if Stock().fichierExiste(image.get()) == False:
|
||
ok = False
|
||
mauvaisChamps.append(image)
|
||
# vérification pour le type
|
||
if typeElement.get() not in Stock().listeTypes():
|
||
ok = False
|
||
# Pas de coloration orange si le type est mauvais parce que on ne peut pas changé la couleur de fond d'une ComboBox
|
||
# vérification pour le nom
|
||
def ___nomValide(nom: str) -> bool:
|
||
if len(nom) <= 0:
|
||
return False
|
||
if Stock().stockExistant(nom) == True:
|
||
return False
|
||
return True
|
||
if ___nomValide(nom.get()) == False:
|
||
ok = False
|
||
mauvaisChamps.append(nom)
|
||
# vérification pour la quantité
|
||
try:
|
||
int(quantite.get()) # conversion en int
|
||
except: # si la conversion a échoué
|
||
ok = False
|
||
mauvaisChamps.append(quantite)
|
||
# vérification pour le prix
|
||
try:
|
||
float(prix.get()) # conversion en float
|
||
except: # si la conversion a échoué
|
||
ok = False
|
||
mauvaisChamps.append(prix)
|
||
|
||
if ok == False:
|
||
"""
|
||
Tous les champs qui n'ont pas réunies les conditions nécéssaires
|
||
sont mis en orange pendant 3 secondes pour bien comprendre quelles champs
|
||
sont à modifié.
|
||
|
||
La fonction lambda `remettreCouleur` permet de remettre la couleur initial
|
||
après les 3 secondes.
|
||
"""
|
||
remettreCouleur = lambda widget, ancienneCouleur: widget.configure(bg=ancienneCouleur)
|
||
for champs in mauvaisChamps:
|
||
couleur = champs["background"] # couleur d'avant changement
|
||
champs.configure(bg="orange") # on change la couleur du champs en orange
|
||
# dans 3 secondes on fait : `remettreCouleur(champs, couleur)`
|
||
champs.after(3000, remettreCouleur, champs, couleur)
|
||
else:
|
||
"""
|
||
Tous les tests sont passés, on peut ajouter l'utilisateur à la base de donnée
|
||
Pas besoin de gérer les erreurs lors des casts car on a déjà vérifié que c'était bien les bons types avant
|
||
"""
|
||
Stock().ajoutStock(
|
||
typeElement.get(),
|
||
nom.get(),
|
||
int(quantite.get()),
|
||
float(prix.get()),
|
||
image.get()
|
||
)
|
||
__affichageTableau() # met à jour le tableau
|
||
|
||
# Champs de saisie
|
||
# Image
|
||
Label(enfant, text="Image :").grid(column=0, row=0, sticky='e')
|
||
image = Entry(enfant)
|
||
image.grid(column=1, row=0, sticky='w')
|
||
def ___selectionImage():
|
||
"""Fonction qui permet de choisir une image dans l'arborescence de fichiers de l'utilisateur."""
|
||
try:
|
||
chemin = askopenfile(title="Choisir une image", filetypes=[("Image GIF", ".gif")])
|
||
image.delete(0, "end")
|
||
image.insert(0, chemin.name)
|
||
except:
|
||
pass
|
||
|
||
Button(enfant, image=self.dossierImage, command=___selectionImage).grid(column=1, row=0, sticky='e')
|
||
# Type (ComboBox)
|
||
Label(enfant, text="Type :").grid(column=0, row=1, sticky='e')
|
||
typeElement = Combobox(enfant, values=Stock().listeTypes())
|
||
# typeElement.current(0) # valeur 0 par défaut
|
||
typeElement.grid(column=1, row=1, sticky='w')
|
||
# Nom
|
||
Label(enfant, text="Nom :").grid(column=0, row=2, sticky='e')
|
||
nom = Entry(enfant)
|
||
nom.grid(column=1, row=2, sticky='w')
|
||
# Quantité
|
||
Label(enfant, text="Quantité :").grid(column=0, row=3, sticky='e')
|
||
quantite = Entry(enfant)
|
||
quantite.grid(column=1, row=3, sticky='w')
|
||
# Prix à l'unité
|
||
Label(enfant, text="Prix à l'unité :").grid(column=0, row=4, sticky='e')
|
||
prix = Entry(enfant)
|
||
prix.grid(column=1, row=4, sticky='w')
|
||
|
||
def ___viderChamps():
|
||
"""Vide tout les champs de leur contenu"""
|
||
# On récupère toutes les `Entry` de la fenêtre et on change leur contenu
|
||
for champ in [widget for typeElement, widget in enfant.children.items() if "entry" in typeElement]:
|
||
champ.delete(0, "end")
|
||
champ.update()
|
||
|
||
# Boutons
|
||
Button(enfant, text="Valider", command=___verification).grid(column=0, row=8, columnspan=3, sticky='w')
|
||
Button(enfant, text="Vider les champs", command=___viderChamps).grid(column=0, row=8, columnspan=3)
|
||
Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=8, columnspan=3, sticky='e')
|
||
|
||
Button(self.f, text="Ajouter un élément\nau stock", font=self.font, command=__ajouterElementStock).grid(column=1, row=2)
|
||
|
||
# Boutton pour passer en mode manager si la personne est un manager
|
||
if caissier["metier"] == 0:
|
||
Button(self.f, text="Passer en mode Manager", font=self.font, command=lambda: self._interfaceManager(id)).grid(column=0, row=2)
|
||
|
||
|
||
def _interfaceManager(self, id: int):
|
||
"""Affiche l'interface du manager."""
|
||
manager = Utilisateurs().recuperationUtilisateur(id=id)
|
||
# Dans le cas où un utilisateur réussi à trouvé cette interface alors qu'il n'a pas le droit, il sera bloqué
|
||
if manager["metier"] != 0:
|
||
showerror("Erreur", "Vous ne pouvez pas accéder à cette interface.")
|
||
return
|
||
self.parent.title(f"Manager {manager['nom']} {manager['prenom']} – {self.nomApp}")
|
||
self.dimensionsFenetre(self.parent, 580, 310)
|
||
|
||
# Suppresssion de la dernière Frame
|
||
self.f.destroy()
|
||
# Instanciation d'une nouvelle Frame, on va donc ajouter tout nos widgets à cet Frame
|
||
self.f = Frame(self.parent)
|
||
self.f.grid()
|
||
|
||
Label(self.f, text="Interface Manager", font=(self.font[0], 20)).grid(column=0, row=0)
|
||
|
||
Button(self.f, text="Se déconnecter", font=self.font, command=self._interfaceConnexion).grid(column=1, row=0, padx=50)
|
||
|
||
Label(self.f).grid(row = 1, pady=10) # séparateur
|
||
|
||
def __ajouterUtilisateur(metier: int):
|
||
"""Permet de créer un nouvel utilisateur, manager (`metier = 0`) et caissier (`metier = 1`)."""
|
||
"""
|
||
L'enfant (`TopLevel`) dépend de la `Frame` et non du parent (`Tk`)
|
||
pour éviter de resté ouverte meme lorsque le manager se déconnecte.
|
||
"""
|
||
enfant = Toplevel(self.f)
|
||
enfant.title(f"Ajouter un {'manager' if metier == 0 else 'caissier'} – {self.nomApp}")
|
||
|
||
def ___verification():
|
||
"""Vérifie si les champs renseignées sont valides."""
|
||
"""
|
||
Les valeurs `Entry` qui ne sont pas passés seront dans
|
||
la liste `mauvaisChamps`.
|
||
Si la liste `mauvaisChamps` contient un élément alors un test n'est pas ok.
|
||
"""
|
||
mauvaisChamps = []
|
||
# vérification pour le nom d'utilisateur
|
||
if self.utilisateurCorrect(pseudo.get())[0] == False or Utilisateurs().utilisateurExistant(pseudo.get()) == True:
|
||
mauvaisChamps.append(pseudo)
|
||
# vérification pour le mot de passe
|
||
if self.motDePasseCorrect(passe.get())[0] == False:
|
||
mauvaisChamps.append(passe)
|
||
# vérification pour le nom
|
||
if self.nomCorrect(nom.get()) == False:
|
||
mauvaisChamps.append(nom)
|
||
# vérification pour le prénom
|
||
if self.prenomCorrect(prenom.get()) == False:
|
||
mauvaisChamps.append(prenom)
|
||
# vérification pour la date de naissance
|
||
if self.naissanceCorrect(naissance.get()) == False:
|
||
mauvaisChamps.append(naissance)
|
||
# vérification pour l'adresse
|
||
if self.adresseCorrect(adresse.get()) == False:
|
||
mauvaisChamps.append(adresse)
|
||
# vérification pour le code postal
|
||
if self.postalCorrect(postal.get()) == False:
|
||
mauvaisChamps.append(postal)
|
||
|
||
if len(mauvaisChamps) != 0:
|
||
"""
|
||
Tous les champs qui n'ont pas réunies les conditions nécéssaires
|
||
sont mis en orange pendant 3 secondes pour bien comprendre quelles champs
|
||
sont à modifié.
|
||
|
||
La fonction lambda `remettreCouleur` permet de remettre la couleur initial
|
||
après les 3 secondes.
|
||
"""
|
||
remettreCouleur = lambda widget, ancienneCouleur: widget.configure(bg=ancienneCouleur)
|
||
for champs in mauvaisChamps:
|
||
couleur = champs["background"] # couleur d'avant changement
|
||
champs.configure(bg="orange") # on change la couleur du champs en orange
|
||
# dans 3 secondes on fait : `remettreCouleur(champs, couleur)`
|
||
champs.after(3000, remettreCouleur, champs, couleur)
|
||
else:
|
||
# Tous les tests sont passés, on peut ajouter l'utilisateur à la base de donnée
|
||
Utilisateurs().ajoutUtilisateur(
|
||
pseudo.get(),
|
||
passe.get().strip(),
|
||
metier,
|
||
nom.get(),
|
||
prenom.get(),
|
||
naissance.get(),
|
||
adresse.get(),
|
||
int(postal.get()), # pas besoin de gérer d'erreur lors du cast car on a vérifié avant que c'était bien une suite de chiffre
|
||
)
|
||
__ajouterUtilisateursListe(listeUtilisateurs) # met à jour la liste
|
||
|
||
# Champs de saisie
|
||
# Nom d'utilisateurs
|
||
Label(enfant, text="Nom d'utilisateur :").grid(column=0, row=0, sticky='e')
|
||
Label(enfant, text="Pas de caractères spéciaux", font=("Arial", 10, "italic")).grid(column=2, row=0, sticky='w')
|
||
pseudo = Entry(enfant)
|
||
pseudo.grid(column=1, row=0, sticky='w')
|
||
# Mot de passe
|
||
Label(enfant, text="Mot de passe :").grid(column=0, row=1, sticky='e')
|
||
Label(enfant, text="1 majuscule, miniscule et caractère spécial minimum", font=("Arial", 10, "italic")).grid(column=2, row=1, sticky='w')
|
||
passe = Entry(enfant)
|
||
passe.grid(column=1, row=1, sticky='w')
|
||
# Nom
|
||
Label(enfant, text="Nom :").grid(column=0, row=2, sticky='e')
|
||
Label(enfant, text="Pas de caractères spéciaux", font=("Arial", 10, "italic")).grid(column=2, row=2, sticky='w')
|
||
nom = Entry(enfant)
|
||
nom.grid(column=1, row=2, sticky='w')
|
||
# Prénom
|
||
Label(enfant, text="Prénom :").grid(column=0, row=3, sticky='e')
|
||
Label(enfant, text="Pas de caractères spéciaux", font=("Arial", 10, "italic")).grid(column=2, row=3, sticky='w')
|
||
prenom = Entry(enfant)
|
||
prenom.grid(column=1, row=3, sticky='w')
|
||
# Date de naissance
|
||
Label(enfant, text="Date de naissance :").grid(column=0, row=4, sticky='e')
|
||
Label(enfant, text="Format : AAAA/MM/JJ", font=("Arial", 10, "italic")).grid(column=2, row=4, sticky='w')
|
||
naissance = Entry(enfant)
|
||
naissance.grid(column=1, row=4, sticky='w')
|
||
# Adresse
|
||
Label(enfant, text="Adresse").grid(column=0, row=5, sticky='e')
|
||
adresse = Entry(enfant)
|
||
adresse.grid(column=1, row=5, sticky='w')
|
||
# Code postal
|
||
Label(enfant, text="Code postal :").grid(column=0, row=6, sticky='e')
|
||
Label(enfant, text="5 chiffres", font=("Arial", 10, "italic")).grid(column=2, row=6, sticky='w')
|
||
postal = Entry(enfant)
|
||
postal.grid(column=1, row=6, sticky='w')
|
||
|
||
def ___viderChamps():
|
||
"""Vide tout les champs de leur contenu"""
|
||
# On récupère toutes les `Entry` de la fenêtre et on change leur contenu
|
||
for champ in [widget for typeElement, widget in enfant.children.items() if "entry" in typeElement]:
|
||
champ.delete(0, "end")
|
||
champ.update()
|
||
|
||
# Boutons
|
||
Button(enfant, text="Valider", command=___verification).grid(column=0, row=8, columnspan=3, sticky='w')
|
||
Button(enfant, text="Vider les champs", command=___viderChamps).grid(column=0, row=8, columnspan=3)
|
||
Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=8, columnspan=3, sticky='e')
|
||
|
||
def __retirerUtilisateur(metier: int):
|
||
"""Permet de supprimer un utilisateur existant, manager (`metier = 0`) et caissier (`metier = 1`)."""
|
||
enfant = Toplevel(self.f) # cf. l'explication dans `__ajouterUtilisateur`
|
||
enfant.title(f"Retirer un {'manager' if metier == 0 else 'caissier'} – {self.nomApp}")
|
||
|
||
# Liste des utilisateurs
|
||
Label(enfant, text=f"Liste des {'manager' if metier == 0 else 'caissier'}", font=self.font).grid(column=0, row=0) # titre
|
||
# On définit une barre pour pouvoir scroller dans la liste
|
||
scroll_retirer = Scrollbar(enfant)
|
||
scroll_retirer.grid(column=1, row=1, sticky="nse")
|
||
# On définit notre liste et on la lie à notre `Scrollbar`
|
||
listeUtilisateurs_retirer = Listbox(enfant, width=25, height=4, yscrollcommand=scroll_retirer.set)
|
||
scroll_retirer.config(command=listeUtilisateurs_retirer.yview) # scroll à la verticale dans notre liste
|
||
# On ajoute nos utilisateurs à notre liste
|
||
__ajouterUtilisateursListe(listeUtilisateurs_retirer, metier)
|
||
listeUtilisateurs_retirer.grid(column=0, row=1)
|
||
# On affiche l'utilisateur quand on double-clique dessus
|
||
|
||
def ___suppressionUtilisateur():
|
||
"""Supprime l'utilisateur actuellement sélectionné dans la liste"""
|
||
element = listeUtilisateurs_retirer.curselection()
|
||
if len(element) == 0: # si aucun élément n'est selectionné
|
||
showwarning("Attention", "Aucun utilisateur n'a été selectionné.")
|
||
else:
|
||
utilisateur = listeUtilisateurs_retirer.get(listeUtilisateurs_retirer.curselection()[0]).split('(')[0][:-1]
|
||
reponse = askyesno("Confirmation", f"Voulez vous supprimer {utilisateur} ?")
|
||
if reponse == True:
|
||
Utilisateurs().suppressionUtilisateurs(utilisateur)
|
||
__ajouterUtilisateursListe(listeUtilisateurs_retirer) # met à jour la liste dans la fenêtre de suppression
|
||
__ajouterUtilisateursListe(listeUtilisateurs) # met à jour la liste dans l'interface principale
|
||
showinfo("Information", f"Utilisateur {utilisateur} supprimé.")
|
||
|
||
# Boutons
|
||
Button(enfant, text="Supprimer", command=___suppressionUtilisateur).grid(column=0, row=8, columnspan=3, sticky='w')
|
||
Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=8, columnspan=3, sticky='e')
|
||
|
||
def __afficherInformationsUtilisateur(_):
|
||
"""Permet d'afficher les informations d'un utilisateur"""
|
||
element = listeUtilisateurs.curselection()
|
||
if len(element) == 0: # si aucun élément n'est selectionné
|
||
return
|
||
"""
|
||
On split le champs car dans la liste on affiche le métier entre
|
||
parenthèses et on doit donner que le nom d'utilisateur à
|
||
la fonction `recuperationUtilisateur`, aussi on retire le dernièr
|
||
charactère avec [:-1] car c'est un espace.
|
||
"""
|
||
utilisateur = Utilisateurs().recuperationUtilisateur(pseudo=listeUtilisateurs.get(element[0]).split('(')[0][:-1])
|
||
enfant = Toplevel(self.f) # cf. l'explication dans `__ajouterUtilisateur`
|
||
enfant.title(f"{utilisateur['nom']} {utilisateur['prenom']} – {self.nomApp}")
|
||
|
||
# Informations sur l'utilisateur
|
||
frameInfos = LabelFrame(enfant, text="Informations utilisateur", font=self.font)
|
||
frameInfos.grid(column=0, row=0, sticky='n', padx=5)
|
||
utilisateur["metier"] = "Manager" if utilisateur["metier"] == 0 else "Caissier"
|
||
del utilisateur["passe"] # le manager ne doit pas connaître le mot de passe de l'utilisateur
|
||
for idx, cle in enumerate(utilisateur):
|
||
if cle == "stats_journaliere":
|
||
cleAffichage = cle.replace('_', ' ').title() # remplace le `_` en espace et met une majuscule à tous les mots
|
||
else:
|
||
cleAffichage = cle.capitalize()
|
||
if cle == "date":
|
||
continue
|
||
Label(frameInfos, text=f"{cleAffichage} :").grid(column=0, row=idx + 1, sticky='e')
|
||
Label(frameInfos, text=utilisateur[cle]).grid(column=1, row=idx + 1, sticky='w')
|
||
|
||
|
||
frameSuivi = LabelFrame(enfant, text="Suivi des ventes", font=self.font)
|
||
frameSuivi.grid(column=1, row=0, sticky='n', padx=5)
|
||
Label(frameSuivi, text="Aucun résultat récemment enrengistré").grid()
|
||
|
||
Button(enfant, text="Quitter", command=enfant.destroy).grid(column=0, row=1, columnspan=2)
|
||
|
||
Button(self.f, text="Ajouter un caissier", font=self.font, command=lambda: __ajouterUtilisateur(1)).grid(column=0, row=2)
|
||
Button(self.f, text="Retirer un caissier", font=self.font, command=lambda: __retirerUtilisateur(1)).grid(column=1, row=2)
|
||
|
||
Label(self.f).grid(row = 3, pady=10) # séparateur
|
||
|
||
# Liste des utilisateurs
|
||
managerVerif = IntVar(self.f) # filtre pour afficher ou non les managers dans la liste
|
||
caissierVerif = IntVar(self.f) # filtre pour afficher ou non les caissiers ou non dans la liste
|
||
|
||
caissierVerif.set(1) # par défaut on affiche que les caissiers
|
||
|
||
def __ajouterUtilisateursListe(liste: Listbox, force: int = None):
|
||
"""
|
||
Ajoute des utilisateurs à la liste du Manager.
|
||
-> metier = 0 : manager uniquement
|
||
-> metier = 1 : caissier uniquement
|
||
-> metier = 2 : manager et caissier
|
||
"""
|
||
liste.delete(0, "end") # vidé la liste des utilisateurs
|
||
if force: # si `force` n'est pas `None`, alors on force l'utilisation d'un métier
|
||
metier = force
|
||
else: # sinon on fait une vérification normale en fonction des filtres de l'interface manager
|
||
if managerVerif.get() == 1:
|
||
if caissierVerif.get() == 1:
|
||
metier = None # on affiche les 2
|
||
else:
|
||
metier = 0 # on affiche seulement les managers
|
||
else:
|
||
metier = 1 # on affiche les caissiers
|
||
if caissierVerif.get() == 0: # rien est coché, on revient à la configuration par défaut (caissiers uniquement)
|
||
metier = 1
|
||
caissierVerif.set(1)
|
||
|
||
if metier == None: # on ajoute tous les utilisateurs
|
||
for idx, utilisateur in enumerate(Utilisateurs().listUtilisateurs()):
|
||
liste.insert(idx, f"{utilisateur[0]} ({'manager' if utilisateur[1] == 0 else 'caissier'})")
|
||
elif metier == 0: # on ajoute que les managers
|
||
for idx, utilisateur in enumerate(Utilisateurs().listUtilisateurs()):
|
||
if utilisateur[1] == metier:
|
||
liste.insert(idx, f"{utilisateur[0]} ({'manager' if utilisateur[1] == 0 else 'caissier'})")
|
||
elif metier == 1: # on ajoute que les caissiers
|
||
for idx, utilisateur in enumerate(Utilisateurs().listUtilisateurs()):
|
||
if utilisateur[1] == metier:
|
||
liste.insert(idx, f"{utilisateur[0]} ({'manager' if utilisateur[1] == 0 else 'caissier'})")
|
||
else: # ce cas est là au cas où mais n'est pas sensé être appellé
|
||
raise NameError("Métier inconnu.")
|
||
|
||
# Label d'information
|
||
Label(self.f, text="""
|
||
Double-cliquez sur un
|
||
utilisateur de la liste
|
||
pour obtenir des informations
|
||
supplémentaire à son sujet.
|
||
""", justify="right").grid(column=1, row=4, rowspan=2, sticky="e")
|
||
|
||
Label(self.f, text="Liste des utilisateurs", font=self.font).grid(column=0, row=4) # titre
|
||
# On définit une barre pour pouvoir scroller dans la liste
|
||
scroll = Scrollbar(self.f)
|
||
scroll.grid(column=0, row=5, sticky="nse")
|
||
# On définit notre liste et on la lie à notre `Scrollbar`
|
||
listeUtilisateurs = Listbox(self.f, width=25, height=4, yscrollcommand=scroll.set)
|
||
scroll.config(command=listeUtilisateurs.yview) # scroll à la verticale dans notre liste
|
||
# On ajoute nos utilisateurs à notre liste
|
||
__ajouterUtilisateursListe(listeUtilisateurs)
|
||
listeUtilisateurs.grid(column=0, row=5)
|
||
listeUtilisateurs.bind('<Double-Button>', __afficherInformationsUtilisateur) # on affiche l'utilisateur quand on double-clique dessus
|
||
|
||
# Filtre pour la liste
|
||
Label(self.f, text="Filtre", font=self.font).grid(column=1, row=4, sticky='w', padx=10) # titre
|
||
filtres = Frame(self.f) # Morceau qui va contenir nos checkbutton
|
||
filtres.grid(column=1, row=4, rowspan=2, sticky='w')
|
||
Checkbutton(filtres, text="Manager", variable=managerVerif, command=lambda: __ajouterUtilisateursListe(listeUtilisateurs)).grid(sticky='w')
|
||
Checkbutton(filtres, text="Caissier", variable=caissierVerif, command=lambda: __ajouterUtilisateursListe(listeUtilisateurs)).grid(sticky='w')
|
||
|
||
Button(self.f, text="Passer en mode caissier", font=self.font, command=lambda: self._interfaceCaissier(id)).grid(column=0, row=6, columnspan=3, pady=10)
|
||
|
||
if __name__ == "__main__":
|
||
"""Application "GesMag" pour le module de Programmation d'interfaces (2021-2022)"""
|
||
"""
|
||
Si presentation = True alors une base de donnée par défaut sera généré.
|
||
Si presentation = False ou n'est même pas mentionné, alors aucune base de donnée par défaut ne sera généré.
|
||
"""
|
||
GesMag(presentation = True).demarrer()
|