diff --git a/src/bonsai/bonsai/bim/module/cost/__init__.py b/src/bonsai/bonsai/bim/module/cost/__init__.py index 5fdd668ed6..8f472b5488 100644 --- a/src/bonsai/bonsai/bim/module/cost/__init__.py +++ b/src/bonsai/bonsai/bim/module/cost/__init__.py @@ -61,6 +61,7 @@ classes = ( operator.ExportCostSchedules, operator.HighlightProductCostItem, operator.ImportCostScheduleCsv, + operator.RefreshCostScheduleCsv, operator.LoadCostItemElementQuantities, operator.LoadCostItemQuantities, operator.LoadCostItemResourceQuantities, @@ -82,6 +83,7 @@ classes = ( prop.CostItem, prop.CostItemQuantity, prop.CostItemType, + prop.CostItemsMapping, prop.ScheduleColumn, prop.BIMCostProperties, ui.BIM_PT_cost_schedules, diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index db64860598..8092ef2b4a 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -552,9 +552,19 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator) return True def _execute(self, context): - core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates) + cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates) + core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule) return {"FINISHED"} + +class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.refresh_cost_schedule_csv" + bl_label = "Refresh Cost Schedule CSV" + bl_options = {"REGISTER", "UNDO"} + @classmethod + def _execute(self, context): + core.refresh_cost_schedule_csv(tool.Ifc, tool.Cost) + return {"FINISHED"} class AddCostColumn(bpy.types.Operator): bl_idname = "bim.add_cost_column" diff --git a/src/bonsai/bonsai/bim/module/cost/prop.py b/src/bonsai/bonsai/bim/module/cost/prop.py index 1ce39c6f1a..5a568d1925 100644 --- a/src/bonsai/bonsai/bim/module/cost/prop.py +++ b/src/bonsai/bonsai/bim/module/cost/prop.py @@ -166,6 +166,13 @@ class CostItemQuantity(PropertyGroup): unit_symbol: str total_cost_quantity: float +class CostItemsMapping(PropertyGroup): + cost_item_id: IntProperty(name="cost_item_id") + csv_filepath: StringProperty(name="filepath") + + if TYPE_CHECKING: + cost_item_id: int + csv_filepath: str class CostItemType(PropertyGroup): name: StringProperty(name="Name") @@ -272,6 +279,7 @@ class BIMCostProperties(PropertyGroup): custom_currency: StringProperty( name="Custom Currency", default="USD", description="Custom Currency in ISO 4217 format" ) + cost_schedule_files: CollectionProperty(name="Cost Schedule Files", type=CostItemsMapping) if TYPE_CHECKING: cost_schedule_predefined_types: str @@ -326,3 +334,4 @@ class BIMCostProperties(PropertyGroup): show_cost_item_operators: bool currency: str custom_currency: str + cost_schedule_files: bpy.types.bpy_prop_collection_idprop[CostItemsMapping] diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py index 4303af64b6..3ce0653fd5 100644 --- a/src/bonsai/bonsai/bim/module/cost/ui.py +++ b/src/bonsai/bonsai/bim/module/cost/ui.py @@ -71,6 +71,19 @@ class BIM_PT_cost_schedules(Panel): text="Currently editing: {}[{}]".format(cost_schedule["name"], cost_schedule["predefined_type"]), icon="LINENUMBERS_ON", ) + + row0 = self.layout.row(align=True) + col = row0.column() + col.label(text="Linked CSV:") + row_1 = col.row(align=True) + if self.props.active_cost_schedule_id in [item.cost_item_id for item in self.props.cost_schedule_files]: + file = next((item.csv_filepath for item in self.props.cost_schedule_files if item.cost_item_id == self.props.active_cost_schedule_id), None) + row_1.label(text=file) + row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="") + else: + row_1.label(text="No CSV file found") + row_1.operator("bim.import_cost_schedule_csv", icon="IMPORT", text="") + grid = self.layout.grid_flow(columns=2, even_columns=True) col = grid.column() row1 = col.row(align=True) diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index 4811738d4c..113af74bf4 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -42,6 +42,7 @@ def disable_editing_cost_schedule(cost: tool.Cost) -> None: def remove_cost_schedule(ifc: tool.Ifc, cost: tool.Cost, cost_schedule: ifcopenshell.entity_instance) -> None: cost.remove_stored_schedule_columns(cost_schedule) + cost.remove_csv_filepath(cost_schedule) ifc.run("cost.remove_cost_schedule", cost_schedule=cost_schedule) @@ -311,10 +312,20 @@ def select_cost_schedule_products( products = cost.get_cost_schedule_products(cost_schedule) spatial.select_products(products) +def import_cost_schedule_csv(cost: tool.Cost, file_path: str, is_schedule_of_rates: bool) -> ifcopenshell.entity_instance: + cost_schedule = cost.import_cost_schedule_csv(file_path, is_schedule_of_rates) + return cost_schedule -def import_cost_schedule_csv(cost: tool.Cost, file_path: str, is_schedule_of_rates: bool) -> None: - cost.import_cost_schedule_csv(file_path, is_schedule_of_rates) +def add_csv_filepath(cost: tool.Cost, file_path: str, is_schedule_of_rates: bool, cost_schedule) -> None: + cost.add_csv_filepath(file_path, is_schedule_of_rates, cost_schedule) +def remove_csv_filepath(cost: tool.Cost, cost_schedule) -> None: + cost.remove_csv_filepath(cost_schedule) + +def refresh_cost_schedule_csv(ifc: tool.Ifc, cost:tool.Cost) -> None: + cost.delete_all_cost_items() + cost.refresh_cost_schedule_csv() + cost.load_cost_schedule_tree() def add_cost_column(cost: tool.Cost, name: str) -> None: cost.add_cost_column(name) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index 3ff9a6e90a..3a7871268f 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -246,6 +246,10 @@ class Cost: def has_schedules(cls): pass def highlight_cost_item(cls, cost_item): pass def import_cost_schedule_csv(cls, file_path, is_schedule_of_rates): pass + def add_csv_filepath(cls, file_path, is_schedule_of_rates, cost_schedule): pass + def remove_csv_filepath(cls, cost_schedule): pass + def delete_all_cost_items(cls): pass + def refresh_cost_schedule_csv(cls): pass def is_active_schedule_of_rates(cls): pass def is_cost_schedule_active(cls, cost_schedule): pass def is_root_cost_item(cls, cost_item): pass diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index b507ff01cb..482783718c 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -561,7 +561,7 @@ class Cost(bonsai.core.tool.Cost): return products @classmethod - def import_cost_schedule_csv(cls, file_path: Optional[str] = None, is_schedule_of_rates: bool = False) -> None: + def import_cost_schedule_csv(cls, file_path: Optional[str] = None, is_schedule_of_rates: bool = False) -> ifcopenshell.entity_instance: if not file_path: return from ifc5d.csv2ifc import Csv2Ifc @@ -571,7 +571,67 @@ class Cost(bonsai.core.tool.Cost): csv2ifc = Csv2Ifc(file_path, tool.Ifc.get(), is_schedule_of_rates=is_schedule_of_rates) csv2ifc.execute() print("Import finished in {:.2f} seconds".format(time.time() - start)) + return csv2ifc.cost_schedule + + @classmethod + def add_csv_filepath(cls, file_path: Optional[str] = None, is_schedule_of_rates: bool = False, cost_schedule: ifcopenshell.entity_instance = None) -> None: + if not file_path: + return + + props = cls.get_cost_props() + if not props.active_cost_schedule_id in [item.cost_item_id for item in props.cost_schedule_files]: + item = props.cost_schedule_files.add() + item.cost_item_id = cost_schedule.id() + item.csv_filepath = file_path + else: + return + + @classmethod + def remove_csv_filepath(cls, cost_schedule: ifcopenshell.entity_instance = None) -> None: + if not cost_schedule: + return + + props = cls.get_cost_props() + cost_schedule_id = cost_schedule.id() + if cost_schedule_id in [item.cost_item_id for item in props.cost_schedule_files]: + for i, item in enumerate(props.cost_schedule_files): + if item.cost_item_id == cost_schedule_id: + props.cost_schedule_files.remove(i) + print(f"Cost schedule id={cost_schedule_id} csv filepath correctly removed") + return + else: + return + + @classmethod + def delete_all_cost_items(cls): + cost_schedule = tool.Cost.get_active_cost_schedule() + items = ifcopenshell.util.cost.get_root_cost_items(cost_schedule) + for item in items: + cost_item_id = item.id() + ifcopenshell.api.run("cost.remove_cost_item", tool.Ifc.get(), cost_item = item) + tool.Cost.clean_up_cost_item_tree(cost_item_id) + + @classmethod + def refresh_cost_schedule_csv(cls): + from ifc5d.csv2ifc import Csv2Ifc + props = cls.get_cost_props() + cost_schedule_id = props.active_cost_schedule_id + file_path = next((item.csv_filepath for item in props.cost_schedule_files if item.cost_item_id == cost_schedule_id), None) + if not file_path: + return + + cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id) + + csv2ifc = Csv2Ifc() + csv2ifc.csv = file_path + csv2ifc.file = tool.Ifc.get() + csv2ifc.cost_schedule = cost_schedule + csv2ifc.is_schedule_of_rates = False + csv2ifc.refresh() + + print("Csv file correctly refreshed") + @classmethod def add_cost_column(cls, name: str) -> None: props = cls.get_cost_props() diff --git a/src/ifc5d/ifc5d/csv2ifc.py b/src/ifc5d/ifc5d/csv2ifc.py index 95b7731909..197ff9b0be 100644 --- a/src/ifc5d/ifc5d/csv2ifc.py +++ b/src/ifc5d/ifc5d/csv2ifc.py @@ -127,6 +127,10 @@ class Csv2Ifc: self.parse_csv() self.create_ifc() + def refresh(self) -> None: + self.parse_csv() + self.create_cost_items(self.cost_items) + def parse_csv(self) -> None: """Fill ``headers`` and ``cost_items`` based on data from csv file.""" self.cost_items = []