From 8df40dc4a6fde8baca736d731e94dc0ec24ef7d2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 12 Jan 2024 18:14:49 +1100 Subject: [PATCH] Fix #4203. Fix bug where removing nested cost items left orphaned relationships. --- .../blenderbim/bim/module/cost/operator.py | 10 ++-- src/blenderbim/blenderbim/core/cost.py | 4 +- .../api/cost/add_cost_schedule.py | 6 +-- .../ifcopenshell/api/cost/remove_cost_item.py | 2 +- .../test/api/control/__init__.py | 17 +++++++ .../test/api/cost/__init__.py | 17 +++++++ .../test/api/cost/test_add_cost_item.py | 36 ++++++++++++++ .../test/api/cost/test_add_cost_schedule.py | 35 ++++++++++++++ .../test/api/cost/test_remove_cost_item.py | 47 +++++++++++++++++++ .../api/cost/test_remove_cost_schedule.py | 37 +++++++++++++++ 10 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 src/ifcopenshell-python/test/api/control/__init__.py create mode 100644 src/ifcopenshell-python/test/api/cost/__init__.py create mode 100644 src/ifcopenshell-python/test/api/cost/test_add_cost_item.py create mode 100644 src/ifcopenshell-python/test/api/cost/test_add_cost_schedule.py create mode 100644 src/ifcopenshell-python/test/api/cost/test_remove_cost_item.py create mode 100644 src/ifcopenshell-python/test/api/cost/test_remove_cost_schedule.py diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index f1629b25a7..ce64211be3 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -37,12 +37,10 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator): object_type: bpy.props.StringProperty() def _execute(self, context): - core.add_cost_schedule( - tool.Ifc, - name=self.name, - predefined_type=context.scene.BIMCostProperties.cost_schedule_predefined_types, - object_type=self.object_type, - ) + predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types + if predefined_type == "USERDEFINED": + predefined_type = self.object_type + core.add_cost_schedule(tool.Ifc, name=self.name, predefined_type=predefined_type) def draw(self, context): layout = self.layout diff --git a/src/blenderbim/blenderbim/core/cost.py b/src/blenderbim/blenderbim/core/cost.py index 19bf23f02a..ec8cc3e7e1 100644 --- a/src/blenderbim/blenderbim/core/cost.py +++ b/src/blenderbim/blenderbim/core/cost.py @@ -1,5 +1,5 @@ -def add_cost_schedule(ifc, name, predefined_type, object_type): - ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type, object_type=object_type) +def add_cost_schedule(ifc, name, predefined_type): + ifc.run("cost.add_cost_schedule", name=name, predefined_type=predefined_type) def edit_cost_schedule(ifc, cost, cost_schedule): diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py index 2ceff0b955..1d9882ff45 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py @@ -22,7 +22,7 @@ from datetime import datetime class Usecase: - def __init__(self, file, name=None, predefined_type="NOTDEFINED", object_type=None): + def __init__(self, file, name=None, predefined_type="NOTDEFINED"): """Add a new cost schedule A cost schedule is a group of cost items which typically represent a @@ -54,7 +54,7 @@ class Usecase: item = ifcopenshell.api.run("cost.add_cost_item", model, cost_schedule=schedule) """ self.file = file - self.settings = {"name": name, "predefined_type": predefined_type, "object_type": object_type} + self.settings = {"name": name, "predefined_type": predefined_type} def execute(self): cost_schedule = ifcopenshell.api.run( @@ -65,6 +65,4 @@ class Usecase: name=self.settings["name"], ) cost_schedule.UpdateDate = ifcopenshell.util.date.datetime2ifc(datetime.now(), "IfcDateTime") - if self.settings["object_type"]: - cost_schedule.ObjectType = self.settings["object_type"] return cost_schedule diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py index eaadf832d7..06d1f2ef26 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py @@ -52,7 +52,7 @@ class Usecase: if inverse.RelatingObject == self.settings["cost_item"]: for related_object in inverse.RelatedObjects: ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=related_object) - elif inverse.RelatedObjects == tuple(self.settings["cost_item"]): + elif inverse.RelatedObjects == (self.settings["cost_item"],): history = inverse.OwnerHistory self.file.remove(inverse) if history: diff --git a/src/ifcopenshell-python/test/api/control/__init__.py b/src/ifcopenshell-python/test/api/control/__init__.py new file mode 100644 index 0000000000..921ec6d8bb --- /dev/null +++ b/src/ifcopenshell-python/test/api/control/__init__.py @@ -0,0 +1,17 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 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 . diff --git a/src/ifcopenshell-python/test/api/cost/__init__.py b/src/ifcopenshell-python/test/api/cost/__init__.py new file mode 100644 index 0000000000..921ec6d8bb --- /dev/null +++ b/src/ifcopenshell-python/test/api/cost/__init__.py @@ -0,0 +1,17 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 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 . diff --git a/src/ifcopenshell-python/test/api/cost/test_add_cost_item.py b/src/ifcopenshell-python/test/api/cost/test_add_cost_item.py new file mode 100644 index 0000000000..488f10aadc --- /dev/null +++ b/src/ifcopenshell-python/test/api/cost/test_add_cost_item.py @@ -0,0 +1,36 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 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 test.bootstrap +import ifcopenshell.api + + +class TestAddCostItem(test.bootstrap.IFC4): + def test_add_a_cost_item(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo") + item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule) + assert item1.is_a("IfcCostItem") + assert item1.HasAssignments[0].is_a("IfcRelAssignsToControl") + assert item1.HasAssignments[0].RelatingControl == schedule + + def test_add_a_sub_cost_item(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo") + item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule) + item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1) + assert item2.is_a("IfcCostItem") + assert item2.Nests[0].RelatingObject == item1 diff --git a/src/ifcopenshell-python/test/api/cost/test_add_cost_schedule.py b/src/ifcopenshell-python/test/api/cost/test_add_cost_schedule.py new file mode 100644 index 0000000000..e02acb0ff6 --- /dev/null +++ b/src/ifcopenshell-python/test/api/cost/test_add_cost_schedule.py @@ -0,0 +1,35 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 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 test.bootstrap +import ifcopenshell.api + + +class TestAddCostSchedule(test.bootstrap.IFC4): + def test_add_a_cost_schedule(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET") + assert schedule.is_a("IfcCostSchedule") + assert schedule.Name == "Foo" + assert schedule.PredefinedType == "BUDGET" + + def test_adding_a_userdefined_type(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="FOO") + assert schedule.is_a("IfcCostSchedule") + assert schedule.Name == "Foo" + assert schedule.PredefinedType == "USERDEFINED" + assert schedule.ObjectType == "FOO" diff --git a/src/ifcopenshell-python/test/api/cost/test_remove_cost_item.py b/src/ifcopenshell-python/test/api/cost/test_remove_cost_item.py new file mode 100644 index 0000000000..e6768df8b8 --- /dev/null +++ b/src/ifcopenshell-python/test/api/cost/test_remove_cost_item.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 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 test.bootstrap +import ifcopenshell.api + + +class TestRemoveCostItem(test.bootstrap.IFC4): + def test_remove_a_cost_item(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET") + item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule) + ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=item1) + assert not self.file.by_type("IfcCostItem") + assert not self.file.by_type("IfcRelAssignsToControl") + + def test_remove_a_sub_cost_item(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET") + item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule) + item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1) + ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=item2) + assert self.file.by_type("IfcCostItem") == [item1] + assert self.file.by_type("IfcRelAssignsToControl") + assert not self.file.by_type("IfcRelNests") + + def test_remove_a_parent_cost_item(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET") + item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule) + item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1) + ifcopenshell.api.run("cost.remove_cost_item", self.file, cost_item=item1) + assert not self.file.by_type("IfcCostItem") + assert not self.file.by_type("IfcRelAssignsToControl") + assert not self.file.by_type("IfcRelNests") diff --git a/src/ifcopenshell-python/test/api/cost/test_remove_cost_schedule.py b/src/ifcopenshell-python/test/api/cost/test_remove_cost_schedule.py new file mode 100644 index 0000000000..6b88d81a3f --- /dev/null +++ b/src/ifcopenshell-python/test/api/cost/test_remove_cost_schedule.py @@ -0,0 +1,37 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2024 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 test.bootstrap +import ifcopenshell.api + + +class TestRemoveCostSchedule(test.bootstrap.IFC4): + def test_remove_a_cost_schedule(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET") + ifcopenshell.api.run("cost.remove_cost_schedule", self.file, cost_schedule=schedule) + assert not self.file.by_type("IfcCostSchedule") + + def test_remove_a_schedule_with_items(self): + schedule = ifcopenshell.api.run("cost.add_cost_schedule", self.file, name="Foo", predefined_type="BUDGET") + item1 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_schedule=schedule) + item2 = ifcopenshell.api.run("cost.add_cost_item", self.file, cost_item=item1) + ifcopenshell.api.run("cost.remove_cost_schedule", self.file, cost_schedule=schedule) + assert not self.file.by_type("IfcCostSchedule") + assert not self.file.by_type("IfcCostItem") + assert not self.file.by_type("IfcRelNests") + assert not self.file.by_type("IfcRelAssignsToControl")