#!/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 = [] filtre = u"(&(fid=*)(recuPaiement=*)%(modes)s)" if args.mode: _modes = args.mode.split(',') _modes = u"".join([u"(modePaiement=%s)" % (_mode,) for _mode in _modes]) _modes = u"(|%s)" % (_modes,) else: _modes = u"" filtre = filtre % { 'modes': _modes, } factures = ldap.search(filterstr=filtre, 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("-m", "--mode", help="Filtre sur le mode de paiement", type=str, action="store") PARSER.add_argument("-h", "--help", help="Affiche cette aide et quitte.", action="store_true") 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)