diff --git a/src/blenderbim/blenderbim/bim/module/cost/__init__.py b/src/blenderbim/blenderbim/bim/module/cost/__init__.py index c90892500e..49ed24868b 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/cost/__init__.py @@ -30,6 +30,7 @@ classes = ( operator.EnableEditingCostSchedule, operator.EnableEditingCostItems, operator.EnableEditingCostItem, + operator.ExportCostSchedules, operator.ExpandCostItems, operator.EnableEditingCostItemQuantities, operator.EnableEditingCostItemQuantity, diff --git a/src/blenderbim/blenderbim/bim/module/cost/data.py b/src/blenderbim/blenderbim/bim/module/cost/data.py index 55a47e143e..6016877c0c 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/data.py +++ b/src/blenderbim/blenderbim/bim/module/cost/data.py @@ -21,6 +21,8 @@ import ifcopenshell import ifcopenshell.util.cost import ifcopenshell.util.element import blenderbim.tool as tool +import blenderbim.bim.schema +from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc def refresh(): @@ -36,6 +38,8 @@ class CostSchedulesData: @classmethod def load(cls): cls.data = { + "predefined_types": cls.get_cost_schedule_types(), + "total_cost_schedules": cls.total_cost_schedules(), "schedules": cls.schedules(), "is_editing_rates": cls.is_editing_rates(), "cost_items": cls.cost_items(), @@ -45,6 +49,10 @@ class CostSchedulesData: } cls.is_loaded = True + @classmethod + def total_cost_schedules(cls): + return len(tool.Ifc.get().by_type("IfcCostSchedule")) + @classmethod def schedules(cls): results = [] @@ -212,6 +220,21 @@ class CostSchedulesData: for t in tool.Ifc.schema().declaration_by_name("IfcPhysicalSimpleQuantity").subtypes() ] + @classmethod + def get_cost_schedule_types(cls): + results = [] + declaration = tool.Ifc().schema().declaration_by_name("IfcCostSchedule") + version = tool.Ifc.get_schema() + for attribute in declaration.attributes(): + if attribute.name() == "PredefinedType": + results.extend( + [ + (e, e, get_predefined_type_doc(version, "IfcCostSchedule", e)) + for e in attribute.type_of_attribute().declared_type().enumeration_items() + ] + ) + break + return results class CostItemRatesData: data = {} diff --git a/src/blenderbim/blenderbim/bim/module/cost/operator.py b/src/blenderbim/blenderbim/bim/module/cost/operator.py index 50cd7d16cf..ba5e353e03 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/operator.py +++ b/src/blenderbim/blenderbim/bim/module/cost/operator.py @@ -36,7 +36,7 @@ class AddCostSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Add a new cost schedule" def _execute(self, context): - core.add_cost_schedule(tool.Ifc) + core.add_cost_schedule(tool.Ifc, predefined_type = context.scene.BIMCostProperties.cost_schedule_predefined_types) class EditCostSchedule(bpy.types.Operator, tool.Ifc.Operator): @@ -328,7 +328,7 @@ class DisableEditingCostItemQuantity(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "Disable editing cost item quantity" - def _execute(self, context): + def execute(self, context): core.disable_editing_cost_item_quantity(tool.Cost) return {"FINISHED"} @@ -586,3 +586,22 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator): core.calculate_cost_item_resource_value(tool.Ifc, cost_item=tool.Ifc.get().by_id(self.cost_item)) return {"FINISHED"} + +class ExportCostSchedules(bpy.types.Operator): + bl_idname = "bim.export_cost_schedules" + bl_label = "Export Cost Schedule" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Export a cost schedule to a CSV, XSLX OR ODS file" + format: bpy.props.EnumProperty("Format", items=(("CSV", "CSV", ""), ("XLSX", "XLSX", ""), ("ODS", "ODS", ""))) + + def execute(self, context): + core.export_cost_schedules(tool.Cost, format=self.format) + return {"FINISHED"} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self, context): + self.layout.label(text="Choose a format") + self.layout.prop(self, "format") diff --git a/src/blenderbim/blenderbim/bim/module/cost/prop.py b/src/blenderbim/blenderbim/bim/module/cost/prop.py index 739a221b70..a5bdf4b4da 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/prop.py +++ b/src/blenderbim/blenderbim/bim/module/cost/prop.py @@ -109,6 +109,12 @@ def update_cost_item_name(self, context): attribute.string_value = self.name +def get_schedule_predefined_types(self, context): + if not CostSchedulesData.is_loaded: + CostSchedulesData.load() + return CostSchedulesData.data["predefined_types"] + + class CostItem(PropertyGroup): name: StringProperty(name="Name", update=update_cost_item_name) identification: StringProperty(name="Identification", update=update_cost_item_identification) @@ -130,6 +136,9 @@ class CostItemType(PropertyGroup): class BIMCostProperties(PropertyGroup): + cost_schedule_predefined_types: EnumProperty( + items=get_schedule_predefined_types, name="Predefined Type", default=None + ) is_cost_update_enabled: BoolProperty(name="Is Cost Update Enabled", default=True) cost_schedule_attributes: CollectionProperty(name="Cost Schedule Attributes", type=Attribute) is_editing: StringProperty(name="Is Editing") diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 5cc10fd691..c0e8c365f2 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -42,9 +42,16 @@ class BIM_PT_cost_schedules(Panel): CostSchedulesData.load() self.props = context.scene.BIMCostProperties + row = self.layout.row() + if CostSchedulesData.data["total_cost_schedules"]: + row.label(text=f"{CostSchedulesData.data['total_cost_schedules']} Cost Schedules Found", icon="TEXT") + row.operator("bim.export_cost_schedules", text="Export as spreadsheet", icon="EXPORT") + else: + row.label(text="No Cost Schedules Found found.", icon="COMMUNITY") row = self.layout.row() - row.operator("bim.add_cost_schedule", icon="ADD") + row.prop(self.props, "cost_schedule_predefined_types") + row.operator("bim.add_cost_schedule", icon="ADD", text="Add new") for schedule in CostSchedulesData.data["schedules"]: self.draw_cost_schedule_ui(schedule) @@ -57,7 +64,6 @@ class BIM_PT_cost_schedules(Panel): op = row.operator("bim.select_cost_schedule_products", icon="RESTRICT_SELECT_OFF", text="") op.cost_schedule = cost_schedule["id"] row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY") - row.operator("bim.export_cost_schedule", text="", icon="EXPORT").cost_schedule = cost_schedule["id"] if self.props.is_editing == "COST_SCHEDULE_ATTRIBUTES": row.operator("bim.edit_cost_schedule", text="", icon="CHECKMARK") elif self.props.is_editing == "COST_ITEMS": diff --git a/src/blenderbim/blenderbim/core/cost.py b/src/blenderbim/blenderbim/core/cost.py index 8e5f0437db..76326f4ed7 100644 --- a/src/blenderbim/blenderbim/core/cost.py +++ b/src/blenderbim/blenderbim/core/cost.py @@ -1,5 +1,5 @@ -def add_cost_schedule(ifc): - ifc.run("cost.add_cost_schedule") +def add_cost_schedule(ifc, predefined_type): + ifc.run("cost.add_cost_schedule", predefined_type=predefined_type) def edit_cost_schedule(ifc, cost, cost_schedule): @@ -209,4 +209,7 @@ def contract_cost_item_rate(cost, cost_item): cost.contract_cost_item_rate(cost_item) def calculate_cost_item_resource_value(ifc, cost_item): - ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item) \ No newline at end of file + ifc.run("cost.calculate_cost_item_resource_value", cost_item=cost_item) + +def export_cost_schedules(cost, format): + cost.export_cost_schedules(format) \ No newline at end of file diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 0c67e9529e..b4ae6da6cd 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -176,8 +176,8 @@ class Cost: def load_cost_schedule_tree(cls): pass def load_schedule_of_rates_tree(cls, schedule_of_rates): pass def play_chaching_sound(cls): pass - def print_all_cost_items(cls, cost_schedule): pass def remove_cost_column(cls, name): pass + def export_cost_schedules(cls, format): pass @interface class Debug: diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index f28eae6163..114600cc17 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -279,9 +279,7 @@ class Cost(blenderbim.core.tool.Cost): prop.float_value = ( 0.0 if prop.is_null - else ifcopenshell.util.cost.calculate_applied_value( - tool.Ifc.get().by_id(props.active_cost_item_id), cost_value - ) + else cls.calculate_applied_value(tool.Ifc.get().by_id(props.active_cost_item_id), cost_value) ) return True if name == "UnitBasis" and is_rates: @@ -299,33 +297,11 @@ class Cost(blenderbim.core.tool.Cost): prop.name = "UnitBasisUnit" prop.data_type = "enum" prop.is_null = prop.is_optional = False - units = {} - - def format_unit(unit): - if unit.is_a("IfcContextDependentUnit"): - return f"{unit.UnitType} / {unit.Name}" - else: - name = unit.Name - if unit.get_info().get("Prefix", None): - name = f"{unit.Prefix} {name}" - return f"{unit.UnitType} / {name}" - - for unit in tool.Ifc.get().by_type("IfcNamedUnit"): - if unit.get_info().get("UnitType", None) in [ - "AREAUNIT", - "LENGTHUNIT", - "TIMEUNIT", - "VOLUMEUNIT", - "MASSUNIT", - "USERDEFINED", - ]: - units[unit.id()] = format_unit(unit) + units = cls.get_units() prop.enum_items = json.dumps(units) if data["UnitBasis"] and data["UnitBasis"].UnitComponent: - # prop.enum_value = prop.enum_items[data["UnitBasis"].UnitComponent.id()] - print(prop.enum_items) for key, value in json.loads(prop.enum_items).items(): - if value == format_unit(data["UnitBasis"].UnitComponent): + if value == cls.format_unit(data["UnitBasis"].UnitComponent): prop.enum_value = key break return True @@ -384,7 +360,6 @@ class Cost(blenderbim.core.tool.Cost): "ValueComponent": prop.float_value or 1, "UnitComponent": cls.get_cost_value_unit_component(), } - print(attributes) return True if prop.name == "UnitBasisUnit": return True @@ -424,9 +399,6 @@ class Cost(blenderbim.core.tool.Cost): def get_nested_cost_items(cls, cost_item): return [obj for rel in cost_item.IsNestedBy for obj in rel.RelatedObjects] - @classmethod - def print_all_cost_items(cls, cost_schedule): - print(cls.get_schedule_cost_items(cost_schedule)) @classmethod def get_schedule_cost_items(cls, cost_schedule): @@ -526,3 +498,25 @@ class Cost(blenderbim.core.tool.Cost): for cost_item in rel.RelatedObjects ] props.is_cost_update_enabled = True + + @classmethod + def export_cost_schedules(cls, format=None): + path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "build", "cost_schedules") + if format == "CSV": + from ifc5d.ifc5Dspreadsheet import Ifc5DCsvWriter + + if not os.path.exists(path): + os.makedirs(path) + writer = Ifc5DCsvWriter(file=tool.Ifc.get(), output=path) + writer.write() + elif format == "ODS": + from ifc5d.ifc5Dspreadsheet import Ifc5DOdsWriter + + writer = Ifc5DOdsWriter(file=tool.Ifc.get(), output=path) + writer.write() + elif format == "XLSX": + from ifc5d.ifc5Dspreadsheet import Ifc5DXlsxWriter + + writer = Ifc5DXlsxWriter(file=tool.Ifc.get(), output=path + ".xlsx") + writer.write() + diff --git a/src/blenderbim/test/bim/feature/cost.feature b/src/blenderbim/test/bim/feature/cost.feature index 9e4c778024..4852c4c19d 100644 --- a/src/blenderbim/test/bim/feature/cost.feature +++ b/src/blenderbim/test/bim/feature/cost.feature @@ -379,3 +379,29 @@ Scenario: Select cost item products And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')" When I press "bim.select_cost_item_products(cost_item={cost_item})" Then nothing happens + +Scenario: Select Cost Schedule Products + Given an empty IFC project + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item(cost_schedule={cost_schedule})" + And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And the object "IfcWall/Cube" is selected + And I press "bim.assign_cost_item_quantity(cost_item={cost_item}, related_object_type='PRODUCT', prop_name='')" + When I press "bim.select_cost_schedule_products(cost_schedule={cost_schedule})" + Then nothing happens +Scenario: Load Cost Item Types + Given an empty IFC project + And I press "bim.add_cost_schedule" + And the variable "cost_schedule" is "{ifc}.by_type('IfcCostSchedule')[0].id()" + And I press "bim.enable_editing_cost_items(cost_schedule={cost_schedule})" + And I press "bim.add_summary_cost_item(cost_schedule={cost_schedule})" + And the variable "cost_item" is "{ifc}.by_type('IfcCostItem')[0].id()" + When I press "bim.add_cost_item(cost_item={cost_item})" + And I press "bim.load_cost_item_types" + Then nothing happens diff --git a/src/ifc5d/README.md b/src/ifc5d/README.md index d1a014a114..a431107580 100644 --- a/src/ifc5d/README.md +++ b/src/ifc5d/README.md @@ -1,18 +1,57 @@ # ifc5d +## Description + Ifc5D is a collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines. Currently supported: - CSV to IFC + - IFC to XLSX + - IFC to CSV + - IFC to ODS + Planned (would you like to contribute? Please reach out!): - - IFC to CSV + - IFC to PDF - - IFC to ODS - - IFC to XLSX - ODS to CSV - XLSX to CSV - IFC to Graph + +## Usage CSV to IFC + +## Usage IFC to CSV, ODS, XSLS + +### CLI app for converting IFC files to CSV, ODS or XLSX format. + +Usage: + python ifc5Dspreadsheet.py input_file output_file [-l log_file] [-f format_type] + +Arguments: + input_file (str): The path to the input IFC file to process. + output_file (str): The output directory for CSV or filename for other formats. + +Options: + -l, --log log_file (str): The path to the file where errors should be logged. Default is process.log. + -f, --format format_type (str): The output format to export in (csv/ods/xlsx). Default is csv. + +Examples: + python ifc5Dspreadsheet.py "C:\Users\Username\Desktop\test_cost.ifc" rev_01_schedule -l error.log -f ods + python ifc5Dspreadsheet.py "C:\Users\Dev-Machine\Desktop\test_cost.ifc" "C:\Users\Dev-Machine\Desktop" -l error.log -f csv +### Scripting: + +Example for ODS exports: +""" +import ifcopenshell +from ifc5d.ifc5Dspreadsheet import Ifc5DOdsWriter + +file = "path_to_file/file.ifc" + +path = "directory/cost_schedule" +writer = Ifc5DOdsWriter(file=file, output=path) +writer.write() + +""" \ No newline at end of file