From 5dd8869caf843f7ba51d4422a26cc8234b7944ea Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 8 May 2025 08:29:11 +0200 Subject: [PATCH] update --- Export/PVPlantBOQMechanical.py | 346 +++++++++++++++++---------------- Export/layoutToExcel.py | 37 ++-- requirements.txt | 3 +- 3 files changed, 194 insertions(+), 192 deletions(-) diff --git a/Export/PVPlantBOQMechanical.py b/Export/PVPlantBOQMechanical.py index b392405..a70cd73 100644 --- a/Export/PVPlantBOQMechanical.py +++ b/Export/PVPlantBOQMechanical.py @@ -20,32 +20,43 @@ # * * # *********************************************************************** +import os +import platform +import subprocess +from typing import Dict, List 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 -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 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: thin = Side(border_style="thin", color="7DA4B8") 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 + +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): """ 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: 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 - sheet.column_dimensions['B'].width = 30 - sheet.column_dimensions['C'].width = 20 - sheet.column_dimensions['D'].width = 20 - sheet.column_dimensions['E'].width = 20 - sheet.column_dimensions['F'].width = 20 - sheet.column_dimensions['G'].width = 15 - sheet.column_dimensions['H'].width = 15 - sheet.column_dimensions['I'].width = 15 - sheet.row_dimensions[1].height = 40 +def create_sheet_headers(sheet: Worksheet, headers: List[str], widths: Dict[str, float]) -> None: + """Create standardized sheet headers.""" + for col, header in enumerate(headers, start=1): + cell = sheet.cell(row=1, column=col, value=header) + cell.fill = HEADER_FILL + cell.font = HEADER_FONT + cell.alignment = CENTER_ALIGN - style_range(sheet, 'A1:I1', - border=Border(top=thin, left=thin, right=thin, bottom=thin), - fill=PatternFill("solid", fgColor="7DA4B8"), - font=Font(name='Quicksand', size=10, b=True, color="FFFFFF"), - alignment=Alignment(horizontal="center", vertical="center")) + for col, width in widths.items(): + sheet.column_dimensions[col].width = width - for ind in range(0, len(sel)): - row = ind + 2 - sheet['A{0}'.format(row)] = ind + 1 - sheet['B{0}'.format(row)] = sel[ind].Label - sheet['C{0}'.format(row)] = sel[ind].Setup.Label - sheet['D{0}'.format(row)] = sel[ind].Placement.Base.x * scale - 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")) + sheet.row_dimensions[1].height = 30 + style_range(sheet, f'A1:{chr(64 + len(headers))}1', + border=THIN_BORDER, + fill=HEADER_FILL, + font=HEADER_FONT, + alignment=CENTER_ALIGN) -def spreadsheetBOQPoles(sheet, sel): - import MeshPart as mp - from Mechanical.Frame import PVPlantFrame - # Data: - terrain = PVPlantSite.get().Terrain.Mesh # Shape +def spreadsheetBOQFrames(sheet: Worksheet, selection: List[FreeCAD.DocumentObject]) -> None: + """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: - 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' + create_sheet_headers(sheet, headers, widths) - sheet.column_dimensions['A'].width = 30 - sheet.column_dimensions['B'].width = 8 - sheet.column_dimensions['C'].width = 20 - sheet.column_dimensions['D'].width = 20 - sheet.column_dimensions['E'].width = 20 - sheet.column_dimensions['F'].width = 20 - sheet.column_dimensions['G'].width = 20 - sheet.column_dimensions['H'].width = 20 - sheet.column_dimensions['I'].width = 20 - sheet.column_dimensions['J'].width = 20 - sheet.row_dimensions[1].height = 40 - style_range(sheet, 'A1:J1', - border=Border(top=thin, left=thin, right=thin, bottom=thin), - fill=PatternFill("solid", fgColor="7DA4B8"), - font=Font(name='Quicksand', size=11, b=True, color="FFFFFF"), - alignment=Alignment(horizontal="center", vertical="center")) - sheet['A2'] = "" + for idx, obj in enumerate(selection, start=2): + placement = obj.Placement + sheet[f'A{idx}'] = idx - 1 + sheet[f'B{idx}'] = obj.Label + sheet[f'C{idx}'] = obj.Setup.Label + sheet[f'D{idx}'] = placement.Base.x * SCALE + sheet[f'E{idx}'] = placement.Base.y * SCALE + sheet[f'F{idx}'] = placement.Base.z * SCALE + sheet[f'G{idx}'] = placement.Rotation.toEuler()[0] + sheet[f'H{idx}'] = placement.Rotation.toEuler()[1] + sheet[f'I{idx}'] = obj.Setup.NumberPole.Value + + style_range(sheet, f'A{idx}:I{idx}', + border=THIN_BORDER, + font=DATA_FONT, + alignment=CENTER_ALIGN) + +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 - data = {"Frame": [], - #"FrameType": [], - "Pole": [], - "PoleType": [], - "PoleLength": [], - "Center": [], - "Head": []} - cnt = 0 - for frame_ind, frame in enumerate(sel): - poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes - numpoles = int(frame.Setup.NumberPole.Value) - seq = frame.Setup.PoleSequence - 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 + import MeshPart as mp + from Mechanical.Frame import PVPlantFrame + # Data: + terrain = PVPlantSite.get().Terrain.Mesh # Shape + poles_data = [] - pts = mp.projectPointsOnMesh(data["Center"], terrain, FreeCAD.Vector(0, 0, 1)) - #if cnt == len(pts): - data["Soil"] = pts + for frame in selection: + try: + 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 group_from = row - f = data["Frame"][0] - for i in range(0, len(data["Frame"])): - if f != data["Frame"][i]: + print(poles_data[0]) + f = poles_data[0]['frame'] + for i, data in enumerate(poles_data): + if f != data["frame"]: style_range(sheet, 'A' + str(group_from) + ':F' + str(row - 1), border=Border(top=thin, left=thin, right=thin, bottom=thin), font=Font(name='Quicksand', size=11, ), @@ -221,27 +237,27 @@ def spreadsheetBOQPoles(sheet, sel): border=Border(top=thin, left=thin, right=thin, bottom=thin), font=Font(name='Quicksand', size=11, ), alignment=Alignment(horizontal="center", vertical="center")) - #sheet['A{0}'.format(row)] = "" + # sheet['A{0}'.format(row)] = "" sheet.row_dimensions[row].height = 5 row += 1 - f = data["Frame"][i] + f = data["frame"] group_from = row - sheet['A{0}'.format(row)] = data['Frame'][i] - sheet['B{0}'.format(row)] = data['Pole'][i] - sheet['C{0}'.format(row)] = data['PoleType'][i] - sheet['D{0}'.format(row)] = round(data['Center'][i].x, 0) * scale + sheet['A{0}'.format(row)] = data['frame'] + sheet['B{0}'.format(row)] = data['number'] + sheet['C{0}'.format(row)] = data['type'] + sheet['D{0}'.format(row)] = round(data['center'].x, 0) * scale 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" 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" except: 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['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['I{0}'.format(row)] = '=G{0}-F{0}'.format(row) 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), 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")) row += 1 @@ -302,37 +318,39 @@ class CommandBOQMechanical: 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Calcular el BOQ de la")} def Activated(self): - # make file global: - #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) + doc = FreeCAD.ActiveDocument - sheet = mywb.create_sheet("Poles information") - spreadsheetBOQPoles(sheet, sel) - mywb.save(filename) + if not doc or not doc.FileName: + FreeCAD.Console.PrintError("Document must be saved first\n") + return - print("Se ha generado el BOQMechanical: ") - print(filename) - '''import sys - path = r'C:\Program Files (x86)\IronPython 2.7\Lib' - sys.path.append(path)''' + try: + sel = [obj for obj in FreeCAD.ActiveDocument.Objects if (hasattr(obj, "Proxy") and (obj.Proxy.Type == "Tracker"))] - import subprocess - subprocess.Popen('explorer ' + path) + if not sel: + 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): if FreeCAD.ActiveDocument: @@ -340,5 +358,3 @@ class CommandBOQMechanical: else: return False -'''if FreeCAD.GuiUp: - FreeCADGui.addCommand('BOQMechanical', CommandBOQMechanical())''' diff --git a/Export/layoutToExcel.py b/Export/layoutToExcel.py index 824fc8b..a084f59 100644 --- a/Export/layoutToExcel.py +++ b/Export/layoutToExcel.py @@ -20,47 +20,32 @@ # * * # *********************************************************************** -import FreeCAD, Draft -import PVPlantSite -import copy +import FreeCAD +import Draft +import os +import platform +import subprocess +import openpyxl +from openpyxl.styles import Alignment, Border, Side, Font + +from PVPlantPlacement import getCols if FreeCAD.GuiUp: from DraftTools import translate 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" __author__ = "Javier Braña" __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): + """Open a file or directory using the default system handler""" if platform.system() == "Windows": os.startfile(path) elif platform.system() == "Darwin": subprocess.Popen(["open", path]) else: - subprocess.Popen(["xdg-open", path])''' + subprocess.Popen(["xdg-open", path]) from PVPlantPlacement import getCols diff --git a/requirements.txt b/requirements.txt index 55b0dcb..8bb799c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,5 @@ pyproj~=3.7.1 simplekml~=1.3.6 geojson~=3.1.0 certifi~=2023.11.17 -SciPy~=1.11.4 \ No newline at end of file +SciPy~=1.11.4 +ezdxf~=1.4.1 \ No newline at end of file