Added relative CSV file paths

This commit is contained in:
falken10
2025-06-23 23:20:57 +02:00
committed by Massimo Fabbro
parent 72fbe3a6d9
commit 6aa6803490
4 changed files with 97 additions and 72 deletions
+58 -12
View File
@@ -541,6 +541,7 @@ class SelectCostScheduleProducts(bpy.types.Operator):
) )
return {"FINISHED"} return {"FINISHED"}
class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator): class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
bl_idname = "bim.import_cost_schedule_csv" bl_idname = "bim.import_cost_schedule_csv"
bl_label = "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" filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
is_schedule_of_rates: bpy.props.BoolProperty(name="Is Schedule Of Rates", default=False) 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 @classmethod
def poll(cls, context): def poll(cls, context):
@@ -559,9 +565,28 @@ class ImportCostScheduleCsv(bpy.types.Operator, ImportHelper, tool.Ifc.Operator)
return True return True
def _execute(self, context): def _execute(self, context):
cost_schedule = core.import_cost_schedule_csv(tool.Cost, self.filepath, self.is_schedule_of_rates) from pathlib import Path
core.add_csv_filepath(tool.Cost, self.filepath, self.is_schedule_of_rates, cost_schedule)
return {"FINISHED"} 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): 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: if not props.active_cost_schedule_id:
cls.poll_message_set("No active cost schedule") cls.poll_message_set("No active cost schedule")
return False 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 return True
def _execute(self, context): 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.delete_all_cost_items()
tool.Cost.refresh_cost_schedule_csv()
tool.Cost.load_cost_schedule_tree() cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id)
return {"FINISHED"} 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): class AddCostColumn(bpy.types.Operator):
+20 -2
View File
@@ -77,8 +77,26 @@ class BIM_PT_cost_schedules(Panel):
col.label(text="Linked CSV:") col.label(text="Linked CSV:")
row_1 = col.row(align=True) row_1 = col.row(align=True)
# Get filepath from document reference instead of props ifc_file = tool.Ifc.get()
file_path = tool.Cost.get_cost_schedule_csv_filepath(self.props.active_cost_schedule_id) 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: if file_path:
row_1.label(text=file_path) row_1.label(text=file_path)
+1
View File
@@ -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: 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) cost.add_csv_filepath(file_path, is_schedule_of_rates, cost_schedule)
def remove_csv_filepath(cost: type[tool.Cost], cost_schedule) -> None: def remove_csv_filepath(cost: type[tool.Cost], cost_schedule) -> None:
cost.remove_csv_filepath(cost_schedule) cost.remove_csv_filepath(cost_schedule)
+8 -48
View File
@@ -584,14 +584,16 @@ class Cost(bonsai.core.tool.Cost):
import time import time
start = time.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() csv2ifc.execute()
print("Import finished in {:.2f} seconds".format(time.time() - start)) print("Import finished in {:.2f} seconds".format(time.time() - start))
return csv2ifc.cost_schedule return csv2ifc.cost_schedule
@classmethod @classmethod
def add_csv_filepath(cls, file_path: str, is_schedule_of_rates: bool, cost_schedule) -> None: 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: if not file_path or not cost_schedule:
return return
@@ -610,14 +612,11 @@ class Cost(bonsai.core.tool.Cost):
cost_docs_document.Name = "BBIM_Cost_Documents" cost_docs_document.Name = "BBIM_Cost_Documents"
cost_docs_document.Description = "Bonsai internal document containing references to cost CSV files" 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 = ifcopenshell.api.document.add_reference(ifc_file, cost_docs_document)
reference.Location = file_path reference.Location = file_path
# Store the cost schedule ID in the reference description
reference.Description = f"Cost Schedule ID: {cost_schedule.id()}" 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: if is_schedule_of_rates:
reference.Identification = "SCHEDULE_OF_RATES" reference.Identification = "SCHEDULE_OF_RATES"
else: else:
@@ -625,7 +624,6 @@ class Cost(bonsai.core.tool.Cost):
@classmethod @classmethod
def remove_csv_filepath(cls, cost_schedule: ifcopenshell.entity_instance = None) -> None: def remove_csv_filepath(cls, cost_schedule: ifcopenshell.entity_instance = None) -> None:
"""Remove CSV filepath reference from IFC file."""
if not cost_schedule: if not cost_schedule:
return return
@@ -646,7 +644,6 @@ class Cost(bonsai.core.tool.Cost):
references = tool.Document.get_document_references(cost_docs_document) references = tool.Document.get_document_references(cost_docs_document)
for reference in references: 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: if reference.Description and f"Cost Schedule ID: {cost_schedule_id}" in reference.Description:
ifcopenshell.api.document.remove_reference(ifc_file, reference) ifcopenshell.api.document.remove_reference(ifc_file, reference)
print(f"Cost schedule id={cost_schedule_id} csv filepath correctly removed") print(f"Cost schedule id={cost_schedule_id} csv filepath correctly removed")
@@ -687,7 +684,6 @@ class Cost(bonsai.core.tool.Cost):
@classmethod @classmethod
def get_cost_schedule_csv_filepath(cls, cost_schedule_id: int) -> Optional[str]: 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() ifc_file = tool.Ifc.get()
cost_docs_document = next( cost_docs_document = next(
( (
@@ -709,11 +705,11 @@ class Cost(bonsai.core.tool.Cost):
return None return None
@classmethod @classmethod
def refresh_cost_schedule_csv(cls): def refresh_cost_schedule_csv(cls):
"""Refresh cost schedule from CSV file stored in document references.""" """Refresh cost schedule from CSV file stored in document references."""
from ifc5d.csv2ifc import Csv2Ifc from ifc5d.csv2ifc import Csv2Ifc
import os
props = cls.get_cost_props() props = cls.get_cost_props()
cost_schedule_id = props.active_cost_schedule_id cost_schedule_id = props.active_cost_schedule_id
@@ -722,11 +718,13 @@ class Cost(bonsai.core.tool.Cost):
if not file_path: if not file_path:
return return
resolved_path = tool.Ifc.resolve_uri(file_path)
cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id) cost_schedule = tool.Ifc.get_entity_by_id(cost_schedule_id)
is_schedule_of_rates = cls.is_schedule_of_rates_csv(cost_schedule_id) is_schedule_of_rates = cls.is_schedule_of_rates_csv(cost_schedule_id)
csv2ifc = Csv2Ifc() csv2ifc = Csv2Ifc()
csv2ifc.csv = file_path csv2ifc.csv = resolved_path
csv2ifc.file = tool.Ifc.get() csv2ifc.file = tool.Ifc.get()
csv2ifc.cost_schedule = cost_schedule csv2ifc.cost_schedule = cost_schedule
csv2ifc.is_schedule_of_rates = is_schedule_of_rates csv2ifc.is_schedule_of_rates = is_schedule_of_rates
@@ -1120,42 +1118,4 @@ class Cost(bonsai.core.tool.Cost):
return document return document
return None 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"