Script permettant le contrôle en masse de factures.
This commit is contained in:
parent
467565b90f
commit
4624363443
2 changed files with 391 additions and 0 deletions
388
tresorerie/controle_rapide.py
Executable file
388
tresorerie/controle_rapide.py
Executable file
|
@ -0,0 +1,388 @@
|
|||
#!/bin/bash /usr/scripts/python.sh
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# controle_rapide.py -- Outil de contrôle de factures en masse
|
||||
#
|
||||
# Copyright (C) 2015 Cr@ns <roots@crans.org>
|
||||
# Author: Pierre-Elliott Bécue <becue@crans.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the Cr@ns nor the names of its contributors may
|
||||
# be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT
|
||||
# HOLDER> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""Outil permettant de valider les factures d'adhérents
|
||||
en masse.
|
||||
|
||||
La construction est un peu méta pour éviter la redondance
|
||||
de code."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import pythondialog
|
||||
|
||||
import lc_ldap.shortcuts as shortcuts
|
||||
import lc_ldap.attributs as attributs
|
||||
import gestion.affichage as affichage
|
||||
|
||||
TIMEOUT = 600
|
||||
|
||||
VALIDER = 'V'
|
||||
INVALIDER = 'I'
|
||||
SUPPRIMER = 'S'
|
||||
|
||||
STYLES = {
|
||||
'fid': 'cyan',
|
||||
'proprio': 'rouge',
|
||||
'total': 'vert',
|
||||
'recuPaiement': 'rouge',
|
||||
'modePaiement': 'bleu',
|
||||
}
|
||||
|
||||
# Tout commence ici.
|
||||
def traiter_factures(ldap, args):
|
||||
"""Liste les factures et les trie suivant trois catégories
|
||||
(contrôle oui, non ou non renseigné), puis appelle un menu
|
||||
dialog pour pouvoir faire le contrôle. Reçoit une connexion
|
||||
LDAP valide en argument."""
|
||||
|
||||
# On commence par lister toutes les factures répondant aux critères fournis
|
||||
# dans args. Par défaut, on ratisse large.
|
||||
controle_ok, controle_non, sans_controle = trie_factures(ldap, args)
|
||||
|
||||
# On crée une interface dialog.
|
||||
dialog_interface = pythondialog.Dialog()
|
||||
|
||||
_prompt = "Que voulez-vous faire ?"
|
||||
|
||||
# Il existe trois états de contrôle, TRUE, FALSE ou rien, on propose donc trois menus
|
||||
# qui permettent de basculer une facture dans un état a ou b vers un état c.
|
||||
# On pointe une fonction de callback qui s'appelle contrôle, et qui retourne une fonction
|
||||
# customisée en fonction de son argument.
|
||||
_choices = {
|
||||
VALIDER: {
|
||||
'txt': 'Valider des factures en masse',
|
||||
'help': 'Permet de valider des facture non validée ou à contrôle faux.',
|
||||
'callback': controle(VALIDER),
|
||||
},
|
||||
INVALIDER: {
|
||||
'txt': 'Invalider des factures en masse',
|
||||
'help': 'Permet d\'invalider des facture non validée ou à contrôle positif.',
|
||||
'callback': controle(INVALIDER),
|
||||
},
|
||||
SUPPRIMER: {
|
||||
'txt': 'Supprimer des contrôles en masse',
|
||||
'help': 'Permet de supprimer le contrôle de factures (qu\'il soit vrai ou faux).',
|
||||
'callback': controle(SUPPRIMER),
|
||||
},
|
||||
}
|
||||
|
||||
# Un dico n'est pas ordonné en python
|
||||
_order = [VALIDER, INVALIDER, SUPPRIMER]
|
||||
|
||||
# On donne un choix par défaut à surligner dans le menu
|
||||
_last_choice = VALIDER
|
||||
|
||||
while True:
|
||||
# On trie les factures par fid en ordre décroissant.
|
||||
controle_ok.sort(cmp=lambda x,y: cmp(int(x['fid'][0]), int(y['fid'][0])), reverse=True)
|
||||
controle_non.sort(cmp=lambda x,y: cmp(int(x['fid'][0]), int(y['fid'][0])), reverse=True)
|
||||
sans_controle.sort(cmp=lambda x,y: cmp(int(x['fid'][0]),int(y['fid'][0])), reverse=True)
|
||||
|
||||
# Menu principal
|
||||
(code, tag) = dialog_interface.menu(
|
||||
_prompt,
|
||||
width=0,
|
||||
height=0,
|
||||
menu_height=0,
|
||||
item_help=1,
|
||||
default_item=_last_choice,
|
||||
title="Menu principal",
|
||||
scrollbar=True,
|
||||
timeout=TIMEOUT,
|
||||
cancel_label="Quitter",
|
||||
backtitle="Tréso rapide",
|
||||
choices=[(key, _choices[key]['txt'], _choices[key]['help']) for key in _order]
|
||||
)
|
||||
|
||||
if int(code) > 0:
|
||||
return
|
||||
|
||||
# On met à jour le dernier choix
|
||||
_last_choice = tag
|
||||
|
||||
# On charge la fonction de callback
|
||||
_callback = _choices[tag]['callback']
|
||||
|
||||
# S'il y a eu une couille, elle peut valoir None
|
||||
if _callback is not None:
|
||||
_callback(dialog_interface, controle_ok, controle_non, sans_controle)
|
||||
|
||||
def format_facture(facture):
|
||||
"""Construit une ligne colorée joliement à partir d'une facture
|
||||
et retourne le tout de façon compréhensible par dialog"""
|
||||
proprietaire = facture.proprio()
|
||||
|
||||
# String formatting de gros sac
|
||||
txt = u"[%s] %s € le %s par %s (%s)" % (
|
||||
affichage.style(
|
||||
facture['fid'][0],
|
||||
STYLES['fid'],
|
||||
dialog=True
|
||||
),
|
||||
affichage.style(
|
||||
facture.total(),
|
||||
STYLES['total'],
|
||||
dialog=True
|
||||
),
|
||||
affichage.style(
|
||||
facture['recuPaiement'][0],
|
||||
STYLES['recuPaiement'],
|
||||
dialog=True
|
||||
),
|
||||
affichage.style(
|
||||
facture['modePaiement'][0],
|
||||
STYLES['modePaiement'],
|
||||
dialog=True
|
||||
),
|
||||
affichage.style(
|
||||
u"%s %s" % (
|
||||
proprietaire.get('prenom', [u"Club"])[0],
|
||||
proprietaire['nom'][0]
|
||||
),
|
||||
STYLES['proprio'],
|
||||
dialog=True),
|
||||
)
|
||||
|
||||
return txt
|
||||
|
||||
def structure_liste_factures(factures, idx=0):
|
||||
"""Prend une liste de factures et retourne une liste de choix utilisable par dialog.
|
||||
|
||||
Pour cela, elle boucle sur les factures et appelle format_facture"""
|
||||
|
||||
choix = []
|
||||
|
||||
# Index initial
|
||||
# Les index servent à repérer les entrées
|
||||
i = idx
|
||||
|
||||
# À chaque itération, on rajoute un tuple, le premier élément est l'index,
|
||||
# le second le texte, et le troisième indique que la case n'est pas cochée.
|
||||
for facture in factures:
|
||||
choix.append((
|
||||
str(i),
|
||||
format_facture(facture),
|
||||
0,
|
||||
))
|
||||
i += 1
|
||||
|
||||
return choix
|
||||
|
||||
def show_list_factures(choix, dialog_interface, titre, description):
|
||||
"""Construit un menu avec les factures listées dedans"""
|
||||
# Affiche la fenêtre dialog et retourne le résultat fourni
|
||||
return dialog_interface.checklist(description,
|
||||
height=LIGNES-10,
|
||||
width=0,
|
||||
timeout=TIMEOUT,
|
||||
list_height=LIGNES-14,
|
||||
choices=choix,
|
||||
colors=True,
|
||||
title=titre
|
||||
)
|
||||
|
||||
def proceed_with(selected, bloc_a, bloc_b, bloc_append, new_value=None):
|
||||
"""Traite la liste des factures sélectionnées en effectuant l'opération désirée
|
||||
dessus
|
||||
|
||||
bloc_a et bloc_b sont deux listes parmi (controle_ok controle_non, sans_controle),
|
||||
ils contiennent les états a et b qu'on veut passer à c. bloc_append reçoit les
|
||||
factures dont l'état est changé."""
|
||||
|
||||
# Fonction de la situation, new_value vaut u"TRUE", u"FALSE" ou None,
|
||||
# qui sont les trois changements d'état possibles pour contrôle
|
||||
if new_value is None:
|
||||
new_value = []
|
||||
else:
|
||||
new_value = [new_value]
|
||||
|
||||
# Ces deux listes vont contenir les factures cochées dans
|
||||
# le menu, dont l'état va changer. On les stocke séparément
|
||||
# pour plus de facilité de gestion
|
||||
_todo_first = []
|
||||
_todo_second = []
|
||||
|
||||
# selected est le retour de la commande dialog dans show_list_factures,
|
||||
# il s'agit d'une liste d'index qui correspondent aux factures cochées.
|
||||
for index in selected:
|
||||
# Les séparateurs ont pour index '', on ne souhaite pas les prendre
|
||||
# en compte
|
||||
if not index:
|
||||
continue
|
||||
|
||||
index = int(index)
|
||||
|
||||
# Selon l'index, on a une facture à l'état a, ou à l'état b
|
||||
if index < len(bloc_a):
|
||||
_todo_first.append(bloc_a[index])
|
||||
else:
|
||||
_todo_second.append(bloc_b[index-len(bloc_a)])
|
||||
|
||||
# Une fois les deux todo listes remplies, on procède aux modifications
|
||||
for facture in _todo_first:
|
||||
# Dans un contexte, c'est plus propre
|
||||
with facture:
|
||||
# On appelle list pour générer une nouvelle liste propre et non
|
||||
# travailler par référence
|
||||
facture['controle'] = list(new_value)
|
||||
facture.history_gen()
|
||||
facture.save()
|
||||
# L'état de la facture est passé de a à c
|
||||
bloc_a.remove(facture)
|
||||
bloc_append.append(facture)
|
||||
|
||||
for facture in _todo_second:
|
||||
with facture:
|
||||
facture['controle'] = list(new_value)
|
||||
facture.history_gen()
|
||||
facture.save()
|
||||
bloc_b.remove(facture)
|
||||
bloc_append.append(facture)
|
||||
|
||||
def controle(controle_type):
|
||||
"""Retourne une fonction qui effectue les opérations de contrôle en fonction du type donné"""
|
||||
|
||||
if controle_type not in [VALIDER, INVALIDER, SUPPRIMER]:
|
||||
return None
|
||||
|
||||
# Descriptif des trois cas possibles (valider, invalider ou supprimer)
|
||||
_sentences = {
|
||||
VALIDER: [
|
||||
'%(padding)s Factures non contrôlées %(padding)s' % {'padding': '-' * (max(0, COLONNES - 60)/2)},
|
||||
'%(padding)s Factures à contrôle faux %(padding)s' % {'padding': '-' * (max(0, COLONNES - 61)/2)},
|
||||
'Contrôle en masse.',
|
||||
'Cochez les factures dont vous voulez valider le contrôle.',
|
||||
],
|
||||
INVALIDER: [
|
||||
'%(padding)s Factures contrôlées %(padding)s' % {'padding': '-' * (max(0, COLONNES - 56)/2)},
|
||||
'%(padding)s Factures non contrôlées %(padding)s' % {'padding': '-' * (max(0, COLONNES - 60)/2)},
|
||||
'Décontrôle en masse.',
|
||||
"Cochez les factures dont vous voulez passer le contrôle à faux.",
|
||||
],
|
||||
SUPPRIMER: [
|
||||
'%(padding)s Factures contrôlées %(padding)s' % {'padding': '-' * (max(0, COLONNES - 56)/2)},
|
||||
'%(padding)s Factures à contrôle faux %(padding)s' % {'padding': '-' * (max(0, COLONNES - 61)/2)},
|
||||
'Suppression de contrôle en masse.',
|
||||
"Cochez les factures dont vous voulez invalider le contrôle actuel.",
|
||||
],
|
||||
}
|
||||
|
||||
# On crée une fonction qui dépend de controle_type
|
||||
def _controle(dialog_interface, controle_ok, controle_non, sans_controle):
|
||||
"""Méthode générée à la volée pour effectuer les opérations qui vont bien"""
|
||||
# Exemple, si controle_type vaut VALIDER, c'est qu'on cherche à valider des factures.
|
||||
# On va donc lister celles non contrôlées et celles à contrôle invalide, et les factures
|
||||
# nouvellement validées iront dans bloc_append qui sera la liste des factures validées.
|
||||
# Pour que les modifications se propagent, on passe les listes par référence.
|
||||
if controle_type == VALIDER:
|
||||
bloc_a = sans_controle
|
||||
bloc_b = controle_non
|
||||
bloc_append = controle_ok
|
||||
new_value = u"TRUE"
|
||||
elif controle_type == INVALIDER:
|
||||
bloc_a = controle_ok
|
||||
bloc_b = sans_controle
|
||||
bloc_append = controle_non
|
||||
new_value = u"FALSE"
|
||||
elif controle_type == SUPPRIMER:
|
||||
bloc_a = controle_ok
|
||||
bloc_b = controle_non
|
||||
bloc_append = sans_controle
|
||||
new_value = None
|
||||
|
||||
# On place le premier séparateur
|
||||
_choices = [
|
||||
('', _sentences[controle_type][0], 0),
|
||||
]
|
||||
|
||||
# On ajoute toutes les factures correspondant à l'état a
|
||||
_choices.extend(structure_liste_factures(bloc_a))
|
||||
|
||||
# Second séparateur
|
||||
_choices.append(('', _sentences[controle_type][1], 0))
|
||||
|
||||
# Factures à l'état b
|
||||
_choices.extend(structure_liste_factures(bloc_b, len(_choices)-2))
|
||||
|
||||
# On balance le tout
|
||||
(code, selected) = show_list_factures(
|
||||
_choices,
|
||||
dialog_interface,
|
||||
_sentences[controle_type][2],
|
||||
_sentences[controle_type][3]
|
||||
)
|
||||
|
||||
if int(code) > 0:
|
||||
return
|
||||
|
||||
# On appelle proceed_with avec les résultats
|
||||
proceed_with(selected, bloc_a, bloc_b, bloc_append, new_value)
|
||||
|
||||
# On retourne notre fonction customisée
|
||||
return _controle
|
||||
|
||||
def trie_factures(ldap, args):
|
||||
"""Récupère et trie les factures"""
|
||||
|
||||
# Récupère les factures correspondant aux critères de recherche décrits dans args, et
|
||||
# les stocke dans trois listes en fonction de l'état du contrôle de chacune.
|
||||
controle_ok = []
|
||||
controle_non = []
|
||||
sans_controle = []
|
||||
|
||||
factures = ldap.search(filterstr=u"(&(fid=*)(recuPaiement=*))", mode="w", sizelimit=0)
|
||||
for facture in factures:
|
||||
if unicode(facture.get('controle', [u''])[0]) == u"TRUE":
|
||||
controle_ok.append(facture)
|
||||
elif unicode(facture.get('controle', [u''])[0]) == u"FALSE":
|
||||
controle_non.append(facture)
|
||||
else:
|
||||
sans_controle.append(facture)
|
||||
|
||||
return controle_ok, controle_non, sans_controle
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
(COLONNES, LIGNES) = affichage.getTerminalSize()
|
||||
|
||||
PARSER = argparse.ArgumentParser(description="Script d'analyse d'échange de données entre un truc et un autre.", add_help=False)
|
||||
PARSER.add_argument("-l", "--last", help="Date de début, dans un format compréhensible par postgresql (\"AAAA/MM/JJ HH:MM:SS\" fonctionne bien)", type=str, action="store")
|
||||
PARSER.add_argument("-h", "--help", help="Affiche cette aide et quitte.", action="store_true")
|
||||
|
||||
MEG = PARSER.add_mutually_exclusive_group()
|
||||
|
||||
ARGS = PARSER.parse_args()
|
||||
LDAP = shortcuts.lc_ldap_admin()
|
||||
if not set([attributs.tresorier, attributs.nounou, attributs.bureau]).intersection(LDAP.droits):
|
||||
print "Vous n'avez pas le droit d'exécuter ce programme."
|
||||
sys.exit(127)
|
||||
traiter_factures(LDAP, ARGS)
|
Loading…
Add table
Add a link
Reference in a new issue