From d2e02c8ab3f926a77a149e9fdd271bd9e5ffddb8 Mon Sep 17 00:00:00 2001 From: Sigma Dimensions <79010126+myoualid@users.noreply.github.com> Date: Wed, 15 Feb 2023 23:09:15 +0100 Subject: [PATCH] complete cost schedule exports commit 9dc69f3 --- src/blenderbim/blenderbim/core/tool.py | 5 +- src/blenderbim/blenderbim/tool/cost.py | 24 ++ src/ifc5d/ifc5d/ifc5Dspreadsheet.py | 438 +++++++++++++++++++++++++ 3 files changed, 465 insertions(+), 2 deletions(-) create mode 100644 src/ifc5d/ifc5d/ifc5Dspreadsheet.py diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index b4ae6da6cd..94221f45dc 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -148,15 +148,16 @@ class Cost: def expand_cost_item_rate(cls, cost_item): pass def expand_cost_item(cls, cost_item): pass def expand_cost_items(cls): pass + def export_cost_schedules(cls, format): pass def get_active_cost_item(cls): pass def get_all_nested_cost_items(cls, cost_item): pass + def get_attributes_for_cost_value(cls, cost_type, cost_category): pass def get_cost_item_attributes(cls, cost_item): pass def get_cost_item_products(cls): pass def get_cost_item_quantity_attributes(cls): pass def get_cost_item_value_formula(cls): pass def get_cost_schedule_attributes(cls): pass def get_cost_schedule_products(cls, cost_schedule): pass - def get_attributes_for_cost_value(cls, cost_type, cost_category): pass def get_cost_value_attributes(cls): pass def get_cost_value_unit_component(cls): pass def get_direct_cost_item_products(cls): pass @@ -165,6 +166,7 @@ class Cost: def get_products(cls, related_object_type): pass def get_root_cost_items(cls, cost_schedule): pass def get_schedule_cost_items(cls, cost_schedule): pass + def get_units(cls):pass def import_cost_schedule_csv(cls, file_path, is_schedule_of_rates): pass def is_active_schedule_of_rates(cls): pass def load_cost_item_attributes(cls): pass @@ -177,7 +179,6 @@ class Cost: def load_schedule_of_rates_tree(cls, schedule_of_rates): pass def play_chaching_sound(cls): pass def remove_cost_column(cls, name): pass - def export_cost_schedules(cls, format): pass @interface class Debug: diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index 114600cc17..cb93868ad4 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -520,3 +520,27 @@ class Cost(blenderbim.core.tool.Cost): writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path + ".xlsx") writer.write() + @classmethod + def get_units(cls): + units = {} + for unit in tool.Ifc.get().by_type("IfcNamedUnit"): + if unit.get_info().get("UnitType", None) in [ + "AREAUNIT", + "LENGTHUNIT", + "TIMEUNIT", + "VOLUMEUNIT", + "MASSUNIT", + "USERDEFINED", + ]: + units[unit.id()] = cls.format_unit(unit) + return units + + @staticmethod + def format_unit(unit): + if unit.is_a("IfcContextDependentUnit"): + return f"{unit.UnitType} / {unit.Name}" + else: + name = unit.Name + if unit.get_info().get("Prefix", None): + name = f"{unit.Prefix} {name}" + return f"{unit.UnitType} / {name}" diff --git a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py new file mode 100644 index 0000000000..0c12c98afd --- /dev/null +++ b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py @@ -0,0 +1,438 @@ +# Ifc5D - Extract Cost Data from IFC to spreadsheets +# Copyright (C) 2019, 2020, 2021 Dion Moult , Yassine Oualid +# +# This file is part of Ifc5D. +# +# Ifc5D is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ifc5D is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Ifc5D. If not, see . + + +import os +import time +import argparse +import datetime +import logging +import ifcopenshell +import ifcopenshell.util.element +import ifcopenshell.util.cost + + +class IfcDataGetter: + @staticmethod + def get_schedules(file): + if not file: + return None + return file.by_type("IfcCostSchedule") + + @staticmethod + def get_root_costs(cost_schedule): + return [obj for rel in cost_schedule.Controls or [] for obj in rel.RelatedObjects or []] + + @staticmethod + def get_cost_item_values(cost_item=None): + if not cost_item: + return None + values = [] + for cost_value in cost_item.CostValues or []: + name = cost_value.Name + applied_value = ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value) + unit = IfcDataGetter.get_cost_value_unit(cost_value) + values.append( + { + "id": cost_value.id(), + "name": name, + "applied_value": float(applied_value) if applied_value else 0.0, + "unit": unit if unit else "", + "category": cost_value.Category or "General", + } + ) + return values + + @staticmethod + def process_categories(cost_item, categories): + for cost_value in cost_item.CostValues or []: + if cost_value.Category: + categories.add("{}{}".format(cost_value.Category, " Cost")) + return categories + + @staticmethod + def process_cost_item_categories(cost_item, categories): + IfcDataGetter.process_categories(cost_item, categories) + for rel in cost_item.IsNestedBy or []: + for child in rel.RelatedObjects or []: + IfcDataGetter.process_cost_item_categories(child, categories) + return categories + + @staticmethod + def get_cost_rates_categories(schedule): + categories = set() + for cost_item in IfcDataGetter.get_root_costs(schedule): + IfcDataGetter.process_cost_item_categories(cost_item, categories) + print("COST CATEGORIES", categories) + return categories + + @staticmethod + def process_cost_data(file, cost_item, cost_items_data, index, hierarchy="1"): + def listToString(s): + return ", ".join([str(i) for i in s]) + + quantity_data = IfcDataGetter.get_cost_item_quantity(file, cost_item) + cost_values_data = IfcDataGetter.get_cost_item_values(cost_item) + children = [obj for rel in cost_item.IsNestedBy or [] for obj in rel.RelatedObjects or []] + data = { + "Index": index, + "Hierarchy": hierarchy, + "Id": cost_item.id(), + "Identification": cost_item.Identification, + "Description": cost_item.Name, + "Unit": cost_values_data[0]["unit"] if cost_values_data else "", + "Quantity": quantity_data["quantity"]["total_quantity"], + # "Total": str(quantity_data["total_quantity"] * cost_values_data[0]["applied_value"] if cost_values_data else 0), + # "Children": listToString(children), + "ChildrenData": [], + } + for cost_value in cost_values_data or []: + cost_category = "{}{}".format(cost_value["category"], " Cost") + data[cost_category] = cost_value["applied_value"] + if data.get("* Cost", None): + data["Total"] = data["* Cost"] + data["* Cost"] = "" + rate_subtotal = 0 + for key, value in data.items(): + if "Cost" in key and not "*" in key: + rate_subtotal += value + + data["Rate Subtotal"] = rate_subtotal + + cost_items_data.append(data) + + index += 1 + child_hierarchy = hierarchy + ".1" + for nested_cost in [obj for rel in cost_item.IsNestedBy or [] for obj in rel.RelatedObjects or []]: + IfcDataGetter.process_cost_data(file, nested_cost, cost_items_data, index, child_hierarchy) + child_hierarchy = ( + ".".join(child_hierarchy.split(".")[:-1]) + "." + str(int(child_hierarchy.split(".")[-1]) + 1) + ) + + @staticmethod + def get_cost_items_data(file, schedule): + cost_items_data = [] + index = 0 + for cost_item in IfcDataGetter.get_root_costs(schedule): + IfcDataGetter.process_cost_data(file, cost_item, cost_items_data, index) + return cost_items_data + + @staticmethod + def format_unit(unit): + if unit.is_a("IfcContextDependentUnit"): + return f"{unit.UnitType} / {unit.Name}" + else: + name = unit.Name + if unit.get_info().get("Prefix", None): + name = f"{unit.Prefix} {name}" + return f"{unit.UnitType} / {name}" + + @staticmethod + def get_cost_value_unit(cost_value=None): + if not cost_value: + return None + unit = cost_value.UnitBasis + if not unit: + return None + return IfcDataGetter.format_unit(unit.UnitComponent) + + @staticmethod + def get_cost_item_quantity(file, cost_item=None): + # TODO: handle multiple quantities, THOSE WHHICH ARE JUYST ASSIGNED TO THE COST ITEM DIRECTLY, NOT THROUGH OBJECTS. + def add_quantity(quantity, take_off_name): + accounted_for.append(quantity) + if take_off_name == "": + take_off_name = quantity[0] + if quantity[0] != take_off_name: + take_off_name = "mixed-takeoff-quantities" + return quantity[3] + + if not cost_item: + return None + take_off_name = "" + has_changed_name = False + total_cost_quantity = 0 + accounted_for = [] + for rel in cost_item.Controls or []: + for related_object in rel.RelatedObjects: + qtos = ifcopenshell.util.element.get_psets(related_object, qtos_only=True) + for quantities in qtos.values() or []: + qto = file.by_id(quantities["id"]) + for quantity in qto.Quantities: + if not quantity in cost_item.CostQuantities: + continue + total_cost_quantity += add_quantity(quantity, take_off_name) + accounted_for.append(quantity) + + for quantity in cost_item.CostQuantities or []: + if not quantity in accounted_for: + total_cost_quantity += add_quantity(quantity, take_off_name) + print(cost_item.Name, quantity[0], quantity[3], "was not accounted for parametrically") + + print("Total Cost Quantity Including Non Parametric", total_cost_quantity) + return { + "id": cost_item.id(), + "name": cost_item.Name, + "quantity": {"take_off_name": take_off_name, "total_quantity": total_cost_quantity}, + } + + +class Ifc5Dwriter: + def __init__(self, file=None, output=None): + self.output = output + if isinstance(file, str): + self.file = ifcopenshell.open(file) + else: + self.file = file + self.cost_schedules = [] + self.sheet_data = {} + self.column_indexes = [] + self.colours = { + 0: "0839C2", # 1st Row - Dark Blue + 1: "266EF6", # Internal reference + 2: "47C9FF", # External reference + 3: "82E9FF", # Optional + 4: "B8F2FF", # Secondary information + 5: "DAECF5", # Project specific + 6: "000000", # Not used + 7: "fed8b1", # 2nd Row - Light Orange + } + + def parse(self): + self.column_indexes = [] + for i in range(26): + self.column_indexes.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ"[i % 26]) + for cost_schedule in self.cost_schedules: + sheet_id = cost_schedule.id() + self.sheet_data[sheet_id] = {} + self.sheet_data[sheet_id]["headers"] = [ + "Id", + "Hierarchy", + "Index", + "Identification", + "Description", + "Quantity", + "Unit", + ] + cost_rate_categories = IfcDataGetter.get_cost_rates_categories(cost_schedule) + self.sheet_data[sheet_id]["headers"].extend(list(cost_rate_categories)) + self.sheet_data[sheet_id]["headers"].extend(["General Cost", "Rate Subtotal", "Total", "Children"]) + self.sheet_data[sheet_id]["cost_items"] = IfcDataGetter.get_cost_items_data(self.file, cost_schedule) + + print(self.sheet_data) + + def multiply_cells(self, cell1, cell2): + return "={}*{}".format(cell1, cell2) + + def sum_cells(self, list_of_cells): + return "=SUM({})".format(",".join(list_of_cells)) + + def get_cell_position(self, schedule_id, attribute): + def get_position_in_list(item, item_list): + try: + return item_list.index(item) + except ValueError: + return -1 + + if self.sheet_data and self.sheet_data[schedule_id]: + attribute = get_position_in_list(attribute, self.sheet_data[schedule_id]["headers"]) + return "{}{}".format(self.column_indexes[attribute], self.row_count) + + def write(self): + self.cost_schedules = IfcDataGetter.get_schedules(self.file) + self.sheet_data = {} + self.parse() + + +class Ifc5DCsvWriter(Ifc5Dwriter): + def write(self): + import csv + + super().write() + for sheet, data in self.sheet_data.items(): + print("-------------------------------------") + print(sheet, data) + print("-------------------------------------") + with open(os.path.join(self.output, "{}.csv".format(sheet)), "w", newline="", encoding="utf-8") as file: + writer = csv.writer(file) + writer.writerow(data["headers"]) + row = [] + for cost_item_data in data["cost_items"]: + print(cost_item_data) + writer.writerow([cost_item_data.get(column, "") for column in data["headers"]]) + + +class Ifc5DOdsWriter(Ifc5Dwriter): + def write(self): + from odf.opendocument import OpenDocumentSpreadsheet + from odf.style import Style, TableCellProperties + from odf.number import NumberStyle, CurrencyStyle, CurrencySymbol, Number, Text + + super().write() + + self.doc = OpenDocumentSpreadsheet() + + for value in self.colours.values(): + style = Style(name=value, family="table-cell") + bg_color = TableCellProperties(backgroundcolor="#" + value) + style.addElement(bg_color) + self.doc.styles.addElement(style) + + ns1 = CurrencyStyle(name="positive-AUD", volatile="true") + ns1.addElement(CurrencySymbol(language="en", country="AU", text="$")) + ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true")) + self.doc.styles.addElement(ns1) + + for cost_schedule in self.cost_schedules: + self.write_table(cost_schedule) + + self.doc.save(self.output, True) + + def write_table(self, cost_schedule): + from odf.table import Table, TableRow, TableCell + from odf.text import P + from odf.number import NumberStyle, CurrencyStyle, CurrencySymbol, Number, Text + + def row(): + return TableRow() + + def add_cell(type, value=None, row=None, style=None): + if type == "currency": + cell = TableCell(valuetype="currency", stylename=style) + cell.addElement(Number(numberstyle="Currency", value=value)) + cell.addElement(CurrencySymbol(currency="USD")) + cell.setAttribute("number:currency-style", "USD") + elif type == "number": + cell = TableCell(valuetype="float", stylename=style) + cell.addElement(Number(numberstyle="Number", value=value)) + elif type == "text": + cell = TableCell(valuetype="string", stylename=style) + cell.addElement(P(text=value)) + elif type == "formula": + cell = TableCell(formula=value, stylename=style) + row.addElement(cell) + + def add_cost_item_rows(table, cost_data): + row = TableRow() + self.row_count += 1 + + for i, column in enumerate(self.sheet_data[cost_schedule.id()]["headers"]): + if column == "Total" and cost_data["Quantity"] != 0 and cost_data["Rate Subtotal"]: + cell_quantity = self.get_cell_position(cost_schedule.id(), "Quantity") + cell_subtotal_rate = self.get_cell_position(cost_schedule.id(), "Rate Subtotal") + value = self.multiply_cells(cell_quantity, cell_subtotal_rate) + cell = TableCell(formula=value, stylename=self.colours.get(cost_data["Index"])) + else: + value = cost_data.get(column, "") + cell = TableCell(valuetype="string", stylename=self.colours.get(cost_data["Index"])) + cell.addElement(P(text=value)) + # TODO:FIX QUANTITY AND COST TO SHOW AS NUMBERS AND CURRENCIES + # elif "Cost" in column or "Rate" in column: + # value = cost_data.get(column, "") + # cell = TableCell(valuetype="string", stylename=self.colours.get(cost_data["Index"])) + # cell.addElement(P(text=value)) + # # cell.addElement(P(text=u"${}".format(value))) # The current displayed value + # print("Should add rate ", "${}".format(value)) + # elif "Quantity" in column: + # value = cost_data.get(column, "") + # cell = TableCell(valuetype="float", stylename=self.colours.get(cost_data["Index"])) + # print("Should add quantity",value) + # cell.addElement(P(text=value)) + row.addElement(cell) + table.addElement(row) + + table = Table(name=cost_schedule.Name) + + new = row() + add_cell(type="text", value="Cost Schedule:", row=new, style="fed8b1") + add_cell(type="text", value=cost_schedule.Name or "Unnamed Cost Schedule", row=new, style="fed8b1") + table.addElement(new) + + header_row = TableRow() + print("currently data is ", self.sheet_data) + for header in self.sheet_data[cost_schedule.id()]["headers"]: + add_cell(type="text", value=header, row=header_row, style="fed8b1") + table.addElement(header_row) + + new = row() + new2 = row() + table.addElement(new) + table.addElement(new2) + + self.row_count = 4 + for cost_item_data in self.sheet_data[cost_schedule.id()]["cost_items"]: + add_cost_item_rows(table, cost_item_data) + + self.doc.spreadsheet.addElement(table) + +class Ifc5DXlsxWriter(Ifc5Dwriter): + def write(self): + import xlsxwriter + super().write() + + self.workbook = xlsxwriter.Workbook(self.output) + self.used_names = [] + for cost_schedule in self.cost_schedules: + self.write_table(cost_schedule) + self.workbook.close() + + def write_table(self, cost_schedule): + schedule_name = cost_schedule.Name or "Unnamed Cost Schedule" + if schedule_name in self.used_names: + schedule_name = "{}_{}".format(schedule_name, self.used_names.count(schedule_name)) + worksheet = self.workbook.add_worksheet(schedule_name) + self.used_names.append(schedule_name) + + headers = self.sheet_data[cost_schedule.id()]["headers"] + for i, header in enumerate(headers): + worksheet.write(0, i, header) + + row = 1 + for cost_item_data in self.sheet_data[cost_schedule.id()]["cost_items"]: + col = 0 + for header in headers: + worksheet.write(row, col, cost_item_data.get(header, "")) + col += 1 + row += 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("input", type=str, help="Specify an IFC file to process") + parser.add_argument("output", help="The output directory for CSV or filename for other formats") + parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log") + parser.add_argument( + "-f", "--format", type=str, help="Choose which format to export in (csv/ods/xlsx)", default="csv" + ) + args = vars(parser.parse_args()) + + print("Processing IFC file...") + start = time.time() + logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG) + logger = logging.getLogger(__name__) + logger.info("Starting conversion and Generating report") + if args["format"] == "CSV": + writer = Ifc5DCsvWriter(args["input"], args["output"]) + elif args["format"] == "ODS": + writer = Ifc5DOdsWriter(args["input"], args["output"]) + elif args["format"] == "XLSX": + writer = Ifc5DXlsxWriter(args["input"], args["output"]) + writer.write() + + logger.info("Finished conversion in %ss", time.time() - start) + logger.info("Conversion and report generation complete")