cost schedule - prohibit adding multiple quantity types on 1 cost item

All IfcCostItem.CostQuantities supposed to use only 1 type of quantity, so they can be added up.

Changed UI to reflect that requirement - dropdown for selecting quantity type is now only visible if cost item has no quantities, otherwise it just reuses already used quantity type.

See https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcCostItem.htm

Before - https://i.imgur.com/gFlwHEk.png
After - https://i.imgur.com/pk3wXfe.png
This commit is contained in:
Andrej730
2024-05-21 16:48:58 +05:00
parent 9aacafd575
commit 295d0a677e
5 changed files with 77 additions and 36 deletions
+6 -2
View File
@@ -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}
@@ -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)
@@ -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"])
+35 -18
View File
@@ -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
@@ -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)*