From e50215ddcec551334dbb558fcd45021ab8f0b3a9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 23 Jul 2025 18:57:35 +0500 Subject: [PATCH] cost.copy_cost_schedule #6901 A feature allowing duplicate existing IfcCostSchedule - either from API or from Bonsai UI Button location in Bonsai - https://files.catbox.moe/ct058q.png --- src/bonsai/bonsai/bim/module/cost/__init__.py | 1 + src/bonsai/bonsai/bim/module/cost/operator.py | 14 ++++++ src/bonsai/bonsai/bim/module/cost/ui.py | 1 + src/bonsai/bonsai/core/cost.py | 4 ++ src/bonsai/bonsai/tool/cost.py | 6 +++ .../ifcopenshell/api/cost/__init__.py | 2 + .../api/cost/copy_cost_schedule.py | 48 +++++++++++++++++++ 7 files changed, 76 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py diff --git a/src/bonsai/bonsai/bim/module/cost/__init__.py b/src/bonsai/bonsai/bim/module/cost/__init__.py index c148faeedd..6f42a71bf9 100644 --- a/src/bonsai/bonsai/bim/module/cost/__init__.py +++ b/src/bonsai/bonsai/bim/module/cost/__init__.py @@ -37,6 +37,7 @@ classes = ( operator.ContractCostItemRate, operator.ContractCostItems, operator.CopyCostItem, + operator.CopyCostSchedule, operator.CopyCostItemValues, operator.DisableEditingCostItem, operator.DisableEditingCostItemQuantity, diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index bf43ee8956..5bf5de0382 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -82,6 +82,20 @@ class RemoveCostSchedule(bpy.types.Operator, tool.Ifc.Operator): core.remove_cost_schedule(tool.Ifc, tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule)) +class CopyCostSchedule(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.copy_cost_schedule" + bl_label = "Copy Cost Schedule" + bl_description = "Create a duplicate of the provided cost schedule." + bl_options = {"REGISTER", "UNDO"} + cost_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration] + + if TYPE_CHECKING: + cost_schedule: int + + def _execute(self, context): + core.copy_cost_schedule(tool.Cost, cost_schedule=tool.Ifc.get().by_id(self.cost_schedule)) + + class EnableEditingCostSchedule(bpy.types.Operator): bl_idname = "bim.enable_editing_cost_schedule_attributes" bl_label = "Enable Editing Cost Schedule" diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py index c1a8e3a1ae..8e2033aa6a 100644 --- a/src/bonsai/bonsai/bim/module/cost/ui.py +++ b/src/bonsai/bonsai/bim/module/cost/ui.py @@ -129,6 +129,7 @@ class BIM_PT_cost_schedules(Panel): row.operator("bim.enable_editing_cost_schedule_attributes", text="", icon="GREASEPENCIL").cost_schedule = ( cost_schedule["id"] ) + row.operator("bim.copy_cost_schedule", text="", icon="DUPLICATE").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": diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index d2eedbb40d..144b6b44cf 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -48,6 +48,10 @@ def remove_cost_schedule( ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule) +def copy_cost_schedule(cost: type[tool.Cost], cost_schedule: ifcopenshell.entity_instance) -> None: + cost.copy_cost_schedule(cost_schedule) + + def enable_editing_cost_schedule_attributes(cost: type[tool.Cost], cost_schedule: ifcopenshell.entity_instance) -> None: cost.load_cost_schedule_attributes(cost_schedule) cost.enable_editing_cost_schedule_attributes(cost_schedule) diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 42aa38dbd6..7e1a3f7d74 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -1114,3 +1114,9 @@ class Cost(bonsai.core.tool.Cost): if results["quantity_type"] == "IfcQuantityCount": results["unit_symbol"] = "U" return results + + @classmethod + def copy_cost_schedule(cls, cost_schedule: ifcopenshell.entity_instance) -> None: + ifc_file = tool.Ifc.get() + new_schedule = ifcopenshell.api.cost.copy_cost_schedule(ifc_file, cost_schedule=cost_schedule) + new_schedule.Name = (cost_schedule.Name or "Unnamed") + " Copy" diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/cost/__init__.py index 30321d7a0f..5c0695b272 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/__init__.py @@ -34,6 +34,7 @@ from .assign_cost_value import assign_cost_value from .calculate_cost_item_resource_value import calculate_cost_item_resource_value from .copy_cost_item import copy_cost_item from .copy_cost_item_values import copy_cost_item_values +from .copy_cost_schedule import copy_cost_schedule from .edit_cost_item import edit_cost_item from .edit_cost_item_quantity import edit_cost_item_quantity from .edit_cost_schedule import edit_cost_schedule @@ -57,6 +58,7 @@ __all__ = [ "calculate_cost_item_resource_value", "copy_cost_item", "copy_cost_item_values", + "copy_cost_schedule", "edit_cost_item", "edit_cost_item_quantity", "edit_cost_schedule", diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py new file mode 100644 index 0000000000..bd22d3833a --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py @@ -0,0 +1,48 @@ +# 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.api.control +import ifcopenshell.api.cost +import ifcopenshell.util.element + + +def copy_cost_schedule( + file: ifcopenshell.file, cost_schedule: ifcopenshell.entity_instance +) -> ifcopenshell.entity_instance: + """Copy a cost schedule. + + :param cost_schedule: IfcCostSchedule to copy. + :return: The duplicated IfcCostSchedule entity + + Example: + + .. code:: python + + schedule = ifcopenshell.api.cost.add_cost_schedule(model) + new_schedule = ifcopenshell.api.cost.copy_cost_schedule(schedule) + """ + new_schedule = ifcopenshell.util.element.copy(file, cost_schedule) + + for rel in cost_schedule.Controls: + for cost_item in rel.RelatedObjects: + duplicated_cost_item = ifcopenshell.api.cost.copy_cost_item(file, cost_item) + if isinstance(duplicated_cost_item, list): + # All other nested items are not connected to the cost schedule explicitly. + duplicated_cost_item = duplicated_cost_item[0] + ifcopenshell.api.control.assign_control(file, new_schedule, duplicated_cost_item) + return new_schedule