gf2analytique.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. '''
  2. Script d'import des données de facturation depuis
  3. la base de données ASTRE-GF vers les tables de la base Analytique
  4. du Parc Départemental d'Erstein
  5. L'import se déroule en trois étapes:
  6. 1- chargement des données issues de Astre (via le web service CG67.AstreGf) dans le fichier /work/gf2analytique/import.csv
  7. 2- Contrôle de la validité des données, prompt éventuel pour une correction des donneés
  8. 3- Une fois les données valides, import dans Analytique
  9. **IMPORTANT**: pour lancer le script sans interaction avec l'utilisateur
  10. (par ex, dans le cas d'une tâche planifiée), appeller le script avec l'option '-n'.
  11. '''
  12. import logging
  13. import re
  14. import sys
  15. from path import Path # @UnusedImport
  16. from core import logconf
  17. from core.pde import AnalytiqueDb, mk_workdir
  18. from core.webservice import GfWebservice
  19. logger = logging.getLogger("gf2analytique")
  20. logconf.start("gf2analytique", logging.DEBUG)
  21. # # POUR TESTER, décommenter les lignes suivantes
  22. ##-----------------------------------------------
  23. # logger.warning("<<<<<<<<<<<<<< Mode TEST >>>>>>>>>>>>>>>>>")
  24. # GfWebservice._url = r"http://webservices-t.bas-rhin.fr/CG67.AstreGF.WebServices/public/WsPDE.asmx"
  25. # AnalytiqueDb._path = Path(r"\\h2o\local\4-transversal\BDD\mdb_test\Db_analytique.mdb")
  26. ##-----------------------------------------------
  27. # *** Initialisation
  28. logger.info("Initialisation...")
  29. no_prompt = ("-n" in sys.argv)
  30. if no_prompt:
  31. logger.info("> Lancé en mode automatique (sans interruption)")
  32. # Connect to factures.mdb
  33. analytique_db = AnalytiqueDb(autocommit=False)
  34. # Connect to the astre gf webservice
  35. ws = GfWebservice("GetPDEFactures")
  36. # Make the working directory
  37. workdir = mk_workdir("gf2analytique")
  38. importfile = workdir / "import.csv"
  39. # > Supprime le fichier d'import s'il existe, et le recréé avec la ligne d'en-tête
  40. if importfile.exists():
  41. logger.debug("Supprime le fichier %s", importfile)
  42. importfile.remove()
  43. class Facture():
  44. """ Modèle de données d'une facture """
  45. _FIELDS = ["numExBudget", "codeColl", "codeBudg", "numEnv", "codeSection", "typeMvt", "numMandat", "numLiqMandat",
  46. "numLigneMandat", "codeAxe", "libAxe", "codeCout", "libCout", "dateMandat", "numBj", "numTiers",
  47. "libRai", "refIntMandat", "codePeriode", "dateDepDelai", "typeNomencMarche", "mntTtcMandat",
  48. "mntTvaMandat", "mntVent"]
  49. def __init__(self):
  50. """ Génère un objet facture vide, les propriétés sont générrées automatiquement à partir de la variable _FIELDS"""
  51. for fld in self._FIELDS:
  52. setattr(self, fld, None)
  53. @classmethod
  54. def from_dict(cls, data):
  55. """ Retourne un objet Facture à partir d'un dictionnaire de données """
  56. facture = cls()
  57. for key, value in data.items():
  58. setattr(facture, key, value)
  59. return facture
  60. def to_csv(self):
  61. """ Renvoie une chaine de caractère correspondant aux données de la Facture au format CSV
  62. Le séparateur est une tabulation (car c'est un caractère interdit dans Access) """
  63. return "\t".join([str(getattr(self, field)).replace("\t", " ") for field in self._FIELDS] + ["\n"])
  64. @classmethod
  65. def from_csv(cls, line):
  66. """ Retourne un objet Facture à partir d'une ligne de texte au format CSV
  67. Le séparateur est une tabulation (car c'est un caractère interdit dans Access) """
  68. return cls.from_dict(dict(zip(cls._FIELDS, line.split("\t"))))
  69. def est_importee(self):
  70. """ Renvoie True si la facture a déjà été importée dans Analytique
  71. ATTENTION: en l'absence d'identifiants uniques, il est difficile de contrôler de manière certaine si une ligne a déjà été importée.
  72. C'est pour cette raison que les données sont importées 'par blocs' """
  73. sql = """SELECT dblFactureId FROM tbl_Factures
  74. WHERE intExercice = {} AND strLiquidation = '{}' AND strEngagement = '{}' AND strService='7710'
  75. """.format(self.numExBudget, self.numLiqMandat, self.numMandat)
  76. return analytique_db.exists(sql)
  77. def autocorrection(self):
  78. """ Procède à certaines corrections automatiques sur les données de la facture """
  79. # correction auto des codes chantiers
  80. if self.codeAxe == "AFFAI" and re.match(r"\d{2}5\d{3}", self.codeCout):
  81. self.codeCout += "/1"
  82. # echappe les apostrophes
  83. self.libRai = self.libRai.replace("'", "''")
  84. # renomme automatiquement les noms de materiels
  85. if self.codeAxe == "ENGIN":
  86. row = analytique_db.first("""SELECT txtMateriel FROM tbl_materiel
  87. WHERE txtMateriel='{codeCout}' or txtMateriel='ZZ {codeCout}'
  88. """.format(codeCout=self.codeCout))
  89. if row:
  90. self.codeCout = row.txtMateriel
  91. def is_valid(self):
  92. """ controle la validité des données d'une facture """
  93. if not int(self.numExBudget) > 2000:
  94. logger.error("Exercice budgetaire invalide: %s", self.numExBudget)
  95. return False
  96. if self.codeColl != "CG67":
  97. logger.error("Code collectivité invalide: %s", self.codeColl)
  98. return False
  99. if self.codeBudg != "02":
  100. logger.error("Code budgetaire invalide: %s", self.codeBudg)
  101. return False
  102. if self.codeAxe == "ENGIN":
  103. # Controle l'existence du materiel
  104. if not analytique_db.first("SELECT intlMaterielID FROM tbl_materiel WHERE txtMateriel='{}'".format(self.codeCout)):
  105. logger.error("Le materiel n'existe pas: %s", self.codeCout)
  106. return False
  107. elif self.codeAxe == "AFFAI":
  108. # Controle l'existence de l'affaire
  109. if not analytique_db.first("SELECT dblAffaireId FROM tbl_Affaires WHERE strLiaisonControle='{}'".format(self.codeCout)):
  110. logger.error("L'affaire n'existe pas: %s", self.codeCout)
  111. return False
  112. else:
  113. # CodeAxe invalide
  114. logger.error("Code axe inconnu: %s", self.codeAxe)
  115. return False
  116. return True
  117. # *** 1- Parcourt les factures renvoyées par le webservice, et stoque toutes les lignes non-importées dans Analytique dans un fichier import.csv
  118. logger.info("Parcourt les données fournies par le webservice")
  119. logger.info("(les ligne à importer sont ajoutées au fichier %s)", importfile)
  120. logger.debug("Génère le fichier %s", importfile)
  121. firstline = "\t".join(Facture._FIELDS + ["\n"])
  122. with open(importfile, 'w+') as f:
  123. f.write(firstline)
  124. for data in ws:
  125. # Génère la facture à partir des données fournies par le web-service
  126. facture = Facture.from_dict(data)
  127. # Contrôle si la facture est déjà importée. Si c'est le cas, passe à la facture suivante.
  128. if facture.est_importee():
  129. continue
  130. logger.info("* Facture %s/%s/%s: import", facture.numExBudget, facture.numMandat, facture.numLiqMandat)
  131. # procède à une auto-correction des données
  132. facture.autocorrection()
  133. # Ajoute les données au format CSV au fichier d'import
  134. with open(importfile, 'a') as f:
  135. f.write(facture.to_csv())
  136. # *** 2- Contrôle les données. En cas d'erreur, le script est interrompu et la position et la description des erreurs sont loggés.
  137. errors = -1
  138. while errors:
  139. errors = 0
  140. logger.info("Contrôle des données")
  141. # Parcourt les lignes du fichier d'import, et teste la validité de chacune.
  142. with open(importfile) as f:
  143. next(f) # saute la première ligne
  144. for line in f:
  145. facture = Facture.from_csv(line)
  146. if not facture.is_valid():
  147. errors += 1
  148. if errors:
  149. logger.error("<!> Une ou plusieurs erreurs ont été détectées, voir le fichier de log pour plus d'information <!>")
  150. logger.info("Veuillez corriger les données du fichier %s", importfile)
  151. # En cas d'erreur(s), deux possibilités:
  152. # - Le script a été lancé en mode sans interruption avec l'option '-n', on interrompt le script.
  153. # - Le script a été lancé normalement, sans option: on attend une correction manuelle de l'utilisateur.
  154. if no_prompt:
  155. sys.exit(errors)
  156. else:
  157. input("Presser une touche pour continuer...")
  158. logger.info("Les données sont valides.")
  159. # 3- Si toutes les données sont valides, parcourt les lignes du fichier import.csv et les insère dans la table tbl_Facture.
  160. logger.info("Mise à jour des tables de %s", AnalytiqueDb._path)
  161. with open(importfile) as f:
  162. next(f) # saute la première ligne
  163. for line in f:
  164. facture = Facture.from_csv(line)
  165. logger.info("* Facture %s/%s/%s: traitement", facture.numExBudget, facture.numMandat, facture.numLiqMandat)
  166. # NB: les données ne sont committées qu'aprés l'exécution de toutes les requêtes suivantes
  167. logger.info("> mise à jour de tbl_Factures")
  168. # Insère les données dans la table tbl_Factures
  169. sql = """INSERT INTO tbl_Factures ( intExercice, strLiquidation, intLiquidationLigne, strEngagement,
  170. strEnveloppe, strService, strTiers, strTiersLibelle, strMotsClefs,
  171. dtmDeb, intOperation, strNomenclature0, strAXE, strCentreCout,
  172. strObjet, dblMontantTotal, dblMontantTVA, strORIGINE_DONNEES
  173. )
  174. VALUES ({intExercice}, '{strLiquidation}', {intLiquidationLigne}, '{strEngagement}',
  175. '{strEnveloppe}', '{strService}', '{strTiers}', '{strTiersLibelle}', '{strMotsClefs}',
  176. #{dtmDeb}#, {intOperation}, '{strNomenclature0}', '{strAxe}', '{strCentreCout}',
  177. '{strObjet}', {dblMontantTotal}, {dblMontantTVA}, '{strORIGINE_DONNEES}')
  178. """.format(
  179. intExercice=facture.numExBudget,
  180. strLiquidation=facture.numLiqMandat,
  181. intLiquidationLigne=facture.numLigneMandat,
  182. strEngagement=facture.numMandat,
  183. strEnveloppe=facture.numEnv,
  184. strService='7710',
  185. strTiers=facture.numTiers,
  186. strTiersLibelle=facture.libRai,
  187. strMotsClefs=AnalytiqueDb.nz(facture.refIntMandat),
  188. dtmDeb=AnalytiqueDb.format_date(facture.dateDepDelai),
  189. intOperation=AnalytiqueDb.nz(facture.codePeriode, "Null"),
  190. strNomenclature0=facture.typeNomencMarche,
  191. strAxe=facture.codeAxe,
  192. strCentreCout=facture.codeCout,
  193. strObjet=AnalytiqueDb.format_date(facture.dateMandat, out_format="%d/%m/%Y"),
  194. dblMontantTVA=facture.mntTvaMandat,
  195. dblMontantTotal=facture.mntVent,
  196. strORIGINE_DONNEES='ASTRE'
  197. )
  198. logger.debug("> %s", sql)
  199. analytique_db.execute(sql)
  200. facture.factureId = analytique_db.first("SELECT TOP 1 dblFactureId FROM tbl_Factures ORDER BY dblFactureId DESC").dblFactureId
  201. if facture.codeAxe == "ENGIN":
  202. # La ligne concerne un engin: insère les données dans la table tbl_Facture_Engin
  203. logger.info("> mise à jour de tbl_Facture_Engin")
  204. materiel = analytique_db.first("SELECT intlMaterielID FROM tbl_Materiel WHERE [txtMateriel]='{}'".format(facture.codeCout))
  205. materielId = materiel.intlMaterielID if materiel else '859'
  206. logger.debug("retrieve intlMaterielID: %s", materielId)
  207. sql = """INSERT INTO tbl_Facture_Engin ( intlMaterielID, txtMateriel, dblFactureId, strLibelle, dblMontant, strType )
  208. VALUES ({}, '{}', {}, '{}', {}, '{}')
  209. """.format(materielId,
  210. facture.codeCout,
  211. facture.factureId,
  212. AnalytiqueDb.nz(facture.libCout),
  213. facture.mntVent,
  214. facture.libRai
  215. )
  216. logger.debug("> %s", sql)
  217. analytique_db.execute(sql)
  218. elif facture.codeAxe == "AFFAI":
  219. # La ligne concerne une affaire: insère les données dans la table tbl_Facture_Affaire
  220. logger.info("> mise à jour de tbl_Facture_Affaire")
  221. sql = """INSERT INTO tbl_Facture_Affaire ( strAffaireId, dblFactureId, strLibelle, dblMontant, strType )
  222. VALUES ('{}', {}, '{}', {}, '{}')
  223. """.format(facture.codeCout,
  224. facture.factureId,
  225. facture.libRai ,
  226. facture.mntVent,
  227. AnalytiqueDb.nz(facture.libCout),
  228. )
  229. logger.debug("> %s", sql)
  230. analytique_db.execute(sql)
  231. logger.info("> mise à jour de tbl_Mandatement")
  232. # Insère les données dans la table tbl_Mandatement
  233. sql = """INSERT INTO tbl_Mandatement ( dblFacture, strNumMandat, dtmMandat, strBordereau )
  234. VALUES ({}, '{}', #{}#, '{}')
  235. """.format(facture.factureId,
  236. facture.numMandat,
  237. AnalytiqueDb.format_date(facture.dateMandat),
  238. facture.numBj
  239. )
  240. logger.debug("> %s", sql)
  241. analytique_db.execute(sql)
  242. # Commit les insertions dans la base
  243. analytique_db.commit()
  244. logger.info("Facture %s : ok", facture.factureId)
  245. logging.shutdown()