mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 14:23:53 +00:00
ifc5d - continue refactor
This commit is contained in:
+37
-11
@@ -23,6 +23,14 @@ Planned (would you like to contribute? Please reach out!):
|
|||||||
|
|
||||||
## Usage CSV to IFC
|
## Usage CSV to IFC
|
||||||
|
|
||||||
|
Simple example:
|
||||||
|
```python
|
||||||
|
import ifc5d.csv2ifc
|
||||||
|
|
||||||
|
csv2ifc = ifc5d.csv2ifc.Csv2Ifc(csv_filepath, ifc_file)
|
||||||
|
csv2ifc.execute()
|
||||||
|
```
|
||||||
|
|
||||||
See example files as a CSV file format reference:
|
See example files as a CSV file format reference:
|
||||||
- `sample_cost_schedule_house_FR.csv` / `.ods`
|
- `sample_cost_schedule_house_FR.csv` / `.ods`
|
||||||
- `schedule.csv`, `rates.csv` (schedule of rates example)
|
- `schedule.csv`, `rates.csv` (schedule of rates example)
|
||||||
@@ -31,21 +39,39 @@ Some notes on the format:
|
|||||||
- Empty lines are ignored.
|
- Empty lines are ignored.
|
||||||
- Importing ods/xlsx is not currently supported, only csv.
|
- Importing ods/xlsx is not currently supported, only csv.
|
||||||
- 'Property', 'Query' columns are required only for non-schedule of rates cost schedules.
|
- 'Property', 'Query' columns are required only for non-schedule of rates cost schedules.
|
||||||
- Some older exports may have 'Description' field instead of 'Name', it is safe to just rename it.
|
- 'Index' was preferred for import hierarchy source over 'Hierarchy' as it's easier to edit from the table view.
|
||||||
|
- In non-SoR if cost values are not provided in any way and cost item has subitems, then `SUM()` will added automatically as it's cost value.
|
||||||
|
Mixing `SUM()` cost items and items with their own value is not supported.
|
||||||
|
|
||||||
|
## Columns Description
|
||||||
|
|
||||||
|
**General Columns**
|
||||||
|
- 'Index' - is a hierarchy depth that's used for building hierarchy during csv import. Starts from 1.
|
||||||
|
E.g. root items of the same level have index '1', their children have '2', etc.
|
||||||
|
- 'Name' - IfcCostItem.Name.
|
||||||
|
Some older exports may have 'Description' field instead of 'Name', it is safe to just rename it.
|
||||||
|
- 'Identification' - IfcCostItem.Identification.
|
||||||
|
- 'Unit' - IfcCostValue unit, should be provided as a unit symbol.
|
||||||
|
E.g. 'm2', 'm3', 'kg', etc.
|
||||||
|
- 'Value' - overall cost value to assign to IfcCostItem.
|
||||||
|
- All other columns that are not mentioned in this list or the one below will be interpeted as cost value categories.
|
||||||
|
Note that if 'Value' is provided, it takes priority over subcategories.
|
||||||
|
|
||||||
|
Non-schedule of rates columns:
|
||||||
|
- 'Quantity' - total cost item quantity.
|
||||||
|
- 'Property' - quantity name that should be added to IfcCostItem from 'Query' elements.
|
||||||
|
If 'Query' is provided, 'Property' can be left empty or set to "COUNT" to count queried elements.
|
||||||
|
- 'Query' - selector query for elements to assign to IfcCostItem.
|
||||||
|
If query is provided, it takes priority over 'Quantity' (using both is not supported).
|
||||||
|
|
||||||
|
**Exported informational columns (not used for import)**
|
||||||
- 'Hierarchy' is just an informational column that doesn't affect the import.
|
- 'Hierarchy' is just an informational column that doesn't affect the import.
|
||||||
E.g. '1', '1.1', '1.1.1', etc.
|
E.g. '1', '1.1', '1.1.1', etc.
|
||||||
- 'Index' column is a hierarchy depth that's used for building hierarchy during csv import, starting from 1.
|
- 'Id' - IfcCostItem.id
|
||||||
E.g. root items of the same level have index '1', their children have '2', etc.
|
- 'RateSubtotal' - all IfcCostItem specific costs, not including subitem costs.
|
||||||
- 'Index' was preferred for import hierarchy source over 'Hierarchy' as it's easier to edit from the table view.
|
- 'TotalPrice' - IfcCostItem total cost, including subitem sum calculations.
|
||||||
|
|
||||||
|
|
||||||
Simple example:
|
|
||||||
```python
|
|
||||||
import ifc5d.csv2ifc
|
|
||||||
|
|
||||||
csv2ifc = ifc5d.csv2ifc.Csv2Ifc(csv_filepath, ifc_file)
|
|
||||||
csv2ifc.execute()
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Usage IFC to CSV, ODS, XSLS
|
## Usage IFC to CSV, ODS, XSLS
|
||||||
|
|||||||
+31
-32
@@ -32,27 +32,30 @@ from typing import Any, Union, Optional, TypedDict, NotRequired
|
|||||||
class CsvHeader(TypedDict):
|
class CsvHeader(TypedDict):
|
||||||
Index: int
|
Index: int
|
||||||
Name: int
|
Name: int
|
||||||
Quantity: int
|
|
||||||
Unit: int
|
Unit: int
|
||||||
Identification: NotRequired[int]
|
Identification: NotRequired[int]
|
||||||
Value: NotRequired[int]
|
Value: NotRequired[int]
|
||||||
|
|
||||||
# Not schedule of rates:
|
# Not schedule of rates:
|
||||||
Property: int
|
Quantity: NotRequired[int]
|
||||||
Query: int
|
Property: NotRequired[int]
|
||||||
|
Query: NotRequired[int]
|
||||||
|
|
||||||
|
|
||||||
class CostItem(TypedDict):
|
class CostItem(TypedDict):
|
||||||
children: list[CostItem]
|
children: list[CostItem]
|
||||||
assignments: dict[str, Any]
|
|
||||||
ifc: NotRequired[ifcopenshell.entity_instance]
|
ifc: NotRequired[ifcopenshell.entity_instance]
|
||||||
|
|
||||||
Identification: Union[str, None]
|
Identification: Union[str, None]
|
||||||
Name: Union[str, None]
|
Name: Union[str, None]
|
||||||
Unit: Union[str, None]
|
Unit: Union[str, None]
|
||||||
Quantity: Union[float, None]
|
|
||||||
CostValues: Union[dict[str, float], float, None]
|
CostValues: Union[dict[str, float], float, None]
|
||||||
|
|
||||||
|
# Only might be available in non-SOR.
|
||||||
|
Quantity: Union[float, None]
|
||||||
|
Property: Union[str, None]
|
||||||
|
Query: Union[str, None]
|
||||||
|
|
||||||
|
|
||||||
class Csv2Ifc:
|
class Csv2Ifc:
|
||||||
# Inputs.
|
# Inputs.
|
||||||
@@ -118,12 +121,9 @@ class Csv2Ifc:
|
|||||||
# parse header
|
# parse header
|
||||||
if not self.headers:
|
if not self.headers:
|
||||||
self.has_categories = True
|
self.has_categories = True
|
||||||
for i, col in enumerate(row):
|
self.headers = {col: i for i, col in enumerate(row) if col}
|
||||||
if not col:
|
if "Value" in self.headers:
|
||||||
continue
|
self.has_categories = False
|
||||||
if col == "Value":
|
|
||||||
self.has_categories = False
|
|
||||||
self.headers[col] = i
|
|
||||||
|
|
||||||
# validate header
|
# validate header
|
||||||
mandatory_fields = {"Name", "Quantity", "Unit"}
|
mandatory_fields = {"Name", "Quantity", "Unit"}
|
||||||
@@ -169,18 +169,14 @@ class Csv2Ifc:
|
|||||||
def get_row_cost_data(self, row: list[str]) -> CostItem:
|
def get_row_cost_data(self, row: list[str]) -> CostItem:
|
||||||
name = row[self.headers["Name"]]
|
name = row[self.headers["Name"]]
|
||||||
identification = row[self.headers["Identification"]] if "Identification" in self.headers else None
|
identification = row[self.headers["Identification"]] if "Identification" in self.headers else None
|
||||||
quantity = row[self.headers["Quantity"]]
|
quantity = row[(self.headers["Quantity"])] if "Quantity" in self.headers else None
|
||||||
unit = row[self.headers["Unit"]]
|
unit = row[self.headers["Unit"]]
|
||||||
if not self.is_schedule_of_rates:
|
if self.is_schedule_of_rates:
|
||||||
assignments = {
|
property_name, query = None, None
|
||||||
"PropertyName": row[self.headers["Property"]],
|
|
||||||
"Query": row[self.headers["Query"]],
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
assignments = {
|
property_name = row[(self.headers["Property"])] if "Property" in self.headers else None
|
||||||
"PropertyName": None,
|
query = row[(self.headers["Query"])] if "Query" in self.headers else None
|
||||||
"Query": None,
|
|
||||||
}
|
|
||||||
if self.has_categories:
|
if self.has_categories:
|
||||||
cost_values = {
|
cost_values = {
|
||||||
k: locale.atof(row[v])
|
k: locale.atof(row[v])
|
||||||
@@ -195,10 +191,11 @@ class Csv2Ifc:
|
|||||||
return {
|
return {
|
||||||
"Identification": str(identification) if identification else None,
|
"Identification": str(identification) if identification else None,
|
||||||
"Name": str(name) if name else None,
|
"Name": str(name) if name else None,
|
||||||
"Quantity": float(quantity) if quantity else None,
|
|
||||||
"Unit": str(unit) if unit else None,
|
"Unit": str(unit) if unit else None,
|
||||||
"CostValues": cost_values,
|
"CostValues": cost_values,
|
||||||
"assignments": assignments,
|
"Quantity": float(quantity) if quantity else None,
|
||||||
|
"Property": property_name,
|
||||||
|
"Query": query,
|
||||||
"children": [],
|
"children": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,13 +226,16 @@ class Csv2Ifc:
|
|||||||
cost_item["ifc"].Name = cost_item["Name"]
|
cost_item["ifc"].Name = cost_item["Name"]
|
||||||
cost_item["ifc"].Identification = cost_item["Identification"]
|
cost_item["ifc"].Identification = cost_item["Identification"]
|
||||||
|
|
||||||
if not cost_item["CostValues"] and cost_item["children"]:
|
cost_values = cost_item["CostValues"]
|
||||||
|
if ((isinstance(cost_values, dict) and len(cost_values) == 0) or (cost_values is None)) and cost_item[
|
||||||
|
"children"
|
||||||
|
]:
|
||||||
if not self.is_schedule_of_rates:
|
if not self.is_schedule_of_rates:
|
||||||
cost_value = ifcopenshell.api.cost.add_cost_value(self.file, parent=cost_item["ifc"])
|
cost_value = ifcopenshell.api.cost.add_cost_value(self.file, parent=cost_item["ifc"])
|
||||||
cost_value.Category = "*"
|
cost_value.Category = "*"
|
||||||
elif self.has_categories:
|
elif self.has_categories:
|
||||||
assert isinstance(cost_item["CostValues"], dict)
|
assert isinstance(cost_values, dict)
|
||||||
for category, value in cost_item["CostValues"].items():
|
for category, value in cost_values.items():
|
||||||
cost_value = ifcopenshell.api.cost.add_cost_value(self.file, parent=cost_item["ifc"])
|
cost_value = ifcopenshell.api.cost.add_cost_value(self.file, parent=cost_item["ifc"])
|
||||||
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(value)
|
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(value)
|
||||||
if category != "Rate" or category != "Price":
|
if category != "Rate" or category != "Price":
|
||||||
@@ -243,7 +243,7 @@ class Csv2Ifc:
|
|||||||
category = category.replace("Rate", "")
|
category = category.replace("Rate", "")
|
||||||
category = category.strip()
|
category = category.strip()
|
||||||
cost_value.Category = category
|
cost_value.Category = category
|
||||||
elif cost_item["CostValues"]:
|
elif cost_values:
|
||||||
cost_value = ifcopenshell.api.cost.add_cost_value(self.file, parent=cost_item["ifc"])
|
cost_value = ifcopenshell.api.cost.add_cost_value(self.file, parent=cost_item["ifc"])
|
||||||
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(cost_item["CostValues"])
|
cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(cost_item["CostValues"])
|
||||||
if self.is_schedule_of_rates:
|
if self.is_schedule_of_rates:
|
||||||
@@ -267,10 +267,9 @@ class Csv2Ifc:
|
|||||||
|
|
||||||
quantity = None
|
quantity = None
|
||||||
quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
|
quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
|
||||||
if not cost_item["assignments"]["PropertyName"] or cost_item["assignments"]["PropertyName"].upper() == "COUNT":
|
prop_name = cost_item["Property"]
|
||||||
|
if not prop_name or prop_name.upper() == "COUNT":
|
||||||
prop_name = ""
|
prop_name = ""
|
||||||
else:
|
|
||||||
prop_name = cost_item["assignments"]["PropertyName"]
|
|
||||||
|
|
||||||
if not self.is_schedule_of_rates and cost_item["Quantity"] is not None:
|
if not self.is_schedule_of_rates and cost_item["Quantity"] is not None:
|
||||||
quantity = ifcopenshell.api.cost.add_cost_item_quantity(
|
quantity = ifcopenshell.api.cost.add_cost_item_quantity(
|
||||||
@@ -281,8 +280,8 @@ class Csv2Ifc:
|
|||||||
if prop_name:
|
if prop_name:
|
||||||
quantity.Name = prop_name
|
quantity.Name = prop_name
|
||||||
|
|
||||||
if cost_item["assignments"]["Query"]:
|
if cost_item["Query"]:
|
||||||
results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["assignments"]["Query"])
|
results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["Query"])
|
||||||
results = [r for r in results if has_property(self.file, r, prop_name)]
|
results = [r for r in results if has_property(self.file, r, prop_name)]
|
||||||
# NOTE: currently we do not support count quantities that have
|
# NOTE: currently we do not support count quantities that have
|
||||||
# both defined quantity in .csv "Quantity" column
|
# both defined quantity in .csv "Quantity" column
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
# along with Ifc5D. If not, see <http://www.gnu.org/licenses/>.
|
# along with Ifc5D. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import argparse
|
import argparse
|
||||||
@@ -31,17 +32,32 @@ from typing import Union, Optional, Any, TypedDict, NotRequired
|
|||||||
|
|
||||||
|
|
||||||
class CostItem(TypedDict):
|
class CostItem(TypedDict):
|
||||||
|
# Exported columns.
|
||||||
Index: int
|
Index: int
|
||||||
Hierarchy: str
|
Hierarchy: str
|
||||||
Id: int
|
Id: int
|
||||||
Identification: Union[str, None]
|
Identification: Union[str, None]
|
||||||
Name: Union[str, None]
|
Name: Union[str, None]
|
||||||
Unit: str
|
Unit: str
|
||||||
Quantity: int
|
Quantity: Union[float, None]
|
||||||
ChildrenData: list["CostItem"]
|
RateSubtotal: float
|
||||||
# Total Price: float
|
TotalPrice: float
|
||||||
# * Cost: str
|
|
||||||
# Rate Subtotal: float
|
# Internal.
|
||||||
|
cost_categories: dict[str, float]
|
||||||
|
|
||||||
|
|
||||||
|
class CostItemQuantity(TypedDict):
|
||||||
|
quantity: Union[float, None]
|
||||||
|
|
||||||
|
|
||||||
|
class CostValue(TypedDict):
|
||||||
|
id: int
|
||||||
|
"""Cost Value id."""
|
||||||
|
name: Union[str, None]
|
||||||
|
applied_value: float
|
||||||
|
unit: str
|
||||||
|
category: str
|
||||||
|
|
||||||
|
|
||||||
class IfcDataGetter:
|
class IfcDataGetter:
|
||||||
@@ -64,10 +80,10 @@ class IfcDataGetter:
|
|||||||
return ifcopenshell.util.cost.get_root_cost_items(cost_schedule)
|
return ifcopenshell.util.cost.get_root_cost_items(cost_schedule)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cost_item_values(cost_item: Union[ifcopenshell.entity_instance, None]) -> Union[list[dict[str, Any]], None]:
|
def get_cost_item_values(cost_item: Union[ifcopenshell.entity_instance, None]) -> Union[list[CostValue], None]:
|
||||||
if not cost_item:
|
if not cost_item:
|
||||||
return None
|
return None
|
||||||
values = []
|
values: list[CostValue] = []
|
||||||
for cost_value in cost_item.CostValues or []:
|
for cost_value in cost_item.CostValues or []:
|
||||||
name = cost_value.Name
|
name = cost_value.Name
|
||||||
applied_value = ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value)
|
applied_value = ifcopenshell.util.cost.calculate_applied_value(cost_item, cost_value)
|
||||||
@@ -84,48 +100,33 @@ class IfcDataGetter:
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_categories(cost_item: ifcopenshell.entity_instance, categories: set[str]) -> set[str]:
|
def get_cost_items_data(
|
||||||
"""
|
|
||||||
:param categories: A set to fill with categories.
|
|
||||||
"""
|
|
||||||
for cost_value in cost_item.CostValues or []:
|
|
||||||
if cost_value.Category:
|
|
||||||
categories.add("{}{}".format(cost_value.Category, " Cost"))
|
|
||||||
return categories
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_cost_item_categories(cost_item: ifcopenshell.entity_instance, categories: set[str]) -> set[str]:
|
|
||||||
IfcDataGetter.process_categories(cost_item, categories)
|
|
||||||
for child in ifcopenshell.util.cost.get_nested_cost_items(cost_item):
|
|
||||||
IfcDataGetter.process_cost_item_categories(child, categories)
|
|
||||||
return categories
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_cost_rates_categories(schedule: ifcopenshell.entity_instance) -> set[str]:
|
|
||||||
"""
|
|
||||||
:param categories: A set to fill with categories.
|
|
||||||
"""
|
|
||||||
categories: set[str] = set()
|
|
||||||
for cost_item in IfcDataGetter.get_root_costs(schedule):
|
|
||||||
IfcDataGetter.process_cost_item_categories(cost_item, categories)
|
|
||||||
return categories
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_cost_data(
|
|
||||||
file: ifcopenshell.file,
|
file: ifcopenshell.file,
|
||||||
cost_item: ifcopenshell.entity_instance,
|
cost_item: ifcopenshell.entity_instance,
|
||||||
cost_items_data: list[CostItem],
|
|
||||||
index: int = 1,
|
index: int = 1,
|
||||||
hierarchy: str = "1",
|
hierarchy: str = "1",
|
||||||
) -> None:
|
) -> list[CostItem]:
|
||||||
"""
|
"""
|
||||||
:param cost_items_data: A list to fill with cost items.
|
:param cost_items_data: A list to fill with cost items.
|
||||||
:param index: Current hierarchy depth.
|
:param index: Current hierarchy depth.
|
||||||
"""
|
"""
|
||||||
|
cost_items_data: list[CostItem] = []
|
||||||
|
|
||||||
quantity_data = IfcDataGetter.get_cost_item_quantity(file, cost_item)
|
quantity_data = IfcDataGetter.get_cost_item_quantity(file, cost_item)
|
||||||
cost_values_data = IfcDataGetter.get_cost_item_values(cost_item)
|
cost_values_data = IfcDataGetter.get_cost_item_values(cost_item)
|
||||||
|
|
||||||
|
rate_subtotal = 0.0
|
||||||
|
total_price = 0.0
|
||||||
|
cost_categories: dict[str, float] = {}
|
||||||
|
for cost_value in cost_values_data or []:
|
||||||
|
category = cost_value["category"]
|
||||||
|
if cost_value["category"] == "*": # A sum.
|
||||||
|
total_price = cost_value["applied_value"]
|
||||||
|
else:
|
||||||
|
cost_category = "{}{}".format(category, " Cost")
|
||||||
|
cost_categories[cost_category] = cost_value["applied_value"]
|
||||||
|
rate_subtotal += cost_value["applied_value"]
|
||||||
|
|
||||||
data: CostItem = {
|
data: CostItem = {
|
||||||
"Index": index,
|
"Index": index,
|
||||||
"Hierarchy": hierarchy,
|
"Hierarchy": hierarchy,
|
||||||
@@ -133,35 +134,25 @@ class IfcDataGetter:
|
|||||||
"Identification": cost_item.Identification,
|
"Identification": cost_item.Identification,
|
||||||
"Name": cost_item.Name,
|
"Name": cost_item.Name,
|
||||||
"Unit": cost_values_data[0]["unit"] if cost_values_data else "",
|
"Unit": cost_values_data[0]["unit"] if cost_values_data else "",
|
||||||
"Quantity": quantity_data["quantity"]["total_quantity"],
|
"Quantity": quantity_data["quantity"],
|
||||||
"ChildrenData": [],
|
"RateSubtotal": rate_subtotal,
|
||||||
|
"TotalPrice": total_price,
|
||||||
|
"cost_categories": cost_categories,
|
||||||
}
|
}
|
||||||
for cost_value in cost_values_data or []:
|
|
||||||
cost_category = "{}{}".format(cost_value["category"], " Cost")
|
|
||||||
data[cost_category] = cost_value["applied_value"]
|
|
||||||
if data.get("* Cost", None):
|
|
||||||
data["Total Price"] = data["* Cost"]
|
|
||||||
data["* Cost"] = ""
|
|
||||||
rate_subtotal = 0
|
|
||||||
for key, value in data.items():
|
|
||||||
if "Cost" in key and not "*" in key:
|
|
||||||
rate_subtotal += value
|
|
||||||
|
|
||||||
data["Rate Subtotal"] = rate_subtotal
|
|
||||||
|
|
||||||
cost_items_data.append(data)
|
cost_items_data.append(data)
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
child_hierarchy = hierarchy + ".1"
|
child_hierarchy = hierarchy + ".1"
|
||||||
for i, nested_cost in enumerate(ifcopenshell.util.cost.get_nested_cost_items(cost_item), 1):
|
for i, nested_cost in enumerate(ifcopenshell.util.cost.get_nested_cost_items(cost_item), 1):
|
||||||
child_hierarchy = f"{hierarchy}.{i}"
|
child_hierarchy = f"{hierarchy}.{i}"
|
||||||
IfcDataGetter.process_cost_data(file, nested_cost, cost_items_data, index, child_hierarchy)
|
cost_items_data.extend(IfcDataGetter.get_cost_items_data(file, nested_cost, index, child_hierarchy))
|
||||||
|
return cost_items_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cost_items_data(file: ifcopenshell.file, schedule: ifcopenshell.entity_instance) -> list[CostItem]:
|
def get_schedule_cost_items_data(file: ifcopenshell.file, schedule: ifcopenshell.entity_instance) -> list[CostItem]:
|
||||||
cost_items_data: list[CostItem] = []
|
cost_items_data: list[CostItem] = []
|
||||||
for cost_item in IfcDataGetter.get_root_costs(schedule):
|
for cost_item in IfcDataGetter.get_root_costs(schedule):
|
||||||
IfcDataGetter.process_cost_data(file, cost_item, cost_items_data)
|
cost_items_data.extend(IfcDataGetter.get_cost_items_data(file, cost_item))
|
||||||
return cost_items_data
|
return cost_items_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -184,40 +175,45 @@ class IfcDataGetter:
|
|||||||
return IfcDataGetter.format_unit(unit.UnitComponent)
|
return IfcDataGetter.format_unit(unit.UnitComponent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cost_item_quantity(file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance) -> dict[str, Any]:
|
def get_cost_item_quantity(file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance) -> CostItemQuantity:
|
||||||
|
accounted_for: set[ifcopenshell.entity_instance] = set()
|
||||||
|
|
||||||
|
# NOTE: take_off_name is not used anywhere.
|
||||||
|
take_off_name: str = ""
|
||||||
|
|
||||||
# TODO: handle multiple quantities, THOSE WHHICH ARE JUYST ASSIGNED TO THE COST ITEM DIRECTLY, NOT THROUGH OBJECTS.
|
# TODO: handle multiple quantities, THOSE WHHICH ARE JUYST ASSIGNED TO THE COST ITEM DIRECTLY, NOT THROUGH OBJECTS.
|
||||||
def add_quantity(quantity: ifcopenshell.entity_instance, take_off_name: str) -> float:
|
def add_quantity(quantity: ifcopenshell.entity_instance, take_off_name: str) -> float:
|
||||||
accounted_for.append(quantity)
|
accounted_for.add(quantity)
|
||||||
if take_off_name == "":
|
if take_off_name == "":
|
||||||
|
# 0 IfcPhysicalSimpleQuantity.Name
|
||||||
take_off_name = quantity[0]
|
take_off_name = quantity[0]
|
||||||
if quantity[0] != take_off_name:
|
if quantity[0] != take_off_name:
|
||||||
take_off_name = "mixed-takeoff-quantities"
|
take_off_name = "mixed-takeoff-quantities"
|
||||||
|
# 3 IfcPhysicalSimpleQuantity.Value
|
||||||
return quantity[3]
|
return quantity[3]
|
||||||
|
|
||||||
take_off_name = ""
|
cost_item_quantities: list[ifcopenshell.entity_instance] = cost_item.CostQuantities
|
||||||
total_cost_quantity = 0
|
|
||||||
accounted_for = []
|
|
||||||
cost_item_quantities = cost_item.CostQuantities
|
|
||||||
if cost_item_quantities:
|
if cost_item_quantities:
|
||||||
for rel in cost_item.Controls or []:
|
total_cost_quantity = 0.0
|
||||||
for related_object in rel.RelatedObjects:
|
# Add quantities from cost assignments.
|
||||||
qtos = ifcopenshell.util.element.get_psets(related_object, qtos_only=True)
|
for related_object in ifcopenshell.util.cost.get_cost_assignments_by_type(cost_item):
|
||||||
for quantities in qtos.values() or []:
|
qtos = ifcopenshell.util.element.get_psets(related_object, qtos_only=True)
|
||||||
qto = file.by_id(quantities["id"])
|
for quantities in qtos.values() or []:
|
||||||
for quantity in qto.Quantities:
|
qto = file.by_id(quantities["id"])
|
||||||
if quantity not in cost_item_quantities:
|
for quantity in qto.Quantities:
|
||||||
continue
|
if quantity not in cost_item_quantities:
|
||||||
total_cost_quantity += add_quantity(quantity, take_off_name)
|
continue
|
||||||
accounted_for.append(quantity)
|
total_cost_quantity += add_quantity(quantity, take_off_name)
|
||||||
|
|
||||||
|
# Add cost item quantities assigned to the cost item directly.
|
||||||
for quantity in cost_item_quantities:
|
for quantity in cost_item_quantities:
|
||||||
if not quantity in accounted_for:
|
if quantity not in accounted_for:
|
||||||
total_cost_quantity += add_quantity(quantity, take_off_name)
|
total_cost_quantity += add_quantity(quantity, take_off_name)
|
||||||
|
else:
|
||||||
|
total_cost_quantity = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": cost_item.id(),
|
"quantity": total_cost_quantity,
|
||||||
"name": cost_item.Name,
|
|
||||||
"quantity": {"take_off_name": take_off_name, "total_quantity": total_cost_quantity},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -282,31 +278,42 @@ class Ifc5Dwriter:
|
|||||||
counter: Counter[str] = Counter()
|
counter: Counter[str] = Counter()
|
||||||
for cost_schedule in self.cost_schedules:
|
for cost_schedule in self.cost_schedules:
|
||||||
sheet_id = cost_schedule.id()
|
sheet_id = cost_schedule.id()
|
||||||
self.sheet_data[sheet_id] = sheet_data = SheetData()
|
cost_items = IfcDataGetter.get_schedule_cost_items_data(self.file, cost_schedule)
|
||||||
sheet_data["headers"] = [
|
headers: list[str] = [
|
||||||
"Id",
|
"Id",
|
||||||
"Hierarchy",
|
"Hierarchy",
|
||||||
"Index",
|
"Index",
|
||||||
"Identification",
|
"Identification",
|
||||||
"Name",
|
"Name",
|
||||||
"Quantity",
|
|
||||||
"Unit",
|
"Unit",
|
||||||
]
|
]
|
||||||
cost_rate_categories = IfcDataGetter.get_cost_rates_categories(cost_schedule)
|
if cost_schedule.PredefinedType != "SCHEDULEOFRATES":
|
||||||
sheet_data["headers"].extend(list(cost_rate_categories))
|
headers.insert(-1, "Quantity")
|
||||||
sheet_data["headers"].extend(["Rate Subtotal", "Total Price", "Children"])
|
headers.extend(["RateSubtotal", "TotalPrice"])
|
||||||
sheet_data["cost_items"] = IfcDataGetter.get_cost_items_data(self.file, cost_schedule)
|
|
||||||
sheet_data["UpdateDate"] = IfcDataGetter.canonicalise_time(
|
|
||||||
ifcopenshell.util.date.ifc2datetime(cost_schedule.UpdateDate)
|
|
||||||
)
|
|
||||||
sheet_data["PredefinedType"] = cost_schedule.PredefinedType
|
|
||||||
schedule_name = cost_schedule.Name or "Unnamed"
|
|
||||||
|
|
||||||
|
# Handle cost categories.
|
||||||
|
categories: set[str] = set()
|
||||||
|
for cost_item in cost_items:
|
||||||
|
for category, value in cost_item["cost_categories"].items():
|
||||||
|
categories.add(category)
|
||||||
|
cost_item[category] = value
|
||||||
|
assert not (intersection := categories.intersection(headers)), intersection
|
||||||
|
headers.extend(categories)
|
||||||
|
|
||||||
|
schedule_name = cost_schedule.Name or "Unnamed"
|
||||||
counter[schedule_name] += 1
|
counter[schedule_name] += 1
|
||||||
if (count := counter[schedule_name]) > 1:
|
if (count := counter[schedule_name]) > 1:
|
||||||
schedule_name = f"{schedule_name}_{count - 1}"
|
schedule_name = f"{schedule_name}_{count - 1}"
|
||||||
|
|
||||||
sheet_data["Name"] = schedule_name
|
self.sheet_data[sheet_id] = {
|
||||||
|
"Name": schedule_name,
|
||||||
|
"headers": headers,
|
||||||
|
"cost_items": cost_items,
|
||||||
|
"UpdateDate": IfcDataGetter.canonicalise_time(
|
||||||
|
ifcopenshell.util.date.ifc2datetime(cost_schedule.UpdateDate)
|
||||||
|
),
|
||||||
|
"PredefinedType": cost_schedule.PredefinedType,
|
||||||
|
}
|
||||||
|
|
||||||
def multiply_cells(self, cell1, cell2):
|
def multiply_cells(self, cell1, cell2):
|
||||||
return "={}*{}".format(cell1, cell2)
|
return "={}*{}".format(cell1, cell2)
|
||||||
|
|||||||
Reference in New Issue
Block a user