357 lines
13 KiB
Python
357 lines
13 KiB
Python
# 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("<h2>Procesador de Documentos Word</h2>")
|
|
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() |