''' process(xmlpath) : Process principal de Pardit * Parse un fichier XML * Transforme les données selon les consignes issues des fichiers de configurations pardit.yaml et userdata.yaml * Génère un fichier XFDF avec ces données * Injecte ce fichier dans le formulaire PDF. 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. @author: olivier.massot, sept. 2017 ''' import datetime import re from shutil import SameFileError import subprocess import tempfile from path import Path from core import config from core.constants import TEMPLATE_PATH from core.outlook import Mail from core.pdfform import fill_form, gen_xfdf from core.xmlparser import parse class XmlFileError(Exception): pass class ProcessNotNeeded(Exception): pass class MissingValue(ValueError): """ erreur levée par les fonctions additionnelles lorsque la valeur manque """ pass # *************** METHODES SPECIALES DU FICHIER DE CONFIG *************** def _parsedate(datestring): """ parse a string to a datetime """ if not datestring: raise MissingValue return datetime.datetime.strptime(datestring[:10], "%Y-%m-%d") def _day(datestring): """ extrait le jour 'dd' d'une datestring ('dd/mm/yyyy') """ return _parsedate(datestring).strftime("%d") def _month(datestring): """ extrait le mois 'mm' d'une datestring ('dd/mm/yyyy') """ return _parsedate(datestring).strftime("%m") def _year(datestring): """ extrait l'année 'yyyy' d'une datestring ('dd/mm/yyyy') """ return _parsedate(datestring).strftime("%Y") def _now(*args): # @UnusedVariable """ retourne la date du jour ('dd/mm/yyyy') """ # return datestring: dd/mm/yyyy return datetime.date.today().strftime("%Y-%m-%d") _FUNCTIONS = {"JOUR": _day, "MOIS": _month, "ANNEE": _year, "MAINTENANT": _now} # *************** FONCTIONS DE TRAITEMENT DES DONNEES *************** def process(xmlpath): """ Traite les données XML au moyen du fichier de configuration pour générer le ou les Pdf de réponse """ # Chemin d'accès au fichier de données xmlpath = Path(xmlpath) # Détermine et créé le répertoire de sortie output_dir = Path(config.get("repertoire_sortie")).abspath() / xmlpath.namebase output_dir.makedirs_p() # Parse une premiere fois les données du fichier XML xmldata = parse(xmlpath) # Determine le type de demande (dt, dict, conjointe) first_nodes = {node.split(".")[0] for node in xmldata} if "dtDictConjointes" in first_nodes: if xmldata.get("dtDictConjointes.partieDT.souhaitsPourLeRecepisse.souhaiteRecevoirLeRecepisse", "") == "true": a_traiter = {"dict": parse(xmlpath, "partieDICT"), "dt": parse(xmlpath, "partieDT")} else: a_traiter = {"dict": parse(xmlpath, "partieDICT")} elif "DT" in first_nodes: a_traiter = {"dt": parse(xmlpath, "DT")} elif "DICT" in first_nodes: a_traiter = {"dict": parse(xmlpath, "DICT")} else: raise XmlFileError("Le format du fichier XML n'est pas reconnu") # Traite la ou les demandes de réponse outputfiles = [] mails = [] for doctype, xmldata in a_traiter.items(): # Construit le modele de reponse modele = {} modele.update(config.get("donnees", "commun")) # Donnees communes à toutes les réponses modele.update(config.get("donnees", doctype)) # Données spécifique au type de réponse # inject_data remplace les <...> par les valeurs des champs correspondants # eval_ parse et évalue les éventuelles fonctions de l'expression reponse = {field: eval_(inject_data(str(value), xmldata)) for field, value in modele.items()} # Patch 1: on coche la case 'demande conjointe' lorsque la demande est conjointe if "dtDictConjointes" in first_nodes: reponse["Recepisse_DT"] = "Non" reponse["Recepisse_DICT"] = "Non" reponse["Recepisse_DC"] = "Oui" outputname = output_dir / "Recepisse_{}.pdf".format(doctype.upper()) make_pdf(reponse, outputname, keep_xfdf=True) outputfiles.append(outputname) if doctype == "dict" or xmldata.get("souhaitsPourLeRecepisse.souhaiteRecevoirLeRecepisse") == "true": contact = eval_(inject_data(config.get("mail", doctype, "dest"), xmldata)) subject = eval_(inject_data(config.get("mail", doctype, "objet"), xmldata)) content = eval_(inject_data(config.get("mail", doctype, "texte"), xmldata)) mail = Mail(contact, subject, content) if not contact in [mail.to for mail in mails]: mails.append(mail) with open(output_dir / "contact.txt", "w+") as f: f.write("\n".join([mail.to for mail in mails])) # 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. # On supprime le recepisse DT, et on renomme le recepisse DICT en recepisse DC if len(mails) == 1 and len(outputfiles) > 1: rec_dict, rec_dt = outputfiles rec_dc = rec_dict.parent / "Recepisse_DC.pdf" rec_dc.remove_p() rec_dict.rename(rec_dc) rec_dt.remove_p() outputfiles = [rec_dc] # Créé une svg du fichier XML traité try: xmlpath.copy(output_dir) except SameFileError: pass return outputfiles, mails def inject_data(value, xmldata): """ injecte les données issues du fichier XML parsé dans la valeur du champ. """ def _get(matchobj): key = matchobj.group(1) return xmldata.get(key, "") value = re.sub(r"<([^<>]*)>", _get, value) return value def eval_(value): """ applique les éventuelles fonctions à la valeur du champ """ value = str(value) if value != None else "" for name, fct in _FUNCTIONS.items(): match = re.search(r"{}\((.*)\)".format(name), str(value)) if match: value = match.group(1) if match.group(1) else '' value = eval_(value) try: value = fct(value) except MissingValue: value = "" return value def make_pdf(data, outputname, keep_xfdf=False): """ injecte les données 'data' dans le Pdf modèle """ with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) tmpform = TEMPLATE_PATH.copy(tmpdir) # @UndefinedVariable filledform = tmpdir / "filled.pdf" xfdf = gen_xfdf(tmpdir / "data.fdf", data) try: fill_form(tmpform, xfdf, filledform, flatten=False) except subprocess.CalledProcessError: print("Erreur lors de l'écriture du PDF, assurez-vous qu'il n'est pas ouvert") return if outputname.isfile(): outputname.remove() filledform.move(outputname) if keep_xfdf: xfdf.copy(outputname.parent)