diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index 00bfb6efd7..f7706ab603 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -541,6 +541,7 @@ class SelectCostScheduleProducts(bpy.types.Operator): ) return {"FINISHED"} + class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator): bl_idname = "bim.import_cost_schedule_csv" bl_label = "Import Cost Schedule CSV" @@ -549,6 +550,11 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator) filename_ext = ".csv" filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False) + use_relative_path: bpy.props.BoolProperty( + name="Use Relative Path", + description="Store the CSV filepath relative to the currently opened IFC file", + default=False, + ) @classmethod def poll(cls, context): @@ -559,9 +565,28 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator) return True def _execute(self, context): - 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"} + from pathlib import Path + + store_path = self.filepath + if self.use_relative_path: + store_path = tool.Ifc.get_uri(self.filepath, use_relative_path=True) + + resolved_path = Path(tool.Ifc.resolve_uri(self.filepath)) + if not resolved_path.exists(): + self.report({"ERROR"}, f"File does not exist: '{store_path}' (resolved to '{resolved_path}')") + return {"CANCELLED"} + + cost_schedule = core.import_cost_schedule_csv(tool.Cost, str(resolved_path), self.is_schedule_of_rates) + if cost_schedule: + core.add_csv_filepath(tool.Cost, store_path, self.is_schedule_of_rates, cost_schedule) + return {"FINISHED"} + return {"CANCELLED"} + + def draw(self, context): + row = self.layout.row() + row.prop(self, "is_schedule_of_rates") + row = self.layout.row() + row.prop(self, "use_relative_path") class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator): @@ -576,19 +601,40 @@ class RefreshCostScheduleCsv(bpy.types.Operator, tool.Ifc.Operator): if not props.active_cost_schedule_id: cls.poll_message_set("No active cost schedule") return False - - filepath = tool.Cost.get_cost_schedule_csv_filepath(props.active_cost_schedule_id) - if not filepath: - cls.poll_message_set("No CSV file associated with this cost schedule") - return False - return True def _execute(self, context): + from pathlib import Path + + props = tool.Cost.get_cost_props() + cost_schedule_id = props.active_cost_schedule_id + + file_path = tool.Cost.get_cost_schedule_csv_filepath(cost_schedule_id) + resolved_path = Path(tool.Ifc.resolve_uri(file_path)) + if not resolved_path.exists(): + self.report({"ERROR"}, f"File does not exist: '{file_path}' (resolved to '{resolved_path}')") + return {"CANCELLED"} + tool.Cost.delete_all_cost_items() - tool.Cost.refresh_cost_schedule_csv() - tool.Cost.load_cost_schedule_tree() - return {"FINISHED"} + + cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id) + is_schedule_of_rates = tool.Cost.is_schedule_of_rates_csv(cost_schedule_id) + + try: + from ifc5d.csv2ifc import Csv2Ifc + + csv2ifc = Csv2Ifc() + csv2ifc.csv = str(resolved_path) + csv2ifc.file = tool.Ifc.get() + csv2ifc.cost_schedule = cost_schedule + csv2ifc.is_schedule_of_rates = is_schedule_of_rates + csv2ifc.refresh() + + tool.Cost.load_cost_schedule_tree() + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, f"Error refreshing CSV: {str(e)}") + return {"CANCELLED"} class AddCostColumn(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py index 13c6c76f19..8a81368d9e 100644 --- a/src/bonsai/bonsai/bim/module/cost/ui.py +++ b/src/bonsai/bonsai/bim/module/cost/ui.py @@ -76,10 +76,28 @@ class BIM_PT_cost_schedules(Panel): col = row0.column() col.label(text="Linked CSV:") row_1 = col.row(align=True) - - # Get filepath from document reference instead of props - file_path = tool.Cost.get_cost_schedule_csv_filepath(self.props.active_cost_schedule_id) - + + ifc_file = tool.Ifc.get() + cost_docs_document = next( + ( + document + for document in ifc_file.by_type("IfcDocumentInformation") + if document.Name == "BBIM_Cost_Documents" + ), + None, + ) + + file_path = None + if cost_docs_document: + references = tool.Document.get_document_references(cost_docs_document) + for reference in references: + if ( + reference.Description + and f"Cost Schedule ID: {self.props.active_cost_schedule_id}" in reference.Description + ): + file_path = reference.Location + break + if file_path: row_1.label(text=file_path) row_1.operator("bim.refresh_cost_schedule_csv", icon="FILE_REFRESH", text="") diff --git a/src/bonsai/bonsai/core/cost.py b/src/bonsai/bonsai/core/cost.py index c4b6731d19..f6c26a5ac7 100644 --- a/src/bonsai/bonsai/core/cost.py +++ b/src/bonsai/bonsai/core/cost.py @@ -349,6 +349,7 @@ def import_cost_schedule_csv( def add_csv_filepath(cost: type[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: type[tool.Cost], cost_schedule) -> None: cost.remove_csv_filepath(cost_schedule) diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index 8d55425069..0ab82f6e7b 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -584,14 +584,16 @@ class Cost(bonsai.core.tool.Cost): import time start = time.time() - csv2ifc = Csv2Ifc(file_path, tool.Ifc.get(), is_schedule_of_rates=is_schedule_of_rates) + + resolved_path = tool.Ifc.resolve_uri(file_path) + + csv2ifc = Csv2Ifc(resolved_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: str, is_schedule_of_rates: bool, cost_schedule) -> None: - """Store CSV filepath as a document reference in the IFC file.""" if not file_path or not cost_schedule: return @@ -610,14 +612,11 @@ class Cost(bonsai.core.tool.Cost): cost_docs_document.Name = "BBIM_Cost_Documents" cost_docs_document.Description = "Bonsai internal document containing references to cost CSV files" - # Create a reference with the filepath reference = ifcopenshell.api.document.add_reference(ifc_file, cost_docs_document) reference.Location = file_path - - # Store the cost schedule ID in the reference description + reference.Description = f"Cost Schedule ID: {cost_schedule.id()}" - - # If it's a schedule of rates, store that info too + if is_schedule_of_rates: reference.Identification = "SCHEDULE_OF_RATES" else: @@ -625,7 +624,6 @@ class Cost(bonsai.core.tool.Cost): @classmethod def remove_csv_filepath(cls, cost_schedule: ifcopenshell.entity_instance = None) -> None: - """Remove CSV filepath reference from IFC file.""" if not cost_schedule: return @@ -644,9 +642,8 @@ class Cost(bonsai.core.tool.Cost): cost_schedule_id = cost_schedule.id() references = tool.Document.get_document_references(cost_docs_document) - + for reference in references: - # Check if this reference is for the given cost schedule if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description: ifcopenshell.api.document.remove_reference(ifc_file, reference) print(f"Cost schedule id={cost_schedule_id} csv filepath correctly removed") @@ -678,16 +675,15 @@ class Cost(bonsai.core.tool.Cost): return False references = tool.Document.get_document_references(cost_docs_document) - + for reference in references: if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description: return reference.Identification == "SCHEDULE_OF_RATES" - + return False @classmethod def get_cost_schedule_csv_filepath(cls, cost_schedule_id: int) -> Optional[str]: - """Get CSV filepath for a cost schedule from document references.""" ifc_file = tool.Ifc.get() cost_docs_document = next( ( @@ -702,31 +698,33 @@ class Cost(bonsai.core.tool.Cost): return None references = tool.Document.get_document_references(cost_docs_document) - + for reference in references: if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description: return reference.Location - - return None + return None @classmethod def refresh_cost_schedule_csv(cls): """Refresh cost schedule from CSV file stored in document references.""" from ifc5d.csv2ifc import Csv2Ifc + import os props = cls.get_cost_props() cost_schedule_id = props.active_cost_schedule_id file_path = cls.get_cost_schedule_csv_filepath(cost_schedule_id) - + if not file_path: return + resolved_path = tool.Ifc.resolve_uri(file_path) + cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id) is_schedule_of_rates = cls.is_schedule_of_rates_csv(cost_schedule_id) csv2ifc = Csv2Ifc() - csv2ifc.csv = file_path + csv2ifc.csv = resolved_path csv2ifc.file = tool.Ifc.get() csv2ifc.cost_schedule = cost_schedule csv2ifc.is_schedule_of_rates = is_schedule_of_rates @@ -1120,42 +1118,4 @@ class Cost(bonsai.core.tool.Cost): return document return None - @classmethod - def add_csv_filepath( - cls, - file_path: Optional[str] = None, - is_schedule_of_rates: bool = False, - cost_schedule: ifcopenshell.entity_instance = None, - ) -> None: - """Store CSV filepath as a document reference in the IFC file.""" - if not file_path or not cost_schedule: - return - - ifc_file = tool.Ifc.get() - cost_docs_document = next( - ( - document - for document in ifc_file.by_type("IfcDocumentInformation") - if document.Name == "BBIM_Cost_Documents" - ), - None, - ) - - if not cost_docs_document: - cost_docs_document = ifcopenshell.api.document.add_information(ifc_file) - cost_docs_document.Name = "BBIM_Cost_Documents" - cost_docs_document.Description = "Bonsai internal document containing references to cost CSV files" - - # Create a reference with the filepath - reference = ifcopenshell.api.document.add_reference(ifc_file, cost_docs_document) - reference.Location = file_path - - # Store the cost schedule ID in the reference description - reference.Description = f"Cost Schedule ID: {cost_schedule.id()}" - - # If it's a schedule of rates, store that info too - if is_schedule_of_rates: - reference.Identification = "SCHEDULE_OF_RATES" - else: - reference.Identification = "COST_SCHEDULE"