Browse Source

Merge branch 'master' of https://code.ffdn.org/franciliens.net/fcn-toolbox

Guillaume 7 years ago
parent
commit
8e5889746f
1 changed files with 200 additions and 0 deletions
  1. 200 0
      treso/ccoop_resplit.py

+ 200 - 0
treso/ccoop_resplit.py

@@ -0,0 +1,200 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Moulinette permettant de produire des relevés de comptes mensuels
+# au format CSV à partir d'exports CSV venant de l'interface Web du
+# crédit coopératif et ayant des chevauchements.
+
+import os, sys, json
+import csv
+from collections import OrderedDict
+from datetime import datetime
+import hashlib
+import locale
+locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8')
+
+class CsvStatementParser(object):
+    
+    def __init__(self):
+        self.lines = OrderedDict()
+        self.fieldnames = None
+        self.date_fieldname = "Date"
+        self.overlap_detector = {}
+        self.first_ops = {}
+        self.last_ops = {}
+        self.daterange = [datetime.now(), datetime.fromordinal(1)]
+        self.dups = dict() # holds counters for duplicate lines
+    
+    
+    def parse(self, filename):
+        with open(filename, encoding='iso-8859-1') as csvfile:
+            reader = csv.DictReader(csvfile, delimiter=';')
+            if self.fieldnames is None:
+                # Le premier fichier parcourru détermine les noms de
+                # colonnes attendus dans les prochains fichiers.
+                self.fieldnames = [k for k in reader.fieldnames if k != '']
+                
+                # On identifie également la permière colonne qui
+                # ressemble à une date, elle servira ensuite de clef
+                # d'indexation.
+                for fname in self.fieldnames:
+                    if "date" in fname.lower():
+                        self.date_fieldname = fname
+                        break
+            
+            if self.fieldnames != [k for k in reader.fieldnames if k != '']:
+                print("""Fichier ignoré : %s. Cause: does not have the expected column names.
+  Found:    %s
+  Expected: %s
+""" % (filename, ",".join(reader.fieldnames), ",".join(self.fieldnames)))
+            else:
+                self._parse_file(filename, reader)
+    
+    
+    def _parse_file(self, filename, reader):
+        self.dups = dict() # Duplicate counters must be reset for each file
+        print("Lecture du fichier %s" % os.path.basename(filename))
+        for row in reader:
+            opdate = datetime.strptime(row[self.date_fieldname], '%d/%m/%Y')
+            ophash = datetime.strftime(opdate, '%Y-%m-%d') + hashlib.md5(json.dumps(row).encode()).hexdigest()
+            # Special use case: one file contains multiple identical lines.
+            # Then we append a counter to the duplicate ophash.
+            if ophash in self.lines:
+                print("*** Duplicate line found in {}: {}".format(filename, ';'.join(row.values())))
+                if ophash not in self.dups:
+                    self.dups[ophash] = 1
+                self.dups[ophash] = self.dups[ophash] + 1
+                ophash = ophash + "-" + str(self.dups[ophash])
+                # print("   We have now :\n  {}\n  {}".format("\n  ".join([h + "   // " + "".join(v.values()) for h,v in self.lines.items() if h.startswith(ophash[0:10])]), ophash + " // " + "".join(row.values()))) # XXX DEBUG
+            self.lines[ophash] = {k:v for k,v in row.items() if k != ''}
+            # Adjust dateranges
+            if opdate < self.daterange[0]:
+                self.daterange[0] = opdate
+            if opdate > self.daterange[1]:
+                self.daterange[1] = opdate
+            # Prepare overlap detection
+            if ophash not in self.overlap_detector:
+                self.overlap_detector[ophash] = set()
+            self.overlap_detector[ophash].add(filename)
+            # Remember first line of each CSV file
+            if filename not in self.first_ops:
+                self.first_ops[filename] = ophash
+        # Remember last line of each CSV file
+        if filename not in self.last_ops:
+            self.last_ops[filename] = ophash
+        # CSV files are sometimes sorted by date ASC and sometimes
+        # sorted by date DESC. So we may need to swap first_op and last_op.
+        if (int(self.first_ops[filename][0:10].replace('-', '')) > int(self.last_ops[filename][0:10].replace('-', ''))):
+            tmp = self.first_ops[filename]
+            self.first_ops[filename] = self.last_ops[filename]
+            self.last_ops[filename] = tmp
+    
+    
+    def dump_full(self, output_filename):
+        with open(output_filename, 'w') as outfile:
+            writer = csv.DictWriter(outfile, self.fieldnames, delimiter=';')
+            writer.writeheader()
+            for line in reversed(sorted(self.lines.items())):
+                writer.writerow(line[1])
+        print("Relevé intégral généré dans le fichier %s" % os.path.abspath(output_filename))
+    
+    
+    def dump_monthly_reports(self, outputdir):
+        firstmonth = int('{:%Y%m}'.format(self.daterange[0])) + 1
+        lastmonth  = int('{:%Y%m}'.format(self.daterange[1])) - 1
+        if firstmonth >= lastmonth:
+            print("Impossible de générer des relevés mensuels car la plage de dates traitée est trop petite.")
+            return
+        curmonth = firstmonth
+        def __openfile__(curmonth):
+            fname = "releve_{0}.csv".format(curmonth)
+            outfile = open(os.path.join(outputdir, fname), 'w')
+            writer = csv.DictWriter(outfile, self.fieldnames, delimiter=';')
+            writer.writeheader()
+            return outfile, writer
+        outfile, writer = __openfile__(curmonth)
+        writer = csv.DictWriter(outfile, self.fieldnames, delimiter=';')
+        for line in sorted(self.lines.items()):
+            month = int(line[0][0:4] + line[0][5:7])
+            if month < curmonth:
+                continue
+            if month > lastmonth:
+                break
+            if month > curmonth:
+                outfile.close()
+                curmonth = month
+                if month in self.badmonths:
+                    outfile, writer = __openfile__(str(curmonth) + "_potentiellement_incomplet")
+                else:
+                    outfile, writer = __openfile__(curmonth)
+                
+            writer.writerow(line[1])
+        outfile.close()
+        print("Relevés mensuels générés dans le dossier %s" % os.path.abspath(outputdir))
+    
+    
+    def check_overlaps(self):
+        """
+        Helps finding possible missing operations if exported CSV files
+        are not "contiguous".
+        """
+        self.badmonths = set()
+        print("\nRecherche de chevauchements, car les chevauchements de fichiers CSV c'est bien, ça confirme qu'il n'y a pas d'écritures manquantes...")
+        for filename, first_op in self.first_ops.items():
+            if first_op in self.overlap_detector:
+                otherfiles = [v for v in self.overlap_detector.get(first_op)]
+                otherfiles.remove(filename)
+                if len(otherfiles) > 0:
+                    # Eliminate files having the same first_op
+                    otherfiles[:] = [candidate for candidate in otherfiles if self.first_ops[candidate] != first_op]
+                if len(otherfiles) == 0 and first_op[0:10] != "{0:%Y-%m-%d}".format(self.daterange[0]):
+                    self.badmonths.add(int(first_op[0:7].replace('-', '')))
+                    print("Attention. Il y a peut-être des écritures manquantes avant le %s (fichier %s)." % (first_op[0:10], os.path.basename(filename)))
+        
+        for filename, last_op in self.last_ops.items():
+            if last_op in self.overlap_detector:
+                otherfiles = [v for v in self.overlap_detector.get(last_op)]
+                otherfiles.remove(filename)
+                if len(otherfiles) > 0:
+                    # Eliminate files having the same last_op
+                    otherfiles[:] = [candidate for candidate in otherfiles if self.last_ops[candidate] != last_op]
+                if len(otherfiles) == 0 and last_op[0:10] != "{0:%Y-%m-%d}".format(self.daterange[1]):
+                    self.badmonths.add(int(last_op[0:7].replace('-', '')))
+                    print("Attention. Il y a peut-être des écritures manquantes après le %s (fichier %s)." % (last_op[0:10], os.path.basename(filename)))
+        print("")
+
+
+def start_cli(dirpath, outputdir):
+    # Lecture des fichiers CSV présents dans le dossier
+    p = CsvStatementParser()
+    for f in sorted(os.listdir(dirpath)):
+        if f.endswith('.csv') or f.endswith('.CSV'):
+            p.parse(os.path.join(dirpath, f))
+    print("Les écritures lues s'étalent entre le {0:%d %B %Y} et le {1:%d %B %Y}.".format(p.daterange[0], p.daterange[1]))
+    
+    # Recherche de chevauchements
+    p.check_overlaps()
+    
+    # Générer un relevé intégral et des relevés mensuels
+    suffix = "_{0:%Y-%m-%d}__{1:%Y-%m-%d}".format(p.daterange[0], p.daterange[1])
+    if len(p.badmonths): suffix += "_avec_des_trous"
+    p.dump_full(os.path.join(outputdir, "integral%s.csv" % suffix))
+    p.dump_monthly_reports(outputdir)
+
+
+if __name__ == '__main__':
+    if len(sys.argv) < 2:
+        print("Erreur. Merci de préciser le chemin du dossier où se trouvent les fichiers CSV à analyser.")
+        print("Usage:")
+        print("  %s exports_csv/ csv_mensuels/" % sys.argv[0])
+        sys.exit(1)
+    inputdir = sys.argv[1]
+    if len(sys.argv) > 2:
+        outputdir = sys.argv[2]
+    else:
+        outputdir = os.path.join(inputdir, "outputdir")
+    # Création d'un dossier output si besoin
+    if not os.path.isdir(outputdir):
+        os.makedirs(outputdir)
+    start_cli(inputdir, outputdir)
+