diff --git a/src/bonsai/bonsai/bim/module/resource/__init__.py b/src/bonsai/bonsai/bim/module/resource/__init__.py
index 002952b3c1..33b507e0a8 100644
--- a/src/bonsai/bonsai/bim/module/resource/__init__.py
+++ b/src/bonsai/bonsai/bim/module/resource/__init__.py
@@ -43,6 +43,7 @@ classes = (
operator.EnableEditingResourceBaseQuantity,
operator.EnableEditingResourceCosts,
operator.EnableEditingResourceCostValue,
+ operator.ExportResources,
operator.CalculateResourceUsage,
operator.EnableEditingResourceCostValueFormula,
operator.EnableEditingResourceQuantity,
diff --git a/src/bonsai/bonsai/bim/module/resource/operator.py b/src/bonsai/bonsai/bim/module/resource/operator.py
index b09069f997..7a72ac0f32 100644
--- a/src/bonsai/bonsai/bim/module/resource/operator.py
+++ b/src/bonsai/bonsai/bim/module/resource/operator.py
@@ -17,7 +17,7 @@
# along with Bonsai. If not, see .
import bpy
-from bpy_extras.io_utils import ImportHelper
+from bpy_extras.io_utils import ImportHelper, ExportHelper
from bonsai.bim.module.resource.ui import draw_productivity_ui
import bonsai.core.resource as core
import bonsai.tool as tool
@@ -334,6 +334,7 @@ class EditResourceQuantity(bpy.types.Operator, tool.Ifc.Operator):
class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
bl_idname = "bim.import_resources"
bl_label = "Import Resources"
+ bl_description = "Import resources from a .csv file"
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".csv"
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
@@ -351,6 +352,24 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
self.report({"INFO"}, f"{resources_after - resources_before} resources are imported.")
+class ExportResources(bpy.types.Operator, tool.Ifc.Operator, ExportHelper):
+ bl_idname = "bim.export_resources"
+ bl_label = "Export Resources"
+ bl_description = "Export resources to a .csv file"
+ bl_options = {"REGISTER", "UNDO"}
+ filename_ext = ".csv"
+ filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
+
+ @classmethod
+ def poll(cls, context):
+ ifc_file = tool.Ifc.get()
+ return ifc_file is not None
+
+ def _execute(self, context):
+ core.export_resources(tool.Resource, file_path=self.filepath)
+ self.report({"INFO"}, f"Resources are exported to {self.filepath}")
+
+
class AddProductivityData(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_productivity_data"
bl_label = "Add Productivity"
diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py
index 5899e21275..41e20ae6f1 100644
--- a/src/bonsai/bonsai/bim/module/resource/ui.py
+++ b/src/bonsai/bonsai/bim/module/resource/ui.py
@@ -54,6 +54,7 @@ class BIM_PT_resources(Panel):
else:
row.operator("bim.load_resources", text="", icon="GREASEPENCIL")
row.operator("bim.import_resources", text="", icon="IMPORT")
+ row.operator("bim.export_resources", text="", icon="EXPORT")
if not self.props.is_editing:
return
diff --git a/src/bonsai/bonsai/core/resource.py b/src/bonsai/bonsai/core/resource.py
index 12c6dc2b14..5a470094d0 100644
--- a/src/bonsai/bonsai/core/resource.py
+++ b/src/bonsai/bonsai/core/resource.py
@@ -171,6 +171,10 @@ def import_resources(resource_tool: tool.Resource, file_path: str) -> None:
resource_tool.load_resources()
+def export_resources(resource_tool: tool.Resource, file_path: str) -> None:
+ resource_tool.export_resources(file_path)
+
+
def expand_resource(resource_tool: tool.Resource, resource: ifcopenshell.entity_instance) -> None:
resource_tool.expand_resource(resource)
resource_tool.load_resources()
diff --git a/src/bonsai/bonsai/tool/resource.py b/src/bonsai/bonsai/tool/resource.py
index e00b71748e..45c64d3653 100644
--- a/src/bonsai/bonsai/tool/resource.py
+++ b/src/bonsai/bonsai/tool/resource.py
@@ -304,8 +304,17 @@ class Resource(bonsai.core.tool.Resource):
from ifc4d.csv2ifc import Csv2Ifc
start = time.time()
- p62ifc = Csv2Ifc(file_path, tool.Ifc.get())
- p62ifc.execute()
+ csv2ifc = Csv2Ifc(file_path, tool.Ifc.get())
+ csv2ifc.execute()
+ print("Importing Resources CSV finished in {:.2f} seconds".format(time.time() - start))
+
+ @classmethod
+ def export_resources(cls, file_path: str) -> None:
+ from ifc4d.csv2ifc import Ifc2Csv
+
+ start = time.time()
+ csv2ifc = Ifc2Csv(file_path, tool.Ifc.get())
+ csv2ifc.execute()
print("Importing Resources CSV finished in {:.2f} seconds".format(time.time() - start))
@classmethod
diff --git a/src/ifc4d/ifc4d/csv2ifc.py b/src/ifc4d/ifc4d/csv2ifc.py
index da40fe980a..7c4034a051 100644
--- a/src/ifc4d/ifc4d/csv2ifc.py
+++ b/src/ifc4d/ifc4d/csv2ifc.py
@@ -16,13 +16,18 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcResources. If not, see .
+from __future__ import annotations
import csv
+import _csv
+import isodate
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.date
+import ifcopenshell.util.element
+import ifcopenshell.util.resource
import ifcopenshell.util.unit
import datetime
-from typing import Any, Optional, Union, Literal, get_args
+from typing import Any, Optional, Union, Literal, get_args, NamedTuple, cast
SUPPORTED_COLUMN = Literal[
@@ -36,6 +41,15 @@ SUPPORTED_COLUMN = Literal[
"EQUIPMENT OUTPUT",
]
+RESOURCE_MAP = {
+ "CREW": "IfcCrewResource",
+ "LABOR": "IfcLaborResource",
+ "EQUIPMENT": "IfcConstructionEquipmentResource",
+ "SUBCONTRACTOR": "IfcSubContractResource",
+ "MATERIAL": "IfcConstructionMaterialResource",
+ "PRODUCT": "IfcConstructionProductResource",
+}
+
class Csv2Ifc:
"""Class to import resources from a CSV file into an IFC file.
@@ -64,14 +78,6 @@ class Csv2Ifc:
self.file = ifc_file
self.resources: list[dict[str, Any]] = []
self.units = {} # TODO: never used
- self.resource_map = {
- "CREW": "IfcCrewResource",
- "LABOR": "IfcLaborResource",
- "EQUIPMENT": "IfcConstructionEquipmentResource",
- "SUBCONTRACTOR": "IfcSubContractResource",
- "MATERIAL": "IfcConstructionMaterialResource",
- "PRODUCT": "IfcConstructionProductResource",
- }
def execute(self) -> None:
self.parse_csv()
@@ -114,7 +120,7 @@ class Csv2Ifc:
def get_row_resource_data(self, row: list[str]) -> dict[str, Any]:
name = row[self.headers["ACTIVITY/RESOURCE NAME"]]
- resource_class = self.resource_map[row[self.headers["TYPE"]]]
+ resource_class = RESOURCE_MAP[row[self.headers["TYPE"]]]
base_cost_value = row[self.headers["COST"]]
productivity = {}
@@ -194,3 +200,87 @@ class Csv2Ifc:
self.file = ifcopenshell.file(schema="IFC4")
ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject")
ifcopenshell.api.run("unit.assign_unit", self.file)
+
+
+class Ifc2CsvRow(NamedTuple):
+ hierarchy: int
+ resource_type: str
+ name: str
+ description: str
+ cost: Union[float, None]
+ quantity_name: Union[str, None]
+ labor_output: Union[float, None]
+ equipment_output: Union[float, None]
+
+
+class Ifc2Csv:
+ """Class to export resources from an IFC file to csv file."""
+
+ HEADER = (
+ "HIERARCHY",
+ "TYPE",
+ "ACTIVITY/RESOURCE NAME",
+ "DESCRIPTION",
+ "COST",
+ "QUANTITY NAME",
+ "LABOR OUTPUT",
+ "EQUIPMENT OUTPUT",
+ "GUID",
+ )
+ assert len(HEADER) == len(Ifc2CsvRow._fields)
+
+ def __init__(self, filepath: str, ifc_file: ifcopenshell.file):
+ self.filepath = filepath
+ self.file = ifc_file
+ self.inverse_resource_map = {value: key for key, value in RESOURCE_MAP.items()}
+
+ def execute(self) -> None:
+ # TODO: unused columns: USAGE, UNIT, PRODUCTIVITY UNIT.
+ with open(self.filepath, "w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerow(self.HEADER)
+ for resource in self.file.by_type("IfcConstructionResource"):
+ if resource.Nests:
+ continue
+ self.process_recourse(writer, resource, 1)
+
+ def process_recourse(
+ self, writer: _csv._writer, resource: ifcopenshell.entity_instance, hierarchy_level: int
+ ) -> None:
+ row = self.get_row(resource, hierarchy_level)
+ writer.writerow(row)
+ resources = ifcopenshell.util.resource.get_nested_resources(resource)
+ for resource in resources:
+ self.process_recourse(writer, resource, hierarchy_level + 1)
+ if resources:
+ writer.writerow((None,) * len(self.HEADER))
+
+ def get_row(self, resource: ifcopenshell.entity_instance, hierarchy_level: int) -> Ifc2CsvRow:
+ resource_class = resource.is_a()
+ cost, unit = ifcopenshell.util.resource.get_cost(resource)
+
+ quantity_name = None
+ labor_output, equipment_output = None, None
+ if resource_class in ("IfcConstructionEquipmentResource", "IfcLaborResource"):
+ pset: dict[str, Any] = ifcopenshell.util.element.get_pset(resource, "EPset_Productivity")
+ if pset:
+ quantity_name = cast(str, pset["BaseQuantityProducedName"])
+ time_consumed: float = isodate.parse_duration(pset["BaseQuantityConsumed"]).total_seconds() / 3600
+ produced: float = pset["BaseQuantityProducedValue"]
+ output = time_consumed / produced
+ if resource_class == "IfcLaborResource":
+ labor_output = output
+ else:
+ equipment_output, labor_output = None, output
+
+ return Ifc2CsvRow(
+ hierarchy=hierarchy_level,
+ resource_type=self.inverse_resource_map[resource.is_a()],
+ name=resource.Name,
+ description=resource.Description,
+ cost=cost,
+ quantity_name=quantity_name,
+ labor_output=labor_output,
+ equipment_output=equipment_output,
+ guid=resource.GlobalId,
+ )