Kaynağa Gözat

CHG Ajout du TSV Viewer pour faciliter l'edition des fichiers d'import
tsv

olivier.massot 7 yıl önce
ebeveyn
işleme
adb4b7692b

+ 2 - 1
analytique2facture.py

@@ -22,10 +22,11 @@ logconf.start("analytique2facture", logging.DEBUG)
 # # POUR TESTER, décommenter les lignes suivantes
 ##-----------------------------------------------
 
-# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 # AnalytiqueDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Db_analytique.mdb")
 # FacturesDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Facture_data.mdb")
 # CommunDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Commun_Data.mdb")
+# logger.handlers = [h for h in logger.handlers if (type(h) == logging.StreamHandler)]
+# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 
 ##-----------------------------------------------
 

+ 13 - 8
core/model.py

@@ -3,6 +3,7 @@
 
     @author: olivier.massot, févr. 2018
 '''
+import csv
 from datetime import datetime
 import logging
 
@@ -15,6 +16,8 @@ logger = logging.getLogger("model")
 
 Sql = SqlFormatter()
 
+csv.register_dialect('tsv', delimiter='\t', quotechar='', quoting=csv.QUOTE_NONE)
+
 class Model():
     _mapping = {}
 
@@ -90,12 +93,13 @@ class Model():
         Créé le fichier s'il n'existe pas, avec une ligne d'en-tête """
         if not path.exists():
             logger.debug("Génère le fichier %s", path)
-            firstline = "\t".join(self._fields + ["\n"])
-            with open(path, 'w+') as f:
-                f.write(firstline)
+            with open(path, 'w+', newline='') as f:
+                writer = csv.writer(f, 'tsv')
+                writer.writerow(self._fields)
 
-        with open(path, 'a') as f:
-            f.write("\t".join([str(getattr(self, field)).replace("\t", " ") for field in self._fields] + ["\n"]))
+        with open(path, "a", newline='') as f:
+            writer = csv.writer(f, 'tsv')
+            writer.writerow([str(getattr(self, field)).replace("\t", " ") for field in self._fields])
 
     @classmethod
     def load_csv(cls, path):
@@ -103,7 +107,8 @@ class Model():
         ATTENTION: chaque propriété dont le type n'est pas précisé dans _mapping aura le type 'string'
         """
         with open(path) as f:
-            fields = next(f).split("\t")
-            for line in f:
-                data = {key: cls._parse(key, value) for key, value in zip(fields, line.split("\t"))}
+            reader = csv.reader(f, 'tsv')
+            fields = next(reader)
+            for row in csv.reader(f, 'tsv'):
+                data = {key: cls._parse(key, value) for key, value in zip(fields, row)}
                 yield(cls.from_dict(data))

+ 80 - 0
core/tsv_editor.py

@@ -0,0 +1,80 @@
+'''
+@author: olivier.massot
+'''
+
+import csv
+import sys
+
+from PyQt5 import uic
+from PyQt5.Qt import QMainWindow, QApplication, QMessageBox, QStandardItemModel, \
+    QStandardItem, Qt, QFileDialog
+from path import Path
+
+
+Ui_window, _ = uic.loadUiType(Path(__file__).parent / 'tsv_editor.ui')
+
+csv.register_dialect('tsv', delimiter='\t', quotechar='', quoting=csv.QUOTE_NONE)
+
+def exec_(filename=''):
+
+    app = QApplication(sys.argv)
+
+    SYS_HOOK = sys.excepthook
+    def error_handler(typ, value, trace):
+        while QApplication.overrideCursor():
+            QApplication.restoreOverrideCursor()
+        QMessageBox.critical(iface, typ.__name__, "{}".format(value))
+        SYS_HOOK(typ, value, trace)
+    sys.excepthook = error_handler
+
+    if not filename:
+        filename, _ = QFileDialog.getOpenFileName(None, "Selectionner un fichier TSV", ".", "Tsv Files (*.tsv *.csv)")
+
+    iface = MainWindow(filename)
+    iface.show()
+
+    r = app.exec_()
+    return r
+
+class MainWindow(QMainWindow):
+
+    def __init__(self, csvpath):
+        super (MainWindow, self).__init__()
+        self.csvpath = csvpath
+        self.createWidgets()
+
+    def createWidgets(self):
+        self.ui = Ui_window()
+        self.ui.setupUi(self)
+
+        self.ui.btn_cancel.clicked.connect(self.cancel)
+        self.ui.btn_ok.clicked.connect(self.ok)
+
+        self.model = QStandardItemModel(self)
+        self.ui.tbl_data.setModel(self.model)
+        self.ui.lbl_path.setText(self.csvpath)
+
+        with open(self.csvpath, newline='') as csvfile:
+            reader = csv.reader(csvfile, 'tsv')
+            self.headers = next(reader)
+            self.model.setHorizontalHeaderLabels(self.headers)
+            for row in reader:
+                items = [ QStandardItem(field) for field in row ]
+                self.model.appendRow(items)
+
+    def ok(self):
+        with open(self.csvpath, "w", newline='') as fileOutput:
+            writer = csv.writer(fileOutput, "tsv")
+            writer.writerow(self.headers)
+            for rowNumber in range(self.model.rowCount()):
+                fields = [self.model.data(self.model.index(rowNumber, columnNumber), Qt.DisplayRole)
+                          for columnNumber in range(self.model.columnCount())]
+                writer.writerow(fields)
+        self.close()
+
+    def cancel(self):
+        self.close()
+
+
+if __name__ == '__main__':
+    exec_()

+ 124 - 0
core/tsv_editor.ui

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>window</class>
+ <widget class="QMainWindow" name="window">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>910</width>
+    <height>704</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>TSV - Editor</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <item>
+       <widget class="QLabel" name="lbl_path">
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>25</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Fichier</string>
+        </property>
+        <property name="textInteractionFlags">
+         <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item>
+     <widget class="QTableView" name="tbl_data">
+      <property name="minimumSize">
+       <size>
+        <width>521</width>
+        <height>221</height>
+       </size>
+      </property>
+      <property name="alternatingRowColors">
+       <bool>true</bool>
+      </property>
+      <property name="selectionMode">
+       <enum>QAbstractItemView::NoSelection</enum>
+      </property>
+      <property name="gridStyle">
+       <enum>Qt::DotLine</enum>
+      </property>
+      <attribute name="horizontalHeaderCascadingSectionResizes">
+       <bool>true</bool>
+      </attribute>
+      <attribute name="horizontalHeaderDefaultSectionSize">
+       <number>80</number>
+      </attribute>
+      <attribute name="horizontalHeaderMinimumSectionSize">
+       <number>30</number>
+      </attribute>
+      <attribute name="verticalHeaderVisible">
+       <bool>false</bool>
+      </attribute>
+      <attribute name="verticalHeaderShowSortIndicator" stdset="0">
+       <bool>false</bool>
+      </attribute>
+     </widget>
+    </item>
+    <item>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QPushButton" name="btn_cancel">
+        <property name="minimumSize">
+         <size>
+          <width>100</width>
+          <height>27</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Annuler</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="btn_ok">
+        <property name="minimumSize">
+         <size>
+          <width>100</width>
+          <height>27</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Valider</string>
+        </property>
+        <property name="default">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 17 - 4
ctrl2analytique.py

@@ -28,10 +28,11 @@ logconf.start("ctrl2analytique", logging.DEBUG)
 # > Lancer le script /resources/test_ctrl2analytique.py pour reinitialiser les données de la base de test
 ##-----------------------------------------------
 
-# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 # ControlesDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\cg67Parc_data.mdb")
 # AnalytiqueDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Db_analytique.mdb")
 # CommunDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Commun_Data.mdb")
+# logger.handlers = [h for h in logger.handlers if (type(h) == logging.StreamHandler)]
+# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 
 ##-----------------------------------------------
 
@@ -454,22 +455,33 @@ while errors:
         logging.error("<!> Des erreurs ont été détectées dans les données à importer. <!>")
         for msg in errors:
             logging.error(msg)
+        if no_prompt:
+            logger.info("# Annulation de l'import")
+            sys.exit(1)
     else:
         logging.info("Aucune erreur n'a été détectée dans les données.")
         if no_prompt:
             break
 
+    # Même si aucune erreur n'a été détectée, on demande un controle visuel.
     prompt = ""
     while prompt != "v":
-        prompt = input(">> Veuillez contrôler les données, puis taper: \n\t'v' pour continuer et relancer le contrôle des données\n\t'f' pour forcer le traitement à se poursuivre\n\t'q' pour annuler...")
+        logger.info(">> Veuillez contrôler les données, puis taper: \n\t'v' pour continuer\n\t'f' pour forcer le traitement à se poursuivre\n\t'q' pour annuler")
+        try:
+            from core import tsv_editor
+            tsv_editor.exec_(affaires_file.abspath())
+            tsv_editor.exec_(intervs_file.abspath())
+        except:
+            logger.error("Erreur à l'ouverture du fichier %s", affaires_file)
+            logger.error("Erreur à l'ouverture du fichier %s", intervs_file)
+
+        prompt = input("")
         if prompt == "f":
             break
         if prompt == "q":
             logger.info("# Annulation de l'import")
             sys.exit(1)
 
-
-
 # ########## MISE A JOUR DE LA BASE DE DONNEES ANALYTIQUE ##########
 
 # On charge en mémoire les affaires et les interventions
@@ -642,6 +654,7 @@ for interv in analytique_db.read_all(sql):
     analytique_db.execute(sql)
 
     logger.debug("* Mise à jour du temps d'installation de l'intervention {}".format(interv.dblInterventionId))
+logger.info("Commit des modifications...")
 analytique_db.commit()
 
 

+ 21 - 20
gf2analytique.py

@@ -15,7 +15,6 @@
 '''
 import logging
 import re
-from subprocess import call
 import sys
 
 from path import Path  # @UnusedImport
@@ -32,9 +31,10 @@ logconf.start("gf2analytique", logging.DEBUG)
 # # POUR TESTER, décommenter les lignes suivantes
 ##-----------------------------------------------
 
-# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 # GfWebservice._url = r"http://webservices-t.bas-rhin.fr/CG67.AstreGF.WebServices/public/WsPDE.asmx"
 # AnalytiqueDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Db_analytique.mdb")
+# logger.handlers = [h for h in logger.handlers if (type(h) == logging.StreamHandler)]
+# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 
 ##-----------------------------------------------
 
@@ -60,6 +60,8 @@ if importfile.exists():
     logger.debug("Supprime le fichier %s", importfile)
     importfile.remove()
 
+class CsvFormatError(Exception):
+    pass
 
 class Facture(Model):
     """ Modèle de données d'une facture """
@@ -100,43 +102,41 @@ class Facture(Model):
 
     def is_valid(self):
         """ controle la validité des données d'une facture """
+        for field in self._FIELDS:
+            if not hasattr(self, field):
+                raise CsvFormatError("Un ou plusieurs champs sont manquants dans le fichier '{}'".format(importfile))
+
+        errors = False
         if not int(self.numExBudget) > 2000:
             logger.error("Exercice budgetaire invalide: %s", self.numExBudget)
-            return False
+            errors = True
         if self.codeColl != "CG67":
             logger.error("Code collectivité invalide: %s", self.codeColl)
-            return False
+            errors = True
         if self.codeBudg != "02":
             logger.error("Code budgetaire invalide: %s", self.codeBudg)
-            return False
+            errors = True
         if self.codeAxe == "ENGIN":
             # Controle l'existence du materiel
             if not analytique_db.first("SELECT intlMaterielID FROM tbl_materiel WHERE txtMateriel='{}'".format(self.codeCout)):
                 logger.error("Le materiel n'existe pas: %s", self.codeCout)
-                return False
+                errors = True
         elif self.codeAxe == "AFFAI":
             # Controle l'existence de l'affaire
             if not analytique_db.first("SELECT dblAffaireId FROM tbl_Affaires WHERE strLiaisonControle='{}'".format(self.codeCout)):
                 logger.error("L'affaire n'existe pas: %s", self.codeCout)
-                return False
+                errors = True
         else:
             # CodeAxe invalide
             logger.error("Code axe inconnu: %s", self.codeAxe)
-            return False
-        return True
-
+            errors = True
+        return (not errors)
 
 
 # *** 1- Parcourt les factures renvoyées par le webservice, et stoque toutes les lignes non-importées dans Analytique dans un fichier import.csv
 logger.info("Parcourt les données fournies par le webservice")
 logger.info("(les ligne à importer sont ajoutées au fichier %s)", importfile)
 
-logger.debug("Génère le fichier %s", importfile)
-firstline = "\t".join(Facture._FIELDS + ["\n"])
-with open(importfile, 'w+') as f:
-    f.write(firstline)
-
-
 for data in ws:
     # Génère la facture à partir des données fournies par le web-service
     facture = Facture.from_dict(data)
@@ -165,7 +165,7 @@ while errors:
     # Parcourt les lignes du fichier d'import, et teste la validité de chacune.
     for facture in Facture.load_csv(importfile):
         if not facture.is_valid():
-                errors += 1
+            errors += 1
 
     if errors:
         logger.error("<!> Une ou plusieurs erreurs ont été détectées, voir le fichier de log pour plus d'information <!>")
@@ -181,10 +181,11 @@ while errors:
             sys.exit(errors)
         else:
             try:
-                call(["start", "", importfile.abspath()], shell=True)
+                from core import tsv_editor
+                tsv_editor.exec_(importfile.abspath())
             except:
-                logger.error("Erreur au lancement du fichier %s", importfile)
-            input("Presser une touche pour continuer...")
+                logger.error("Erreur à l'ouverture du fichier %s", importfile)
+                input("Presser une touche pour continuer...")
 
 logger.info("Les données sont valides.")
 

+ 2 - 1
gf2factures.py

@@ -20,9 +20,10 @@ logconf.start("gf2factures", logging.INFO)
 # # POUR TESTER, décommenter les lignes suivantes
 ##-----------------------------------------------
 
-# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 # GfWebservice._url = r"http://webservices-t.bas-rhin.fr/CG67.AstreGF.WebServices/public/WsPDE.asmx"
 # FacturesDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Facture_data.mdb")
+# logger.handlers = [h for h in logger.handlers if (type(h) == logging.StreamHandler)]
+# logger.warning("<<<<<<<<<<<<<<   Mode TEST   >>>>>>>>>>>>>>>>>")
 
 ##-----------------------------------------------
 

+ 2 - 1
mails_rappel_ctrl.py

@@ -26,10 +26,11 @@ wrkdir = ""
 # # POUR TESTER, décommenter les lignes suivantes
 ##-----------------------------------------------
 
-# logger.warning("Mode TEST")
 # ControlesDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\cg67Parc_data.mdb")
 # DEBUG = True
 # wrkdir = mk_workdir("mails_rappel_ctrl_test")
+# logger.handlers = [h for h in logger.handlers if (type(h) == logging.StreamHandler)]
+# logger.warning("Mode TEST")
 
 ##-----------------------------------------------
 

+ 2 - 1
pda2suiviactivite.py

@@ -26,9 +26,10 @@ logger.info("Initialization")
 # # POUR TESTER, décommenter les lignes suivantes
 ##-----------------------------------------------
 
-# logger.warning("Mode TEST")
 # PDA_FILES_DEST = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\PDA\Fichiers_PDA")
 # PdaDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\PDA\db_PDA.mdb")
+# logger.handlers = [h for h in logger.handlers if (type(h) == logging.StreamHandler)]
+# logger.warning("Mode TEST")
 
 ##-----------------------------------------------
 

+ 4 - 1
requirements.txt

@@ -2,4 +2,7 @@ pypyodbc
 path.py
 lxml
 python-dateutil
-pyyaml
+pyyaml
+
+# Pour l'edition manuelle
+PyQt5

BIN
resources/CSVpad/CSVpad.exe


+ 20 - 0
resources/CSVpad/settings.ini

@@ -0,0 +1,20 @@
+[Settings]
+AutoResize=0
+Label=1
+Line1=clWhite
+Line2=$00EEEEEE
+FontColor=clBlack
+FontSize=10
+FontName=Arial
+FontStyle=
+labelc=clBtnFace
+
+[View]
+ToolBar=1
+StatusBar=1
+
+[Main]
+LID=1
+SID=1
+SettingsSave=1
+EID=1