diff --git a/Export/exportDXF.py b/Export/exportDXF.py index c2d5ce6..cc3ded7 100644 --- a/Export/exportDXF.py +++ b/Export/exportDXF.py @@ -971,7 +971,8 @@ class _PVPlantExportDXF(QtGui.QWidget): if FreeCAD.ActiveDocument.Transport: for road in FreeCAD.ActiveDocument.Transport.Group: base = exporter.createPolyline(road, "CIVIL External Roads") - base.dxf.const_width = road.Width + if hasattr(road, 'Width'): + base.dxf.const_width = road.Width axis = exporter.createPolyline(road, "CIVIL External Roads Axis") axis.dxf.const_width = .2 diff --git a/Importer/importOSM.py b/Importer/importOSM.py index 05421b6..bfaf682 100644 --- a/Importer/importOSM.py +++ b/Importer/importOSM.py @@ -43,53 +43,74 @@ class OSMImporter: self.ssl_context = ssl.create_default_context(cafile=certifi.where()) def transform_from_latlon(self, coordinates): + """Transforma coordenadas lat/lon a coordenadas FreeCAD""" + if not coordinates: + return [] + points = ImportElevation.getElevationFromOE(coordinates) pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points] return pts def get_osm_data(self, bbox): - query = f""" - [out:xml][bbox:{bbox}]; - ( - way["building"]; - way["highway"]; - way["railway"]; - way["power"="line"]; - way["power"="substation"]; - way["natural"="water"]; - way["landuse"="forest"]; - node["natural"="tree"]; - ); - (._;>;); - out body; - """ - req = urllib.request.Request( - self.overpass_url, - data=query.encode('utf-8'), - headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'}, - method='POST' - ) - return urllib.request.urlopen(req, context=self.ssl_context, timeout=160).read() + """ Obtiene datos de OpenStreetMap """ + # Modificar la consulta en get_osm_data para incluir más tipos de agua: + query = f"""[out:xml][bbox:{bbox}]; + ( + way["building"]; + way["highway"]; + way["railway"]; + way["power"="line"]; + way["power"="substation"]; + way["natural"="water"]; + way["waterway"]; + way["waterway"="river"]; + way["waterway"="stream"]; + way["waterway"="canal"]; + way["landuse"="basin"]; + way["landuse"="reservoir"]; + node["natural"="tree"]; + way["landuse"="forest"]; + way["landuse"="farmland"]; + ); + (._;>;); + out body; + """ + try: + req = urllib.request.Request( + self.overpass_url, + data=query.encode('utf-8'), + #headers={'User-Agent': 'FreeCAD-OSM-Importer/1.0'}, + method='POST' + ) + + response = urllib.request.urlopen(req, context=self.ssl_context, timeout=160) + return response.read() + except Exception as e: + print(f"Error obteniendo datos OSM: {str(e)}") + return None def create_layer(self, name): + """Crea o obtiene una capa en el documento""" if not FreeCAD.ActiveDocument.getObject(name): return FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) return FreeCAD.ActiveDocument.getObject(name) def process_osm_data(self, osm_data): + """Procesa los datos XML de OSM""" + if not osm_data: + print("No hay datos OSM para procesar") + return + root = ET.fromstring(osm_data) + # Primero, recolectar todos los nodos + print(f"Procesando {len(root.findall('node'))} nodos...") + # Almacenar nodos transformados coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')] coordinates = self.transform_from_latlon(coordinates) for i, node in enumerate(root.findall('node')): - self. nodes[node.attrib['id']] = coordinates[i] - '''return - for node in root.findall('node'): - self.nodes[node.attrib['id']] = self.transform_from_latlon( - float(node.attrib['lat']), - float(node.attrib['lon']) - )''' + self.nodes[node.attrib['id']] = coordinates[i] # Procesar ways for way in root.findall('way'): @@ -166,7 +187,7 @@ class OSMImporter: def create_buildings(self): building_layer = self.create_layer("Buildings") for way_id, data in self.ways_data.items(): - print(data) + #print(data) if 'building' not in data['tags']: continue @@ -226,11 +247,11 @@ class OSMImporter: nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] if 'power' in tags: - print("\n\n") - print(tags) + #print("\n\n") + #print(tags) feature_type = tags['power'] if feature_type == 'line': - print("3.1. Create Power Lines") + #print("3.1. Create Power Lines") FreeCADGui.updateGui() self.create_power_line( nodes=nodes, @@ -239,7 +260,7 @@ class OSMImporter: ) elif feature_type == 'substation': - print("3.1. Create substations") + #print("3.1. Create substations") FreeCADGui.updateGui() self.create_substation( way_id=way_id, @@ -249,7 +270,7 @@ class OSMImporter: ) elif feature_type == 'tower': - print("3.1. Create power towers") + #print("3.1. Create power towers") FreeCADGui.updateGui() self.create_power_tower( position=nodes[0] if nodes else None, @@ -562,13 +583,15 @@ class OSMImporter: if polygon_points[0] != polygon_points[-1]: polygon_points.append(polygon_points[0]) + # 3. Base del terreno base_height = 0.3 try: base_shape = Part.makePolygon(polygon_points) base_face = Part.Face(base_shape) base_extrude = base_face.extrude(FreeCAD.Vector(0, 0, base_height)) - base_obj = layer.addObject("Part::Feature", f"{name}_Base") + base_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Base") + layer.addObject(base_obj) base_obj.Shape = base_extrude base_obj.ViewObject.ShapeColor = (0.2, 0.2, 0.2) except Exception as e: @@ -583,7 +606,8 @@ class OSMImporter: fence_shape = Part.makePolygon(fence_points) fence_face = Part.Face(fence_shape) fence_extrude = fence_face.extrude(FreeCAD.Vector(0, 0, 2.8)) - fence_obj = layer.addObject("Part::Feature", f"{name}_Fence") + fence_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Fence") + layer.addObject(fence_obj) fence_obj.Shape = fence_extrude fence_obj.ViewObject.ShapeColor = (0.4, 0.4, 0.4) except Exception as e: @@ -599,14 +623,15 @@ class OSMImporter: building_shape = Part.makePolygon(building_points) building_face = Part.Face(building_shape) building_extrude = building_face.extrude(FreeCAD.Vector(0, 0, building_height)) - building_obj = layer.addObject("Part::Feature", f"{name}_Building") + building_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"{name}_Building") + layer.addObject(building_obj) building_obj.Shape = building_extrude building_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7) except Exception as e: FreeCAD.Console.PrintWarning(f"Error edificio {way_id}: {str(e)}\n") # 6. Transformadores - try: + '''try: num_transformers = int(tags.get('transformers', 1)) for i in range(num_transformers): transformer_pos = self.calculate_equipment_position( @@ -618,11 +643,11 @@ class OSMImporter: transformer = self.create_transformer( position=transformer_pos, voltage=voltage, - tech_type=tags.get('substation:type', 'outdoor') + technology=tags.get('substation:type', 'outdoor') ) layer.addObject(transformer) except Exception as e: - FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n") + FreeCAD.Console.PrintWarning(f"Error transformadores {way_id}: {str(e)}\n")''' # 7. Torre de seccionamiento para alta tensión if substation_type == 'transmission' and voltage >= 110000: @@ -637,7 +662,8 @@ class OSMImporter: FreeCAD.Console.PrintWarning(f"Error torre {way_id}: {str(e)}\n") # 8. Propiedades técnicas - substation_data = layer.addObject("App::FeaturePython", f"{name}_Data") + substation_data = FreeCAD.ActiveDocument.addObject("App::FeaturePython", f"{name}_Data") + layer.addObject(substation_data) props = { "Voltage": voltage, "Type": substation_type, @@ -651,7 +677,8 @@ class OSMImporter: else: substation_data.addProperty( "App::PropertyFloat" if isinstance(value, float) else "App::PropertyString", - prop, "Technical").setValue(value) + prop, "Technical") + setattr(substation_data, prop, value) except Exception as e: FreeCAD.Console.PrintError(f"Error crítico en subestación {way_id}: {str(e)}\n") @@ -900,9 +927,9 @@ class OSMImporter: if face.isInside(rand_point, 0.1, True): return rand_point - def create_water_bodies(self): + def create_water_bodies_old(self): water_layer = self.create_layer("Water") - + print(self.ways_data) for way_id, data in self.ways_data.items(): if 'natural' in data['tags'] and data['tags']['natural'] == 'water': nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] @@ -915,3 +942,80 @@ class OSMImporter: water.Shape = face.extrude(FreeCAD.Vector(0, 0, 0.1))# * scale - self.Origin ) water.ViewObject.ShapeColor = self.feature_colors['water'] + def create_water_bodies(self): + + water_layer = self.create_layer("Water") + + for way_id, data in self.ways_data.items(): + tags = data['tags'] + nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] + + if len(nodes) < 2: + continue + + # ===== 1) RÍOS / CANALES (líneas) ===== + name = self.get_osm_name(tags, tags["waterway"]) + if 'waterway' in tags: + if len(nodes) < 2: + continue + + try: + width = self.parse_width(tags, default=2.0) + points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes] + wire = Draft.make_wire(points, closed=False, face=False) + wire.Label = f"{name} ({tags['waterway']})" + + wire.ViewObject.LineWidth = max(1, int(width * 0.5)) + wire.ViewObject.ShapeColor = self.feature_colors['water'] + + water_layer.addObject(wire) + + except Exception as e: + print(f"Error creando waterway {way_id}: {e}") + + continue # importante + + # ===== 2) LAGOS / EMBALSES (polígonos) ===== + is_area_water = ( + tags.get('natural') == 'water' or + tags.get('landuse') in ['reservoir', 'basin'] or + tags.get('water') is not None + ) + + if not is_area_water or len(nodes) < 3: + continue + + try: + polygon_points = [FreeCAD.Vector(n.x, n.y, n.z) for n in nodes] + + if polygon_points[0] != polygon_points[-1]: + polygon_points.append(polygon_points[0]) + + polygon = Part.makePolygon(polygon_points) + face = Part.Face(polygon) + + water = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Water_{way_id}") + water.Shape = face + water.ViewObject.ShapeColor = self.feature_colors['water'] + water.Label = f"{name} ({tags['waterway']})" + + water_layer.addObject(water) + + except Exception as e: + print(f"Error creando área de agua {way_id}: {e}") + + def get_osm_name(self, tags, fallback=""): + for key in ["name", "name:es", "name:en", "alt_name", "ref"]: + if key in tags and tags[key].strip(): + return tags[key] + return fallback + + def parse_width(self, tags, default=2.0): + for key in ["width", "est_width"]: + if key in tags: + try: + w = tags[key].replace("m", "").strip() + return float(w) + except: + pass + return default diff --git a/InitGui.py b/InitGui.py index f5afc97..44a4e1a 100644 --- a/InitGui.py +++ b/InitGui.py @@ -39,9 +39,11 @@ class PVPlantWorkbench(Workbench): ToolTip = "Workbench for PV design" Icon = str(os.path.join(DirIcons, "icon.svg")) + def __init__(self): + ''' init ''' + def Initialize(self): - #sys.path.append(r"C:\Users\javie\AppData\Roaming\FreeCAD\Mod") sys.path.append(os.path.join(FreeCAD.getUserAppDataDir(), 'Mod')) import PVPlantTools, reload @@ -144,7 +146,10 @@ class PVPlantWorkbench(Workbench): from widgets import CountSelection def Activated(self): - "This function is executed when the workbench is activated" + """This function is executed when the workbench is activated""" + + FreeCAD.Console.PrintLog("Road workbench activated.\n") + import SelectionObserver import FreeCADGui @@ -153,7 +158,9 @@ class PVPlantWorkbench(Workbench): return def Deactivated(self): - "This function is executed when the workbench is deactivated" + """This function is executed when the workbench is deactivated""" + + FreeCAD.Console.PrintLog("Road workbench deactivated.\n") #FreeCADGui.Selection.removeObserver(self.observer) return @@ -201,4 +208,4 @@ class PVPlantWorkbench(Workbench): return "Gui::PythonWorkbench" -Gui.addWorkbench(PVPlantWorkbench()) +FreeCADGui.addWorkbench(PVPlantWorkbench()) diff --git a/PVPlantGeoreferencing.py b/PVPlantGeoreferencing.py index 064670a..7885931 100644 --- a/PVPlantGeoreferencing.py +++ b/PVPlantGeoreferencing.py @@ -265,7 +265,7 @@ class MapWindow(QtGui.QWidget): for item in items['features']: if item['geometry']['type'] == "Point": # 1. if the feature is a Point or Circle: coord = item['geometry']['coordinates'] - point = ImportElevation.getElevationFromOE([[coord[0], coord[1]],]) + point = ImportElevation.getElevationFromOE([[coord[1], coord[0]],]) c = FreeCAD.Vector(point[0][0], point[0][1], point[0][2]).sub(offset) if item['properties'].get('radius'): r = round(item['properties']['radius'] * 1000, 0) diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py index a4ecca5..fe6970b 100644 --- a/PVPlantImportGrid.py +++ b/PVPlantImportGrid.py @@ -119,21 +119,9 @@ def getElevationFromOE(coordinates): if i != total: locations_str += '|' query = 'https://api.open-elevation.com/api/v1/lookup?locations=' + locations_str + points = [] try: r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí - except RequestException as e: - print(f"Error en la solicitud: {str(e)}") - points = [] - for i, point in enumerate(coordinates): - c = utm.from_latlon(point[0], point[1]) - points.append(FreeCAD.Vector(round(c[0], 0), - round(c[1], 0), - 0) * 1000) - return points - - # Only get the json response in case of 200 or 201 - points = [] - if r.status_code == 200 or r.status_code == 201: results = r.json() for point in results["results"]: c = utm.from_latlon(point["latitude"], point["longitude"]) @@ -141,6 +129,14 @@ def getElevationFromOE(coordinates): round(c[1], 0), round(point["elevation"], 0)) * 1000 points.append(v) + except RequestException as e: + # print(f"Error en la solicitud: {str(e)}") + for i, point in enumerate(coordinates): + c = utm.from_latlon(point[0], point[1]) + points.append(FreeCAD.Vector(round(c[0], 0), + round(c[1], 0), + 0) * 1000) + return points def getSinglePointElevationFromBing(lat, lng): diff --git a/PVPlantTools.py b/PVPlantTools.py index 8240e0d..57dd2c4 100644 --- a/PVPlantTools.py +++ b/PVPlantTools.py @@ -652,6 +652,9 @@ if FreeCAD.GuiUp: from Civil.Fence import PVPlantFence FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup()) +import docgenerator +FreeCADGui.addCommand('GenerateDocuments', docgenerator.generateDocuments()) + projectlist = [ # "Reload", "PVPlantSite", "ProjectSetup", @@ -687,4 +690,6 @@ pv_mechanical = [ ] objectlist = ['PVPlantTree', - 'PVPlantFenceGroup',] \ No newline at end of file + 'PVPlantFenceGroup', + 'GenerateDocuments', + ] \ No newline at end of file diff --git a/Resources/webs/main.html b/Resources/webs/main.html index 2f5c683..d7cac7e 100644 --- a/Resources/webs/main.html +++ b/Resources/webs/main.html @@ -4,14 +4,17 @@ - - - - - + + + - - + + + + + + + diff --git a/docgenerator.py b/docgenerator.py new file mode 100644 index 0000000..0bc7f66 --- /dev/null +++ b/docgenerator.py @@ -0,0 +1,357 @@ +# 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() \ No newline at end of file diff --git a/package.xml b/package.xml index ced4267..7d8b356 100644 --- a/package.xml +++ b/package.xml @@ -2,11 +2,11 @@ PVPlant FreeCAD Fotovoltaic Power Plant Toolkit - 2025.11.20 - 2025.11.20 + 2026.02.12 + 2026.02.15 Javier Braña LGPL-2.1-or-later - https://homehud.duckdns.org/javier/PVPlant + https://homehud.duckdns.org/javier/PVPlant/src/branch/main/ https://homehud.duckdns.org/javier/PVPlant/issues https://homehud.duckdns.org/javier/PVPlant/src/branch/developed/README.md PVPlant/Resources/Icons/PVPlantWorkbench.svg diff --git a/reload.py b/reload.py index b2b3752..1974e80 100644 --- a/reload.py +++ b/reload.py @@ -55,6 +55,10 @@ class _CommandReload: import hydro.hydrological as hydro import Importer.importOSM as iOSM + import docgenerator + + importlib.reload(docgenerator) + importlib.reload(ProjectSetup) importlib.reload(PVPlantPlacement) importlib.reload(PVPlantImportGrid)