#!/usr/bin/env python3 # -*- coding: utf-8 -*- inf1=''' Petite application de gestion comptable --- 13-10-2022 Avec cette appli, on peut par exemple gérer les comptes d'une association. A l'ouverture,on nous demande sur quelle année travailler. Si l'année commence, on nous demande aussi le solde initial. La fenêtre d'accueil nous montre les derniers enregistrements effectués. Dans le bas, trois champs nous permettent d'ajouter, de modifier, ou de supprimer. A la toute première utilisation de l'application, il faut aussi créer les rubriques. Ne pas créer trop de rubriques, car sinon le résultat de l'année sera difficile à lire. Par exemple pour une petites bibliothèque, les rubriques suivantes suffisent : adhésions,achats de livres,frais banque,assurance,subventions,articles bureau, équipements,animations,divers Si une rubrique est trop générale, on se servira du champ commentaire dans les enregistrements, pour apporter des précisions. Dans le panneau des rubriques, il est intéressant de les placer par ordre d'importance, c'est à dire l'ordre dans lequel on les présentera dans le rapport annuel. ''' import sys,os,re, json, datetime from tkinter import * from tkinter.scrolledtext import ScrolledText from tkinter.simpledialog import askstring from tkinter.messagebox import showinfo dossier = "ANNEES/" # le dossier des données (ne pas oublier le slash) frub = "rubriques" # le fichier des rubriques eol= "\n" w3="" oper="" # Position des champs dans les enregistrements P_date =0 # jour-mois P_rub =1 # rubrique P_prix =2 # montant P_com =3 # commentaire dtcour = datetime.datetime.now().strftime('%d-%m-%Y') jour,mois,annee = dtcour.split('-') dateop= jour+'-'+mois ############## FONCTIONS UTILITAIRES ################ def mess(txt, tit=None): showinfo(title=tit,message=str(txt) ) def err(txt): mess(txt,'erreur'); sys.exit() def debug(val): print('['+str(val)+']'+eol) def aff_res(res,tit="Résultat"): w2= Toplevel() w2.title(tit) zs2= ScrolledText(w2, bg="beige", width=110, height=25) zs2.grid() zs2.insert("1.0",res) def not_int(val): try: v=int(val) except: return 1 return 0 def demande(txt,nbmin=2, defval=""): x1= askstring("demande",txt,initialvalue=defval) if not x1 : return "" if len(x1) < nbmin : mess('Réponse trop courte'); return "" return x1 def ecrfic(v, fichier): try: f=open(fichier,"w"); f.write(v); f.close() except: err(fichier+" écriture impossible !") def lecjson(fichier): try: f=open(fichier,"r") except: mess(fichier+" lecture impossible !"); return [] x= json.load(f) f.close() return x def ecrjson(x, fichier): try: f=open(fichier,"w") except: err(fichier+" écriture impossible !") json.dump(x,f) f.close() def addlog(txt): f=open(flog,'a') f.write(eol+dtcour+' : '+str(txt)) f.close() def aff_totaux(lst,annee): larg_col=20 res={} for tb in lst : rx= tb[P_rub].strip() try: v=float(tb[P_prix]) except: v=0 if rx in res : res[rx]+=v else : res[rx] = v out= eol+"Pour l'année "+annee+eol+eol gtot=0 for rub in res.keys() : out+= rub.ljust(larg_col)+ "= {:9.2f}".format(res[rub])+eol gtot += res[rub] out+= eol+"Total".ljust(larg_col)+"= {:9.2f}".format(gtot) aff_res(out,'Résultats') def trouver(champ,val,txt,mode=""): out=""; nl=0; mt=0.0; trouve=0 for tb in lstop : if champ == P_prix : z=str(tb[champ]) else : z= tb[champ].casefold() if val.casefold() in z : out += formate_ligne(nl,tb)+eol mt += tb[P_prix] trouve=1 nl +=1 if trouve == 0 : mess("pas trouvé"); return if mode : out += eol+"Total des montants : {:9.2f}".format(mt) aff_res(out, txt) def new_file(anx,fx): txt="Entrer le solde initial de l'année "+anx mx=demande(txt,1,"0" ) mx= re.sub(",",".",mx) ecrjson([["01-01","Solde initial",float(mx),""]],fx) ######### LES FONCTIONS D'EDITION ######## def sauve(): ecrjson(lstop, fcour) # mise à jour du fichier def formate_ligne(num,ligne): #debug(num) #debug(ligne) return "{:3d}: {:6s} {:15s} {:9.2f} {:s}".format( int(num), ligne[P_date][:6], ligne[P_rub][:15], ligne[P_prix], ligne[P_com][:45]) # transforme une liste d'enregistrements en texte affichable # (on la limite éventuellement avec nmax) def trans(lst, nmax=1000): txt=""; nl=0 if nmax < len(lst) : nl = len(lst) - nmax for lg in lst[-nmax:] : txt += formate_ligne(nl, lg)+eol nl +=1 return txt # recopie les dernières lignes de lstop dans la zone d'affichage zs1 def init_zs1(): global lstop titr= "Dernières opérations de l'année "+annee+eol+eol zs1.delete("1.0",END) zs1.insert("1.0",titr+ trans(lstop, 20)) def e_texte(wx,tbl,num,larg,col): global tres xx= StringVar() Entry(wx, textvariable=xx, width=larg).grid( row=0,column=col,pady=20,padx=10) xx.set(tbl[num]) tres.append(xx) def e_select(wx,tbl,num,valeurs,col): global tres xx= StringVar() Spinbox(wx, repeatinterval=300, font="times 15" , values=valeurs,textvariable=xx).grid( row=0,column=col,pady=20,padx=10) xx.set(tbl[num] ) # il faut placer cette ligne derriere la creation du widget tres.append(xx) def formulaire(num,tbl, titre): global numl,tres,w3,rubriques tres=[] w3= Toplevel() w3.title(titre) numl=num Label(w3,text=str(num),bg="azure").grid(row=0,column=0,pady=20,padx=10) e_texte(w3,tbl,P_date,5,1) e_select(w3,tbl,P_rub,rubriques,2) e_texte(w3,tbl,P_prix,8,3) e_texte(w3,tbl,P_com,38,4) Button(w3,text="ok",command=miseajour_liste).grid(row=0,column=5,padx=20) Label(w3,text= "numéro, jour-mois, rubrique, somme, commentaire"+30*" ", bg="yellow").grid(row=1,columnspan=5, pady=20) def miseajour_liste(): global numl,lstop,oper,tres,w3 prix=tres[P_prix].get() if not prix : prix="0" montant= float(prix) tbl=[tres[P_date].get(),tres[P_rub].get(),montant,tres[P_com].get()] if oper=="ajoute" : lstop.append(tbl) else : lstop[numl]=tbl init_zs1() w3.destroy() addlog(oper+" "+formate_ligne(numl,tbl) ) sauve() ######### LES FONCTIONS DU MENU ########## def nodef(xx=""): if xx : xx= eol+eol+"Evénement reçu : "+str(xx) mess("Pas encore défini. "+xx) def infos(): aff_res(inf1,"Informations générales") def ajout_ligne(): global oper oper="ajoute" formulaire(len(lstop),[dateop,rubriques[0],"",""], "Ajoute une ligne") def edit_ligne(): global numlig, lstop, oper nl=demande("Numéro de la ligne à modifier",1) if not_int(nl) : return oper="edite " tblig= lstop[int(nl)] formulaire(int(nl),tblig, "Edite la ligne") def suppr_ligne(): global lstop nl=demande("Numéro de la ligne à supprimer",1) if not_int(nl) : return addlog("suppr. "+formate_ligne(nl,lstop[int(nl)]) ) lstop.pop(int(nl)) init_zs1() sauve() def depl_ligne(): global lstop rep=demande("Entrer :"+eol+"position actuelle,position souhaité",3) if not rep : return a= re.search("^(\d+)[ ,;]+(\d+)",rep) if not a : mess(rep+" ???"); return i1=int(a.group(1)); i2=int(a.group(2)) if i1 < 0 or i2 < 0 : mess(rep+" ???"); return if i1 >= len(lstop) or i2 >= len(lstop) : mess(rep+" ???"); return if i1 == i2 : return v= lstop.pop(i1) lstop.insert(i2,v) init_zs1() sauve() addlog("Déplacement de ligne : "+rep) def voir_mois(): mois=demande("Numéro du mois à visualiser",1) if not_int(mois) : return if len(mois) < 2 : mois= '0'+mois trouver(P_date,'-'+mois,"Opérations pour la période "+mois+'-'+annee, "total") def voir_rubr(): global strvar w2= Toplevel() w2.title("Voir une rubrique") Spinbox(w2,values=rubriques,textvariable=strvar).grid(row=0,column=0,pady=30,padx=20) Button(w2,text="ok",command=vrub2).grid(row=0,column=1,padx=20) def vrub2(): global strvar rep= strvar.get() trouver(P_rub,rep, annee+" -- Lignes de la rubrique : "+rep, "total") def trouver_comm(): rep= demande("Entrez une partie de commentaire"+eol) trouver(P_com,rep, annee+" -- Recherche dans les commentaires : "+rep) def trouver_montant(): mx= demande("Entrez un montant à chercher "+eol , 2) mx= re.sub(",",".",mx) trouver(P_prix,mx , annee+" -- Recherche dans les montants : "+mx) def f25(): # calcul totaux annee courante ax=annee aff_totaux(lstop,ax) def f26(): # calcul totaux pour une autre annee ax=demande("Choix de l'année ",4,str(int(annee)-1)) if not_int(ax) : return lst=lecjson(dossier+ax) aff_totaux(lst,ax) def voir_annee(): # tous les enregistrements d'une annee ax=demande("Choix de l'année ",4,annee) if not_int(ax) : return lst=lecjson(dossier+ax) aff_res(trans( lst),'Année '+ax) def edit_rubr(): # edit rubriques global w4,rubriques,t0 w4= Toplevel(); w4.title("Edition des rubriques") Label(w4,text="Ne pas dépasser 15 caractères pour les noms.").grid() t0= Text(w4, width=60, height=20) t0.insert("1.0",eol.join(rubriques)) t0.grid() Button(w4,text='cliquez si vous avez modifié', command=editrub).grid() def editrub(): global w4,rubriques,t0 txt=t0.get("1.0","end") rubriques=re.split(eol,txt) ecrjson(rubriques,frub) addlog("Edition "+frub) w4.destroy() def journal(): nl=15 # nb lignes f=open(flog,"r") lst=f.readlines() f.close() cont=' '.join(lst[-nl:]) aff_res(' '+cont,"Dernières opérations") def anomalies(): tx="Repèrage des rubriques qui ne sont pas utilisées comme prévu."+eol+eol tx+="+ = présence de crédits"+eol tx+="- = présence de débits"+eol tx+="0 = absence de montant"+eol+eol res={} for tb in lstop : rx= tb[P_rub].strip() try: v=float(tb[P_prix]) except: v=0.0 m='0' if v > 0 : m='+' if v < 0 : m='-' if rx in res : res[rx]+= m else : res[rx] = m for rub in res.keys() : tx += rub.ljust(20)+': '+res[rub]+eol tx +=eol+"Les rubriques affichant des signes différents ont peut-être une erreur de saisie..." aff_res(tx, 'Vérification dans les enregistrements') def f98(): # pour debug bx="" for lx in lstop[-20:] : bx+= eol+str(lx) aff_res(bx,"Liste au format brut des 20 dernières lignes.") ########## DEBUT DU PROGRAMME ########## w1= Tk() w1.title("Gestion de compte") strvar= StringVar() # declaration d'une variable globale d'echange pour les widgets strvar.set("??") # mise en place d'une zone scrollable pour les résultats zs1= ScrolledText(w1, bg="tan", width=100, height=25) zs1.grid(row=1, columnspan=4) # barre d'édition Button(w1,text="Ajouter une ligne",command=ajout_ligne).grid(row=2,column=0) Button(w1,text="Modifier une ligne",command=edit_ligne).grid(row=2,column=1) Button(w1,text="Déplacer une ligne",command=depl_ligne).grid(row=2,column=2) Button(w1,text="Supprimer une ligne",command=suppr_ligne).grid(row=2,column=3) menu= Menu(w1) m1= Menu(menu, tearoff =0) m1.add_command(label="Voir une rubrique particulière",command=voir_rubr) m1.add_command(label="Recherche dans les commentaires",command=trouver_comm) m1.add_command(label="Recherche dans les montants",command=trouver_montant) m1.add_command(label="Toutes les opérations d'un mois",command=voir_mois) m1.add_command(label="Toutes les opérations de l'année", command=voir_annee) m2= Menu(menu, tearoff =0) m2.add_command(label="Pour l'année courante", command=f25) m2.add_command(label='Pour une autre année', command=f26) m3= Menu(menu, tearoff =0) m3.add_command(label="Edition des rubriques",command=edit_rubr) m3.add_command(label="Journal des opérations",command=journal) m3.add_command(label='Recherche des anomalies', command=anomalies) m3.add_command(label='Informations', command=infos) m3.add_command(label="Extrait de la liste des données (pour debug)", command=f98) menu.add_cascade(label='Recherche',menu=m1) menu.add_cascade(label='Résultats',menu=m2) menu.add_cascade(label='Divers',menu=m3) w1.config(menu=menu, bg="silver") # sur quelle année va-t-on travailler ? while 1 : ax=demande("Choix de l'année ",4,annee) if ax and re.search("^\d+$",ax) : annee=ax; break # détermination du nom des fichiers pour l'année fcour= dossier+annee # le fichier des enregistrements de l'année flog = dossier+annee+'.log' # le fichier journal # petites verifications de présence, et sinon création if not os.path.isdir(dossier): os.mkdir(dossier) if not os.path.isfile(fcour): new_file(annee,fcour) if not os.path.isfile(frub): ecrjson(["divers"],frub) if not os.path.isfile(flog): ecrfic(eol,flog) lstop= lecjson(fcour) # les enregistrements de l'année rubriques= lecjson(frub) # liste des rubriques init_zs1() w1.mainloop()