From 856c7fd4bc903e9c55df2c8b56040b40a42cc65b Mon Sep 17 00:00:00 2001 From: Rufus Date: Thu, 4 Jun 2026 00:38:10 +0200 Subject: [PATCH] Fix: remove source sectPr to prevent Word corruption and integrate SDT filling --- apply_template.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/apply_template.py b/apply_template.py index 48e20ca..4b14baf 100644 --- a/apply_template.py +++ b/apply_template.py @@ -77,6 +77,29 @@ def parse_xml(content: bytes) -> etree._Element: return etree.fromstring(content) +def fill_sdt_fields(xml_root: etree._Element, values: dict) -> int: + """ + Rellena los campos SDT (Structured Document Tags) en la portada + con los valores proporcionados. + """ + filled = 0 + for sdt in xml_root.iter(q('w:sdt')): + alias_el = sdt.find(q('w:alias')) + if alias_el is None: + continue + alias = alias_el.get(q('w:val')) + if alias and alias in values and values[alias]: + sdt_content = sdt.find(q('w:sdtContent')) + if sdt_content is not None: + for run in sdt_content.iter(q('w:r')): + for t in run.iter(q('w:t')): + t.text = values[alias] + filled += 1 + break + break + return filled + + def q(tag: str) -> str: """Convierte 'w:body' a la URL completa con namespace.""" prefix, local = tag.split(':') @@ -490,6 +513,7 @@ def replace_content( source_docx_path: str | Path, output_path: str | Path, style_map: dict | None = None, + doc_vars: dict | None = None, ) -> Path: """ Núcleo de la conversión: fusiona template + source en un solo documento. @@ -527,6 +551,11 @@ def replace_content( if body_src is None: raise DocxError("El documento fuente no tiene body") + # ---- Rellenar campos SDT en la portada ---- + if doc_vars: + filled = fill_sdt_fields(tmpl_xml, doc_vars) + log.info(" Campos SDT rellenados: %d", filled) + children_tmpl = list(body_tmpl) children_src = list(body_src) @@ -749,7 +778,31 @@ def run_single(args) -> int: log.info(" 🏁 Dry-run: todo correcto, no se genera nada.") return 0 - replace_content(template, source, output) + # Preparar variables para la portada + doc_vars = { + 'Cliente': args.cliente, + 'Título': args.titulo, + 'Asunto': args.asunto, + 'Categoría': args.tipo, + 'Palabras clave': args.codigo, + } + + # 1. Cargar el XML del template + z_tmpl = zipfile.ZipFile(str(template), 'r') + tmpl_xml = parse_xml(z_tmpl.read('word/document.xml')) + + # 2. Rellenar campos SDT en la portada + filled = fill_sdt_fields(tmpl_xml, doc_vars) + log.info(" Campos SDT rellenados: %d", filled) + + # Guardar el XML modificado temporalmente para que replace_content lo use + # (Sugerencia: pasar el XML ya modificado a replace_content o modificar la función) + # Para evitar re-diseñar replace_content, vamos a inyectar el comportamiento + # en la función principal. + z_tmpl.close() + + # ... (dentro de run_single) ... + replace_content(template, source, output, doc_vars=doc_vars) return 0 @@ -889,6 +942,13 @@ Ejemplos: help='Archivo de salida (solo modo single)', ) + # Variables de portada + parser.add_argument('--cliente', default='', help='Nombre del cliente') + parser.add_argument('--titulo', default='', help='Título del documento') + parser.add_argument('--asunto', default='', help='Subtítulo / asunto') + parser.add_argument('--tipo', default='', help='Tipo de documento') + parser.add_argument('--codigo', default='', help='Código de documento') + return parser