From a4a6dce6defbddd02ed1b1aeba0070540aec7cec Mon Sep 17 00:00:00 2001 From: Sigma Dimensions <79010126+myoualid@users.noreply.github.com> Date: Sun, 21 May 2023 12:42:50 +0100 Subject: [PATCH] BBIM D: Copy cost items, export Cost Schedules individually, start improving Cost Schedule UI --- .../blenderbim/bim/module/cost/__init__.py | 1 + .../blenderbim/bim/module/cost/operator.py | 28 +++-- .../blenderbim/bim/module/cost/prop.py | 1 + .../blenderbim/bim/module/cost/ui.py | 104 ++++++++++------- src/blenderbim/blenderbim/core/cost.py | 13 ++- src/blenderbim/blenderbim/tool/cost.py | 9 +- src/ifc5d/ifc5d/ifc5Dspreadsheet.py | 7 +- .../ifcopenshell/api/cost/copy_cost_item.py | 110 ++++++++++++++++++ .../api/material/edit_material.py | 29 +++++ 9 files changed, 243 insertions(+), 59 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/material/edit_material.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index b05e41a5ad..e11a6ef850 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -76,6 +76,7 @@ classes = ( operator.LoadCostItemTaskQuantities, operator.LoadCostItemResourceQuantities, operator.ChangeParentCostItem, + operator.CopyCostItem, prop.CostItem, prop.CostItemQuantity, prop.CostItemType, diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 29c2fc1141..c76178e320 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -97,10 +97,9 @@ class AddSummaryCostItem(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Add Cost Item" bl_options = {"REGISTER", "UNDO"} bl_description = "Add a summary cost item" - cost_schedule: bpy.props.IntProperty() def _execute(self, context): - core.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule)) + core.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=tool.Cost.get_active_cost_schedule()) class AddCostItem(bpy.types.Operator, tool.Ifc.Operator): @@ -114,6 +113,16 @@ class AddCostItem(bpy.types.Operator, tool.Ifc.Operator): core.add_cost_item(tool.Ifc, tool.Cost, cost_item=tool.Ifc.get().by_id(self.cost_item)) +class CopyCostItem(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.copy_cost_item" + bl_label = "Copy Cost Item" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Copy a cost item" + + def _execute(self, context): + core.copy_cost_item(tool.Ifc, tool.Cost) + + class ExpandCostItem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.expand_cost_item" bl_label = "Expand Cost Item" @@ -214,8 +223,9 @@ class UnassignCostItemType(bpy.types.Operator, tool.Ifc.Operator): core.unassign_cost_item_type( tool.Ifc, tool.Cost, - self.cost_item, - products=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [], + tool.Spatial, + cost_item=tool.Ifc.get().by_id(self.cost_item), + product_types=[tool.Ifc.get().by_id(self.related_object)] if self.related_object else [], ) return {"FINISHED"} @@ -491,7 +501,7 @@ class AddCostColumn(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} name: bpy.props.StringProperty() - def _execute(self, context): + def execute(self, context): core.add_cost_column(tool.Cost, self.name) return {"FINISHED"} @@ -619,10 +629,12 @@ class ExportCostSchedules(bpy.types.Operator): bl_label = "Export Cost Schedule" bl_options = {"REGISTER", "UNDO"} bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file" + cost_schedule: bpy.props.IntProperty() format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", ""))) def execute(self, context): - core.export_cost_schedules(tool.Cost, format=self.format) + cost_schedule = tool.Ifc.get().by_id(self.cost_schedule) if self.cost_schedule else None + core.export_cost_schedules(tool.Cost, format=self.format, cost_schedule=cost_schedule) return {"FINISHED"} def invoke(self, context, event): @@ -667,7 +679,9 @@ class LoadProductCostItems(bpy.types.Operator): return True def execute(self, context): - core.load_product_cost_items(tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)) + core.load_product_cost_items( + tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) + ) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index cdd5e08e92..eddc816e14 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -213,3 +213,4 @@ class BIMCostProperties(PropertyGroup): name="Show Nested Tasks", default=False, update=update_active_cost_item_resources ) change_cost_item_parent: BoolProperty(name="Change Cost Item Parent", default=False, update=update_cost_item_parent) + show_cost_item_operators: BoolProperty(name="Show Cost Item Operators", default=False) diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index f0aa85c2e5..db39cc51f1 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -43,43 +43,54 @@ class BIM_PT_cost_schedules(Panel): self.props = context.scene.BIMCostProperties row = self.layout.row() - if CostSchedulesData.data["total_cost_schedules"]: - row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") - row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT") - else: - row.label(text="No Cost Schedules Found found.", icon="COMMUNITY") - - row = self.layout.row() - row.prop(self.props, "cost_schedule_predefined_types") - row.operator("bim.add_cost_schedule", icon="ADD", text="Add") + if not self.props.active_cost_schedule_id: + if CostSchedulesData.data["total_cost_schedules"]: + row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") + row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT") + else: + row.label(text="No Cost Schedules Found found.", icon="COMMUNITY") + row = self.layout.row() + row.prop(self.props, "cost_schedule_predefined_types") + row.operator("bim.add_cost_schedule", icon="ADD", text="Add") for schedule in CostSchedulesData.data["schedules"]: self.draw_cost_schedule_ui(schedule) def draw_cost_schedule_ui(self, cost_schedule): row = self.layout.row(align=True) - row.label(text=cost_schedule["name"], icon="LINENUMBERS_ON") - if self.props.active_cost_schedule_id and self.props.active_cost_schedule_id == cost_schedule["id"]: - op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned") + row.label(text="Currently editing: {}".format(cost_schedule["name"]), icon="LINENUMBERS_ON") + grid = self.layout.grid_flow(columns=2, even_columns=True) + col = grid.column() + row1 = col.row(align=True) + row1.alignment = "LEFT" + row1.label(text="Schedule tools") + row1 = col.row(align=True) + row1.alignment = "RIGHT" + row1.operator("bim.export_cost_schedules", text="Export", icon="EXPORT").cost_schedule = cost_schedule["id"] + row2 = col.row(align=True) + row2.alignment = "RIGHT" + op = row2.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="Assigned") op.cost_schedule = cost_schedule["id"] - row.operator("bim.select_unassigned_products", icon="RESTRICT_SELECT_OFF", text="Unassigned") + row2.operator("bim.select_unassigned_products", icon="RESTRICT_SELECT_OFF", text="Unassigned") - row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY") + col = grid.column() + row1 = col.row(align=True) + row1.alignment = "LEFT" + row1.label(text="Settings") + row1 = col.row(align=True) + row1.alignment = "RIGHT" + row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY") if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES": row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") - elif self.props.is_editing == "COST_ITEMS": - row.operator("bim.add_summary_cost_item", text="", icon="ADD").cost_schedule = cost_schedule["id"] - row.operator("bim.disable_editing_cost_schedule", text="", icon="CANCEL") - elif self.props.active_cost_schedule_id: - row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"] + row.operator("bim.disable_editing_cost_schedule", text="Disable Editing", icon="CANCEL") else: + row.label(text=cost_schedule["name"], icon="LINENUMBERS_ON") row.operator("bim.enable_editing_cost_items", text="", icon="OUTLINER").cost_schedule = cost_schedule["id"] row.operator( "bim.enable_editing_cost_schedule_attributes", text="", icon="GREASEPENCIL" ).cost_schedule = cost_schedule["id"] row.operator("bim.remove_cost_schedule", text="", icon="X").cost_schedule = cost_schedule["id"] - if self.props.active_cost_schedule_id == cost_schedule["id"]: if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES": self.draw_editable_cost_schedule_ui() @@ -101,30 +112,39 @@ class BIM_PT_cost_schedules(Panel): row = self.layout.row(align=True) row.alignment = "RIGHT" ifc_definition_id = None + row = self.layout.row(align=True) + row.label(text="Cost Item Tools") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.operator("bim.add_summary_cost_item", text="Add Summary Cost", icon="ADD") + row.operator("bim.expand_all_tasks", text="Expand All") + row.operator("bim.contract_all_tasks", text="Contract All") + row = self.layout.row(align=True) + row.alignment = "RIGHT" if self.props.cost_items and self.props.active_cost_item_index < len(self.props.cost_items): ifc_definition_id = self.props.cost_items[self.props.active_cost_item_index].ifc_definition_id if ifc_definition_id: - row.prop(self.props, "change_cost_item_parent", text="", icon="LINKED") - row.prop(self.props, "enable_reorder", text="", icon="SORTALPHA") - if not CostSchedulesData.data["is_editing_rates"]: - op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES") + row.prop(self.props, "show_cost_item_operators", text="Edit", icon="DOWNARROW_HLT") + row.operator("bim.add_cost_item", text="Add", icon="ADD").cost_item = ifc_definition_id + row.operator("bim.copy_cost_item", text="Copy", icon="ADD") + row.operator("bim.remove_cost_item", text="Delete", icon="X").cost_item = ifc_definition_id + if self.props.show_cost_item_operators: + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.prop(self.props, "change_cost_item_parent", text="", icon="LINKED") + row.prop(self.props, "enable_reorder", text="", icon="SORTALPHA") + if not CostSchedulesData.data["is_editing_rates"]: + op = row.operator("bim.enable_editing_cost_item_quantities", text="", icon="PROPERTIES") + op.cost_item = ifc_definition_id + op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC") op.cost_item = ifc_definition_id - - op = row.operator("bim.enable_editing_cost_item_values", text="", icon="DISC") - op.cost_item = ifc_definition_id - - row.operator("bim.add_cost_item", text="", icon="ADD").cost_item = ifc_definition_id - - if self.props.active_cost_item_id == ifc_definition_id: - if self.props.cost_item_editing_type == "ATTRIBUTES": - row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") - else: - op = row.operator("bim.enable_editing_cost_item_attributes", text="", icon="GREASEPENCIL") - op.cost_item = ifc_definition_id - - row.operator("bim.remove_cost_item", text="", icon="X").cost_item = ifc_definition_id - + if self.props.active_cost_item_id == ifc_definition_id: + if self.props.cost_item_editing_type == "ATTRIBUTES": + row.operator("bim.edit_cost_item", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_cost_item", text="", icon="CANCEL") + else: + op = row.operator("bim.enable_editing_cost_item_attributes", text="", icon="GREASEPENCIL") + op.cost_item = ifc_definition_id self.layout.template_list( "BIM_UL_cost_items", "", @@ -698,9 +718,7 @@ class BIM_PT_Costing_Tools(Panel): def draw(self, context): self.props = context.scene.BIMCostProperties row = self.layout.row() - row.operator( - "bim.load_product_cost_items", icon="FILE_REFRESH" - ) + row.operator("bim.load_product_cost_items", icon="FILE_REFRESH") row = self.layout.row() row.template_list( "BIM_UL_product_cost_items", diff --git a/src/blenderbim/blenderbim/core/cost.py b/src/blenderbim/blenderbim/core/cost.py index 94f911099f..f995ad12c0 100644 --- a/src/blenderbim/blenderbim/core/cost.py +++ b/src/blenderbim/blenderbim/core/cost.py @@ -236,8 +236,8 @@ def calculate_cost_item_resource_value(ifc, cost_item): ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item) -def export_cost_schedules(cost, format): - cost.export_cost_schedules(format) +def export_cost_schedules(cost, format, cost_schedule=None): + cost.export_cost_schedules(format, cost_schedule) def clear_cost_item_assignments(ifc, cost, cost_item, related_object_type): @@ -276,4 +276,11 @@ def change_parent_cost_item(ifc, cost, new_parent): if cost_item : ifc.run("nest.change_nest", item=cost_item, new_parent=new_parent) cost.disable_editing_cost_item_parent() - cost.load_cost_schedule_tree() \ No newline at end of file + cost.load_cost_schedule_tree() + +def copy_cost_item(ifc, cost): + cost_item = cost.get_highlighted_cost_item() + if cost_item: + cost_item = ifc.run("cost.copy_cost_item", cost_item=cost_item) + cost.disable_editing_cost_item_parent() + cost.load_cost_schedule_tree() diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index 2ebdcd7f1f..181815eb18 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -498,24 +498,23 @@ class Cost(blenderbim.core.tool.Cost): props.is_cost_update_enabled = True @classmethod - def export_cost_schedules(cls, format=None): + def export_cost_schedules(cls, format=None, cost_schedule=None): path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules") if format == "CSV": from ifc5d.ifc5Dspreadsheet import Ifc5DCsvWriter - if not os.path.exists(path): os.makedirs(path) - writer = Ifc5DCsvWriter(file=tool.Ifc.get(), output=path) + writer = Ifc5DCsvWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() elif format == "ODS": from ifc5d.ifc5Dspreadsheet import Ifc5DOdsWriter - writer = Ifc5DOdsWriter(file=tool.Ifc.get(), output=path) + writer = Ifc5DOdsWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule) writer.write() elif format == "XLSX": from ifc5d.ifc5Dspreadsheet import Ifc5DXlsxWriter - writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path) + writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path, cost_schedule=cost_schedule ) writer.write() @classmethod diff --git a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py index 546f2bd2c1..3fa9e9b1c5 100644 --- a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py +++ b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py @@ -195,12 +195,13 @@ class IfcDataGetter: class Ifc5Dwriter: - def __init__(self, file=None, output=None): + def __init__(self, file=None, output=None, cost_schedule=None): self.output = output if isinstance(file, str): self.file = ifcopenshell.open(file) else: self.file = file + self.cost_schedule = cost_schedule self.cost_schedules = [] self.sheet_data = {} self.column_indexes = [] @@ -220,6 +221,10 @@ class Ifc5Dwriter: self.used_names = [] for i in range(26): self.column_indexes.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ"[i % 26]) + if self.cost_schedule: + for cost_schedule in self.cost_schedules: + if cost_schedule.id() != self.cost_schedule.id(): + self.cost_schedules.remove(cost_schedule) for cost_schedule in self.cost_schedules: sheet_id = cost_schedule.id() self.sheet_data[sheet_id] = {} diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py new file mode 100644 index 0000000000..55c4660b9f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py @@ -0,0 +1,110 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell 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. +# +# IfcOpenShell 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 IfcOpenShell. If not, see . + +import ifcopenshell +import ifcopenshell.util.element + + +class Usecase: + def __init__(self, file, cost_item=None): + """Copies all cost items and related relationships + + The following relationships are also duplicated: + + * The copy will have the same attributes and property sets as the original cost item + * The copy will be assigned to the parent cost schedule + * The copy will have duplicated nested cost items + + :param cost_item: The cost item to be duplicated + :type cost_item: ifcopenshell.entity_instance.entity_instance + :return: The duplicated cost item or the list of duplicated cost items if the latter has children + :rtype: ifcopenshell.entity_instance.entity_instance or list of ifcopenshell.entity_instance.entity_instance + + Example: + .. code:: python + + # We have a cost item + cost_item = CostItem(name="Design new feature", deadline="2023-03-01") + + # And now we have two + duplicated_cost_item = project.duplicate_cost_item(cost_item) + + + """ + self.file = file + self.settings = {"cost_item": cost_item} + + def execute(self): + self.new_cost_items = [] + self.duplicate_cost_item(self.settings["cost_item"]) + + def duplicate_cost_item(self, cost_item): + new_cost_item = ifcopenshell.util.element.copy_deep(self.file, cost_item) + self.new_cost_items.append(new_cost_item) + self.copy_indirect_attributes(cost_item, new_cost_item) + return new_cost_item + + def copy_indirect_attributes(self, from_element, to_element): + for inverse in self.file.get_inverse(from_element): + if inverse.is_a("IfcRelDefinesByProperties"): + inverse = ifcopenshell.util.element.copy(self.file, inverse) + inverse.RelatedObjects = [to_element] + pset = ifcopenshell.util.element.copy_deep( + self.file, inverse.RelatingPropertyDefinition + ) + inverse.RelatingPropertyDefinition = pset + elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element: + nested_cost_items = [e for e in inverse.RelatedObjects] + if nested_cost_items: + new_cost_items = [] + for cost_item in nested_cost_items: + new_cost_item = self.duplicate_cost_item(cost_item) + new_cost_items.append(new_cost_item) + inverse = ifcopenshell.util.element.copy(self.file, inverse) + inverse.RelatingObject = to_element + inverse.RelatedObjects = new_cost_items + for cost_item in new_cost_items: + ifcopenshell.api.run( + "nest.unassign_object", self.file, related_object=cost_item + ) + rel = ifcopenshell.api.run( + "nest.assign_object", + self.file, + related_object=cost_item, + relating_object=to_element, + ) + # elif inverse.is_a("IfcRelAssignsToProduct"): + # continue + # to_element.IsDefinedBy = inverse.RelatingOrder + # elif inverse.is_a("IfcRelAssignsToProcess"): + # to_element.IsDefinedBy = inverse.RelatingProcess + # elif inverse.is_a("IfcRelAssignsToResource"): + # continue + # elif inverse.is_a("IfcRelAssignsToControl"): + # continue + # elif inverse.is_a("IfcRelDefinesByObject"): + # continue + else: + for i, value in enumerate(inverse): + if value == from_element: + new_inverse = ifcopenshell.util.element.copy(self.file, inverse) + new_inverse[i] = to_element + elif isinstance(value, (tuple, list)) and from_element in value: + new_value = list(value) + new_value.append(to_element) + inverse[i] = new_value \ No newline at end of file diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_material.py new file mode 100644 index 0000000000..c712af15ac --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_material.py @@ -0,0 +1,29 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell 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. +# +# IfcOpenShell 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 IfcOpenShell. If not, see . + + +class Usecase: + def __init__(self, file, material=None, attributes=None): + """Edits the attributes of an IfcMaterial""" + + self.file = file + self.settings = {"material": material, "attributes": attributes or {}} + + def execute(self): + for name, value in self.settings["attributes"].items(): + setattr(self.settings["material"], name, value)