mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-16 02:24:30 +00:00
Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
Index,Identification,Name,Unit,Value,Quantity,Query,Property,Formula
|
||||
1,E.01,Walls,m3,,,,,
|
||||
2,E.01.01,Ground floor walls,m3,100,,"IfcWall, location=""Ground Floor""",GrossVolume,
|
||||
2,E.01.02,First floor walls,m3,200,,"IfcWall, location=""First Floor""",GrossVolume,
|
||||
1,A.02,Paintings,m2,,,,,
|
||||
2,A.03,Paintings with water,m2,,,,,
|
||||
3,B.05,White paintings,m2,25,,IfcWall,GrossVolume,
|
||||
3,B.06,Colored paintings,m2,32,33,,,
|
||||
3,B.07,Double paintings,m,,,IfcWall,,NetSideArea*2
|
||||
2,C-01,Paintings with machine,m2,17,133,,,
|
||||
2,C-02,Decorated paintings,m2,40,8,,,
|
||||
1,D,Reinforcements,,,,,,
|
||||
2,D.1,Walls reinforcements weight,kg,,,IfcWall,,Pset_ConcreteElementGeneral.ReinforcementVolumeRatio * GrossVolume
|
||||
2,D.2,Beams reinforcements weight,kg,,,,,
|
||||
|
@@ -39,6 +39,7 @@ See example files as a CSV file format reference:
|
||||
- Ex5 - SoR_with_description.csv (a simple SoR with description column)
|
||||
- Ex6 - BoQ with categories.csv (a simple BoQ with categories columns)
|
||||
- Ex7 - BoQ with Rates.csv (a simple BoQ that connect to an existing SoR. It needs an already loaded SoR.)
|
||||
- Ex8 - Boq with formula.csv (a simple BoQ with formula field used to calculate quantities when specified)
|
||||
- `sample_cost_schedule_house_FR.csv` / `.ods`
|
||||
- `schedule.csv`, `rates.csv` (schedule of rates example)
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ class CsvHeader(TypedDict):
|
||||
RateSchedule: NotRequired[str]
|
||||
RateID: NotRequired[str]
|
||||
|
||||
# Formula
|
||||
Formula: NotRequired[str]
|
||||
#QuantityClass: NotRequired[str]
|
||||
|
||||
# Currently we assume that if column is not part of the main header,
|
||||
# then it is a cost value category. So here we list any additional column
|
||||
@@ -65,6 +68,8 @@ MAIN_CSV_HEADER_COLUMNS.extend(
|
||||
# Not sure what this for but it's present in sample .csv.
|
||||
"Subtotal",
|
||||
# Columns from exporter.
|
||||
"ItemIsASum",
|
||||
"Quantities",
|
||||
"RateSubtotal",
|
||||
"TotalPrice",
|
||||
# Deprecated columns from exporter, shouldn't be exported any longer.
|
||||
@@ -91,6 +96,8 @@ class CostItem(TypedDict):
|
||||
Property: Union[str, None]
|
||||
Query: Union[str, None]
|
||||
|
||||
Formula: Union[str, None]
|
||||
#QuantityClass: Union[str, None]
|
||||
|
||||
class Csv2Ifc:
|
||||
# Inputs.
|
||||
@@ -108,6 +115,7 @@ class Csv2Ifc:
|
||||
categories: dict[str, int]
|
||||
has_categories: bool
|
||||
has_rates: bool
|
||||
has_formula: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -163,9 +171,12 @@ class Csv2Ifc:
|
||||
if not self.headers:
|
||||
self.has_categories = True
|
||||
self.has_rates = False
|
||||
self.has_formula = False
|
||||
self.headers = {col: i for i, col in enumerate(row) if col}
|
||||
if "RateSchedule" in self.headers and "RateID" in self.headers:
|
||||
self.has_rates = True
|
||||
if "Formula" in self.headers:
|
||||
self.has_formula = True
|
||||
if "Value" in self.headers:
|
||||
self.has_categories = False
|
||||
else:
|
||||
@@ -233,6 +244,11 @@ class Csv2Ifc:
|
||||
else:
|
||||
cost_rate = None
|
||||
|
||||
if self.has_formula:
|
||||
cost_formula = row[(self.headers["Formula"])] if "Formula" in self.headers else None
|
||||
else:
|
||||
cost_formula = None
|
||||
|
||||
return {
|
||||
"Identification": str(identification) if identification else None,
|
||||
"Name": str(name) if name else None,
|
||||
@@ -244,6 +260,7 @@ class Csv2Ifc:
|
||||
"Query": query,
|
||||
"children": [],
|
||||
"CostRate": cost_rate,
|
||||
"Formula": cost_formula,
|
||||
}
|
||||
|
||||
def create_ifc(self) -> None:
|
||||
@@ -320,6 +337,7 @@ class Csv2Ifc:
|
||||
|
||||
if cost_rate.get("Schedule") and cost_rate.get("RateID"):
|
||||
# if cost_rate["Schedule"] is not "":
|
||||
rate_cost_schedule = None
|
||||
schedules = self.file.by_type("IfcCostSchedule")
|
||||
for schedule in schedules:
|
||||
if schedule.Name == cost_rate["Schedule"]:
|
||||
@@ -381,17 +399,28 @@ class Csv2Ifc:
|
||||
# and some query in "Query" column.
|
||||
# If query is provided it will override the defined value
|
||||
# due current behaviour in cost.assign_cost_item_quantity.
|
||||
if results:
|
||||
if results and not cost_item["Formula"]:
|
||||
ifcopenshell.api.cost.assign_cost_item_quantity(
|
||||
self.file,
|
||||
cost_item=cost_item["ifc"],
|
||||
products=results,
|
||||
prop_name=prop_name,
|
||||
)
|
||||
elif not quantity:
|
||||
elif not quantity and not cost_item["Formula"]:
|
||||
quantity = ifcopenshell.api.cost.add_cost_item_quantity(
|
||||
self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class
|
||||
)
|
||||
if cost_item["Formula"]:
|
||||
results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["Query"])
|
||||
results = [r for r in results]
|
||||
ifc_quantity_class = ifcopenshell.util.unit.get_symbol_quantity_class(cost_item["Unit"])
|
||||
quantity = ifcopenshell.api.cost.assign_cost_item_quantity(
|
||||
self.file,
|
||||
cost_item=cost_item["ifc"],
|
||||
products=results,
|
||||
formula=cost_item["Formula"],
|
||||
ifc_class=ifc_quantity_class,
|
||||
)
|
||||
|
||||
self.create_cost_items(cost_item["children"], cost_item["ifc"])
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
@@ -256,26 +257,31 @@ class IfcDataGetter:
|
||||
return ""
|
||||
if cost_item.CostQuantities is None:
|
||||
return ""
|
||||
string = "["
|
||||
result = []
|
||||
for quantity in cost_item.CostQuantities:
|
||||
string += '["'
|
||||
prefix = ""
|
||||
for rel in file.get_inverse(quantity):
|
||||
if rel.is_a("IfcPropertySet") or rel.is_a("IfcElementQuantity"):
|
||||
prop_set = rel
|
||||
# Find elements that have this property set
|
||||
for prop_rel in file.get_inverse(prop_set):
|
||||
for prop_rel in file.get_inverse(rel):
|
||||
if prop_rel.is_a("IfcRelDefinesByProperties"):
|
||||
for obj in prop_rel.RelatedObjects:
|
||||
if obj.is_a("IfcElement"):
|
||||
string += obj.Name + " - "
|
||||
string += quantity.Name
|
||||
prefix += (obj.Name or "") + " - "
|
||||
name = prefix + (quantity.Name or "")
|
||||
# Formula is an optional IfcLabel on IfcQuantity* in IFC4+; absent in
|
||||
# IFC2X3, hence the schema-safe getattr.
|
||||
formula = getattr(quantity, "Formula", None) or ""
|
||||
if quantity.is_a("IfcPhysicalSimpleQuantity"):
|
||||
string += '", ' + str(quantity[3]) + "],"
|
||||
value = quantity[3]
|
||||
try:
|
||||
value = float(value) if value is not None else 0.0
|
||||
except (TypeError, ValueError):
|
||||
value = 0.0
|
||||
result.append([name, value, formula])
|
||||
else:
|
||||
string += ' ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0],'
|
||||
string = string.removesuffix(",")
|
||||
string += "]"
|
||||
return string
|
||||
result.append([name + " ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0, formula])
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
class SheetData(TypedDict):
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import csv
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -118,3 +119,44 @@ class TestCsv2Ifc:
|
||||
writer.write()
|
||||
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
|
||||
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
|
||||
|
||||
|
||||
class TestSerialiseCostQuantities:
|
||||
def test_quantity_name_with_special_characters_round_trips_as_json(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
name = 'Prospetto est "Np=256,667-23"'
|
||||
quantity = ifc_file.create_entity("IfcQuantityArea", Name=name, AreaValue=12.5)
|
||||
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
|
||||
|
||||
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
|
||||
|
||||
assert json.loads(result) == [[name, 12.5, ""]]
|
||||
|
||||
def test_unset_name_does_not_crash(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
# Name left unset so quantity.Name resolves to None at access time.
|
||||
quantity = ifc_file.create_entity("IfcQuantityArea", AreaValue=3.0)
|
||||
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
|
||||
|
||||
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
|
||||
|
||||
assert json.loads(result) == [["", 3.0, ""]]
|
||||
|
||||
def test_formula_is_included_when_present(self):
|
||||
ifc_file = ifcopenshell.file()
|
||||
quantity = ifc_file.create_entity("IfcQuantityArea", Name="Area", AreaValue=12.5, Formula="Length * Width")
|
||||
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
|
||||
|
||||
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
|
||||
|
||||
assert json.loads(result) == [["Area", 12.5, "Length * Width"]]
|
||||
|
||||
def test_quantity_without_formula_attribute_does_not_crash(self):
|
||||
# IfcPhysicalComplexQuantity has no Formula attribute and is unsupported.
|
||||
ifc_file = ifcopenshell.file()
|
||||
quantity = ifc_file.create_entity("IfcPhysicalComplexQuantity", Name="Complex", Discrimination="layer")
|
||||
cost_item = ifc_file.create_entity("IfcCostItem", CostQuantities=[quantity])
|
||||
|
||||
result = ifc5d.ifc5Dspreadsheet.IfcDataGetter.serialise_cost_quantities(ifc_file, cost_item)
|
||||
|
||||
assert json.loads(result) == [["Complex ERROR: Only IfcPhysicalSimpleQuantity is supported", 0.0, ""]]
|
||||
|
||||
Reference in New Issue
Block a user