Commit 46243634 authored by Pierre-Elliott Bécue's avatar Pierre-Elliott Bécue

Script permettant le contrôle en masse de factures.

parent 467565b9
#!/bin/bash
sudo -u respbats /usr/scripts/tresorerie/controle_rapide.py
#!/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)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment