# Script para FreeCAD - Procesador de Documentos Word con Carátula import os import glob from PySide2 import QtWidgets, QtCore from PySide2.QtWidgets import (QFileDialog, QMessageBox, QProgressDialog, QApplication, QVBoxLayout, QWidget, QPushButton, QLabel, QTextEdit) import FreeCAD import FreeCADGui import PVPlantResources from PVPlantResources import DirIcons as DirIcons try: from docx import Document from docx.shared import Pt, RGBColor, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn DOCX_AVAILABLE = True except ImportError: DOCX_AVAILABLE = False FreeCAD.Console.PrintError("Error: python-docx no está instalado. Instala con: pip install python-docx\n") class DocumentProcessor(QtWidgets.QDialog): def __init__(self, parent=None): super(DocumentProcessor, self).__init__(parent) self.caratula_path = "" self.carpeta_path = "" self.setup_ui() def setup_ui(self): self.setWindowTitle("Procesador de Documentos Word") self.setMinimumWidth(600) self.setMinimumHeight(500) layout = QVBoxLayout() # Título title = QLabel("

Procesador de Documentos Word

") title.setAlignment(QtCore.Qt.AlignCenter) layout.addWidget(title) # Información info_text = QLabel( "Este script buscará recursivamente todos los archivos .docx en una carpeta,\n" "insertará una carátula y aplicará formato estándar a todos los documentos." ) info_text.setAlignment(QtCore.Qt.AlignCenter) layout.addWidget(info_text) # Botón seleccionar carátula self.btn_caratula = QPushButton("1. Seleccionar Carátula") self.btn_caratula.clicked.connect(self.seleccionar_caratula) layout.addWidget(self.btn_caratula) self.label_caratula = QLabel("No se ha seleccionado carátula") self.label_caratula.setWordWrap(True) layout.addWidget(self.label_caratula) # Botón seleccionar carpeta self.btn_carpeta = QPushButton("2. Seleccionar Carpeta de Documentos") self.btn_carpeta.clicked.connect(self.seleccionar_carpeta) layout.addWidget(self.btn_carpeta) self.label_carpeta = QLabel("No se ha seleccionado carpeta") self.label_carpeta.setWordWrap(True) layout.addWidget(self.label_carpeta) # Botón procesar self.btn_procesar = QPushButton("3. Procesar Documentos") self.btn_procesar.clicked.connect(self.procesar_documentos) self.btn_procesar.setEnabled(False) layout.addWidget(self.btn_procesar) # Área de log self.log_area = QTextEdit() self.log_area.setReadOnly(True) layout.addWidget(self.log_area) # Botón cerrar self.btn_cerrar = QPushButton("Cerrar") self.btn_cerrar.clicked.connect(self.close) layout.addWidget(self.btn_cerrar) self.setLayout(layout) def log(self, mensaje): """Agrega un mensaje al área de log""" self.log_area.append(mensaje) QApplication.processEvents() # Para actualizar la UI def seleccionar_caratula(self): """Abre un diálogo para seleccionar el archivo de carátula""" archivo, _ = QFileDialog.getOpenFileName( self, "Seleccionar archivo de carátula", "", "Word documents (*.docx);;All files (*.*)" ) if archivo and os.path.exists(archivo): self.caratula_path = archivo self.label_caratula.setText(f"Carátula: {os.path.basename(archivo)}") self.verificar_estado() self.log(f"✓ Carátula seleccionada: {archivo}") def seleccionar_carpeta(self): """Abre un diálogo para seleccionar la carpeta de documentos""" carpeta = QFileDialog.getExistingDirectory( self, "Seleccionar carpeta con documentos" ) if carpeta: self.carpeta_path = carpeta self.label_carpeta.setText(f"Carpeta: {carpeta}") self.verificar_estado() self.log(f"✓ Carpeta seleccionada: {carpeta}") def verificar_estado(self): """Habilita el botón procesar si ambos paths están seleccionados""" if self.caratula_path and self.carpeta_path: self.btn_procesar.setEnabled(True) def buscar_docx_recursivamente(self, carpeta): """Busca recursivamente todos los archivos .docx en una carpeta""" archivos_docx = [] patron = os.path.join(carpeta, "**", "*.docx") for archivo in glob.glob(patron, recursive=True): archivos_docx.append(archivo) return archivos_docx def aplicar_formato_estandar(self, doc): """Aplica formato estándar al documento""" try: # Configurar estilos por defecto style = doc.styles['Normal'] font = style.font font.name = 'Arial' font.size = Pt(11) font.color.rgb = RGBColor(0, 0, 0) # Negro # Configurar encabezados try: heading_style = doc.styles['Heading 1'] heading_font = heading_style.font heading_font.name = 'Arial' heading_font.size = Pt(14) heading_font.bold = True heading_font.color.rgb = RGBColor(0, 51, 102) # Azul oscuro except: pass except Exception as e: self.log(f" ⚠ Advertencia en formato: {str(e)}") def aplicar_formato_avanzado(self, doc): """Aplica formato más avanzado y personalizado""" try: # Configurar márgenes sections = doc.sections for section in sections: section.top_margin = Inches(1) section.bottom_margin = Inches(1) section.left_margin = Inches(1) section.right_margin = Inches(1) # Configurar estilos de párrafo for paragraph in doc.paragraphs: paragraph.paragraph_format.space_after = Pt(6) paragraph.paragraph_format.space_before = Pt(0) paragraph.paragraph_format.line_spacing = 1.15 # Alinear párrafos justificados paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY # Aplicar fuente específica a cada run for run in paragraph.runs: run.font.name = 'Arial' run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Arial') run.font.size = Pt(11) except Exception as e: self.log(f" ⚠ Advertencia en formato avanzado: {str(e)}") def insertar_caratula_y_formatear(self, archivo_docx, archivo_caratula): """Inserta la carátula y aplica formato al documento""" try: # Abrir el documento de carátula doc_caratula = Document(archivo_caratula) # Abrir el documento destino doc_destino = Document(archivo_docx) # Crear un nuevo documento que contendrá la carátula + contenido original nuevo_doc = Document() # Copiar todo el contenido de la carátula for elemento in doc_caratula.element.body: print(elemento) nuevo_doc.element.body.append(elemento) # Agregar un salto de página después de la carátula nuevo_doc.add_page_break() # Copiar todo el contenido del documento original for elemento in doc_destino.element.body: nuevo_doc.element.body.append(elemento) # Aplicar formatos self.aplicar_formato_estandar(nuevo_doc) self.aplicar_formato_avanzado(nuevo_doc) # Guardar el documento (sobrescribir el original) nombre_base = os.path.splitext(os.path.basename(archivo_docx))[0] extension = os.path.splitext(archivo_docx)[1] name = f"{nombre_base}{extension}" nuevo_docx = os.path.join(self.output_carpeta, name) nuevo_doc.save(nuevo_docx) return True, "" except Exception as e: return False, str(e) def procesar_documentos(self): """Función principal que orquesta todo el proceso""" if not DOCX_AVAILABLE: QMessageBox.critical(self, "Error", "La biblioteca python-docx no está disponible.\n\n" "Instala con: pip install python-docx") return # Verificar paths if not os.path.exists(self.caratula_path): QMessageBox.warning(self, "Error", "El archivo de carátula no existe.") return if not os.path.exists(self.carpeta_path): QMessageBox.warning(self, "Error", "La carpeta de documentos no existe.") return self.log("\n=== INICIANDO PROCESAMIENTO ===") self.log(f"Carátula: {self.caratula_path}") self.log(f"Carpeta: {self.carpeta_path}") directorio_padre = os.path.dirname(self.carpeta_path) self.output_carpeta = os.path.join(directorio_padre, "03.Outputs") os.makedirs(self.output_carpeta, exist_ok=True) # Buscar archivos .docx self.log("Buscando archivos .docx...") archivos_docx = self.buscar_docx_recursivamente(self.carpeta_path) if not archivos_docx: self.log("No se encontraron archivos .docx en la carpeta seleccionada.") QMessageBox.information(self, "Información", "No se encontraron archivos .docx en la carpeta seleccionada.") return self.log(f"Se encontraron {len(archivos_docx)} archivos .docx") # Crear diálogo de progreso progress = QProgressDialog("Procesando documentos...", "Cancelar", 0, len(archivos_docx), self) progress.setWindowTitle("Procesando") progress.setWindowModality(QtCore.Qt.WindowModal) progress.show() # Procesar cada archivo exitosos = 0 fallidos = 0 errores_detallados = [] for i, archivo_docx in enumerate(archivos_docx): if progress.wasCanceled(): self.log("Proceso cancelado por el usuario.") break progress.setValue(i) progress.setLabelText(f"Procesando {i + 1}/{len(archivos_docx)}: {os.path.basename(archivo_docx)}") QApplication.processEvents() self.log(f"Procesando: {os.path.basename(archivo_docx)}") success, error_msg = self.insertar_caratula_y_formatear(archivo_docx, self.caratula_path) if success: self.log(f" ✓ Completado") exitosos += 1 else: self.log(f" ✗ Error: {error_msg}") fallidos += 1 errores_detallados.append(f"{os.path.basename(archivo_docx)}: {error_msg}") progress.setValue(len(archivos_docx)) # Mostrar resumen self.log("\n=== RESUMEN ===") self.log(f"Documentos procesados exitosamente: {exitosos}") self.log(f"Documentos con errores: {fallidos}") self.log(f"Total procesados: {exitosos + fallidos}") # Mostrar mensaje final mensaje = (f"Procesamiento completado:\n" f"✓ Exitosos: {exitosos}\n" f"✗ Fallidos: {fallidos}\n" f"Total: {len(archivos_docx)}") if fallidos > 0: mensaje += f"\n\nErrores encontrados:\n" + "\n".join( errores_detallados[:5]) # Mostrar solo primeros 5 errores if len(errores_detallados) > 5: mensaje += f"\n... y {len(errores_detallados) - 5} más" QMessageBox.information(self, "Proceso Completado", mensaje) # Función para ejecutar desde FreeCAD def run_document_processor(): """Función principal para ejecutar el procesador desde FreeCAD""" # Verificar si python-docx está disponible if not DOCX_AVAILABLE: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("Biblioteca python-docx no encontrada") msg.setInformativeText( "Para usar este script necesitas instalar python-docx:\n\n" "1. Abre la consola de FreeCAD\n" "2. Ejecuta: import subprocess, sys\n" "3. Ejecuta: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-docx'])\n\n" "O instala desde una terminal externa con: pip install python-docx" ) msg.setWindowTitle("Dependencia faltante") msg.exec_() return # Crear y mostrar la interfaz dialog = DocumentProcessor(FreeCADGui.getMainWindow()) dialog.exec_() class generateDocuments: def GetResources(self): return {'Pixmap': str(os.path.join(DirIcons, "house.svg")), 'MenuText': "DocumentGenerator", 'Accel': "D, G", 'ToolTip': "Creates a Building object from setup dialog."} def IsActive(self): return not FreeCAD.ActiveDocument is None def Activated(self): run_document_processor()