This repository has been archived on 2022-03-31. You can view files and clone it, but cannot push or open issues or pull requests.
GesMag/main.py

919 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tkinter
from tkinter import Canvas, 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, asksaveasfile
# Regex
from re import sub
# Date
from datetime import date
# Import des fichiers pour gérer la base de donnée et l'export en CSV
from users import Utilisateurs
from stock import Stock
from stats import Stats
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à
Stats().creationCSV() # on créer le fichier CSV qui stockera les statistiques des utilisateurs
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)
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, 1160, 710)
self.panier = [] # remet le panier à 0
# 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
def __formatPrix(prix: str) -> str:
"""
Renvoie un string pour un meilleur affichage du prix :
- `,` au lieu de `.`
- Symbole `€`
- 2 chiffres après la virgule
"""
return f"{float(prix):.2f}".replace('.', ',')
# -> 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 __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 actuel dans le ticket de caisse."""
self.panierAffichage.destroy()
self.panierAffichage = Frame(ticket)
self.panierAffichage.grid(column=0, row=1, pady=ecart)
elementsAchetes = Label(self.panierAffichage)
elementsAchetes.grid(column=0, columnspan=2)
prixTotal = 0
compteurElements = 0
for idx, element in enumerate(self.panier):
Label(self.panierAffichage, text=f"[{element[0]['id']}] -").grid(column=0, row=idx + 1, sticky='e')
if element[1] > 1:
message = f"{element[1]}x {element[0]['nom'].capitalize()} ({__formatPrix(element[0]['prix'])} | total: {__formatPrix(element[0]['prix'] * element[1])})"
else:
message = f"{element[1]}x {element[0]['nom'].capitalize()} ({__formatPrix(element[0]['prix'])})"
Label(self.panierAffichage, text=message).grid(column=1, row=idx + 1, sticky='w')
prixTotal += (element[0]["prix"] * element[1]) # ajout du prix
compteurElements += element[1]
elementsAchetes.config(text=f"Élément{'s' if compteurElements > 1 else ''} acheté{'s' if compteurElements > 1 else ''} ({compteurElements}) :")
try: # désactive le bouton si rien n'est dans le panier
if len(self.panier) <= 0:
validationTicketDeCaisseBouton.config(state="disabled")
else:
validationTicketDeCaisseBouton.config(state="active")
except NameError: # si pas renseigné, alors = panier vide, déjà désactiver
pass
Label(self.panierAffichage, text=f"Prix total : {__formatPrix(prixTotal)}").grid(column=0, pady=ecart, columnspan=2)
__affichagePanier()
def __validationTicketDeCaisse():
"""Lance plusieurs méthodes pour valider le ticket de caisse."""
# Met à jour la valeur dans le fichier `CSV` (statistiques)
Stats().miseAJourStatsUtilisateur(id, sum([element[0]["prix"] * element[1] for element in self.panier]))
# Informe l'utilisateur que tout est validé
showinfo("Validation", "Ticket de caisse validé !")
# Retire les éléments renseigné dans le panier du stock
for element in self.panier:
Stock().reduitQuantiteStock(element[0]["id"], element[1])
# Remet le panier à 0
self.panier = []
# Met-à-jour le panier et le tableau du stock
__affichagePanier()
__affichageTableau()
validationTicketDeCaisseBouton = Button(ticket, text="Valider le\nticket de caisse", font=self.font, command=__validationTicketDeCaisse, state="disabled")
validationTicketDeCaisseBouton.grid(column=0, pady=ecart)
# -> 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 ValueError: # si la conversion a échoué
ok = False
mauvaisChamps.append(quantite)
# vérification pour le prix
try:
float(prix.get()) # conversion en float
except ValueError: # 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 AttributeError: # si l'utilisateur n'a pas choisit d'image
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)
# -> Partie export des statistiques
def __exportation():
"""Exporte dans un fichier choisie par l'utilisateur ses statistiques de la journée."""
chemin = asksaveasfile(title=f"Exportation des statistiques de {caissier['nom']} {caissier['prenom']}", filetypes=[("Fichier CSV", ".csv")])
if chemin == None: # si rien n'a été spécifie on arrête l'exportation
return
Stats().exporteCSV(chemin.name, id)
Button(self.f, text="Exporter les statistiques", font=self.font, command=__exportation).grid(column=0, row=2, sticky='e', padx=ecart)
# -> 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, sticky='w', padx=ecart)
Button(self.f, text="Se déconnecter", font=self.font, command=self._interfaceConnexion).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 == "id": # on ignore l'ID
continue
cleAffichage = cle.capitalize()
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="Histogramme des ventes", font=self.font)
frameSuivi.grid(column=1, row=0, sticky="ns", padx=5)
def ___actualisationCanvas():
"""Affiche l'histogramme des vente d'un utilisateur dans un canvas."""
donnees = Stats().recuperationDonneesCSV(utilisateur['id'])
if len(donnees) <= 0:
histogramme.create_text(10, 10, anchor='w', text="Aucun résultat récemment enregistré")
else:
# Les dates dans le fichier CSV ne sont pas dans l'ordre
# On retire l'ID et le pseudo du dictionnaire
donnees.pop("id")
donnees.pop("pseudo")
ecart = 10
couleurs = [
"CadetBlue3",
"HotPink2",
"IndianRed1",
"MediumPurple2",
"burlywood2",
"brown3",
"chocolate1",
"goldenrod2"
]
maxVente = 0 # par défaut la meilleur vente est de 0
for prix in donnees.values():
prix = float(prix)
if prix > maxVente: # si on trouve une valeure plus grande
maxVente = prix
for date in sorted(donnees.keys()): # on regarde les dates dans l'ordre
# Affichage de la date
histogramme.create_text(ecart + 10, 60, anchor='w', text=date, font=("Arial", 8), angle=90)
# Affichage de la barre
hauteur = 190 - (float(donnees[date]) * 100) / maxVente # calcul de la hauteur en fonction de la plus grosse vente
# On fait `- 20` au résultat pour allonger la barre, aussi on met une barre de `2` pixel quand valeur petite
histogramme.create_rectangle(ecart, 180, ecart + 15, hauteur - 20 if hauteur < 180 else 178, fill=couleurs.pop())
# Affichage du montant
histogramme.create_text(ecart, 190, anchor='w', text=donnees[date], font=("Arial", 8))
ecart += 30
histogramme = Canvas(frameSuivi, width=270, height=200)
histogramme.grid()
___actualisationCanvas()
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()