complete cost schedule exports commit 9dc69f3

This commit is contained in:
Sigma Dimensions
2023-02-15 23:09:15 +01:00
parent 2d4e4d2445
commit d2e02c8ab3
3 changed files with 465 additions and 2 deletions
+3 -2
View File
@@ -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:
+24
View File
@@ -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}"
+438
View File
@@ -0,0 +1,438 @@
# Ifc5D - Extract Cost Data from IFC to spreadsheets
# Copyright (C) 2019, 2020, 2021 Dion Moult <dion@thinkmoult.com>, Yassine Oualid <yassine@sigmadimensions.com>
#
# 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 <http://www.gnu.org/licenses/>.
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")