process.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. '''
  2. process(xmlpath) : Process principal de Pardit
  3. * Parse un fichier XML
  4. * Transforme les données selon les consignes issues des fichiers de configurations pardit.yaml et userdata.yaml
  5. * Génère un fichier XFDF avec ces données
  6. * Injecte ce fichier dans le formulaire PDF.
  7. Retourne le(s) chemin(s) d'accès du/des fichier(s) généré(s), ainsi que la ou les adresses mails des contacts correspondants.
  8. @author: olivier.massot, sept. 2017
  9. '''
  10. import datetime
  11. import re
  12. from shutil import SameFileError
  13. import subprocess
  14. import tempfile
  15. from path import Path
  16. from core import config
  17. from core.constants import TEMPLATE_PATH
  18. from core.outlook import Mail
  19. from core.pdfform import fill_form, gen_xfdf
  20. from core.xmlparser import parse
  21. class XmlFileError(Exception):
  22. pass
  23. class ProcessNotNeeded(Exception):
  24. pass
  25. class MissingValue(ValueError):
  26. """ erreur levée par les fonctions additionnelles lorsque la valeur manque """
  27. pass
  28. # *************** METHODES SPECIALES DU FICHIER DE CONFIG ***************
  29. def _parsedate(datestring):
  30. """ parse a string to a datetime """
  31. if not datestring:
  32. raise MissingValue
  33. return datetime.datetime.strptime(datestring[:10], "%Y-%m-%d")
  34. def _day(datestring):
  35. """ extrait le jour 'dd' d'une datestring ('dd/mm/yyyy') """
  36. return _parsedate(datestring).strftime("%d")
  37. def _month(datestring):
  38. """ extrait le mois 'mm' d'une datestring ('dd/mm/yyyy') """
  39. return _parsedate(datestring).strftime("%m")
  40. def _year(datestring):
  41. """ extrait l'année 'yyyy' d'une datestring ('dd/mm/yyyy') """
  42. return _parsedate(datestring).strftime("%Y")
  43. def _now(*args): # @UnusedVariable
  44. """ retourne la date du jour ('dd/mm/yyyy') """
  45. # return datestring: dd/mm/yyyy
  46. return datetime.date.today().strftime("%Y-%m-%d")
  47. _FUNCTIONS = {"JOUR": _day,
  48. "MOIS": _month,
  49. "ANNEE": _year,
  50. "MAINTENANT": _now}
  51. # *************** FONCTIONS DE TRAITEMENT DES DONNEES ***************
  52. def process(xmlpath):
  53. """ Traite les données XML au moyen du fichier de configuration
  54. pour générer le ou les Pdf de réponse """
  55. # Chemin d'accès au fichier de données
  56. xmlpath = Path(xmlpath)
  57. # Détermine et créé le répertoire de sortie
  58. output_dir = Path(config.get("repertoire_sortie")).abspath() / xmlpath.namebase
  59. output_dir.makedirs_p()
  60. # Parse une premiere fois les données du fichier XML
  61. xmldata = parse(xmlpath)
  62. # Determine le type de demande (dt, dict, conjointe)
  63. first_nodes = {node.split(".")[0] for node in xmldata}
  64. if "dtDictConjointes" in first_nodes:
  65. if xmldata.get("dtDictConjointes.partieDT.souhaitsPourLeRecepisse.souhaiteRecevoirLeRecepisse", "") == "true":
  66. a_traiter = {"dict": parse(xmlpath, "partieDICT"), "dt": parse(xmlpath, "partieDT")}
  67. else:
  68. a_traiter = {"dict": parse(xmlpath, "partieDICT")}
  69. elif "DT" in first_nodes:
  70. a_traiter = {"dt": parse(xmlpath, "DT")}
  71. elif "DICT" in first_nodes:
  72. a_traiter = {"dict": parse(xmlpath, "DICT")}
  73. else:
  74. raise XmlFileError("Le format du fichier XML n'est pas reconnu")
  75. # Traite la ou les demandes de réponse
  76. outputfiles = []
  77. mails = []
  78. for doctype, xmldata in a_traiter.items():
  79. # Construit le modele de reponse
  80. modele = {}
  81. modele.update(config.get("donnees", "commun")) # Donnees communes à toutes les réponses
  82. modele.update(config.get("donnees", doctype)) # Données spécifique au type de réponse
  83. # inject_data remplace les <...> par les valeurs des champs correspondants
  84. # eval_ parse et évalue les éventuelles fonctions de l'expression
  85. reponse = {field: eval_(inject_data(str(value), xmldata)) for field, value in modele.items()}
  86. # Patch 1: on coche la case 'demande conjointe' lorsque la demande est conjointe
  87. if "dtDictConjointes" in first_nodes:
  88. reponse["Recepisse_DT"] = "Non"
  89. reponse["Recepisse_DICT"] = "Non"
  90. reponse["Recepisse_DC"] = "Oui"
  91. outputname = output_dir / "Recepisse_{}.pdf".format(doctype.upper())
  92. make_pdf(reponse, outputname, keep_xfdf=True)
  93. outputfiles.append(outputname)
  94. if doctype == "dict" or xmldata.get("souhaitsPourLeRecepisse.souhaiteRecevoirLeRecepisse") == "true":
  95. contact = eval_(inject_data(config.get("mail", doctype, "dest"), xmldata))
  96. subject = eval_(inject_data(config.get("mail", doctype, "objet"), xmldata))
  97. content = eval_(inject_data(config.get("mail", doctype, "texte"), xmldata))
  98. mail = Mail(contact, subject, content)
  99. if not contact in [mail.to for mail in mails]:
  100. mails.append(mail)
  101. with open(output_dir / "contact.txt", "w+") as f:
  102. f.write("\n".join([mail.to for mail in mails]))
  103. # Patch 2: en cas de demande conjointe, et si le demandeur DT est le même que le demandeur DICT, on ne joint qu'un seul document au mail.
  104. # On supprime le recepisse DT, et on renomme le recepisse DICT en recepisse DC
  105. if len(mails) == 1 and len(outputfiles) > 1:
  106. rec_dict, rec_dt = outputfiles
  107. rec_dc = rec_dict.parent / "Recepisse_DC.pdf"
  108. rec_dc.remove_p()
  109. rec_dict.rename(rec_dc)
  110. rec_dt.remove_p()
  111. outputfiles = [rec_dc]
  112. # Créé une svg du fichier XML traité
  113. try:
  114. xmlpath.copy(output_dir)
  115. except SameFileError:
  116. pass
  117. return outputfiles, mails
  118. def inject_data(value, xmldata):
  119. """ injecte les données issues du fichier XML parsé
  120. dans la valeur du champ. """
  121. def _get(matchobj):
  122. key = matchobj.group(1)
  123. return xmldata.get(key, "")
  124. value = re.sub(r"<([^<>]*)>", _get, value)
  125. return value
  126. def eval_(value):
  127. """ applique les éventuelles fonctions à la valeur du champ """
  128. value = str(value) if value != None else ""
  129. for name, fct in _FUNCTIONS.items():
  130. match = re.search(r"{}\((.*)\)".format(name), str(value))
  131. if match:
  132. value = match.group(1) if match.group(1) else ''
  133. value = eval_(value)
  134. try:
  135. value = fct(value)
  136. except MissingValue:
  137. value = ""
  138. return value
  139. def make_pdf(data, outputname, keep_xfdf=False):
  140. """ injecte les données 'data' dans le Pdf modèle """
  141. with tempfile.TemporaryDirectory() as tmpdir:
  142. tmpdir = Path(tmpdir)
  143. tmpform = TEMPLATE_PATH.copy(tmpdir) # @UndefinedVariable
  144. filledform = tmpdir / "filled.pdf"
  145. xfdf = gen_xfdf(tmpdir / "data.fdf", data)
  146. try:
  147. fill_form(tmpform, xfdf, filledform, flatten=False)
  148. except subprocess.CalledProcessError:
  149. print("Erreur lors de l'écriture du PDF, assurez-vous qu'il n'est pas ouvert")
  150. return
  151. if outputname.isfile():
  152. outputname.remove()
  153. filledform.move(outputname)
  154. if keep_xfdf:
  155. xfdf.copy(outputname.parent)