From 389076bff4dddd453ea605cd31c134781c06f019 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 26 Apr 2021 18:18:42 +1000 Subject: [PATCH] Implement feature for manipulating cost values, with support for calculation of subtotals --- .../blenderbim/bim/module/cost/__init__.py | 5 + .../blenderbim/bim/module/cost/operator.py | 122 +++++++++++++++++- .../blenderbim/bim/module/cost/prop.py | 8 ++ .../blenderbim/bim/module/cost/ui.py | 53 +++++++- .../api/cost/add_cost_item_quantity.py | 1 - .../api/cost/add_cost_item_value.py | 16 +++ .../ifcopenshell/api/cost/data.py | 98 ++++++++++++-- .../api/cost/edit_cost_item_value.py | 13 ++ .../api/cost/remove_cost_item_value.py | 9 ++ 9 files changed, 307 insertions(+), 18 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_value.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_value.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index 65b68aeca9..3ded7ee9e1 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -7,15 +7,18 @@ classes = ( operator.EditCostSchedule, operator.EditCostItem, operator.EditCostItemQuantity, + operator.EditCostItemValue, operator.EnableEditingCostSchedule, operator.EnableEditingCostItems, operator.EnableEditingCostItem, operator.EnableEditingCostItemQuantities, operator.EnableEditingCostItemQuantity, operator.EnableEditingCostItemValues, + operator.EnableEditingCostItemValue, operator.DisableEditingCostItem, operator.DisableEditingCostSchedule, operator.DisableEditingCostItemQuantity, + operator.DisableEditingCostItemValue, operator.AddCostItem, operator.AddSummaryCostItem, operator.ExpandCostItem, @@ -25,6 +28,8 @@ classes = ( operator.UnassignControl, operator.AddCostItemQuantity, operator.RemoveCostItemQuantity, + operator.AddCostItemValue, + operator.RemoveCostItemValue, prop.CostItem, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 6e7a2e40dc..c40f70d0e3 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -104,7 +104,7 @@ class EnableEditingCostItems(bpy.types.Operator): self.props.cost_items.remove(0) self.contracted_cost_items = json.loads(self.props.contracted_cost_items) - for related_object_id in Data.cost_schedules[self.cost_schedule]["RelatedObjects"]: + for related_object_id in Data.cost_schedules[self.cost_schedule]["Controls"]: self.create_new_cost_item_li(related_object_id, 0) self.props.is_editing = "COST_ITEMS" return {"FINISHED"} @@ -116,10 +116,10 @@ class EnableEditingCostItems(bpy.types.Operator): new.name = cost_item["Name"] or "Unnamed" new.is_expanded = related_object_id not in self.contracted_cost_items new.level_index = level_index - if cost_item["RelatedObjects"]: + if cost_item["IsNestedBy"]: new.has_children = True if new.is_expanded: - for related_object_id in cost_item["RelatedObjects"]: + for related_object_id in cost_item["IsNestedBy"]: self.create_new_cost_item_li(related_object_id, level_index + 1) return {"FINISHED"} @@ -457,3 +457,119 @@ class EditCostItemQuantity(bpy.types.Operator): Data.load(IfcStore.get_file()) bpy.ops.bim.disable_editing_cost_item_quantity() return {"FINISHED"} + + +class AddCostItemValue(bpy.types.Operator): + bl_idname = "bim.add_cost_item_value" + bl_label = "Add Cost Item Value" + cost_item: bpy.props.IntProperty() + cost_type: bpy.props.StringProperty() + cost_category: bpy.props.StringProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + if self.cost_type == "FIXED": + category = None + elif self.cost_type == "SUM": + category = "*" + elif self.cost_type == "CATEGORY": + category = self.cost_category + value = ifcopenshell.api.run("cost.add_cost_item_value", self.file, cost_item=self.file.by_id(self.cost_item)) + ifcopenshell.api.run( + "cost.edit_cost_item_value", self.file, cost_value=value, attributes={"Category": category} + ) + Data.load(self.file) + return {"FINISHED"} + + +class RemoveCostItemValue(bpy.types.Operator): + bl_idname = "bim.remove_cost_item_value" + bl_label = "Add Cost Item Value" + cost_value: bpy.props.IntProperty() + + def execute(self, context): + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.remove_cost_item_value", + self.file, + cost_value=self.file.by_id(self.cost_value), + ) + Data.load(self.file) + return {"FINISHED"} + + +class EnableEditingCostItemValue(bpy.types.Operator): + bl_idname = "bim.enable_editing_cost_item_value" + bl_label = "Enable Editing Cost Item Value" + cost_value: bpy.props.IntProperty() + + def execute(self, context): + self.props = context.scene.BIMCostProperties + while len(self.props.cost_value_attributes) > 0: + self.props.cost_value_attributes.remove(0) + self.props.active_cost_item_value_id = self.cost_value + data = Data.cost_values[self.cost_value] + + for attribute in IfcStore.get_schema().declaration_by_name(data["type"]).all_attributes(): + data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) + if data_type == "entity" or isinstance(data_type, tuple): + continue + new = self.props.cost_value_attributes.add() + new.name = attribute.name() + new.is_null = data[attribute.name()] is None + new.is_optional = attribute.optional() + new.data_type = data_type + if attribute.name() == "AppliedValue": + # TODO: for now, only support simple values + new.data_type = "float" + new.float_value = 0.0 if new.is_null else data[attribute.name()] + if data_type == "string": + new.string_value = "" if new.is_null else data[attribute.name()] + elif data_type == "float": + new.float_value = 0.0 if new.is_null else data[attribute.name()] + elif data_type == "integer": + new.int_value = 0 if new.is_null else data[attribute.name()] + elif data_type == "enum": + new.enum_items = json.dumps(ifcopenshell.util.attribute.get_enum_items(attribute)) + if data[attribute.name()]: + new.enum_value = data[attribute.name()] + return {"FINISHED"} + + +class DisableEditingCostItemValue(bpy.types.Operator): + bl_idname = "bim.disable_editing_cost_item_value" + bl_label = "Disable Editing Cost Item Value" + + def execute(self, context): + props = context.scene.BIMCostProperties + props.active_cost_item_value_id = 0 + return {"FINISHED"} + + +class EditCostItemValue(bpy.types.Operator): + bl_idname = "bim.edit_cost_item_value" + bl_label = "Edit Cost Item Value" + cost_value: bpy.props.IntProperty() + + def execute(self, context): + props = context.scene.BIMCostProperties + attributes = {} + for attribute in props.cost_value_attributes: + if attribute.is_null: + attributes[attribute.name] = None + else: + if attribute.data_type == "string": + attributes[attribute.name] = attribute.string_value + if attribute.data_type == "float": + attributes[attribute.name] = attribute.float_value + if attribute.data_type == "integer": + attributes[attribute.name] = attribute.int_value + self.file = IfcStore.get_file() + ifcopenshell.api.run( + "cost.edit_cost_item_value", + self.file, + **{"cost_value": self.file.by_id(self.cost_value), "attributes": attributes}, + ) + Data.load(IfcStore.get_file()) + bpy.ops.bim.disable_editing_cost_item_value() + return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index 6ec1b57444..a604fd9f1a 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -71,3 +71,11 @@ class BIMCostProperties(PropertyGroup): quantity_types: EnumProperty(items=getQuantityTypes, name="Quantity Types") active_cost_item_quantity_id: IntProperty(name="Active Cost Item Quantity Id") quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute) + 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_item_value_id: IntProperty(name="Active Cost Item Value Id") + cost_value_attributes: CollectionProperty(name="Cost Value Attributes", type=Attribute) diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 916b457613..88b16a0bf4 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -143,7 +143,52 @@ class BIM_PT_cost_schedules(Panel): def draw_editable_cost_item_values_ui(self): row = self.layout.row(align=True) - row.label(text="Editing values!") + 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_item_value", text="", icon="ADD") + op.cost_item = self.props.active_cost_item_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.cost_items[self.props.active_cost_item_id]["CostValues"]: + cost_value = Data.cost_values[cost_value_id] + row = self.layout.row(align=True) + row.label(text=str(cost_value["Category"])) + row.label(text=str(cost_value["AppliedValue"])) + if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id: + op = row.operator("bim.edit_cost_item_value", text="", icon="CHECKMARK") + op.cost_value = cost_value_id + row.operator("bim.disable_editing_cost_item_value", text="", icon="CANCEL") + elif self.props.active_cost_item_value_id: + op = row.operator("bim.remove_cost_item_value", text="", icon="X") + op.cost_value = cost_value_id + else: + op = row.operator("bim.enable_editing_cost_item_value", text="", icon="GREASEPENCIL") + op.cost_value = cost_value_id + op = row.operator("bim.remove_cost_item_value", text="", icon="X") + op.cost_value = cost_value_id + + if self.props.active_cost_item_value_id and self.props.active_cost_item_value_id == cost_value_id: + box = self.layout.box() + self.draw_editable_cost_item_value_ui(box) + + def draw_editable_cost_item_value_ui(self, layout): + for attribute in self.props.cost_value_attributes: + row = layout.row(align=True) + if attribute.data_type == "string": + row.prop(attribute, "string_value", text=attribute.name) + elif attribute.data_type == "boolean": + row.prop(attribute, "bool_value", text=attribute.name) + elif attribute.data_type == "integer": + row.prop(attribute, "int_value", text=attribute.name) + elif attribute.data_type == "float": + row.prop(attribute, "float_value", text=attribute.name) + elif attribute.data_type == "enum": + row.prop(attribute, "enum_value", text=attribute.name) + if attribute.is_optional: + row.prop(attribute, "is_null", icon="RADIOBUT_OFF" if attribute.is_null else "RADIOBUT_ON", text="") class BIM_UL_cost_items(UIList): @@ -170,12 +215,12 @@ class BIM_UL_cost_items(UIList): row.label(text="M3") op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES") op.cost_item = item.ifc_definition_id - row.label(text=str(cost_item["TotalCostQuantities"])) + row.label(text=str(cost_item["TotalCostQuantity"])) op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC") op.cost_item = item.ifc_definition_id - row.label(text="$100") - row.label(text="$1500", icon="CON_TRANSLIKE") + row.label(text=str(cost_item["TotalAppliedValue"])) + row.label(text=str(cost_item["TotalCostValue"]), icon="CON_TRANSLIKE") if context.active_object: oprops = context.active_object.BIMObjectProperties diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py index e3c92fbda4..b44282b2b1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py @@ -11,7 +11,6 @@ class Usecase: def execute(self): quantity = self.file.create_entity(self.settings["ifc_class"], Name="Unnamed") quantity[3] = 0.0 - cost_item = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcCostItem") quantities = list(self.settings["cost_item"].CostQuantities or []) quantities.append(quantity) self.settings["cost_item"].CostQuantities = quantities diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_value.py new file mode 100644 index 0000000000..b39b42ff32 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_value.py @@ -0,0 +1,16 @@ +import ifcopenshell.api + + +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_item": None, "ifc_class": "IfcQuantityCount"} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + value = self.file.create_entity("IfcCostValue") + values = list(self.settings["cost_item"].CostValues or []) + values.append(value) + self.settings["cost_item"].CostValues = values + return value diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py index b938dd9cda..79c63ec76a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/data.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/data.py @@ -6,6 +6,7 @@ class Data: cost_schedules = {} cost_items = {} physical_quantities = {} + cost_values = {} @classmethod def purge(cls): @@ -13,49 +14,126 @@ class Data: cls.cost_schedules = {} cls.cost_items = {} cls.physical_quantities = {} + cls.cost_values = {} @classmethod def load(cls, file): + cls.file = file cls.cost_schedules = {} cls.cost_items = {} cls.physical_quantities = {} + cls.cost_values = {} - for cost_schedule in file.by_type("IfcCostSchedule"): + for cost_schedule in cls.file.by_type("IfcCostSchedule"): data = cost_schedule.get_info() del data["OwnerHistory"] if data["SubmittedOn"]: data["SubmittedOn"] = ifcopenshell.util.date.ifc2datetime(data["SubmittedOn"]) if data["UpdateDate"]: data["UpdateDate"] = ifcopenshell.util.date.ifc2datetime(data["UpdateDate"]) - data["RelatedObjects"] = [] + data["Controls"] = [] for rel in cost_schedule.Controls: for related_object in rel.RelatedObjects: if related_object.is_a("IfcCostItem"): - data["RelatedObjects"].append(related_object.id()) - break # We are only allowed one summary cost item + data["Controls"].append(related_object.id()) + break # We are only allowed one summary cost item cls.cost_schedules[cost_schedule.id()] = data - for cost_item in file.by_type("IfcCostItem"): + for cost_item in cls.file.by_type("IfcCostItem"): data = cost_item.get_info() del data["OwnerHistory"] del data["CostValues"] - data["RelatedObjects"] = [] + data["IsNestedBy"] = [] data["Controls"] = [] for rel in cost_item.IsNestedBy: - [data["RelatedObjects"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcCostItem")] + [data["IsNestedBy"].append(o.id()) for o in rel.RelatedObjects if o.is_a("IfcCostItem")] for rel in cost_item.Controls: [data["Controls"].append(o.id()) for o in rel.RelatedObjects or []] cls.cost_items[cost_item.id()] = data cls.load_cost_item_quantities(cost_item, data) - cls.is_loaded=True + cls.load_cost_item_values(cost_item, data) + cls.is_loaded = True @classmethod def load_cost_item_quantities(cls, cost_item, data): data["CostQuantities"] = [] - data["TotalCostQuantities"] = 0.0 + data["TotalCostQuantity"] = cls.get_total_quantity(cost_item) for quantity in cost_item.CostQuantities or []: quantity_data = quantity.get_info() del quantity_data["Unit"] cls.physical_quantities[quantity.id()] = quantity_data data["CostQuantities"].append(quantity.id()) - data["TotalCostQuantities"] += quantity[3] + + @classmethod + def load_cost_item_values(cls, cost_item, data): + data["CostValues"] = [] + data["TotalCostValue"] = 0.0 + data["TotalAppliedValue"] = 0.0 + for cost_value in cost_item.CostValues or []: + cls.load_cost_item_value(cost_item, cost_value) + data["CostValues"].append(cost_value.id()) + print('applied value is', cls.cost_values[cost_value.id()]["AppliedValue"]) + data["TotalAppliedValue"] += cls.cost_values[cost_value.id()]["AppliedValue"] + data["TotalCostValue"] = data["TotalCostQuantity"] * data["TotalAppliedValue"] + + @classmethod + def load_cost_item_value(cls, cost_item, cost_value): + value_data = cost_value.get_info() + del value_data["AppliedValue"] + del value_data["UnitBasis"] + 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) + cls.cost_values[cost_value.id()] = value_data + + @classmethod + def calculate_applied_value(cls, cost_item, cost_value, category_filter=None): + result = 0 + if cost_value.ArithmeticOperator and cost_value.Components: + pass # TODO + 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 result + + @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) + result += child_applied_value * child_quantity + 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): + print('applied value is ', 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/edit_cost_item_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_value.py new file mode 100644 index 0000000000..b80a9ff01e --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_value.py @@ -0,0 +1,13 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_value": None, "attributes": {}} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + for name, value in self.settings["attributes"].items(): + if name == "AppliedValue" and value is not None: + # TODO: support all applied value select types + value = self.file.createIfcReal(value) + setattr(self.settings["cost_value"], name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py new file mode 100644 index 0000000000..05d166790c --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_value.py @@ -0,0 +1,9 @@ +class Usecase: + def __init__(self, file, **settings): + self.file = file + self.settings = {"cost_value": None} + for key, value in settings.items(): + self.settings[key] = value + + def execute(self): + self.file.remove(self.settings["cost_value"])