diff --git a/respbats/controle_rapide b/respbats/controle_rapide new file mode 100755 index 0000000000000000000000000000000000000000..c265432aab030e9cbf548245c3bdbc4201c7b9b5 --- /dev/null +++ b/respbats/controle_rapide @@ -0,0 +1,3 @@ +#!/bin/bash + +sudo -u respbats /usr/scripts/tresorerie/controle_rapide.py diff --git a/tresorerie/controle_rapide.py b/tresorerie/controle_rapide.py new file mode 100755 index 0000000000000000000000000000000000000000..0cf22452ff8d12b8243a7e38a997123bf6c58cdf --- /dev/null +++ b/tresorerie/controle_rapide.py @@ -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 +# Author: Pierre-Elliott Bécue +# +# 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 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)