# 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) -> None: """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(f"Erreur – {self.nomApp}", pseudoOk[1]) return mdpOk = self.motDePasseCorrect(motDePasse) if not mdpOk[0]: showerror(f"Erreur – {self.nomApp}", 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(f"Erreur – {self.nomApp}", "Une erreur est survenue : métier inconnue.") else: showerror(f"Erreur – {self.nomApp}", "Utilisateur ou mot de passe incorrect.") def dimensionsFenetre(self, fenetre, nouveauX: int, nouveauY: int) -> None: """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) -> None: """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="Connexion", font=(self.font[0], 30)).grid(column=1, row=0, columnspan=2)# titre 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("", tentativeDeConnexion) def __afficherMDP(self) -> None: """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("", 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) -> None: """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) -> None: """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) -> None: """ 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(f"Erreur – {self.nomApp}", "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(f"Erreur – {self.nomApp}", "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() -> None: """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() -> None: """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(f"Validation – {self.nomApp}", "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() -> None: """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.resizable(False, False) enfant.title(f"Ajouter un élément au stock – {self.nomApp}") def ___verification() -> None: """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: """ Vérifie si un nom est valide pour le stock. (non vide et pas déjà présent dans la base de donnée) """ 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() -> None: """Fonction qui permet de choisir une image dans l'arborescence de fichiers de l'utilisateur.""" try: chemin = askopenfile(title=f"Choisir une image – {self.nomApp}", 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() -> None: """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() -> None: """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']} – {self.nomApp}", 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) Button(self.f, text="Se déconnecter", font=self.font, command=self._interfaceConnexion).grid(column=0, row=2, sticky='w', 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=220) def _interfaceManager(self, id: int) -> None: """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(f"Erreur – {self.nomApp}", "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) -> None: """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.resizable(False, False) enfant.title(f"Ajouter un {'manager' if metier == 0 else 'caissier'} – {self.nomApp}") def ___verification() -> None: """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() -> None: """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) -> None: """Permet de supprimer un utilisateur existant, manager (`metier = 0`) et caissier (`metier = 1`).""" enfant = Toplevel(self.f) # cf. l'explication dans `__ajouterUtilisateur` enfant.resizable(False, False) 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() -> None: """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(f"Attention – {self.nomApp}", "Aucun utilisateur n'a été selectionné.") else: utilisateur = listeUtilisateurs_retirer.get(listeUtilisateurs_retirer.curselection()[0]).split('(')[0][:-1] reponse = askyesno(f"Confirmation – {self.nomApp}", 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(f"Information – {self.nomApp}", 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(_) -> None: """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.resizable(False, False) 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() -> None: """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" ] # On remplace tous les prix `None` par `0.` (au cas où il y est des valeurs vide dans le `CSV`) for date, prix in donnees.items(): if prix == None: donnees[date] = 0. # On récupère la plus grosse vente 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", 7)) ecart += 33 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) -> 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('', __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()