From b272210a97755fc1226353894986552000d47788 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 27 Aug 2021 17:12:21 +1000 Subject: [PATCH] Resource base costs are now supported with all the features coming from the cost module. --- .../blenderbim/bim/module/cost/__init__.py | 4 +- .../blenderbim/bim/module/cost/operator.py | 31 ++- .../blenderbim/bim/module/cost/prop.py | 2 +- .../blenderbim/bim/module/cost/ui.py | 26 +- .../bim/module/resource/__init__.py | 6 + .../bim/module/resource/operator.py | 208 +++++++++++++- .../blenderbim/bim/module/resource/prop.py | 13 + .../blenderbim/bim/module/resource/ui.py | 66 ++++- .../ifcopenshell/api/cost/add_cost_value.py | 4 + .../ifcopenshell/api/cost/data.py | 253 +++++++++--------- ...ost_item_value.py => remove_cost_value.py} | 4 + .../ifcopenshell/api/resource/data.py | 12 +- 12 files changed, 468 insertions(+), 161 deletions(-) rename src/ifcopenshell-python/ifcopenshell/api/cost/{remove_cost_item_value.py => remove_cost_value.py} (78%) diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 068fe6d598..b2188023b7 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -26,8 +26,8 @@ classes = ( operator.EditCostSchedule, operator.EditCostItem, operator.EditCostItemQuantity, - operator.EditCostValue, - operator.EditCostValueFormula, + operator.EditCostItemValue, + operator.EditCostItemValueFormula, operator.EnableEditingCostSchedule, operator.EnableEditingCostItems, operator.EnableEditingCostItem, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index efab5ff5f1..c9807fb922 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -27,6 +27,7 @@ from blenderbim.bim.ifc import IfcStore from bpy_extras.io_utils import ImportHelper from ifcopenshell.api.cost.data import Data from ifcopenshell.api.unit.data import Data as UnitData +from ifcopenshell.api.resource.data import Data as ResourceData class AddCostSchedule(bpy.types.Operator): @@ -590,6 +591,7 @@ class AddCostValue(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() + parent = self.file.by_id(self.parent) if self.cost_type == "FIXED": category = None attributes = {"AppliedValue": 0.0} @@ -599,14 +601,17 @@ class AddCostValue(bpy.types.Operator): elif self.cost_type == "CATEGORY": category = self.cost_category attributes = {"Category": category} - value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=self.file.by_id(self.parent)) + value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=parent) ifcopenshell.api.run("cost.edit_cost_value", self.file, cost_value=value, attributes=attributes) - Data.load(self.file) + if parent.is_a("IfcConstructionResource"): + ResourceData.load(self.file) + else: + Data.load(self.file) return {"FINISHED"} class RemoveCostItemValue(bpy.types.Operator): - bl_idname = "bim.remove_cost_item_value" + bl_idname = "bim.remove_cost_value" bl_label = "Add Cost Item Value" bl_options = {"REGISTER", "UNDO"} parent: bpy.props.IntProperty() @@ -617,13 +622,17 @@ class RemoveCostItemValue(bpy.types.Operator): def _execute(self, context): self.file = IfcStore.get_file() + parent = self.file.by_id(self.parent) ifcopenshell.api.run( - "cost.remove_cost_item_value", + "cost.remove_cost_value", self.file, - parent=self.file.by_id(self.parent), + parent=parent, cost_value=self.file.by_id(self.cost_value), ) - Data.load(self.file) + if parent.is_a("IfcConstructionResource"): + ResourceData.load(self.file) + else: + Data.load(self.file) return {"FINISHED"} @@ -636,7 +645,7 @@ class EnableEditingCostItemValue(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMCostProperties self.props.cost_value_attributes.clear() - self.props.active_cost_item_value_id = self.cost_value + self.props.active_cost_value_id = self.cost_value self.props.cost_value_editing_type = "ATTRIBUTES" data = Data.cost_values[self.cost_value] @@ -706,7 +715,7 @@ class DisableEditingCostItemValue(bpy.types.Operator): def execute(self, context): props = context.scene.BIMCostProperties - props.active_cost_item_value_id = 0 + props.active_cost_value_id = 0 props.cost_value_editing_type = "" return {"FINISHED"} @@ -720,13 +729,13 @@ class EnableEditingCostItemValueFormula(bpy.types.Operator): def execute(self, context): self.props = context.scene.BIMCostProperties self.props.cost_value_attributes.clear() - self.props.active_cost_item_value_id = self.cost_value + self.props.active_cost_value_id = self.cost_value self.props.cost_value_editing_type = "FORMULA" self.props.cost_value_formula = Data.cost_values[self.cost_value]["Formula"] return {"FINISHED"} -class EditCostValueFormula(bpy.types.Operator): +class EditCostItemValueFormula(bpy.types.Operator): bl_idname = "bim.edit_cost_value_formula" bl_label = "Edit Cost Value Formula" bl_options = {"REGISTER", "UNDO"} @@ -748,7 +757,7 @@ class EditCostValueFormula(bpy.types.Operator): return {"FINISHED"} -class EditCostValue(bpy.types.Operator): +class EditCostItemValue(bpy.types.Operator): bl_idname = "bim.edit_cost_value" bl_label = "Edit Cost Value" bl_options = {"REGISTER", "UNDO"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index 3d11601970..69ac4f825f 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -244,7 +244,7 @@ class BIMCostProperties(PropertyGroup): name="Cost Types", ) cost_category: StringProperty(name="Cost Category") - active_cost_item_value_id: IntProperty(name="Active Cost Item Value Id") + active_cost_value_id: IntProperty(name="Active Cost Item Value Id") cost_value_editing_type: StringProperty(name="Cost Value Editing Type") cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute) cost_value_formula: StringProperty(name="Cost Value Formula") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index c0542b40c0..d2062d98ad 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -16,10 +16,10 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.bim.helper import blenderbim.bim.module.cost.prop as CostProp from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore -from blenderbim.bim.helper import draw_attributes from ifcopenshell.api.cost.data import Data from ifcopenshell.api.unit.data import Data as UnitData @@ -94,7 +94,7 @@ class BIM_PT_cost_schedules(Panel): self.layout.template_list("BIM_UL_cost_columns", "", self.props, "columns", self.props, "active_column_index") def draw_editable_cost_schedule_ui(self): - draw_attributes(self.props.cost_schedule_attributes, self.layout) + blenderbim.bim.helper.draw_attributes(self.props.cost_schedule_attributes, self.layout) def draw_editable_cost_item_ui(self, cost_schedule_id): row = self.layout.row(align=True) @@ -140,7 +140,7 @@ class BIM_PT_cost_schedules(Panel): self.draw_editable_cost_item_values_ui() def draw_editable_cost_item_attributes_ui(self): - draw_attributes(self.props.cost_item_attributes, self.layout) + blenderbim.bim.helper.draw_attributes(self.props.cost_item_attributes, self.layout) def draw_editable_cost_item_quantities_ui(self): row = self.layout.row(align=True) @@ -175,7 +175,7 @@ class BIM_PT_cost_schedules(Panel): self.draw_editable_cost_item_quantity_ui(box) def draw_editable_cost_item_quantity_ui(self, layout): - draw_attributes(self.props.quantity_attributes, self.layout) + blenderbim.bim.helper.draw_attributes(self.props.quantity_attributes, self.layout) def draw_editable_cost_item_values_ui(self): row = self.layout.row(align=True) @@ -194,12 +194,12 @@ class BIM_PT_cost_schedules(Panel): if self.props.cost_value_editing_type == "ATTRIBUTES": box = self.layout.box() - self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_item_value_id]) + self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_value_id]) def draw_readonly_cost_value_ui(self, layout, cost_value_id): cost_value = Data.cost_values[cost_value_id] - if self.props.active_cost_item_value_id == cost_value_id and self.props.cost_value_editing_type == "FORMULA": + if self.props.active_cost_value_id == cost_value_id and self.props.cost_value_editing_type == "FORMULA": layout.prop(self.props, "cost_value_formula", text="") else: cost_value_label = "{0:.2f}".format(cost_value["AppliedValue"]) @@ -209,16 +209,16 @@ class BIM_PT_cost_schedules(Panel): self.draw_cost_value_operator_ui(layout, cost_value_id, self.props.active_cost_item_id) def draw_cost_value_operator_ui(self, layout, cost_value_id, parent_id): - if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id: + if self.props.active_cost_value_id and self.props.active_cost_value_id == cost_value_id: if self.props.cost_value_editing_type == "ATTRIBUTES": - op = layout.operator("bim.edit_cost_value", text="", icon="CHECKMARK") + op = layout.operator("bim.edit_cost_item_value", text="", icon="CHECKMARK") op.cost_value = cost_value_id elif self.props.cost_value_editing_type == "FORMULA": - op = layout.operator("bim.edit_cost_value_formula", text="", icon="CHECKMARK") + op = layout.operator("bim.edit_cost_item_value_formula", text="", icon="CHECKMARK") op.cost_value = cost_value_id layout.operator("bim.disable_editing_cost_item_value", text="", icon="CANCEL") - elif self.props.active_cost_item_value_id: - op = layout.operator("bim.remove_cost_item_value", text="", icon="X") + elif self.props.active_cost_value_id: + op = layout.operator("bim.remove_cost_value", text="", icon="X") op.parent = parent_id op.cost_value = cost_value_id else: @@ -226,12 +226,12 @@ class BIM_PT_cost_schedules(Panel): op.cost_value = cost_value_id op = layout.operator("bim.enable_editing_cost_item_value", text="", icon="GREASEPENCIL") op.cost_value = cost_value_id - op = layout.operator("bim.remove_cost_item_value", text="", icon="X") + op = layout.operator("bim.remove_cost_value", text="", icon="X") op.parent = parent_id op.cost_value = cost_value_id def draw_editable_cost_value_ui(self, layout, cost_value): - draw_attributes(self.props.cost_value_attributes, layout) + blenderbim.bim.helper.draw_attributes(self.props.cost_value_attributes, layout) class BIM_PT_cost_item_types(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/resource/__init__.py b/src/blenderbim/blenderbim/bim/module/resource/__init__.py index cb2f056939..39a17db65c 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/resource/__init__.py @@ -34,8 +34,14 @@ classes = ( operator.AssignResource, operator.UnassignResource, operator.EnableEditingResourceTime, + operator.EnableEditingResourceCosts, + operator.EnableEditingResourceCostValueFormula, + operator.EnableEditingResourceCostValue, operator.EditResourceTime, + operator.EditResourceCostValue, + operator.EditResourceCostValueFormula, operator.DisableEditingResourceTime, + operator.DisableEditingResourceCostValue, operator.CalculateResourceWork, prop.Resource, prop.BIMResourceProperties, diff --git a/src/blenderbim/blenderbim/bim/module/resource/operator.py b/src/blenderbim/blenderbim/bim/module/resource/operator.py index 34dbdd12be..547dd64396 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/operator.py +++ b/src/blenderbim/blenderbim/bim/module/resource/operator.py @@ -1,4 +1,3 @@ - # BlenderBIM Add-on - OpenBIM Blender Add-on # Copyright (C) 2020, 2021 Dion Moult # @@ -19,14 +18,15 @@ import bpy import json +import time +import isodate import ifcopenshell.api -from blenderbim.bim.ifc import IfcStore -from ifcopenshell.api.resource.data import Data import blenderbim.bim.helper import blenderbim.bim.module.sequence.helper as helper -import time from datetime import datetime -import isodate +from blenderbim.bim.ifc import IfcStore +from ifcopenshell.api.resource.data import Data +from ifcopenshell.api.unit.data import Data as UnitData class LoadResources(bpy.types.Operator): @@ -95,7 +95,6 @@ class EnableEditingResource(bpy.types.Operator): new.enum_value = data[attribute.name()] - class LoadResourceProperties(bpy.types.Operator): bl_idname = "bim.load_resource_properties" bl_label = "Load Resource Properties" @@ -301,7 +300,9 @@ class EnableEditingResourceTime(bpy.types.Operator): data = Data.resource_times[resource_time_id] - blenderbim.bim.helper.import_attributes("IfcResourceTime", props.resource_time_attributes, data, self.import_attributes) + blenderbim.bim.helper.import_attributes( + "IfcResourceTime", props.resource_time_attributes, data, self.import_attributes + ) props.active_resource_time_id = resource_time_id props.active_resource_id = self.resource props.editing_resource_type = "USAGE" @@ -319,7 +320,9 @@ class EnableEditingResourceTime(bpy.types.Operator): return True def add_resource_time(self): - resource_time = ifcopenshell.api.run("resource.add_resource_time", self.file, resource=self.file.by_id(self.resource)) + resource_time = ifcopenshell.api.run( + "resource.add_resource_time", self.file, resource=self.file.by_id(self.resource) + ) Data.load(self.file) return resource_time @@ -345,7 +348,9 @@ class EditResourceTime(bpy.types.Operator): def _execute(self, context): self.props = context.scene.BIMResourceProperties - attributes = blenderbim.bim.helper.export_attributes(self.props.resource_time_attributes, self.export_attributes) + attributes = blenderbim.bim.helper.export_attributes( + self.props.resource_time_attributes, self.export_attributes + ) self.file = IfcStore.get_file() ifcopenshell.api.run( @@ -365,7 +370,7 @@ class EditResourceTime(bpy.types.Operator): return True attributes[prop.name] = helper.parse_datetime(prop.string_value) return True - elif prop.name =="LevelingDelay" or "Work" in prop.name: + elif prop.name == "LevelingDelay" or "Work" in prop.name: if prop.is_null: attributes[prop.name] = None return True @@ -385,3 +390,186 @@ class CalculateResourceWork(bpy.types.Operator): Data.load(self.file) bpy.ops.bim.load_resources() return {"FINISHED"} + + +class EnableEditingResourceCosts(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_costs" + bl_label = "Enable Editing Resource Costs" + bl_options = {"REGISTER", "UNDO"} + resource: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_resource_id = self.resource + props.editing_resource_type = "COSTS" + bpy.ops.bim.disable_editing_resource_cost_value() + return {"FINISHED"} + + +class DisableEditingResourceCostValue(bpy.types.Operator): + bl_idname = "bim.disable_editing_resource_cost_value" + bl_label = "Disable Editing Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_cost_value_id = 0 + props.cost_value_editing_type = "" + return {"FINISHED"} + + +class DisableEditingResourceCostValue(bpy.types.Operator): + bl_idname = "bim.disable_editing_resource_cost_value" + bl_label = "Disable Editing Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + props = context.scene.BIMResourceProperties + props.active_cost_value_id = 0 + props.cost_value_editing_type = "" + return {"FINISHED"} + + +class EnableEditingResourceCostValueFormula(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_cost_value_formula" + bl_label = "Enable Editing Resource Cost Value Formula" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMResourceProperties + self.props.cost_value_attributes.clear() + self.props.active_cost_value_id = self.cost_value + self.props.cost_value_editing_type = "FORMULA" + self.props.cost_value_formula = Data.cost_values[self.cost_value]["Formula"] + return {"FINISHED"} + + +class EnableEditingResourceCostValue(bpy.types.Operator): + bl_idname = "bim.enable_editing_resource_cost_value" + bl_label = "Enable Editing Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMResourceProperties + self.props.cost_value_attributes.clear() + self.props.active_cost_value_id = self.cost_value + self.props.cost_value_editing_type = "ATTRIBUTES" + data = Data.cost_values[self.cost_value] + + blenderbim.bim.helper.import_attributes( + data["type"], + self.props.cost_value_attributes, + data, + lambda name, prop, data: self.import_attributes(name, prop, data, context), + ) + return {"FINISHED"} + + def import_attributes(self, name, prop, data, context): + if name == "AppliedValue": + # TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure) + prop = self.props.cost_value_attributes.add() + prop.data_type = "float" + prop.name = "AppliedValue" + prop.is_optional = True + prop.float_value = 0.0 if prop.is_null else data[name] + return True + elif name == "UnitBasis": + prop = self.props.cost_value_attributes.add() + prop.name = "UnitBasisValue" + prop.data_type = "float" + prop.is_null = data["UnitBasis"] is None + prop.is_optional = True + if data["UnitBasis"]: + prop.float_value = data["UnitBasis"]["ValueComponent"] or 0 + else: + prop.float_value = 0 + + prop = self.props.cost_value_attributes.add() + prop.name = "UnitBasisUnit" + prop.data_type = "enum" + 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", + "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"]) + return True + + +class EditResourceCostValueFormula(bpy.types.Operator): + bl_idname = "bim.edit_resource_cost_value_formula" + bl_label = "Edit Resource Cost Value Formula" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMResourceProperties + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_value_formula", + self.file, + **{"cost_value": self.file.by_id(self.cost_value), "formula": props.cost_value_formula}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_resource_cost_value() + return {"FINISHED"} + + +class EditResourceCostValue(bpy.types.Operator): + bl_idname = "bim.edit_resource_cost_value" + bl_label = "Edit Resource Cost Value" + bl_options = {"REGISTER", "UNDO"} + cost_value: bpy.props.IntProperty() + + def execute(self, context): + return IfcStore.execute_ifc_operator(self, context) + + def _execute(self, context): + props = context.scene.BIMResourceProperties + attributes = blenderbim.bim.helper.export_attributes( + props.cost_value_attributes, lambda attributes, prop: self.export_attributes(attributes, prop, context) + ) + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_value", + self.file, + **{"cost_value": self.file.by_id(self.cost_value), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_resource_cost_value() + return {"FINISHED"} + + def export_attributes(self, attributes, prop, context): + if prop.name == "UnitBasisValue": + if prop.is_null: + attributes["UnitBasis"] = None + return True + attributes["UnitBasis"] = { + "ValueComponent": prop.float_value or 1, + "UnitComponent": IfcStore.get_file().by_id( + int(context.scene.BIMResourceProperties.cost_value_attributes.get("UnitBasisUnit").enum_value) + ), + } + return True + if prop.name == "UnitBasisUnit": + return True diff --git a/src/blenderbim/blenderbim/bim/module/resource/prop.py b/src/blenderbim/blenderbim/bim/module/resource/prop.py index 5681deba24..f9f436f6f4 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/prop.py +++ b/src/blenderbim/blenderbim/bim/module/resource/prop.py @@ -74,3 +74,16 @@ class BIMResourceProperties(PropertyGroup): active_resource_time_id: IntProperty(name="Active Resource Usage Id") resource_time_attributes: CollectionProperty(name="Resource Usage Attributes", type=Attribute) editing_resource_type: StringProperty(name="Editing Resource Type") + cost_types: EnumProperty( + items=[ + ("FIXED", "Fixed", "The cost value is a fixed number"), + ("SUM", "Sum", "The cost value is automatically derived from the sum of all nested cost items"), + ("CATEGORY", "Category", "The cost value represents a single category"), + ], + name="Cost Types", + ) + cost_category: StringProperty(name="Cost Category") + active_cost_value_id: IntProperty(name="Active Resource Cost Value Id") + cost_value_editing_type: StringProperty(name="Cost Value Editing Type") + cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute) + cost_value_formula: StringProperty(name="Cost Value Formula") diff --git a/src/blenderbim/blenderbim/bim/module/resource/ui.py b/src/blenderbim/blenderbim/bim/module/resource/ui.py index c9e383d628..730133be72 100644 --- a/src/blenderbim/blenderbim/bim/module/resource/ui.py +++ b/src/blenderbim/blenderbim/bim/module/resource/ui.py @@ -16,10 +16,10 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +import blenderbim.bim.helper from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from ifcopenshell.api.resource.data import Data -import blenderbim.bim.helper class BIM_PT_resources(Panel): @@ -61,9 +61,11 @@ class BIM_PT_resources(Panel): self.props, "active_resource_index", ) + if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES": self.draw_editable_resource_attributes_ui() - + elif self.props.active_resource_id and self.props.editing_resource_type == "COSTS": + self.draw_editable_resource_costs_ui() elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE": self.draw_editable_resource_time_attributes_ui() @@ -107,6 +109,8 @@ class BIM_PT_resources(Panel): elif self.props.active_resource_id == ifc_definition_id and self.props.editing_resource_type == "USAGE": row.operator("bim.edit_resource_time", text="", icon="CHECKMARK") row.operator("bim.disable_editing_resource_time", text="", icon="CANCEL") + elif self.props.active_resource_id == ifc_definition_id and self.props.editing_resource_type == "COSTS": + row.operator("bim.disable_editing_resource", text="", icon="CANCEL") elif self.props.active_resource_id: row.operator("bim.add_resource", text="", icon="ADD").resource = ifc_definition_id row.operator("bim.remove_resource", text="", icon="X").resource = ifc_definition_id @@ -115,6 +119,8 @@ class BIM_PT_resources(Panel): op = row.operator("bim.calculate_resource_work", text="", icon="TEMP") op.resource = ifc_definition_id row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id + op = row.operator("bim.enable_editing_resource_costs", text="", icon="DISC") + op.resource = ifc_definition_id row.operator("bim.enable_editing_resource", text="", icon="GREASEPENCIL").resource = ifc_definition_id row.operator("bim.remove_resource", text="", icon="X").resource = ifc_definition_id @@ -124,6 +130,62 @@ class BIM_PT_resources(Panel): def draw_editable_resource_time_attributes_ui(self): blenderbim.bim.helper.draw_attributes(self.props.resource_time_attributes, self.layout) + def draw_editable_resource_costs_ui(self): + row = self.layout.row(align=True) + row.prop(self.props, "cost_types", text="") + if self.props.cost_types == "CATEGORY": + row.prop(self.props, "cost_category", text="") + op = row.operator("bim.add_cost_value", text="", icon="ADD") + op.parent = self.props.active_resource_id + op.cost_type = self.props.cost_types + if self.props.cost_types == "CATEGORY": + op.cost_category = self.props.cost_category + + for cost_value_id in Data.resources[self.props.active_resource_id]["BaseCosts"] or []: + row = self.layout.row(align=True) + self.draw_readonly_cost_value_ui(row, cost_value_id) + + if self.props.cost_value_editing_type == "ATTRIBUTES": + box = self.layout.box() + self.draw_editable_cost_value_ui(box, Data.cost_values[self.props.active_cost_value_id]) + + def draw_readonly_cost_value_ui(self, layout, cost_value_id): + cost_value = Data.cost_values[cost_value_id] + + if self.props.active_cost_value_id == cost_value_id and self.props.cost_value_editing_type == "FORMULA": + layout.prop(self.props, "cost_value_formula", text="") + else: + cost_value_label = "{0:.2f}".format(cost_value["AppliedValue"]) + cost_value_label += " = " + cost_value["Formula"] + layout.label(text=cost_value_label, icon="DISC") + + self.draw_cost_value_operator_ui(layout, cost_value_id, self.props.active_resource_id) + + def draw_cost_value_operator_ui(self, layout, cost_value_id, parent_id): + if self.props.active_cost_value_id and self.props.active_cost_value_id == cost_value_id: + if self.props.cost_value_editing_type == "ATTRIBUTES": + op = layout.operator("bim.edit_resource_cost_value", text="", icon="CHECKMARK") + op.cost_value = cost_value_id + elif self.props.cost_value_editing_type == "FORMULA": + op = layout.operator("bim.edit_resource_cost_value_formula", text="", icon="CHECKMARK") + op.cost_value = cost_value_id + layout.operator("bim.disable_editing_resource_cost_value", text="", icon="CANCEL") + elif self.props.active_cost_value_id: + op = layout.operator("bim.remove_cost_value", text="", icon="X") + op.parent = parent_id + op.cost_value = cost_value_id + else: + op = layout.operator("bim.enable_editing_resource_cost_value_formula", text="", icon="CON_TRANSLIKE") + op.cost_value = cost_value_id + op = layout.operator("bim.enable_editing_resource_cost_value", text="", icon="GREASEPENCIL") + op.cost_value = cost_value_id + op = layout.operator("bim.remove_cost_value", text="", icon="X") + op.parent = parent_id + op.cost_value = cost_value_id + + def draw_editable_cost_value_ui(self, layout, cost_value): + blenderbim.bim.helper.draw_attributes(self.props.cost_value_attributes, layout) + class BIM_UL_resources(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py index eb3c44f2c7..5eaaa80384 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py @@ -11,6 +11,10 @@ class Usecase: values = list(self.settings["parent"].CostValues or []) values.append(value) self.settings["parent"].CostValues = values + elif self.settings["parent"].is_a("IfcConstructionResource"): + values = list(self.settings["parent"].BaseCosts or []) + values.append(value) + self.settings["parent"].BaseCosts = values elif self.settings["parent"].is_a("IfcCostValue"): values = list(self.settings["parent"].Components or []) values.append(value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py index dfb3cc36d9..d402ed423e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py @@ -3,7 +3,137 @@ import ifcopenshell.util.unit import ifcopenshell.util.cost -class Data: +class CostValueTrait: + @classmethod + def load_cost_values(cls, root_element, data): + data["CostValues"] = [] + data["CategoryValues"] = {} + data["UnitBasisValueComponent"] = None + data["UnitBasisUnitSymbol"] = None + data["TotalAppliedValue"] = 0.0 + data["TotalCost"] = 0.0 + if root_element.is_a("IfcCostItem"): + values = root_element.CostValues + elif root_element.is_a("IfcConstructionResource"): + values = root_element.BaseCosts + for cost_value in values or []: + cls.load_cost_value(root_element, data, cost_value) + data["CostValues"].append(cost_value.id()) + data["TotalAppliedValue"] += cls.cost_values[cost_value.id()]["AppliedValue"] + if cost_value.UnitBasis: + cost_value_data = cls.cost_values[cost_value.id()] + data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"] + data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"] + if data["UnitBasisValueComponent"]: + data["TotalCost"] = data["TotalCostQuantity"] / data["UnitBasisValueComponent"] * data["TotalAppliedValue"] + else: + data["TotalCost"] = data["TotalCostQuantity"] * data["TotalAppliedValue"] + + @classmethod + def load_cost_value(cls, root_element, root_element_data, cost_value): + value_data = cost_value.get_info() + del value_data["AppliedValue"] + if value_data["UnitBasis"]: + data = cost_value.UnitBasis.get_info() + data["ValueComponent"] = data["ValueComponent"].wrappedValue + data["UnitComponent"] = data["UnitComponent"].id() + data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(cost_value.UnitBasis.UnitComponent) + value_data["UnitBasis"] = data + if value_data["ApplicableDate"]: + value_data["ApplicableDate"] = ifcopenshell.util.date.ifc2datetime(value_data["ApplicableDate"]) + if value_data["FixedUntilDate"]: + value_data["FixedUntilDate"] = ifcopenshell.util.date.ifc2datetime(value_data["FixedUntilDate"]) + value_data["Components"] = [c.id() for c in value_data["Components"] or []] + value_data["AppliedValue"] = cls.calculate_applied_value(root_element, cost_value) + + if cost_value.Category not in [None, "*"]: + root_element_data["CategoryValues"].setdefault(cost_value.Category, 0) + root_element_data["CategoryValues"][cost_value.Category] += value_data["AppliedValue"] + + value_data["Formula"] = ifcopenshell.util.cost.serialise_cost_value(cost_value) + + cls.cost_values[cost_value.id()] = value_data + for component in cost_value.Components or []: + cls.load_cost_value(root_element, root_element_data, component) + + @classmethod + def calculate_applied_value(cls, root_element, cost_value, category_filter=None): + if cost_value.ArithmeticOperator and cost_value.Components: + component_values = [] + for component in cost_value.Components: + component_values.append(cls.calculate_applied_value(root_element, component, category_filter)) + if cost_value.ArithmeticOperator == "ADD": + return sum(component_values) + result = component_values.pop(0) + if cost_value.ArithmeticOperator == "DIVIDE": + for value in component_values: + try: + result /= value + except ZeroDivisionError: + pass + elif cost_value.ArithmeticOperator == "MULTIPLY": + for value in component_values: + result *= value + elif cost_value.ArithmeticOperator == "SUBTRACT": + for value in component_values: + result -= value + return result + if cost_value.Category is None: + return cls.get_primitive_applied_value(cost_value.AppliedValue) + elif cost_value.Category == "*": + if root_element.IsNestedBy: + return cls.sum_child_root_elements(root_element) + else: + return cls.get_primitive_applied_value(cost_value.AppliedValue) + elif cost_value.Category: + if root_element.IsNestedBy: + return cls.sum_child_root_elements(root_element, category_filter=cost_value.Category) + else: + return cls.get_primitive_applied_value(cost_value.AppliedValue) + return 0 + + @classmethod + def sum_child_root_elements(cls, root_element, category_filter=None): + result = 0 + for rel in root_element.IsNestedBy: + for child_root_element in rel.RelatedObjects: + if root_element.is_a("IfcCostItem"): + values = child_root_element.CostValues + elif root_element.is_a("IfcConstructionResource"): + values = child_root_element.BaseCosts + for child_cost_value in values or []: + if category_filter and child_cost_value.Category != category_filter: + continue + child_applied_value = cls.calculate_applied_value(child_root_element, child_cost_value) + child_quantity = cls.get_total_quantity(child_root_element) + if child_cost_value.UnitBasis: + value_component = child_cost_value.UnitBasis.ValueComponent.wrappedValue + result += child_quantity / value_component * child_applied_value + else: + result += child_quantity * child_applied_value + return result + + @classmethod + def get_total_quantity(cls, root_element): + if root_element.is_a("IfcCostItem"): + return sum([q[3] for q in root_element.CostQuantities or []]) or 1.0 + elif root_element.is_a("IfcConstructionResource"): + return root_element.BaseQuantity[3] if root_element.BaseQuantity else 1.0 + + @classmethod + def get_primitive_applied_value(cls, applied_value): + if not applied_value: + return 0.0 + elif isinstance(applied_value, float): + return applied_value + elif hasattr(applied_value, "wrappedValue") and isinstance(applied_value.wrappedValue, float): + return applied_value.wrappedValue + elif applied_value.is_a("IfcMeasureWithUnit"): + return applied_value.ValueComponent + assert False, "Applied value {applied_value} not implemented" + + +class Data(CostValueTrait): is_loaded = False cost_schedules = {} cost_items = {} @@ -63,7 +193,7 @@ class Data: parametric_quantities.extend(quantities) cls.cost_items[cost_item.id()] = data cls.load_cost_item_quantities(cost_item, data, parametric_quantities) - cls.load_cost_item_values(cost_item, data) + cls.load_cost_values(cost_item, data) cls.is_loaded = True @classmethod @@ -104,122 +234,3 @@ class Data: else: data["Unit"] = None data["UnitSymbol"] = None - - @classmethod - def load_cost_item_values(cls, cost_item, data): - data["CostValues"] = [] - data["CategoryValues"] = {} - data["UnitBasisValueComponent"] = None - data["UnitBasisUnitSymbol"] = None - data["TotalAppliedValue"] = 0.0 - data["TotalCost"] = 0.0 - for cost_value in cost_item.CostValues or []: - cls.load_cost_item_value(cost_item, data, cost_value) - data["CostValues"].append(cost_value.id()) - data["TotalAppliedValue"] += cls.cost_values[cost_value.id()]["AppliedValue"] - if cost_value.UnitBasis: - cost_value_data = cls.cost_values[cost_value.id()] - data["UnitBasisValueComponent"] = cost_value_data["UnitBasis"]["ValueComponent"] - data["UnitBasisUnitSymbol"] = cost_value_data["UnitBasis"]["UnitSymbol"] - if data["UnitBasisValueComponent"]: - data["TotalCost"] = ( - data["TotalCostQuantity"] / data["UnitBasisValueComponent"] * data["TotalAppliedValue"] - ) - else: - data["TotalCost"] = data["TotalCostQuantity"] * data["TotalAppliedValue"] - - @classmethod - def load_cost_item_value(cls, cost_item, cost_item_data, cost_value): - value_data = cost_value.get_info() - del value_data["AppliedValue"] - if value_data["UnitBasis"]: - data = cost_value.UnitBasis.get_info() - data["ValueComponent"] = data["ValueComponent"].wrappedValue - data["UnitComponent"] = data["UnitComponent"].id() - data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(cost_value.UnitBasis.UnitComponent) - value_data["UnitBasis"] = data - if value_data["ApplicableDate"]: - value_data["ApplicableDate"] = ifcopenshell.util.date.ifc2datetime(value_data["ApplicableDate"]) - if value_data["FixedUntilDate"]: - value_data["FixedUntilDate"] = ifcopenshell.util.date.ifc2datetime(value_data["FixedUntilDate"]) - value_data["Components"] = [c.id() for c in value_data["Components"] or []] - value_data["AppliedValue"] = cls.calculate_applied_value(cost_item, cost_value) - - if cost_value.Category not in [None, "*"]: - cost_item_data["CategoryValues"].setdefault(cost_value.Category, 0) - cost_item_data["CategoryValues"][cost_value.Category] += value_data["AppliedValue"] - - value_data["Formula"] = ifcopenshell.util.cost.serialise_cost_value(cost_value) - - cls.cost_values[cost_value.id()] = value_data - for component in cost_value.Components or []: - cls.load_cost_item_value(cost_item, cost_item_data, component) - - @classmethod - def calculate_applied_value(cls, cost_item, cost_value, category_filter=None): - if cost_value.ArithmeticOperator and cost_value.Components: - component_values = [] - for component in cost_value.Components: - component_values.append(cls.calculate_applied_value(cost_item, component, category_filter)) - if cost_value.ArithmeticOperator == "ADD": - return sum(component_values) - result = component_values.pop(0) - if cost_value.ArithmeticOperator == "DIVIDE": - for value in component_values: - try: - result /= value - except ZeroDivisionError: - pass - elif cost_value.ArithmeticOperator == "MULTIPLY": - for value in component_values: - result *= value - elif cost_value.ArithmeticOperator == "SUBTRACT": - for value in component_values: - result -= value - return result - if cost_value.Category is None: - return cls.get_primitive_applied_value(cost_value.AppliedValue) - elif cost_value.Category == "*": - if cost_item.IsNestedBy: - return cls.sum_child_cost_items(cost_item) - else: - return cls.get_primitive_applied_value(cost_value.AppliedValue) - elif cost_value.Category: - if cost_item.IsNestedBy: - return cls.sum_child_cost_items(cost_item, category_filter=cost_value.Category) - else: - return cls.get_primitive_applied_value(cost_value.AppliedValue) - return 0 - - @classmethod - def sum_child_cost_items(cls, cost_item, category_filter=None): - result = 0 - for rel in cost_item.IsNestedBy: - for child_cost_item in rel.RelatedObjects: - for child_cost_value in child_cost_item.CostValues or []: - if category_filter and child_cost_value.Category != category_filter: - continue - child_applied_value = cls.calculate_applied_value(child_cost_item, child_cost_value) - child_quantity = cls.get_total_quantity(child_cost_item) - if child_cost_value.UnitBasis: - value_component = child_cost_value.UnitBasis.ValueComponent.wrappedValue - result += child_quantity / value_component * child_applied_value - else: - result += child_quantity * child_applied_value - return result - - @classmethod - def get_total_quantity(cls, cost_item): - return sum([q[3] for q in cost_item.CostQuantities or []]) or 1.0 - - @classmethod - def get_primitive_applied_value(cls, applied_value): - if not applied_value: - return 0.0 - elif isinstance(applied_value, float): - return applied_value - elif hasattr(applied_value, "wrappedValue") and isinstance(applied_value.wrappedValue, float): - return applied_value.wrappedValue - elif applied_value.is_a("IfcMeasureWithUnit"): - return applied_value.ValueComponent - assert False, "Applied value {applied_value} not implemented" diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py similarity index 78% rename from src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py rename to src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py index 58eb0b9675..e3cab8dc5f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py @@ -13,6 +13,10 @@ class Usecase: values = list(self.settings["parent"].CostValues) values.remove(self.settings["cost_value"]) self.settings["parent"].CostValues = values if values else None + elif self.settings["parent"].is_a("IfcConstructionResource"): + values = list(self.settings["parent"].BaseCosts) + values.remove(self.settings["cost_value"]) + self.settings["parent"].BaseCosts = values if values else None elif self.settings["parent"].is_a("IfcCostValue"): components = list(self.settings["parent"].Components) components.remove(self.settings["cost_value"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/data.py b/src/ifcopenshell-python/ifcopenshell/api/resource/data.py index b917e18920..fae5c37779 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/data.py @@ -1,17 +1,20 @@ import ifcopenshell import ifcopenshell.util.date +from ifcopenshell.api.cost.data import CostValueTrait -class Data: +class Data(CostValueTrait): is_loaded = False resources = {} resource_times = {} + cost_values = {} @classmethod def purge(cls): cls.is_loaded = False cls.resources = {} cls.resource_times = {} + cls.cost_values = {} @classmethod def load(cls, file): @@ -25,6 +28,7 @@ class Data: @classmethod def load_resources(cls): cls.resources = {} + cls.cost_values = {} for resource in cls._file.by_type("IfcResource"): data = resource.get_info() del data["OwnerHistory"] @@ -40,6 +44,12 @@ class Data: data["HasContext"] = resource.HasContext[0].RelatingContext.id() if resource.HasContext else None if resource.Usage: data["Usage"] = data["Usage"].id() + data["TotalCostQuantity"] = cls.get_total_quantity(resource) + if resource.BaseQuantity: + data["BaseQuantity"] = resource.BaseQuantity.id() + if resource.BaseCosts: + data["BaseCosts"] = [e.id() for e in resource.BaseCosts] + cls.load_cost_values(resource, data) cls.resources[resource.id()] = data @classmethod