]> gitweb.pimeys.fr Git - scripts-20-100.git/blob - bde/credits_duplicates.py
[bde/stats/depenses.py] Encore des confusions dépenses/gains
[scripts-20-100.git] / bde / credits_duplicates.py
1 #!/usr/bin/env python
2 # -*- encoding: utf-8 -*-
3
4 """ Pour trouver les chèques dupliqués sur la note. """
5
6 import psycopg2
7 import psycopg2.extras
8 import collections
9 import pprint
10 import subprocess
11 import argparse
12
13 import base
14 import pretty_print
15
16
17 def get_data(cur, delai='1 minute', date='1970-01-01'):
18 """
19 Récupère les paires de crédits effectués à moins de ``delai`` d'intervalle.
20 ``delai`` est un chaîne de caractères que PostGreSQL comprendra comme une durée.
21 Les crédits doivent êtres valides, de même destinataire, de même montant.
22 Pour les crédits chèque, virement et carte bancaire, on vérifie qu'ils aient le même prénom,nom
23 """
24 req_create_credits = """
25 CREATE TEMPORARY TABLE credits AS (
26 (
27 SELECT t.*, c.prenom, c.nom FROM transactions t, cheques c
28 WHERE t.type = 'crédit' AND t.emetteur = -1 AND t.id = c.idtransaction
29 )
30 UNION
31 (
32 SELECT t.*, v.prenom, v.nom FROM transactions t, virements v
33 WHERE t.type = 'crédit' AND t.emetteur = -3 AND t.id = v.idtransaction
34 )
35 UNION
36 (
37 SELECT t.*, cb.prenom, cb.nom FROM transactions t, carte_bancaires cb
38 WHERE t.type = 'crédit' AND t.emetteur = -4 AND t.id = cb.idtransaction
39 )
40 UNION
41 (
42 SELECT t.*, '', '' FROM transactions t
43 WHERE t.type = 'crédit' AND t.emetteur = -2
44 ));
45 """
46 cur.execute(req_create_credits)
47
48 cur.execute("CREATE INDEX credits_index_id ON credits (id);")
49
50 req = u"""
51 SELECT
52 t1.id AS id1,
53 t2.id AS id2,
54 t1.date AS date1,
55 t2.date AS date2,
56 t2.date - t1.date AS deltaT,
57 t1.montant,
58 t1.destinataire,
59 c.pseudo AS destp,
60 t1.description AS desc1,
61 t2.description AS desc2
62 FROM credits t1, credits t2, comptes c
63 WHERE t1.destinataire = c.idbde
64 AND t1.date >= %(date)s AND t2.date >= %(date)s
65 AND t1.type='crédit'
66 AND t2.type='crédit'
67 AND t1.id != t2.id
68 AND t1.valide
69 AND t2.valide
70 AND t1.destinataire = t2.destinataire
71 AND t1.montant = t2.montant
72 AND t1.prenom = t2.prenom
73 AND t1.nom = t2.nom
74 AND (t2.date - t1.date) <= %(delai)s
75 AND (t2.date - t1.date) >= '0 s';
76 """
77 cur.execute(req, {"delai" : delai, "date" : date})
78 l = cur.fetchall()
79 return l
80
81 def sort_by_blocks(data):
82 """
83 Regroupe les transactions qui vont ensemble.
84 Sort une liste de chaînes de caractères contenant les ids de chaque block comma-separated.
85 Oui, le format de sortie est dégueu, mais c'est nettement moin galère pour trouver les doublons.
86 """
87 map = collections.defaultdict(lambda : set())
88 for t in data:
89 id1, id2 = t["id1"], t["id2"]
90 if id1 in map:
91 s = map[id1]
92 elif id2 in map:
93 s = map[id2]
94 else:
95 s = set()
96 # Comme on touche s en place et que c'est le même mutable qui a été mis
97 # comme valeur des 2 clés, il sera modifié par les étapes suivantes aussi
98 s.add(id1)
99 s.add(id2)
100 map[id1] = map[id2] = s
101 # On a tous les blocks, reste à ne garder qu'un exemplaire de chaque
102 result = [list(s) for s in map.values()]
103 for l in result:
104 l.sort()
105 result = [", ".join([str(i) for i in l]) for l in result]
106 result = list(set(result))
107 result.sort(key=lambda x : int(x.split(",")[0]))
108 return result
109
110 def get_transactions(cur, ids):
111 """
112 Récupère les informations de toutes les transactions dans la liste d'identifiants ``ids``
113 qui doit être donné sous la forme d'une chaîne de caractères d'ids comma-separated.
114 """
115 # yuk, mais le format correspond à ce qu'outpute sort_by_blocks
116 cur.execute("SELECT * FROM credits WHERE id IN (%s)" % ids)
117 return cur.fetchall()
118
119 def interactive(blocks, cur, args):
120 """
121 Propose intéractivement (sauf si les ``args`` en décident autrement) de dévalider les transactions doublons
122 ou d'afficher une liste de toutes celles à dévalider.
123 """
124 ids_to_devalidate = []
125 total = 0
126 for b in blocks:
127 lb = [int(i) for i in b.split(",")]
128 l = get_transactions(cur, b)
129 formatted = pretty_print.sql_pretty_print(l, keys=["id", "date", "type", "emetteur", "destinataire", "quantite", "montant", "description", "valide", "cantinvalidate", "prenom", "nom"])
130 if not args.noless:
131 p = subprocess.Popen(["less"], stdin=subprocess.PIPE)
132 p.communicate(formatted.encode("utf-8"))
133 print formatted.encode("utf-8")
134 print "IDs : %s" % b
135 idkeep = lb[0]
136 question = "Ne garder que %s (= dévalider les autres) ? [o/N/s]" % idkeep
137 if args.no or args.store:
138 ans = "s" if args.store else "n"
139 print question, ans
140 else:
141 ans = raw_input(question)
142 if ans.lower() in ["o", "y"]:
143 print "DESTROY !"
144 elif ans.lower() in ["s"]:
145 lb.remove(idkeep)
146 ids_to_devalidate.extend(lb)
147 total += l[0]["montant"] * (len(l) - 1)
148 if ids_to_devalidate:
149 print "\nIDs de transactions à dévalider (%s) :" % len(ids_to_devalidate)
150 print ",".join([str(i) for i in ids_to_devalidate])
151 print "Montant total : %s" % (total,)
152
153 if __name__ == "__main__":
154 parser = argparse.ArgumentParser(description="Liste les crédits semblables trop proches dans le temps et propose de les dévalider tous sauf 1.")
155
156 parser.add_argument('-n', '--no', help="Bypasse l'interactif et répond non à toutes les questions", action="store_true")
157 parser.add_argument('--store', help="Bypasse l'interactif et répond store à toutes les questions", action="store_true")
158 parser.add_argument('--date', help="Se limiter aux transactions après DATE", type=str, action="store", default='1970-01-01')
159 parser.add_argument('-t', '--deltat', help="Choisir la durée au-dessus de laquelle 2 crédits sont considérés comme différents", type=str, action="store", default='1 minute')
160 parser.add_argument('--no-less', '--noless', dest="noless", help="Se dispenser d'afficher les blocks dans un less. À réserver aux terminaux assez larges.", action="store_true")
161
162 args = parser.parse_args()
163
164 con, cur = base.getcursor()
165 data = get_data(cur, delai=args.deltat, date=args.date)
166 blocks = sort_by_blocks(data)
167 interactive(blocks, cur, args)