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