From 6ee0027f33c4b325676ec5c211880c2a604a5c4d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 16 Aug 2021 15:07:09 +1000 Subject: [PATCH] Import of CSV-based schedule of rates is now possible --- .../blenderbim/bim/module/cost/operator.py | 23 +++++-- .../blenderbim/bim/module/cost/ui.py | 2 +- .../blenderbim/bim/module/unit/ui.py | 2 +- src/ifc5d/ifc5d/csv2ifc.py | 55 ++++++++++++---- src/ifc5d/rates.csv | 14 ++++ src/ifc5d/{test.csv => schedule.csv} | 0 .../ifcopenshell/api/cost/edit_cost_value.py | 13 ++-- .../ifcopenshell/util/unit.py | 64 ++++++++++++++----- 8 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 src/ifc5d/rates.csv rename src/ifc5d/{test.csv => schedule.csv} (100%) diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 766c9941bc..08563d8c14 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -592,11 +592,21 @@ class EnableEditingCostItemValue(bpy.types.Operator): prop.is_null = prop.is_optional = False units = {} for unit_id, unit in UnitData.units.items(): - if unit.get("UnitType", None) in ["AREAUNIT", "LENGTHUNIT", "TIMEUNIT", "VOLUMEUNIT", "MASSUNIT"]: - name = unit["Name"] - if unit.get("Prefix", None): - name = f"(unit['Prefix']) {name}" - units[unit_id] = f"{unit['UnitType']} / {name}" + if unit.get("UnitType", None) in [ + "AREAUNIT", + "LENGTHUNIT", + "TIMEUNIT", + "VOLUMEUNIT", + "MASSUNIT", + "USERDEFINED", + ]: + if unit["type"] == "IfcContextDependentUnit": + units[unit_id] = f"{unit['UnitType']} / {unit['Name']}" + else: + name = unit["Name"] + if unit.get("Prefix", None): + name = f"(unit['Prefix']) {name}" + units[unit_id] = f"{unit['UnitType']} / {name}" prop.enum_items = json.dumps(units) if data["UnitBasis"] and data["UnitBasis"]["UnitComponent"]: prop.enum_value = str(data["UnitBasis"]["UnitComponent"]) @@ -719,6 +729,7 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper): bl_options = {"REGISTER", "UNDO"} filename_ext = ".csv" filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False) def execute(self, context): from ifc5d.csv2ifc import Csv2Ifc @@ -728,8 +739,10 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper): csv2ifc = Csv2Ifc() csv2ifc.csv = self.filepath csv2ifc.file = self.file + csv2ifc.is_schedule_of_rates = self.is_schedule_of_rates csv2ifc.execute() Data.load(IfcStore.get_file()) + UnitData.load(IfcStore.get_file()) print("Import finished in {:.2f} seconds".format(time.time() - start)) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 779d329d4f..4170f025b9 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -223,7 +223,7 @@ class BIM_PT_cost_schedules(Panel): op.cost_value = cost_value_id def draw_editable_cost_value_ui(self, layout, cost_value): - draw_attributes(self.props.cost_value_attributes, self.layout) + draw_attributes(self.props.cost_value_attributes, layout) class BIM_PT_cost_item_quantities(Panel): bl_label = "IFC Cost Item Quantities" diff --git a/src/blenderbim/blenderbim/bim/module/unit/ui.py b/src/blenderbim/blenderbim/bim/module/unit/ui.py index 7b904a4b2d..d091e8bf2e 100644 --- a/src/blenderbim/blenderbim/bim/module/unit/ui.py +++ b/src/blenderbim/blenderbim/bim/module/unit/ui.py @@ -24,7 +24,7 @@ class BIM_PT_units(Panel): self.props = context.scene.BIMUnitProperties row = self.layout.row(align=True) - row.label(text="{} Units Found".format(len(Data.unit_assignment)), icon="SNAP_GRID") + row.label(text="{} Units Found".format(len(Data.units)), icon="SNAP_GRID") if self.props.is_editing: row.operator("bim.disable_unit_editing_ui", text="", icon="CANCEL") else: diff --git a/src/ifc5d/ifc5d/csv2ifc.py b/src/ifc5d/ifc5d/csv2ifc.py index 656f7716dc..2e362d302e 100644 --- a/src/ifc5d/ifc5d/csv2ifc.py +++ b/src/ifc5d/ifc5d/csv2ifc.py @@ -28,6 +28,8 @@ class Csv2Ifc: self.file = None self.cost_items = [] self.cost_schedule = None + self.is_schedule_of_rates = False + self.units = {} def execute(self): self.parse_csv() @@ -61,8 +63,8 @@ class Csv2Ifc: def get_row_cost_data(self, row): name = row[self.headers["Name"]] identification = row[self.headers["Identification"]] if "Identification" in self.headers else None - cost_quantities = row[self.headers["Quantity"]] - cost_quantities_unit = row[self.headers["Unit"]] + quantity = row[self.headers["Quantity"]] + unit = row[self.headers["Unit"]] if self.has_categories: cost_values = { k: float(row[v]) @@ -75,8 +77,8 @@ class Csv2Ifc: return { "Identification": str(identification) if identification else None, "Name": str(name) if name else None, - "CostQuantities": float(cost_quantities) if cost_quantities else None, - "CostQuantitiesUnit": str(cost_quantities_unit) if cost_quantities_unit else None, + "Quantity": float(quantity) if quantity else None, + "Unit": str(unit) if unit else None, "CostValues": cost_values, "children": [], } @@ -86,6 +88,8 @@ class Csv2Ifc: self.create_boilerplate_ifc() if not self.cost_schedule: self.cost_schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="CSV Import") + if self.is_schedule_of_rates: + self.cost_schedule.PredefinedType = "SCHEDULEOFRATES" self.create_cost_items(self.cost_items) def create_cost_items(self, cost_items, parent=None): @@ -94,9 +98,7 @@ class Csv2Ifc: def create_cost_item(self, cost_item, parent): if parent is None: - cost_item["ifc"] = ifcopenshell.api.run( - "cost.add_cost_item", self.file, cost_schedule=self.cost_schedule - ) + cost_item["ifc"] = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=self.cost_schedule) else: cost_item["ifc"] = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=parent) @@ -104,8 +106,9 @@ class Csv2Ifc: cost_item["ifc"].Identification = cost_item["Identification"] if not cost_item["CostValues"]: - cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) - cost_value.Category = "*" + if not self.is_schedule_of_rates: + cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) + cost_value.Category = "*" elif self.has_categories: for category, value in cost_item["CostValues"].items(): cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) @@ -114,14 +117,42 @@ class Csv2Ifc: else: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(cost_item["CostValues"]) + if self.is_schedule_of_rates: + measure_class = ifcopenshell.util.unit.get_symbol_measure_class(cost_item["Unit"]) + value_component = self.file.create_entity(measure_class, cost_item["Quantity"]) + unit_component = None - if cost_item["CostQuantities"]: - quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["CostQuantitiesUnit"]) + if measure_class == "IfcNumericMeasure": + unit_component = self.create_unit(cost_item["Unit"]) + else: + unit_type = ifcopenshell.util.unit.get_measure_unit_type(measure_class) + unit_assignment = ifcopenshell.util.unit.get_unit_assignment(self.file) + if unit_assignment: + units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type] + if units: + unit_component = units[0] + if not unit_component: + unit_component = self.create_unit(cost_item["Unit"]) + + cost_value.UnitBasis = self.file.createIfcMeasureWithUnit(value_component, unit_component) + + if not self.is_schedule_of_rates and cost_item["Quantity"]: + quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"]) quantity = ifcopenshell.api.run( "cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class ) - quantity[3] = cost_item["CostQuantities"] + quantity[3] = cost_item["Quantity"] self.create_cost_items(cost_item["children"], cost_item["ifc"]) + def create_unit(self, symbol): + unit = self.units.get(symbol, None) + if unit: + return unit + unit = self.file.createIfcContextDependentUnit( + self.file.createIfcDimensionalExponents(0, 0, 0, 0, 0, 0, 0), "USERDEFINED", symbol + ) + self.units[symbol] = unit + return unit + def create_boilerplate_ifc(self): self.file = ifcopenshell.file(schema="IFC4") diff --git a/src/ifc5d/rates.csv b/src/ifc5d/rates.csv new file mode 100644 index 0000000000..824084c52b --- /dev/null +++ b/src/ifc5d/rates.csv @@ -0,0 +1,14 @@ +"Hierarchy","Identification","Name","Quantity","Unit","Value" +1,1,"Rates",,, +,,,,, +2,1.1,"Category A",,, +,,,,, +3,"1.1.1","Cleaning",1,"m",5 +3,"1.1.2","Hiring",1,"m2",6 +3,"1.1.3","Building",1,"m3",7 +,,,,, +2,1.2,"Category B",,, +,,,,, +3,"1.2.1","Cleaning",1,"hr",8 +3,"1.2.2","Hiring",2,"bob",9 +3,"1.2.3","Building",3,"kg",10 diff --git a/src/ifc5d/test.csv b/src/ifc5d/schedule.csv similarity index 100% rename from src/ifc5d/test.csv rename to src/ifc5d/schedule.csv diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py index 692ccc958b..48bbf4411f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py @@ -16,18 +16,13 @@ class Usecase: # TODO: support all applied value select types value = self.file.createIfcMonetaryMeasure(value) elif name == "UnitBasis": - self.remove_existing_unit_basis() + old_unit_basis = self.settings["cost_value"].UnitBasis if value: value_component = self.file.create_entity( - ifcopenshell.util.unit.get_unit_measure_type(value["UnitComponent"].UnitType), + ifcopenshell.util.unit.get_unit_measure_class(value["UnitComponent"].UnitType), value["ValueComponent"], ) value = self.file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"]) + if old_unit_basis and len(self.file.get_inverse(old_unit_basis)) == 0: + ifcopenshell.util.element.remove_deep(self.file, old_unit_basis) setattr(self.settings["cost_value"], name, value) - - def remove_existing_unit_basis(self): - if ( - self.settings["cost_value"].UnitBasis - and len(self.file.get_inverse(self.settings["cost_value"].UnitBasis)) == 1 - ): - ifcopenshell.util.element.remove_deep(self.file, self.settings["cost_value"].UnitBasis) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 36ed4cd0e6..10992888b7 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -246,45 +246,79 @@ def get_named_dimensions(name): return named_dimensions.get(name, (0, 0, 0, 0, 0, 0, 0)) +def get_unit_assignment(ifc_file): + unit_assignments = ifc_file.by_type("IfcUnitAssignment") + if unit_assignments: + return unit_assignments[0] + + def get_property_unit(prop, ifc_file): unit = getattr(prop, "Unit", None) if unit: return unit - unit_assignment = ifc_file.by_type("IfcUnitAssignment") + unit_assignment = get_unit_assignment(ifc_file) if not unit_assignment: return entity = prop.wrapped_data.declaration().as_entity() if prop.is_a("IfcPhysicalSimpleQuantity"): - measure_type = entity.attribute_by_index(3).type_of_attribute().declared_type().name() + measure_class = entity.attribute_by_index(3).type_of_attribute().declared_type().name() elif prop.is_a("IfcPropertySingleValue") and prop.NominalValue: - measure_type = prop.NominalValue.is_a() - for text in ("Ifc", "Measure", "Non", "Positive", "Negative"): - measure_type = measure_type.replace(text, "") - measure_type = measure_type.upper() + "UNIT" - units = [u for u in unit_assignment[0].Units if getattr(u, "UnitType", None) == measure_type] + measure_class = prop.NominalValue.is_a() + unit_type = get_measure_unit_type(measure_class) + units = [u for u in unit_assignment.Units if getattr(u, "UnitType", None) == unit_type] if units: return units[0] -def get_unit_measure_type(unit_type): +def get_unit_measure_class(unit_type): + if unit_type == "USERDEFINED": + # See https://github.com/buildingSMART/IFC4.3.x-development/issues/71 + return "IfcNumericMeasure" return "Ifc" + unit_type[0:-4].lower().capitalize() + "Measure" +def get_measure_unit_type(measure_class): + if measure_class == "IfcNumericMeasure": + # See https://github.com/buildingSMART/IFC4.3.x-development/issues/71 + return "USERDEFINED" + for text in ("Ifc", "Measure", "Non", "Positive", "Negative"): + measure_class = measure_class.replace(text, "") + return measure_class.upper() + "UNIT" + + +def get_symbol_measure_class(symbol): + # Dumb, but everybody gets it, unlike regex golf + if not symbol: + return "IfcNumericMeasure" + symbol = symbol.lower() + if symbol in ["km", "m", "cm", "mm", "ly", "lf", "lin", "yd", "ft", "in"]: + return "IfcLengthMeasure" + elif symbol in ["km2", "m2", "cm2", "mm2", "sqy", "sqft", "sqin"]: + return "IfcAreaMeasure" + elif symbol in ["km3", "m3", "cm3", "mm3", "cy", "cft", "cin"]: + return "IfcVolumeMeasure" + elif symbol in ["kg", "g", "mt", "kt", "t"]: + return "IfcMassMeasure" + elif symbol in ["day", "d", "hour", "hr", "h", "minute", "min", "m", "second", "sec", "s"]: + return "IfcTimeMeasure" + return "IfcNumericMeasure" + + def get_symbol_quantity_class(symbol): # Dumb, but everybody gets it, unlike regex golf if not symbol: return "IfcQuantityCount" symbol = symbol.lower() - if symbol in ["kg", "g", "mt", "kt", "t"]: + if symbol in ["km", "m", "cm", "mm", "ly", "lf", "lin", "yd", "ft", "in"]: + return "IfcQuantityLength" + elif symbol in ["km2", "m2", "cm2", "mm2", "sqy", "sqft", "sqin"]: + return "IfcQuantityArea" + elif symbol in ["km3", "m3", "cm3", "mm3", "cy", "cft", "cin"]: + return "IfcQuantityVolume" + elif symbol in ["kg", "g", "mt", "kt", "t"]: return "IfcQuantityWeight" elif symbol in ["day", "d", "hour", "hr", "h", "minute", "min", "m", "second", "sec", "s"]: return "IfcQuantityTime" - elif symbol in ["km3", "m3", "cm3", "mm3", "cy", "cft", "cin"]: - return "IfcQuantityVolume" - elif symbol in ["km2", "m2", "cm2", "mm2", "sqy", "sqft", "sqin"]: - return "IfcQuantityArea" - elif symbol in ["km", "m", "cm", "mm", "ly", "lf", "lin", "yd", "ft", "in"]: - return "IfcQuantityLength" return "IfcQuantityCount"