418 lines
16 KiB
Python
Executable file
418 lines
16 KiB
Python
Executable file
#!/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 lc_ldap.crans_utils as crans_utils
|
||
import gestion.affichage as affichage
|
||
|
||
TIMEOUT = 600
|
||
|
||
VALIDER = 'V'
|
||
INVALIDER = 'I'
|
||
SUPPRIMER = 'S'
|
||
|
||
STYLES = {
|
||
'fid': 'cyan',
|
||
'aid': 'violet',
|
||
'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.
|
||
if args.tri == 'fid':
|
||
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)
|
||
else:
|
||
controle_ok.sort(cmp=lambda x, y: cmp(int(x.proprio().oid()[0]), int(y.proprio().oid()[0])), reverse=True)
|
||
controle_non.sort(cmp=lambda x, y: cmp(int(x.proprio().oid()[0]), int(y.proprio().oid()[0])), reverse=True)
|
||
sans_controle.sort(cmp=lambda x, y: cmp(int(x.proprio().oid()[0]), int(y.proprio().oid()[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 (%s), %s € par %s le %s " % (
|
||
affichage.style(
|
||
facture['fid'][0],
|
||
STYLES['fid'],
|
||
dialog=True
|
||
),
|
||
affichage.style(
|
||
"%sid=%s" % (
|
||
u'a' if proprietaire.has_key("aid") else u"c",
|
||
proprietaire.get('aid', proprietaire.get('cid', [u'n/a']))[0],
|
||
),
|
||
STYLES['aid'],
|
||
dialog=True
|
||
),
|
||
affichage.style(
|
||
u"%s %s" % (
|
||
proprietaire.get('prenom', [u"Club"])[0],
|
||
proprietaire['nom'][0],
|
||
),
|
||
STYLES['proprio'],
|
||
dialog=True
|
||
),
|
||
affichage.style(
|
||
facture.total(),
|
||
STYLES['total'],
|
||
dialog=True
|
||
),
|
||
affichage.style(
|
||
facture['modePaiement'][0],
|
||
STYLES['modePaiement'],
|
||
dialog=True
|
||
),
|
||
affichage.style(
|
||
crans_utils.datetime_from_generalized_time_format(unicode(facture['recuPaiement'][0])).strftime("%d/%m/%Y %H:%M:%S"),
|
||
STYLES['recuPaiement'],
|
||
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")
|
||
PARSER.add_argument("-t", "--tri", help="Trie les factures suivant l'aid ou le fid", type=str, action="store")
|
||
|
||
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)
|