This commit is contained in:
2026-02-15 20:23:52 +01:00
parent 4476afc1a2
commit 111df89033
10 changed files with 552 additions and 75 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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())

View File

@@ -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)

View File

@@ -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):

View File

@@ -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',]
'PVPlantFenceGroup',
'GenerateDocuments',
]

View File

@@ -4,14 +4,17 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw.css">
<script src="https://rawgit.com/Leaflet/Leaflet.draw/v1.0.4/dist/leaflet.draw-src.js"></script>
<!-- 1. Core Leaflet library -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/togeojson@0.16.0"></script>
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
<!-- 2. Leaflet.draw Plugin (MUST be loaded AFTER Leaflet) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<!-- 3. Other plugins -->
<script src="https://unpkg.com/togeojson@0.16.0"></script>
<script src="https://unpkg.com/leaflet-filelayer@1.2.0"></script>
<!--script type="text/javascript" src="https://getfirebug.com/firebug-lite.js"></script-->
<script type="text/javascript" src="./qwebchannel.js"></script>

357
docgenerator.py Normal file
View File

@@ -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("<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()

View File

@@ -2,11 +2,11 @@
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>PVPlant</name>
<description>FreeCAD Fotovoltaic Power Plant Toolkit</description>
<version>2025.11.20</version>
<date>2025.11.20</date>
<version>2026.02.12</version>
<date>2026.02.15</date>
<maintainer email="javier.branagutierrez@gmail.com">Javier Braña</maintainer>
<license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository" branch="developed">https://homehud.duckdns.org/javier/PVPlant</url>
<url type="repository" branch="main">https://homehud.duckdns.org/javier/PVPlant/src/branch/main/</url>
<url type="bugtracker">https://homehud.duckdns.org/javier/PVPlant/issues</url>
<url type="readme">https://homehud.duckdns.org/javier/PVPlant/src/branch/developed/README.md</url>
<icon>PVPlant/Resources/Icons/PVPlantWorkbench.svg</icon>

View File

@@ -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)