This commit is contained in:
2025-05-08 08:29:11 +02:00
parent 03464ffafd
commit 5dd8869caf
3 changed files with 194 additions and 192 deletions

View File

@@ -20,32 +20,43 @@
# * * # * *
# *********************************************************************** # ***********************************************************************
import os
import platform
import subprocess
from typing import Dict, List
import FreeCAD import FreeCAD
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import openpyxl import openpyxl
from openpyxl.styles import Alignment, Border, Side, PatternFill, Font from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
import PVPlantResources import PVPlantResources
import PVPlantSite import PVPlantSite
if FreeCAD.GuiUp:
from PySide.QtCore import QT_TRANSLATE_NOOP
__title__ = "PVPlant Mechanical BOQ"
__author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com"
# Constants
SCALE = 0.001 # Millimeters to meters
THIN_BORDER = Border(
top=Side(border_style="thin", color="7DA4B8"),
left=Side(border_style="thin", color="7DA4B8"),
right=Side(border_style="thin", color="7DA4B8"),
bottom=Side(border_style="thin", color="7DA4B8")
)
HEADER_FILL = PatternFill("solid", fgColor="7DA4B8")
HEADER_FONT = Font(name='Arial', size=10, bold=True, color="FFFFFF")
DATA_FONT = Font(name='Arial', size=10)
CENTER_ALIGN = Alignment(horizontal="center", vertical="center")
# Estilos: # Estilos:
thin = Side(border_style="thin", color="7DA4B8") thin = Side(border_style="thin", color="7DA4B8")
double = Side(border_style="double", color="ff0000") double = Side(border_style="double", color="ff0000")
@@ -56,6 +67,16 @@ border_fat = Border(top=thin, left=thin, right=thin, bottom=thin)
scale = 0.001 # milimeters to meter scale = 0.001 # milimeters to meter
def open_file(path: str) -> None:
"""Open a file or directory using the system's default handler"""
if platform.system() == "Windows":
os.startfile(path)
elif platform.system() == "Darwin":
subprocess.Popen(["open", path])
else:
subprocess.Popen(["xdg-open", path])
def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment=None): def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment=None):
""" """
Apply styles to a range of cells as if they were a single cell. Apply styles to a range of cells as if they were a single cell.
@@ -94,124 +115,119 @@ def style_range(ws, cell_range, border=Border(), fill=None, font=None, alignment
for c in row: for c in row:
c.fill = fill c.fill = fill
def spreadsheetBOQFrames(sheet, sel):
sheet['A1'] = 'Index'
sheet['B1'] = 'Frame'
sheet['C1'] = 'Frame Type'
sheet['D1'] = 'X'
sheet['E1'] = 'Y'
sheet['F1'] = 'Z'
sheet['G1'] = 'Angle N-S'
sheet['H1'] = 'Angle L-W'
sheet['I1'] = 'Nº Poles'
sheet.column_dimensions['A'].width = 8 def create_sheet_headers(sheet: Worksheet, headers: List[str], widths: Dict[str, float]) -> None:
sheet.column_dimensions['B'].width = 30 """Create standardized sheet headers."""
sheet.column_dimensions['C'].width = 20 for col, header in enumerate(headers, start=1):
sheet.column_dimensions['D'].width = 20 cell = sheet.cell(row=1, column=col, value=header)
sheet.column_dimensions['E'].width = 20 cell.fill = HEADER_FILL
sheet.column_dimensions['F'].width = 20 cell.font = HEADER_FONT
sheet.column_dimensions['G'].width = 15 cell.alignment = CENTER_ALIGN
sheet.column_dimensions['H'].width = 15
sheet.column_dimensions['I'].width = 15
sheet.row_dimensions[1].height = 40
style_range(sheet, 'A1:I1', for col, width in widths.items():
border=Border(top=thin, left=thin, right=thin, bottom=thin), sheet.column_dimensions[col].width = width
fill=PatternFill("solid", fgColor="7DA4B8"),
font=Font(name='Quicksand', size=10, b=True, color="FFFFFF"),
alignment=Alignment(horizontal="center", vertical="center"))
for ind in range(0, len(sel)): sheet.row_dimensions[1].height = 30
row = ind + 2 style_range(sheet, f'A1:{chr(64 + len(headers))}1',
sheet['A{0}'.format(row)] = ind + 1 border=THIN_BORDER,
sheet['B{0}'.format(row)] = sel[ind].Label fill=HEADER_FILL,
sheet['C{0}'.format(row)] = sel[ind].Setup.Label font=HEADER_FONT,
sheet['D{0}'.format(row)] = sel[ind].Placement.Base.x * scale alignment=CENTER_ALIGN)
sheet['E{0}'.format(row)] = sel[ind].Placement.Base.y * scale
sheet['R{0}'.format(row)] = sel[ind].Placement.Base.z * scale
sheet['G{0}'.format(row)] = sel[ind].Placement.Rotation.toEuler()[0]
sheet['H{0}'.format(row)] = sel[ind].Placement.Rotation.toEuler()[1]
sheet['I{0}'.format(row)] = sel[ind].Setup.NumberPole.Value
style_range(sheet, 'A' + str(row) + ':I' + str(row),
border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=10),
alignment=Alignment(horizontal="center", vertical="center"))
def spreadsheetBOQPoles(sheet, sel):
import MeshPart as mp
from Mechanical.Frame import PVPlantFrame
# Data: def spreadsheetBOQFrames(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None:
terrain = PVPlantSite.get().Terrain.Mesh # Shape """Generate frames information sheet."""
headers = [
'Index', 'Frame', 'Frame Type', 'X (m)', 'Y (m)', 'Z (m)',
'Angle N-S', 'Angle L-W', 'Nº Poles'
]
widths = {'A': 8, 'B': 30, 'C': 20, 'D': 15, 'E': 15,
'F': 15, 'G': 15, 'H': 15, 'I': 10}
# Headers: create_sheet_headers(sheet, headers, widths)
sheet['A1'] = 'Frame'
sheet['B1'] = 'Pole'
sheet['C1'] = 'Pole Type'
sheet['D1'] = 'X'
sheet['E1'] = 'Y'
sheet['F1'] = 'Z frame attach'
sheet['G1'] = 'Z aerial head'
sheet['H1'] = 'Pole length'
sheet['I1'] = 'Pole aerial length'
sheet['J1'] = 'Pole terrain enter length'
sheet.column_dimensions['A'].width = 30 for idx, obj in enumerate(selection, start=2):
sheet.column_dimensions['B'].width = 8 placement = obj.Placement
sheet.column_dimensions['C'].width = 20 sheet[f'A{idx}'] = idx - 1
sheet.column_dimensions['D'].width = 20 sheet[f'B{idx}'] = obj.Label
sheet.column_dimensions['E'].width = 20 sheet[f'C{idx}'] = obj.Setup.Label
sheet.column_dimensions['F'].width = 20 sheet[f'D{idx}'] = placement.Base.x * SCALE
sheet.column_dimensions['G'].width = 20 sheet[f'E{idx}'] = placement.Base.y * SCALE
sheet.column_dimensions['H'].width = 20 sheet[f'F{idx}'] = placement.Base.z * SCALE
sheet.column_dimensions['I'].width = 20 sheet[f'G{idx}'] = placement.Rotation.toEuler()[0]
sheet.column_dimensions['J'].width = 20 sheet[f'H{idx}'] = placement.Rotation.toEuler()[1]
sheet.row_dimensions[1].height = 40 sheet[f'I{idx}'] = obj.Setup.NumberPole.Value
style_range(sheet, 'A1:J1',
border=Border(top=thin, left=thin, right=thin, bottom=thin), style_range(sheet, f'A{idx}:I{idx}',
fill=PatternFill("solid", fgColor="7DA4B8"), border=THIN_BORDER,
font=Font(name='Quicksand', size=11, b=True, color="FFFFFF"), font=DATA_FONT,
alignment=Alignment(horizontal="center", vertical="center")) alignment=CENTER_ALIGN)
sheet['A2'] = ""
def spreadsheetBOQPoles(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None:
"""Generate poles information sheet."""
headers = [
'Frame', 'Pole', 'Pole Type', 'X (m)', 'Y (m)', 'Z Frame Attach (m)',
'Z Aerial Head (m)', 'Pole Length (m)', 'Aerial Length (m)',
'Terrain Enter Length (m)'
]
widths = {chr(65 + i): 20 for i in range(10)}
widths['A'] = 30
widths['B'] = 10
create_sheet_headers(sheet, headers, widths)
sheet.row_dimensions[2].height = 5 sheet.row_dimensions[2].height = 5
data = {"Frame": [],
#"FrameType": [],
"Pole": [],
"PoleType": [],
"PoleLength": [],
"Center": [],
"Head": []}
cnt = 0 import MeshPart as mp
for frame_ind, frame in enumerate(sel): from Mechanical.Frame import PVPlantFrame
poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes # Data:
numpoles = int(frame.Setup.NumberPole.Value) terrain = PVPlantSite.get().Terrain.Mesh # Shape
seq = frame.Setup.PoleSequence poles_data = []
if len(seq) < numpoles:
seq = PVPlantFrame.getarray(frame.Setup.PoleSequence, numpoles)
for pole_ind in range(numpoles):
pole = poles[pole_ind]
poletype = frame.Setup.PoleType[seq[pole_ind]]
data["Frame"].append(frame.Label)
#data["FrameType"].append(frame.Setup.Label)
data["Pole"].append(pole_ind + 1)
data["PoleType"].append(poletype.Label)
data["PoleLength"].append(int(poletype.Height))
data["Center"].append(pole.BoundBox.Center)
data["Head"].append(pole.BoundBox.ZMax)
cnt += 1
pts = mp.projectPointsOnMesh(data["Center"], terrain, FreeCAD.Vector(0, 0, 1)) for frame in selection:
#if cnt == len(pts): try:
data["Soil"] = pts poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes
num_poles = int(frame.Setup.NumberPole.Value)
sequence = frame.Setup.PoleSequence
if len(sequence) < num_poles:
sequence = PVPlantFrame.getarray(frame.Setup.PoleSequence, num_poles)
for pole_idx in range(num_poles):
pole = poles[pole_idx]
pole_type = frame.Setup.PoleType[sequence[pole_idx]]
center = pole.BoundBox.Center
poles_data.append({
'frame': frame.Label,
'number': pole_idx + 1,
'type': pole_type.Label,
'center': center,
'head_z': pole.BoundBox.ZMax,
'length': pole_type.Height.Value
})
except Exception as e:
FreeCAD.Console.PrintError(f"Error processing frame {frame.Label}: {str(e)}\n")
# Project points on terrain
try:
import MeshPart
points = [data['center'] for data in poles_data]
terrain_z = MeshPart.projectPointsOnMesh(points, terrain, FreeCAD.Vector(0, 0, 1))
for data, z in zip(poles_data, terrain_z):
data['terrain_z'] = z.z
except Exception as e:
FreeCAD.Console.PrintError(f"Terrain projection failed: {str(e)}\n")
for data in poles_data:
data['terrain_z'] = 0
# Write data to sheet
row = 3 row = 3
group_from = row group_from = row
f = data["Frame"][0] print(poles_data[0])
for i in range(0, len(data["Frame"])): f = poles_data[0]['frame']
if f != data["Frame"][i]: for i, data in enumerate(poles_data):
if f != data["frame"]:
style_range(sheet, 'A' + str(group_from) + ':F' + str(row - 1), style_range(sheet, 'A' + str(group_from) + ':F' + str(row - 1),
border=Border(top=thin, left=thin, right=thin, bottom=thin), border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=11, ), font=Font(name='Quicksand', size=11, ),
@@ -221,27 +237,27 @@ def spreadsheetBOQPoles(sheet, sel):
border=Border(top=thin, left=thin, right=thin, bottom=thin), border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=11, ), font=Font(name='Quicksand', size=11, ),
alignment=Alignment(horizontal="center", vertical="center")) alignment=Alignment(horizontal="center", vertical="center"))
#sheet['A{0}'.format(row)] = "" # sheet['A{0}'.format(row)] = ""
sheet.row_dimensions[row].height = 5 sheet.row_dimensions[row].height = 5
row += 1 row += 1
f = data["Frame"][i] f = data["frame"]
group_from = row group_from = row
sheet['A{0}'.format(row)] = data['Frame'][i] sheet['A{0}'.format(row)] = data['frame']
sheet['B{0}'.format(row)] = data['Pole'][i] sheet['B{0}'.format(row)] = data['number']
sheet['C{0}'.format(row)] = data['PoleType'][i] sheet['C{0}'.format(row)] = data['type']
sheet['D{0}'.format(row)] = round(data['Center'][i].x, 0) * scale sheet['D{0}'.format(row)] = round(data['center'].x, 0) * scale
sheet['D{0}'.format(row)].number_format = "0.000" sheet['D{0}'.format(row)].number_format = "0.000"
sheet['E{0}'.format(row)] = round(data['Center'][i].y, 0) * scale sheet['E{0}'.format(row)] = round(data['center'].y, 0) * scale
sheet['E{0}'.format(row)].number_format = "0.000" sheet['E{0}'.format(row)].number_format = "0.000"
try: try:
sheet['F{0}'.format(row)] = round(data['Soil'][i].z, 0) * scale sheet['F{0}'.format(row)] = round(data['terrain_z'].z, 0) * scale
sheet['F{0}'.format(row)].number_format = "0.000" sheet['F{0}'.format(row)].number_format = "0.000"
except: except:
pass pass
sheet['G{0}'.format(row)] = round(data['Head'][i]) * scale sheet['G{0}'.format(row)] = round(data['head_z']) * scale
sheet['G{0}'.format(row)].number_format = "0.000" sheet['G{0}'.format(row)].number_format = "0.000"
sheet['H{0}'.format(row)] = data["PoleLength"][i] * scale sheet['H{0}'.format(row)] = data["length"] * scale
sheet['H{0}'.format(row)].number_format = "0.000" sheet['H{0}'.format(row)].number_format = "0.000"
sheet['I{0}'.format(row)] = '=G{0}-F{0}'.format(row) sheet['I{0}'.format(row)] = '=G{0}-F{0}'.format(row)
sheet['I{0}'.format(row)].number_format = "0.000" sheet['I{0}'.format(row)].number_format = "0.000"
@@ -250,7 +266,7 @@ def spreadsheetBOQPoles(sheet, sel):
style_range(sheet, 'A' + str(row) + ':J' + str(row), style_range(sheet, 'A' + str(row) + ':J' + str(row),
border=Border(top=thin, left=thin, right=thin, bottom=thin), border=Border(top=thin, left=thin, right=thin, bottom=thin),
font=Font(name='Quicksand', size=11,), font=Font(name='Quicksand', size=11, ),
alignment=Alignment(horizontal="center", vertical="center")) alignment=Alignment(horizontal="center", vertical="center"))
row += 1 row += 1
@@ -302,37 +318,39 @@ class CommandBOQMechanical:
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Calcular el BOQ de la")} 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Calcular el BOQ de la")}
def Activated(self): def Activated(self):
# make file global: doc = FreeCAD.ActiveDocument
#sel = FreeCAD.ActiveDocument.findObjects(Name="Tracker")
sel = []
for obj in FreeCAD.ActiveDocument.Objects:
'''if not hasattr(obj, "Proxy"):
continue
if issubclass(obj.Proxy.__class__, PVPlantRack.Frame):
objects.append(obj)'''
if obj.Name.startswith("Tracker") and not obj.Name.startswith("TrackerSetup"):
sel.append(obj)
sel = sorted(sel, key=lambda x: x.Label)
if len(sel) > 0:
path = os.path.dirname(FreeCAD.ActiveDocument.FileName)
filename = os.path.join(path, "BOQMechanical.xlsx")
mywb = openpyxl.Workbook()
sheet = mywb.active
sheet.title = 'Frames information'
spreadsheetBOQFrames(sheet, sel)
sheet = mywb.create_sheet("Poles information") if not doc or not doc.FileName:
spreadsheetBOQPoles(sheet, sel) FreeCAD.Console.PrintError("Document must be saved first\n")
mywb.save(filename) return
print("Se ha generado el BOQMechanical: ") try:
print(filename) sel = [obj for obj in FreeCAD.ActiveDocument.Objects if (hasattr(obj, "Proxy") and (obj.Proxy.Type == "Tracker"))]
'''import sys
path = r'C:\Program Files (x86)\IronPython 2.7\Lib'
sys.path.append(path)'''
import subprocess if not sel:
subprocess.Popen('explorer ' + path) FreeCAD.Console.PrintWarning("No valid trackers found\n")
return
path = os.path.dirname(doc.FileName)
filename = os.path.join(path, "BOQ_Mechanical.xlsx")
sel = sorted(sel, key=lambda x: x.Label)
wb = openpyxl.Workbook()
wb.remove(wb.active) # Remove default sheet
frames_sheet = wb.create_sheet("Frames Information")
spreadsheetBOQFrames(frames_sheet, sel)
poles_sheet = wb.create_sheet("Poles information")
spreadsheetBOQPoles(poles_sheet , sel)
wb.save(filename)
FreeCAD.Console.PrintMessage(f"Report generated: {filename}\n")
open_file(path)
except Exception as e:
FreeCAD.Console.PrintError(f"Error generating BOQ: {str(e)}\n")
def IsActive(self): def IsActive(self):
if FreeCAD.ActiveDocument: if FreeCAD.ActiveDocument:
@@ -340,5 +358,3 @@ class CommandBOQMechanical:
else: else:
return False return False
'''if FreeCAD.GuiUp:
FreeCADGui.addCommand('BOQMechanical', CommandBOQMechanical())'''

View File

@@ -20,47 +20,32 @@
# * * # * *
# *********************************************************************** # ***********************************************************************
import FreeCAD, Draft import FreeCAD
import PVPlantSite import Draft
import copy import os
import platform
import subprocess
import openpyxl
from openpyxl.styles import Alignment, Border, Side, Font
from PVPlantPlacement import getCols
if FreeCAD.GuiUp: if FreeCAD.GuiUp:
from DraftTools import translate from DraftTools import translate
from PySide.QtCore import QT_TRANSLATE_NOOP from PySide.QtCore import QT_TRANSLATE_NOOP
import Part
import pivy
from pivy import coin
import os
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
__title__ = "PVPlant Trench" __title__ = "PVPlant Trench"
__author__ = "Javier Braña" __author__ = "Javier Braña"
__url__ = "http://www.sogos-solar.com" __url__ = "http://www.sogos-solar.com"
from PVPlantResources import DirIcons as DirIcons
from PVPlantResources import DirDocuments as DirDocuments
'''import os
import platform
import subprocess
def open_file(path): def open_file(path):
"""Open a file or directory using the default system handler"""
if platform.system() == "Windows": if platform.system() == "Windows":
os.startfile(path) os.startfile(path)
elif platform.system() == "Darwin": elif platform.system() == "Darwin":
subprocess.Popen(["open", path]) subprocess.Popen(["open", path])
else: else:
subprocess.Popen(["xdg-open", path])''' subprocess.Popen(["xdg-open", path])
from PVPlantPlacement import getCols from PVPlantPlacement import getCols

View File

@@ -20,4 +20,5 @@ pyproj~=3.7.1
simplekml~=1.3.6 simplekml~=1.3.6
geojson~=3.1.0 geojson~=3.1.0
certifi~=2023.11.17 certifi~=2023.11.17
SciPy~=1.11.4 SciPy~=1.11.4
ezdxf~=1.4.1