#!/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."""

#pylint: disable=line-too-long

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',
}

def str_to_gtf(time_str):
    """Retourne un GTF sans timezone depuis un truc au format
    JJ/MM/AAAA HH:MM:SS"""

    date, heure = time_str.split()
    jour, mois, annee = date.split("/")
    heure, minute, seconde = heure.split(":")

    return "%s%s%s%s%s%s" % (annee, mois, jour, heure, minute, seconde)

# 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':
            print affichage.style(u"Tri des factures en cours.", what=["gras", "rouge"])
            controle_ok.sort(key=lambda x: int(x['fid'][0]), reverse=True)
            controle_non.sort(key=lambda x: int(x['fid'][0]), reverse=True)
            sans_controle.sort(key=lambda x: int(x['fid'][0]), reverse=True)
        else:
            print affichage.style(u"Tri des factures en cours.", what=["gras", "rouge"])
            controle_ok.sort(key=lambda x: int(x.parent_dn.split(",")[0].split("=")[1]), reverse=True)
            controle_non.sort(key=lambda x: int(x.parent_dn.split(",")[0].split("=")[1]), reverse=True)
            sans_controle.sort(key=lambda x: int(x.parent_dn.split(",")[0].split("=")[1]), 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 = []

    if args.all:
        _recu_paiement = "(recuPaiement=*)"
    else:
        if not args.end:
            args.end = crans_utils.localized_datetime()
        else:
            args.end = crans_utils.localized_datetime(str_to_gtf(args.end))
        if not args.begin:
            args.begin = args.end.replace(year=args.end.year-1)
        else:
            args.begin = crans_utils.localized_datetime(str_to_gtf(args.begin))

        _begin = crans_utils.datetime_to_generalized_time_format(args.begin)
        _end = crans_utils.datetime_to_generalized_time_format(args.end)

        _recu_paiement = "(recuPaiement>=%(begin)s)(recuPaiement<=%(end)s)" % {
            'begin': _begin,
            'end': _end,
        }

    filtre = u"(&(fid=*)%(recuPaiement)s%(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,
        'recuPaiement': _recu_paiement,
    }

    print affichage.style(u"Chargement des factures depuis la base de données.", what=["magenta"])
    factures = ldap.search(filterstr=filtre, mode="w", sizelimit=0)
    _animation = affichage.Animation(texte=u"Classification des factures", nb_cycles=len(factures), couleur=True, kikoo=True)
    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)
        _animation.new_step()
    _animation.end()

    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("-a", "--all", help="Force la récupération de toutes les factures.", action="store_true")
    PARSER.add_argument("-b", "--begin", help="Date de début, au format \"JJ/MM/AAAA HH:MM:SS\"", type=str, action="store")
    PARSER.add_argument("-e", "--end", help="Date de fin, au format \"JJ/MM/AAAA HH:MM:SS\"", type=str, action="store")
    PARSER.add_argument("-h", "--help", help="Affiche cette aide et quitte.", action="store_true")
    PARSER.add_argument("-m", "--mode", help="Filtre sur le mode de paiement", type=str, action="store")
    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)
