Repository: IfcOpenShell/IfcOpenShell (2026-07-23T12:59:22Z)

Issue:
Bonsai: Cost: duplicating a cost schedule also adds the copied items to the original schedule
### Bug Description

### Description
Duplicating a cost schedule leaves the *duplicated* cost items assigned to **both** schedules.
They are the same entity, so deleting them from the original also removes them from the copy.

### Steps to reproduce
1. Create a cost schedule with at least one top-level cost item.
2. Duplicate the schedule (`bim.copy_cost_schedule`, the duplicate icon in the Cost Schedules UI).
3. Open the original schedule → it now contains the duplicated items as well.
4. Delete those duplicates from the original → they also disappear from the new schedule.


### Attachments

_No response_

### Debug and Error Output

```true

```


Discussion:

Patch:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py
index 3101e820c2a..126748af4c0 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_schedule.py
@@ -45,5 +45,6 @@ def copy_cost_schedule(
             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.unassign_control(file, cost_schedule, [duplicated_cost_item])
             ifcopenshell.api.control.assign_control(file, new_schedule, [duplicated_cost_item])
     return new_schedule


Test patch:
diff --git a/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py b/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py
index 119c24387e3..cac94c320fc 100644
--- a/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py
+++ b/src/ifcopenshell-python/test/api/cost/test_copy_cost_schedule.py
@@ -40,6 +40,9 @@ def test_run(self):
         assert len(new_cost_items) == 2
         assert len(new_cost_items.intersection(old_cost_items)) == 0
 
+        # The copies should be removed from the original schedule.
+        assert set(ifcopenshell.util.cost.get_schedule_cost_items(schedule)) == old_cost_items
+
 
 class TestCopyCostScheduleIFC2X3(test.bootstrap.IFC2X3, TestCopyCostSchedule):
     pass