diff --git a/src/blenderbim/blenderbim/bim/helper.py b/src/blenderbim/blenderbim/bim/helper.py index a1bb904f74..9c60b469ab 100644 --- a/src/blenderbim/blenderbim/bim/helper.py +++ b/src/blenderbim/blenderbim/bim/helper.py @@ -29,7 +29,7 @@ from mathutils import geometry from mathutils import Vector import blenderbim.tool as tool from blenderbim.bim.ifc import IfcStore -from typing import Optional, Callable, Any +from typing import Optional, Callable, Any, Union def draw_attributes(props, layout, copy_operator=None, popup_active_attribute=None): @@ -91,7 +91,11 @@ def import_attributes(ifc_class, props, data, callback=None): # A more elegant attribute importer signature, intended to supersede import_attributes -def import_attributes2(element, props, callback=None): +def import_attributes2( + element: Union[str, ifcopenshell.entity_instance], + props: bpy.types.PropertyGroup, + callback: Optional[Callable] = None, +) -> None: if isinstance(element, str): attributes = tool.Ifc.schema().declaration_by_name(element).as_entity().all_attributes() info = {a.name(): None for a in attributes} diff --git a/src/blenderbim/blenderbim/bim/module/cost/data.py b/src/blenderbim/blenderbim/bim/module/cost/data.py index dfc937c2a0..f320b71531 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/data.py +++ b/src/blenderbim/blenderbim/bim/module/cost/data.py @@ -159,6 +159,7 @@ class CostSchedulesData: data["UnitSymbol"] = "-" if cost_item.CostQuantities: quantity = cost_item.CostQuantities[0] + data["QuantityType"] = quantity.is_a() unit = ifcopenshell.util.unit.get_property_unit(quantity, tool.Ifc.get()) if unit: data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit) diff --git a/src/blenderbim/blenderbim/bim/module/cost/ui.py b/src/blenderbim/blenderbim/bim/module/cost/ui.py index 7e77b016e1..42eb5d578a 100644 --- a/src/blenderbim/blenderbim/bim/module/cost/ui.py +++ b/src/blenderbim/blenderbim/bim/module/cost/ui.py @@ -21,6 +21,7 @@ import blenderbim.bim.module.cost.prop as CostProp from bpy.types import Panel, UIList from blenderbim.bim.ifc import IfcStore from blenderbim.bim.module.cost.data import CostSchedulesData +from typing import Any class BIM_PT_cost_schedules(Panel): @@ -178,24 +179,33 @@ class BIM_PT_cost_schedules(Panel): "active_cost_item_index", ) if self.props.active_cost_item_id: + cost_item = CostSchedulesData.data["cost_items"][ifc_definition_id] if self.props.cost_item_editing_type == "ATTRIBUTES": self.draw_editable_cost_item_attributes_ui() elif self.props.cost_item_editing_type == "QUANTITIES": - self.draw_editable_cost_item_quantities_ui() + self.draw_editable_cost_item_quantities_ui(cost_item) elif self.props.cost_item_editing_type == "VALUES": self.draw_editable_cost_item_values_ui() def draw_editable_cost_item_attributes_ui(self): blenderbim.bim.helper.draw_attributes(self.props.cost_item_attributes, self.layout) - def draw_editable_cost_item_quantities_ui(self): + def draw_editable_cost_item_quantities_ui(self, cost_item: dict[str, Any]): + quantities = CostSchedulesData.data["cost_quantities"] row = self.layout.row(align=True) - row.prop(self.props, "quantity_types", text="") + # In IFC, all quantities of IfcCostTime should have 1 type. + if quantities: + quantity_class = cost_item["QuantityType"] + row.label(text=quantity_class) + else: + row.prop(self.props, "quantity_types", text="") + quantity_class = self.props.quantity_types + op = row.operator("bim.add_cost_item_quantity", text="", icon="ADD") op.cost_item = self.props.active_cost_item_id - op.ifc_class = self.props.quantity_types + op.ifc_class = quantity_class - for quantity in CostSchedulesData.data["cost_quantities"]: + for quantity in quantities: row = self.layout.row(align=True) row.label(text=quantity["name"]) row.label(text=quantity["value"]) diff --git a/src/blenderbim/blenderbim/tool/cost.py b/src/blenderbim/blenderbim/tool/cost.py index 2ce904b018..aa4bb8264c 100644 --- a/src/blenderbim/blenderbim/tool/cost.py +++ b/src/blenderbim/blenderbim/tool/cost.py @@ -7,7 +7,7 @@ import ifcopenshell.util.cost import ifcopenshell.util.unit import blenderbim.bim.helper import json -from typing import Optional +from typing import Optional, Any, Generator class Cost(blenderbim.core.tool.Cost): @@ -261,7 +261,7 @@ class Cost(blenderbim.core.tool.Cost): blenderbim.bim.helper.import_attributes2(physical_quantity, props.quantity_attributes) @classmethod - def enable_editing_cost_item_values(cls, cost_item=None): + def enable_editing_cost_item_values(cls, cost_item: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.active_cost_item_id = cost_item.id() props.cost_item_editing_type = "VALUES" @@ -289,7 +289,7 @@ class Cost(blenderbim.core.tool.Cost): return attributes @classmethod - def load_cost_item_value_attributes(cls, cost_value=None): + def load_cost_item_value_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None: def import_attributes(name, prop, data, cost_value, is_rates, props_collection): if name == "AppliedValue": # TODO: for now, only support simple IfcValues (which are effectively IfcMonetaryMeasure) @@ -336,36 +336,38 @@ class Cost(blenderbim.core.tool.Cost): blenderbim.bim.helper.import_attributes2(cost_value, props.cost_value_attributes, callback=callback) @classmethod - def calculate_applied_value(cls, cost_item, cost_value): + def calculate_applied_value( + cls, cost_item: ifcopenshell.entity_instance, cost_value: ifcopenshell.entity_instance + ) -> float: return ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value) @classmethod - def is_active_schedule_of_rates(cls): + def is_active_schedule_of_rates(cls) -> bool: return ( tool.Ifc.get().by_id(bpy.context.scene.BIMCostProperties.active_cost_schedule_id).PredefinedType == "SCHEDULEOFRATES" ) @classmethod - def enable_editing_cost_item_value(cls, cost_value=None): + def enable_editing_cost_item_value(cls, cost_value: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.active_cost_value_id = cost_value.id() props.cost_value_editing_type = "ATTRIBUTES" @classmethod - def disable_editing_cost_item_value(cls): + def disable_editing_cost_item_value(cls) -> None: props = bpy.context.scene.BIMCostProperties props.active_cost_value_id = 0 props.cost_value_editing_type = "" @classmethod - def load_cost_item_value_formula_attributes(cls, cost_value=None): + def load_cost_item_value_formula_attributes(cls, cost_value: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.cost_value_attributes.clear() bpy.context.scene.BIMCostProperties.cost_value_formula = ifcopenshell.util.cost.serialise_cost_value(cost_value) @classmethod - def enable_editing_cost_item_value_formula(cls, cost_value=None): + def enable_editing_cost_item_value_formula(cls, cost_value: ifcopenshell.entity_instance) -> None: props = bpy.context.scene.BIMCostProperties props.active_cost_value_id = cost_value.id() props.cost_value_editing_type = "FORMULA" @@ -375,7 +377,7 @@ class Cost(blenderbim.core.tool.Cost): return bpy.context.scene.BIMCostProperties.cost_value_formula @classmethod - def get_cost_value_attributes(cls): + def get_cost_value_attributes(cls) -> dict[str, Any]: def export_attributes(attributes, prop): if prop.name == "UnitBasisValue": if prop.is_null: @@ -394,13 +396,18 @@ class Cost(blenderbim.core.tool.Cost): return blenderbim.bim.helper.export_attributes(props.cost_value_attributes, callback) @classmethod - def get_cost_value_unit_component(cls): + def get_cost_value_unit_component(cls) -> ifcopenshell.entity_instance: return tool.Ifc.get().by_id( int(bpy.context.scene.BIMCostProperties.cost_value_attributes.get("UnitBasisUnit").enum_value) ) @classmethod - def get_cost_item_assignments(cls, cost_item, filter_by_type=None, is_deep=False): + def get_cost_item_assignments( + cls, + cost_item: ifcopenshell.entity_instance, + filter_by_type: Optional[ifcopenshell.util.cost.FILTER_BY_TYPE] = None, + is_deep: bool = False, + ) -> list[ifcopenshell.entity_instance]: return ifcopenshell.util.cost.get_cost_item_assignments( cost_item, filter_by_type=filter_by_type, is_deep=is_deep ) @@ -410,30 +417,40 @@ class Cost(blenderbim.core.tool.Cost): return bpy.context.scene.BIMCostProperties.show_nested_elements @classmethod - def get_cost_item_products(cls, cost_item, is_deep=False): + def get_cost_item_products( + cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False + ) -> list[ifcopenshell.entity_instance]: return cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=is_deep) @classmethod - def get_cost_item_resources(cls, cost_item, is_deep=False): + def get_cost_item_resources( + cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False + ) -> list[ifcopenshell.entity_instance]: return cls.get_cost_item_assignments(cost_item, filter_by_type="RESOURCE", is_deep=is_deep) @classmethod - def get_cost_item_processes(cls, cost_item, is_deep=False): + def get_cost_item_processes( + cls, cost_item: ifcopenshell.entity_instance, is_deep: bool = False + ) -> list[ifcopenshell.entity_instance]: return cls.get_cost_item_assignments(cost_item, filter_by_type="PROCESS", is_deep=is_deep) @classmethod - def get_schedule_cost_items(cls, cost_schedule): + def get_schedule_cost_items( + cls, cost_schedule: ifcopenshell.entity_instance + ) -> Generator[ifcopenshell.entity_instance, None, None]: return ifcopenshell.util.cost.get_schedule_cost_items(cost_schedule) @classmethod - def get_cost_schedule_products(cls, cost_schedule): + def get_cost_schedule_products( + cls, cost_schedule: ifcopenshell.entity_instance + ) -> list[ifcopenshell.entity_instance]: products = [] for cost_item in ifcopenshell.util.cost.get_schedule_cost_items(cost_schedule): products.extend(cls.get_cost_item_products(cost_item)) return products @classmethod - def import_cost_schedule_csv(cls, file_path=None, is_schedule_of_rates=False): + def import_cost_schedule_csv(cls, file_path: Optional[str] = None, is_schedule_of_rates: bool = False) -> None: if not file_path: return from ifc5d.csv2ifc import Csv2Ifc diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index 910fb9f38d..d383dd66a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -18,11 +18,12 @@ import lark import ifcopenshell -from typing import Optional, Union +from typing import Optional, Union, Literal, Generator, Any arithmetic_operator_symbols = {"ADD": "+", "DIVIDE": "/", "MULTIPLY": "*", "SUBTRACT": "-"} symbol_arithmetic_operators = {"+": "ADD", "/": "DIVIDE", "*": "MULTIPLY", "-": "SUBTRACT"} +FILTER_BY_TYPE = Literal["PRODUCT", "RESOURCE", "PROCESS"] def get_primitive_applied_value(applied_value: Union[ifcopenshell.entity_instance, float, None]) -> float: @@ -150,11 +151,11 @@ def serialise_applied_value(applied_value: ifcopenshell.entity_instance) -> str: return "?" -def unserialise_cost_value(formula, cost_value): +def unserialise_cost_value(formula: str, cost_value: ifcopenshell.entity_instance) -> dict[str, Any]: unserialiser = CostValueUnserialiser() result = unserialiser.parse(formula) - def map_element_to_result(element, result): + def map_element_to_result(element: ifcopenshell.entity_instance, result: dict): result["ifc"] = element for i, component in enumerate(result.get("Components", [])): if element.Components and i < len(element.Components): @@ -164,7 +165,7 @@ def unserialise_cost_value(formula, cost_value): return result -def get_cost_items_for_product(product): +def get_cost_items_for_product(product: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: """ Returns a list of cost items related to the given product. @@ -181,7 +182,7 @@ def get_cost_items_for_product(product): return cost_items -def get_root_cost_items(cost_schedule): +def get_root_cost_items(cost_schedule: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: return [ related_object for rel in cost_schedule.Controls or [] @@ -190,26 +191,32 @@ def get_root_cost_items(cost_schedule): ] -def get_all_nested_cost_items(cost_item): +def get_all_nested_cost_items( + cost_item: ifcopenshell.entity_instance, +) -> Generator[ifcopenshell.entity_instance, None, None]: for cost_item in get_nested_cost_items(cost_item): yield cost_item yield from get_all_nested_cost_items(cost_item) -def get_nested_cost_items(cost_item, is_deep=False): +def get_nested_cost_items(cost_item: ifcopenshell.entity_instance, is_deep=False) -> list[ifcopenshell.entity_instance]: if is_deep: return list(get_all_nested_cost_items(cost_item)) else: return [obj for rel in cost_item.IsNestedBy for obj in rel.RelatedObjects] -def get_schedule_cost_items(cost_schedule): +def get_schedule_cost_items( + cost_schedule: ifcopenshell.entity_instance, +) -> Generator[ifcopenshell.entity_instance, None, None]: for cost_item in get_root_cost_items(cost_schedule): yield cost_item yield from get_all_nested_cost_items(cost_item) -def get_cost_assignments_by_type(cost_item, filter_by_type=None): +def get_cost_assignments_by_type( + cost_item: ifcopenshell.entity_instance, filter_by_type: Optional[FILTER_BY_TYPE] = None +) -> list[ifcopenshell.entity_instance]: if filter_by_type is not None: if filter_by_type == "PRODUCT": filter_by_type = "IfcElement" @@ -225,7 +232,9 @@ def get_cost_assignments_by_type(cost_item, filter_by_type=None): ] -def get_cost_item_assignments(cost_item, filter_by_type=None, is_deep=False): +def get_cost_item_assignments( + cost_item: ifcopenshell.entity_instance, filter_by_type: Optional[FILTER_BY_TYPE] = None, is_deep: bool = False +) -> list[ifcopenshell.entity_instance]: if not is_deep: return get_cost_assignments_by_type(cost_item, filter_by_type) else: @@ -237,7 +246,7 @@ def get_cost_item_assignments(cost_item, filter_by_type=None, is_deep=False): class CostValueUnserialiser: - def parse(self, formula): + def parse(self, formula: str): l = lark.Lark( """start: formula formula: operand (operator operand)*