gf2analytique.py 13 KB

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