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
3
respbats/controle_rapide
Executable file
3
respbats/controle_rapide
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sudo -u respbats /usr/scripts/tresorerie/controle_rapide.py
|
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