diff --git a/src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc b/src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc new file mode 100644 index 0000000000..1b6bb5ab6c --- /dev/null +++ b/src/blenderbim/blenderbim/bim/data/pset/ePSet_Georeferencing.ifc @@ -0,0 +1,23 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2023-09-28T23:00:50',(),(),'IfcOpenShell v0.7.0-fbd8ea1ed','IfcOpenShell v0.7.0-fbd8ea1ed',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROPERTYSETTEMPLATE('1TLQmgUKf0I8Zi07bm9Jqn',$,'ePSet_ProjectedCRS','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#2,#3,#4,#5,#6,#7)); +#2=IFCSIMPLEPROPERTYTEMPLATE('2cH53PtVT3v88uf_uGFblG',$,'Name','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); +#3=IFCSIMPLEPROPERTYTEMPLATE('3JsReA6vXDqR7aNC$haRYW',$,'Description','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#4=IFCSIMPLEPROPERTYTEMPLATE('0u0uEsT5HFtuX5GvozO9ba',$,'GeodeticDatum','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#5=IFCSIMPLEPROPERTYTEMPLATE('1JgEX9Fsf0$ggxYltdkqiY',$,'VerticalDatum','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#6=IFCSIMPLEPROPERTYTEMPLATE('3ahTZjYrD0MAOo5IH6QuGH',$,'MapProjection','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#7=IFCSIMPLEPROPERTYTEMPLATE('3hqJR2ghD0GAti7q4sXKj7',$,'MapZone','',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); +#8=IFCPROPERTYSETTEMPLATE('1jGHTycOj0vA8xHfRl4TNs',$,'ePSet_MapConversion','',.PSET_OCCURRENCEDRIVEN.,'IfcProject',(#9,#10,#11,#12,#13,#15)); +#9=IFCSIMPLEPROPERTYTEMPLATE('0nxsULXo17JRrAvNteYtNR',$,'Eastings','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); +#10=IFCSIMPLEPROPERTYTEMPLATE('26RfaGJs5CqemN0Xin12XL',$,'Northings','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); +#11=IFCSIMPLEPROPERTYTEMPLATE('0ziCn3nZv3m850VEUwWH42',$,'OrthogonalHeight','',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); +#12=IFCSIMPLEPROPERTYTEMPLATE('3d4BOt4In8WR119ms1SaAe',$,'XAxisAbscissa','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#13=IFCSIMPLEPROPERTYTEMPLATE('1Q4Voqj2j84RQRI8l67m28',$,'XAxisOrdinate','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +#15=IFCSIMPLEPROPERTYTEMPLATE('0O1Hc0$XnDNOG_jPBmbW5r',$,'Scale','',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); +ENDSEC; +END-ISO-10303-21; diff --git a/src/blenderbim/blenderbim/bim/module/model/opening.py b/src/blenderbim/blenderbim/bim/module/model/opening.py index 4b4fe6c8e4..9b841cfe2b 100644 --- a/src/blenderbim/blenderbim/bim/module/model/opening.py +++ b/src/blenderbim/blenderbim/bim/module/model/opening.py @@ -748,7 +748,6 @@ class EditOpenings(Operator, tool.Ifc.Operator): building_objs = set() model = tool.Ifc.get() all_openings = model.by_type("IfcOpeningElement") - similar_openings = [] for obj in context.selected_objects: element = tool.Ifc.get_entity(obj) @@ -756,9 +755,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): continue openings = [r.RelatedOpeningElement for r in element.HasOpenings] for opening in openings: - for all_opening in all_openings: - if all_opening.ObjectPlacement == opening.ObjectPlacement: - similar_openings.append(all_opening) + similar_openings = [o for o in all_openings if o.ObjectPlacement == opening.ObjectPlacement] opening_obj = tool.Ifc.get_object(opening) if opening_obj: if tool.Ifc.is_edited(opening_obj): diff --git a/src/blenderbim/blenderbim/bim/module/model/window.py b/src/blenderbim/blenderbim/bim/module/model/window.py index e432be6797..2aa33b5309 100644 --- a/src/blenderbim/blenderbim/bim/module/model/window.py +++ b/src/blenderbim/blenderbim/bim/module/model/window.py @@ -81,8 +81,9 @@ def update_simple_openings(element, opening_width, opening_height): has_replaced_opening_representation = True tool.Model.reload_body_representation(voided_objs) - with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]): - bpy.ops.bim.recalculate_fill() + if fillings: + with bpy.context.temp_override(selected_objects=[tool.Ifc.get_object(f) for f in fillings]): + bpy.ops.bim.recalculate_fill() def update_window_modifier_representation(context, obj): diff --git a/src/blenderbim/blenderbim/bim/module/project/__init__.py b/src/blenderbim/blenderbim/bim/module/project/__init__.py index 383b648053..5adcab8073 100644 --- a/src/blenderbim/blenderbim/bim/module/project/__init__.py +++ b/src/blenderbim/blenderbim/bim/module/project/__init__.py @@ -37,6 +37,7 @@ classes = ( operator.LoadProjectElements, operator.NewProject, operator.RefreshLibrary, + operator.RevertProject, operator.RewindLibrary, operator.SaveLibraryFile, operator.SelectLibraryFile, diff --git a/src/blenderbim/blenderbim/bim/module/project/operator.py b/src/blenderbim/blenderbim/bim/module/project/operator.py index ec431b80bb..0cd756882e 100644 --- a/src/blenderbim/blenderbim/bim/module/project/operator.py +++ b/src/blenderbim/blenderbim/bim/module/project/operator.py @@ -676,6 +676,24 @@ class UnloadProject(bpy.types.Operator): return {"FINISHED"} +class RevertProject(bpy.types.Operator, IFCFileSelector): + bl_idname = "bim.revert_project" + bl_label = "Revert IFC Project" + bl_options = {"REGISTER", "UNDO"} + bl_description = "Reload currently opened IFC project discarding all unsaved changes" + + @classmethod + def poll(cls, context): + if not context.scene.BIMProperties.ifc_file: + cls.poll_message_set("IFC project need to be loaded and saved on the disk.") + return False + return True + + def execute(self, context): + bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file) + return {"FINISHED"} + + class LoadProjectElements(bpy.types.Operator): bl_idname = "bim.load_project_elements" bl_label = "Load Project Elements" diff --git a/src/blenderbim/blenderbim/bim/module/project/ui.py b/src/blenderbim/blenderbim/bim/module/project/ui.py index 0797238bd1..d2bc4feb8b 100644 --- a/src/blenderbim/blenderbim/bim/module/project/ui.py +++ b/src/blenderbim/blenderbim/bim/module/project/ui.py @@ -73,6 +73,8 @@ def file_menu(self, context): op = self.layout.operator("export_ifc.bim", text="Save IFC Project As...") op.should_save_as = True self.layout.separator() + self.layout.operator("bim.revert_project") + self.layout.separator() class BIM_PT_project(Panel): diff --git a/src/blenderbim/blenderbim/bim/module/void/operator.py b/src/blenderbim/blenderbim/bim/module/void/operator.py index b3b914b256..78b9c619f7 100644 --- a/src/blenderbim/blenderbim/bim/module/void/operator.py +++ b/src/blenderbim/blenderbim/bim/module/void/operator.py @@ -147,14 +147,12 @@ class RemoveOpening(bpy.types.Operator, tool.Ifc.Operator): for building_element in decomposed_building_elements: building_obj = tool.Ifc.get_object(building_element) if building_obj and building_obj.data: - body = ifcopenshell.util.representation.get_representation( - building_element, "Model", "Body", "MODEL_VIEW" - ) + representation = tool.Geometry.get_active_representation(building_obj) blenderbim.core.geometry.switch_representation( tool.Ifc, tool.Geometry, obj=building_obj, - representation=body, + representation=representation, should_reload=True, is_global=True, should_sync_changes_first=False, diff --git a/src/blenderbim/blenderbim/bim/operator.py b/src/blenderbim/blenderbim/bim/operator.py index a8a675ef91..7b491e58be 100644 --- a/src/blenderbim/blenderbim/bim/operator.py +++ b/src/blenderbim/blenderbim/bim/operator.py @@ -212,7 +212,7 @@ class FileAssociate(bpy.types.Operator): self.layout.label(text="On the next step to create file association ") self.layout.label(text="the system console will be opened ") self.layout.label(text=f"and you will be asked to type command") - self.layout.label(text=f'"{command}"') + self.layout.label(text=f"{command}") self.layout.label(text="to create an association.") def invoke(self, context, event): @@ -241,7 +241,7 @@ class FileAssociate(bpy.types.Operator): ps_script_path = os.path.join(src_dir, "windows_bbim_association.ps1") # NOTE: call powershell with RunAs to get admin rights from user - subprocess.run(["powershell", "-file", ps_script_path, binary_path], shell=True) + subprocess.run(["powershell", "-ExecutionPolicy", "Bypass", "-File", ps_script_path, binary_path], shell=True) def install_desktop_linux(self, src_dir=None, destdir="/tmp", binary_path="/usr/bin/blender"): """Creates linux file assocations and launcher icon""" diff --git a/src/blenderbim/blenderbim/core/spatial.py b/src/blenderbim/blenderbim/core/spatial.py index 7f2beb3aba..109168847b 100644 --- a/src/blenderbim/blenderbim/core/spatial.py +++ b/src/blenderbim/blenderbim/core/spatial.py @@ -125,7 +125,9 @@ def select_decomposed_elements(spatial): #HERE STARTS SPATIAL TOOL def generate_spaces_from_walls(ifc, spatial, collector): - container, active_obj = spatial.get_container_and_active_obj() + active_obj = bpy.context.active_object + element = ifc.get_entity(active_obj) + container = spatial.get_container(element) if not active_obj: self.report({"ERROR"}, "No active object. Please select a wall") @@ -147,12 +149,12 @@ def generate_spaces_from_walls(ifc, spatial, collector): h = active_obj.dimensions.z selected_objects = bpy.context.selected_objects - union = spatial.get_union_shape_from_selected_objects(selected_objects) + union = spatial.get_union_shape_from_selected_objects() for i, linear_ring in enumerate(union.interiors): poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) - bm = spatial.get_bmesh_from_polygon(poly, mat, h) + bm = spatial.get_bmesh_from_polygon(poly, h) name = "Space" + str(i) mesh = bpy.data.meshes.new(name=name) diff --git a/src/blenderbim/blenderbim/core/tool.py b/src/blenderbim/blenderbim/core/tool.py index 0557e0144d..de5d5f31cb 100644 --- a/src/blenderbim/blenderbim/core/tool.py +++ b/src/blenderbim/blenderbim/core/tool.py @@ -810,8 +810,7 @@ class Spatial: def set_relative_object_matrix(cls, target_obj, relative_to_obj, matrix): pass def show_scene_objects(cls): pass #HERE STARTS SPATIAL TOOL -# def get_container_and_active_obj(cls): pass - def get_union_shape_from_selected_objects(cls, selected_objects): pass + def get_union_shape_from_selected_objects(cls): pass def get_boundary_elements(cls, selected_objects): pass def get_polygons(cls, boundary_elements): pass def get_obj_base_points(cls, obj): pass diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index 6d461cefc5..ae75535699 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -416,7 +416,7 @@ class Drawing(blenderbim.core.tool.Drawing): @classmethod def get_drawing_group(cls, drawing): for rel in drawing.HasAssignments or []: - if rel.is_a("IfcRelAssignsToGroup"): + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": return rel.RelatingGroup @classmethod diff --git a/src/ifc5d/ifc5d/csv2ifc.py b/src/ifc5d/ifc5d/csv2ifc.py index 7632b10f44..7f2241c67b 100644 --- a/src/ifc5d/ifc5d/csv2ifc.py +++ b/src/ifc5d/ifc5d/csv2ifc.py @@ -20,6 +20,8 @@ import csv import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit +import ifcopenshell.util.selector +import ifcopenshell.util.element import locale @@ -67,11 +69,21 @@ class Csv2Ifc: identification = row[self.headers["Identification"]] if "Identification" in self.headers else None quantity = row[self.headers["Quantity"]] unit = row[self.headers["Unit"]] + if not self.is_schedule_of_rates: + assignments = { + "PropertyName": row[self.headers["Property"]], + "Query": row[self.headers["Query"]], + } + else: + assignments = { + "PropertyName": None, + "Query": None, + } if self.has_categories: cost_values = { k: locale.atof(row[v]) for k, v in self.headers.items() - if k not in ["Hierarchy", "Identification", "Name", "Quantity", "Unit", "Subtotal"] and row[v] + if k not in ["Hierarchy", "Identification", "Name", "Quantity", "Unit", "Subtotal", "Property", "Query"] and row[v] } else: cost_values = row[self.headers["Value"]] @@ -82,6 +94,7 @@ class Csv2Ifc: "Quantity": float(quantity) if quantity else None, "Unit": str(unit) if unit else None, "CostValues": cost_values, + "assignments": assignments, "children": [], } @@ -107,7 +120,7 @@ class Csv2Ifc: cost_item["ifc"].Name = cost_item["Name"] cost_item["ifc"].Identification = cost_item["Identification"] - if not cost_item["CostValues"]: + if not cost_item["CostValues"] and cost_item["children"]: if not self.is_schedule_of_rates: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.Category = "*" @@ -115,7 +128,11 @@ class Csv2Ifc: for category, value in cost_item["CostValues"].items(): cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(value) - cost_value.Category = category + if category != "Rate" or category != "Price": + if "Rate" in category or"Price" in category: + category = category.replace("Rate","") + category = category.strip() + cost_value.Category = category else: cost_value = ifcopenshell.api.run("cost.add_cost_value", self.file, parent=cost_item["ifc"]) cost_value.AppliedValue = self.file.createIfcMonetaryMeasure(cost_item["CostValues"]) @@ -144,6 +161,15 @@ class Csv2Ifc: "cost.add_cost_item_quantity", self.file, cost_item=cost_item["ifc"], ifc_class=quantity_class ) quantity[3] = cost_item["Quantity"] + + if cost_item["assignments"]["PropertyName"] and cost_item["assignments"]["Query"]: + print("for query",cost_item["assignments"]["Query"]) + results = ifcopenshell.util.selector.filter_elements(self.file, cost_item["assignments"]["Query"]) + + results = [r for r in results if has_property(self.file, r, cost_item["assignments"]["PropertyName"])] + if results: + ifcopenshell.api.run("cost.assign_cost_item_quantity", self.file, cost_item=cost_item["ifc"], products=results, prop_name=cost_item["assignments"]["PropertyName"]) + self.create_cost_items(cost_item["children"], cost_item["ifc"]) def create_unit(self, symbol): @@ -158,3 +184,11 @@ class Csv2Ifc: def create_boilerplate_ifc(self): self.file = ifcopenshell.file(schema="IFC4") + +def has_property(self, product, property_name): + qtos = ifcopenshell.util.element.get_psets(product, qtos_only=True) + for qset, quantities in qtos.items(): + for quantity, value in quantities.items(): + if quantity == property_name: + return True + return False \ No newline at end of file diff --git a/src/ifc5d/sample_cost_schedule_house_FR.csv b/src/ifc5d/sample_cost_schedule_house_FR.csv new file mode 100644 index 0000000000..18480f0873 --- /dev/null +++ b/src/ifc5d/sample_cost_schedule_house_FR.csv @@ -0,0 +1,147 @@ +Hierarchy,Identification,Name,Quantity,Unit,Contract,Rate,Material Rate,Labor Rate,Subtotal,Property,Query +1,DB,Design and build,,,,,,,,, +2,DB.1,Design,,,,,,,,, +3,DB.1.1,Architecte,1,unit,40000,,,,40000,, +3,DB.1.2,Bet structure,1,unit,10000,,,,10000,, +3,DB.1.3,Bet lots techniques,1,unit,8000,,,,8000,, +2,DB.2,Construction,,,,,,,,, +3,DB.2.1,Go & etancheite,,,,,,,,, +4,DB.2.1.1,Gros oeuvre,,,,,,,,, +4,DB.2.1.2,Etancheite,,,,,,,,, +5,DB.2.1.2.1,Forme de pente et chape de lissage,,,,97,,,,NetArea,"IfcSlab, location=Roof" +5,DB.2.1.2.2,Ecran pare-vapeur et isolation thermique,,,,47,,,,NetArea,"IfcSlab, location=Roof" +5,DB.2.1.2.3,Systeme d’etancheite bicouche,,,,23,,,,NetArea,"IfcCovering, type=Etancheite_H" +5,DB.2.1.2.4,Reliefs d’etancheite bicouche,,,,7,,,,Length,"IfcWall, type=”BLK150”, location=Roof" +5,DB.2.1.2.5,Protection horizontales d’etancheite par carreaux de ciment,,,,,,,,NetArea,"IfcCovering, type=Etancheite_H" +5,DB.2.1.2.6,Protection des reliefs d’etancheite,,,,,,,,, +5,DB.2.1.2.7,Etancheite legere,,,,,,,,, +5,DB.2.1.2.8,Fourniture et pose de gargouilles y compris crapaudines,3,unit,,,,,,, +,,,,,,,,,,, +4,DB.2.1.3,Gros oeuvre piscine,,,,,,,,, +5,DB.2.1.3.1,Fouilles en pleine masse dans tous terrains y compris rocher,,,,,,,,, +5,DB.2.1.3.2,Remblaiement ou evacuation aux decharges publiques,,,,,,,,, +5,DB.2.1.3.3,Beton de proprete,,,,,,,,, +5,DB.2.1.3.4,Beton hydrofuge pour tous les ouvrages en fondation et elevation,,,,,,,,, +5,DB.2.1.3.5,Aciers a haute limite elastique fe500 pour les ouvrages en fondation et elevation,,,,,,,,, +,,,,,,,,,,, +4,DB.2.1.4,Maconnerie et briquetage en elevation,,,,,,,,, +5,DB.2.1.5.1,Cloisons en briques creuses 7t,,,,,,,,, +5,DB.2.1.5.2,Doubles cloisons en briques creuses 7t+vide10+7t,,,,,,,,, +,,,,,,,,,,, +,,,,,,,,,,, +4,DB.2.1.5,Enduits interieurs et exterieurs,,,,,,,,, +5,DB.2.1.5.1,"Enduit interieur au mortier de ciment sur murs et plafonds y +Compris baguettes d'angles",,,,,,,,, +5,DB.2.1.5.1.1,Enduit interieur au platre,,,,,,,,, +5,DB.2.1.5.1.2,"Enduit projete exterieur au mortier de ciment y compris baguettes +D'angles",,,,,,,,, +5,DB.2.1.5.1.3,Enduit de couronnement d'acroteres y compris facon de larmier,,,,,,,,, +,,,,,,,,,,, +4,DB.2.1.5,Reseaux sous dallage,,,,,,,,, +5,DB.2.1.5.1,Canalisation en buse pvc type assainissement serie i,,,,,,,,, +6,DB.2.1.5.1.1,Tranchee ,,,,,,,,, +6,DB.2.1.5.1.2,P.v.c diametre de 200 mm,,,,,,,,, +,,,,,,,,,,, +5,DB.2.1.5.2,Regards en beton arme ,,,,,,,,, +6,DB.2.1.5.2.1,Regards visitable de 50x50 cm,,,,,,,,, +6,DB.2.1.5.2.2,Regards non visitable de 40x40 cm,,,,,,,,, +4,DB.2.1.5,Divers,,,,,,,,, +5,DB.2.1.5.1,Gaine technique d’aeration et habillage des descentes des eaux ,,,,,,,,, +,,,,,,,,,,, +3,DB.2.2,Fluides,,,,,,,,, +4,DB.2.2.1,Plomberie,,,,,,,,, +4,DB.2.2.2,Climatisation,,,,,,,,, +4,DB.2.2.3,Sanitaire,,,,,,,,, +3,DB.2.3,Electricite,,,,,,,,, +4,DB.2.3.1,Eclairage et appareillage,,,,,,,,, +4,DB.2.3.2,Panneaux solaires,,,,,,,,, +3,DB.2.4,Architecture interieure,,,,,,,,, +4,DB.2.4.1,Menuiserie metallique,,,,,,,,, +5,DB.2.4.1.1,G.c 1 en vitrage bord àbord 10*10*2(1unite) rail en alu encastre,,,,,,,,, +5,DB.2.4.1.2,G.c 2 en vitrage bord àbord 10*10*2(1unite) rail en alu encastre,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.2,Menuiserie bois,,,,,,,,, +5,DB.2.4.2.1,Portes,,,,,,,,, +6,DB.2.4.2.1.1,P1,,,,,,,,Count,"IfcDoor, type=P1" +6,DB.2.4.2.1.2,P2,,,,,,,,Count,"IfcDoor, type=P2-LEFT + IfcDoor, type=P2-RIGHT" +6,DB.2.4.2.1.3,P3,,,,,,,,Count,"IfcDoor, type=P3-LEFT + IfcDoor, type=P3-RIGHT" +,,,,,,,,,,, +5,DB.2.4.2.2,Placards et habillage,,,,,,,,, +6,DB.2.4.2.2.1,Fourniture placard type 1,,,,,,,,, +6,DB.2.4.2.2.2,Fourniture placard type 2,,,,,,,,, +6,DB.2.4.2.2.3,Fourniture placard type 3 ,,,,,,,,, +6,DB.2.4.2.2.3,Fourniture placard type 4,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.3,Revetement sol parquet,,,,,,,,, +5,DB.2.4.3.1,Fourniture parquet contrecolle 18mm dont epaisseur noyer americain 3mm,,,,,300,,,NetArea,"IfcCovering, type=RS-2" +5,DB.2.4.3.2,Fournitur basic underlay,,,,,50,,,NetArea,"IfcCovering, type=RS-2" +5,DB.2.4.3.3,"Fourniture profilpas finition laiton 2,5mm",,,,,20,,,Width,IfcDoor +5,DB.2.4.3.4,"Pose du parquet y compris isolant acoustique, chappe de ciment, sous-couche et barre de seuil",,,,,,74,,NetArea,"IfcCovering, type=RS-2" +,,,,,,,,,,, +4,DB.2.4.4,Revetement sol marbre,,,,,,,,, +5,DB.2.4.4.1,Fourniture marbre sols,,,,,,,,NetArea,"IfcCovering, type=RS-5" +5,DB.2.4.4.2,Fourniture marbre- sols sdb,,,,,,,,NetArea,"IfcCovering, type=RS-2" +5,DB.2.4.4.3,Fourniture marbre murs sdb ,,,,,,,,, +5,DB.2.4.4.4,Fourniture marbre marche escalier y/c nez,,,,,,,,, +5,DB.2.4.4.5,Pose marbre sols y compris chappe de ciment,,,,,,,,, +5,DB.2.4.4.6,Pose marbre murs,,,,,,,,, +5,DB.2.4.4.7,Pose marbre marches,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.5,Revetement carreaux,,,,,,,,, +5,DB.2.4.5.1,Revetements,,,,,,,,, +6,DB.2.4.5.1.1,Revêtement sol: cuisine - carreaux - gres cerame (30*60cm) - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage,,,,,,,,, +6,DB.2.4.5.1.2,Revêtement sol: salle de jeux - carreaux - gres cerame (30*60cm) - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage,,,,,,,,, +6,DB.2.4.5.1.3,Revêtement sol: (reserve) - gres cerame (30*60cm) - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage - ,,,,,,,,, +6,DB.2.4.5.1.4,Revêtement mural cuisine - carreaux - effet vitrage noir,,,,,,,,, +5,DB.2.4.5.2,Plinthes,,,,,,,,, +6,DB.2.4.5.2.1,Plinthes - duropolymer orac decor ref : sx157,,,,,,,,, +6,DB.2.4.5.2.2,Plinthes marches - - duropolymer orac decor ref : sx157,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.6,Peinture,,,,,,,,, +5,DB.2.4.6.1,Fourniture et pose peinture murs - marmorex astral y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.2,Fourniture et pose peinture contremarche escalier - marmorex astral y/c preparation (5 couches,,,,,,,,, +5,DB.2.4.6.3,Fourniture et pose peinture murs cuisine - alpha tacto astral y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.4,Foruniture et pose peinture faux-plafond - jotun acrylique mate blanc y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.5,Fourniture et pose peinture murs sdb - jotun acrylique semi-brillant blanc special piece humide y/c preparation (5 couches),,,,,,,,, +5,DB.2.4.6.6,Fourniture et pose peinture faux-plafond sdb + cuisine - jotun acrylique semi-brillant blanc special piece humide y/c preparation (5 couches),,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.7,Faux plafonds,,,,,,,,, +5,DB.2.4.7.1,Type 1,,,,,,,,, +5,DB.2.4.7.2,Type 2,,,,,,,,, +5,DB.2.4.7.3,Type 3 – hydrofuge – exterieur,,,,,,,,, +5,DB.2.4.7.4,Trappes d’acces,,,,,,,,, +,,,,,,,,,,, +4,DB.2.4.8,Cuisine,1,unit,,,,,,, +,,,,,,,,,,, +3,DB.2.5,Facade,,,,,,,,, +4,DB.2.5.1,Revetement exterieur ,,,,,,,,, +5,DB.2.5.1.1,Revetement mural: plus value enduit mural hydrofuge (monocouche),,,,,,,,, +5,DB.2.5.1.2,Revetement mural: pierre volcanique grise ,,,,,,,,, +5,DB.2.5.1.3,Revetement mural: bardage en bois iroko ou tole alu section carre ou rectangulaire avec habillage bois technowood (voir detail architecte),,,,,,,,, +5,DB.2.5.1.4,Revêtement sol: terrasses et cour - tech - choix sous validation d'echantillons par l'architecte et le maitre d'ouvrage ,,,,,,,,, +5,DB.2.5.1.5,Revetement sol: seuill entree en pierre de taza,,,,,,,,, +5,DB.2.5.1.6,Revetement sol: sous bassement perimetre villa en pierre de taza ,,,,,,,,, +,,,,,,,,,,, +4,DB.2.5.2,Menuiserie aluminium,,,,,,,,, +5,DB.2.5.2.1,Fenetres,,,,,,,,, +6,DB.2.5.2.1.1,F1,,,,,,,,, +6,DB.2.5.2.1.2,F2,,,,,,,,, +,,,,,,,,,,, +5,DB.2.5.2.2,Chassis ouvrants,,,,,,,,, +6,DB.2.5.2.2.1,Ch1,,,,,,,,, +6,DB.2.5.2.2.2,Ch2,,,,,,,,, +6,DB.2.5.2.2.3,Ch3,,,,,,,,, +,,,,,,,,,,, +5,DB.2.5.2.3,Portes coulissantes/ mur rideau,,,,,,,,, +6,DB.2.5.2.3.1,P4,,,,,,,,, +6,DB.2.5.2.3.2,P5,,,,,,,,, +6,DB.2.5.2.3.3,P6,,,,,,,,, +,,,,,,,,,,, +5,DB.2.5.2.4,Volets roulants,,,,,,,,, +6,DB.2.5.2.4.1,Type 1,,,,,,,,, +6,DB.2.5.2.4.2,Type 2,,,,,,,,, +6,DB.2.5.2.4.3,Type 3,,,,,,,,, +3,DB.2.6,Exterieur,,,,,,,,, +4,DB.2.6.1,Cloture,,,,,,,,, +4,DB.2.6.2,Piscine,,,,,,,,, +4,DB.2.6.3,Jardin,,,,,,,,, diff --git a/src/ifc5d/sample_cost_schedule_house_FR.ods b/src/ifc5d/sample_cost_schedule_house_FR.ods new file mode 100644 index 0000000000..3d8cb6bb1b Binary files /dev/null and b/src/ifc5d/sample_cost_schedule_house_FR.ods differ diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py index c5414a46ac..36f6679145 100644 --- a/src/ifcfm/ifcfm/__init__.py +++ b/src/ifcfm/ifcfm/__init__.py @@ -57,7 +57,7 @@ class Parser: else: self.config = preset - def parse(self, ifc_file): + def parse(self, ifc_file, name=None): for category_name, category_config in self.config["categories"].items(): self.categories.setdefault(category_name, {}) for element in category_config["get_category_elements"](ifc_file): @@ -81,12 +81,29 @@ class Parser: data.update(custom_data) if data: - key = data["key"] - del data["key"] + key = "-".join([str(data[k]) for k in category_config["keys"]]) if key in self.categories[category_name]: self.duplicate_keys.append((self.categories[category_name][key], data)) self.categories[category_name][key] = data + def federate(self, paths): + for path in paths: + spreadsheet = pd.ExcelFile(path) + sheet_names = spreadsheet.sheet_names + + for category_name, category_config in self.config["categories"].items(): + self.categories.setdefault(category_name, {}) + if category_name not in sheet_names: + continue + + self.categories.setdefault(category_name, {}) + df = pd.read_excel(spreadsheet, sheet_name=category_name, keep_default_na=False) + for _, row in df.iterrows(): + key = "-".join([str(row[k]) for k in category_config["keys"]]) + if key in self.categories[category_name]: + continue + self.categories[category_name][key] = row.to_dict() + def exclude_categories(self, names): for name in names: if name in self.config["categories"]: @@ -148,7 +165,7 @@ class Writer: if isinstance(value, str): convert = lambda text: int(text) if text.isdigit() else text.lower() return [convert(c) for c in re.split("([0-9]+)", value)] - return value + return [str(value)] # Sort least important keys first, then more important keys. # https://stackoverflow.com/questions/11476371/sort-by-multiple-keys-using-different-orderings diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py index f4ccb81ffd..697f9c2bbe 100644 --- a/src/ifcfm/ifcfm/basic.py +++ b/src/ifcfm/ifcfm/basic.py @@ -61,7 +61,6 @@ def get_systems(ifc_file): def get_facility_data(ifc_file, element): return { - "key": element.Name, "Name": element.Name, "ProjectName": ifc_file.by_type("IfcProject")[0].Name, "SiteName": getattr(get_facility_parent(element, "IfcSite"), "Name", None), @@ -80,7 +79,6 @@ def get_facility_data(ifc_file, element): def get_storey_data(ifc_file, element): return { - "key": element.Name, "Name": element.Name, "Category": "Level", "AuthorOrganizationName": get_owner_name(element), @@ -95,7 +93,6 @@ def get_storey_data(ifc_file, element): def get_space_data(ifc_file, element): psets = ifcopenshell.util.element.get_psets(element) return { - "key": element.Name, "Name": element.Name, "Description": element.LongName, "Category": get_classification(element), @@ -112,7 +109,6 @@ def get_space_data(ifc_file, element): def get_zone_data(ifc_file, element): zone, space = element return { - "key": (zone.Name or "Unnamed") + (space.Name or "Unnamed"), "Name": zone.Name, "SpaceName": space.Name, "AuthorOrganizationName": get_owner_name(zone), @@ -125,7 +121,6 @@ def get_zone_data(ifc_file, element): def get_element_type_data(ifc_file, element): psets = ifcopenshell.util.element.get_psets(element) return { - "key": element.Name, "Name": element.Name, "Description": element.Description, "Category": get_classification(element), @@ -150,7 +145,6 @@ def get_element_data(ifc_file, element): system = systems[0].Name if systems else None psets = ifcopenshell.util.element.get_psets(element) return { - "key": element.Name, "Name": element.Name, "TypeName": ifcopenshell.util.element.get_type(element).Name, "SpaceName": space_name, @@ -173,7 +167,6 @@ def get_element_data(ifc_file, element): def get_system_data(ifc_file, element): return { - "key": element.Name, "Name": element.Name, "Description": element.Description, "Category": get_classification(element), @@ -241,6 +234,7 @@ config = { }, "categories": { "Facilities": { + "keys": ["Name"], "headers": [ "Name", "ProjectName", @@ -262,6 +256,7 @@ config = { "get_element_data": get_facility_data, }, "Storeys": { + "keys": ["Name"], "headers": [ "Name", "Category", @@ -278,6 +273,7 @@ config = { "get_element_data": get_storey_data, }, "Spaces": { + "keys": ["Name"], "headers": [ "Name", "Description", @@ -296,6 +292,7 @@ config = { "get_element_data": get_space_data, }, "Zones": { + "keys": ["Name", "SpaceName"], "headers": ["Name", "SpaceName", "AuthorOrganizationName", "AuthorDate", "ModelSoftware", "ModelID"], "colours": "prreee", "sort": [{"name": "Name", "order": "ASC"}], @@ -303,6 +300,7 @@ config = { "get_element_data": get_zone_data, }, "ElementTypes": { + "keys": ["Name"], "headers": [ "Name", "Description", @@ -325,6 +323,7 @@ config = { "get_element_data": get_element_type_data, }, "Elements": { + "keys": ["Name"], "headers": [ "Name", "TypeName", @@ -350,6 +349,7 @@ config = { "get_element_data": get_element_data, }, "Systems": { + "keys": ["Name"], "headers": [ "Name", "Description", diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py index 6f385f1278..29d13cc3c2 100644 --- a/src/ifcfm/ifcfm/cobie24.py +++ b/src/ifcfm/ifcfm/cobie24.py @@ -35,7 +35,7 @@ import ifcopenshell.util.classification def get_contacts(ifc_file): - return ifc_file.by_type("IfcPersonAndOrganization") + return ifc_file.by_type("IfcActor") def get_facilities(ifc_file): @@ -245,7 +245,6 @@ def get_attributes(ifc_file): allowed_values = get_property_unit(props["id"], name) data = { - "key": str(val(name)) + str(sheet_name) + str(val(element.Name)), "Name": val(name), "CreatedBy": pset_created_by, "CreatedOn": pset_created_on, @@ -265,51 +264,54 @@ def get_attributes(ifc_file): def get_contact_data(ifc_file, element): - email = get_email_from_pao(element) + the_actor = element.Theactor - history = get_history(ifc_file) + if the_actor.is_a("IfcPerson"): + pao = None + person = the_actor + organization = None + elif the_actor.is_a("IfcOrganization"): + pao = None + person = None + organization = the_actor + elif the_actor.is_a("IfcPersonAndOrganization"): + pao = the_actor + person = the_actor.ThePerson + organization = the_actor.TheOrganization - roles = [] - for actor in [element, element.ThePerson, element.TheOrganization]: + email = get_email_from_pao(person, organization) + + roles = set() + for actor in [pao, person, organization]: + if not actor: + continue for role in actor.Roles or []: if role.Role == "USERDEFINED": if role.UserDefinedRole: - roles.append(role.UserDefinedRole) + roles.add(role.UserDefinedRole) else: - roles.append(role.Role) - - organization = element.TheOrganization - person = element.ThePerson - - department = get_pao_address(element, "InternalLocation") - if not department: - for rel in organization.Relates: - for org in rel.RelatedOrganizations: - if val(org.Name): - department = org.Name + roles.add(role.Role) return { - "key": email, "Email": email, - "CreatedBy": get_email_from_history(history) if history else None, - "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), "Category": ",".join(roles), "Company": getattr(organization, "Name", None), - "Phone": get_pao_address(element, "TelephoneNumbers"), - "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "Phone": get_pao_address(person, organisation, "TelephoneNumbers"), + "ExternalSystem": get_external_system(element), "ExternalObject": element.is_a(), - "ExternalIdentifier": email, - "Department": department, - "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None)) - or organization.Name, + "ExternalIdentifier": element.GlobalId, + "Department": get_pao_address(person, organisation, "InternalLocation"), + "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None)), "GivenName": getattr(person, "GivenName", None), "FamilyName": getattr(person, "FamilyName", None), - "Street": get_pao_address(element, "AddressLines"), - "PostalBox": get_pao_address(element, "PostalBox"), - "Town": get_pao_address(element, "Town"), - "StateRegion": get_pao_address(element, "Region"), - "PostalCode": get_pao_address(element, "PostalCode"), - "Country": get_pao_address(element, "Country"), + "Street": get_pao_address(person, organisation, "AddressLines"), + "PostalBox": get_pao_address(person, organisation, "PostalBox"), + "Town": get_pao_address(person, organisation, "Town"), + "StateRegion": get_pao_address(person, organisation, "Region"), + "PostalCode": get_pao_address(person, organisation, "PostalCode"), + "Country": get_pao_address(person, organisation, "Country"), } @@ -338,7 +340,6 @@ def get_facility_data(ifc_file, element): name = val(site.Name) or val(site.LongName) return { - "key": name, "Name": name, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -394,7 +395,6 @@ def get_floor_data(ifc_file, element): elevation = "" if elevation is None else str(elevation) return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -434,7 +434,6 @@ def get_space_data(ifc_file, element): net_area = str(value) return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -458,7 +457,6 @@ def get_zone_data(ifc_file, element): name, category = zone history = get_history(ifc_file) return { - "key": "-".join([str(name), str(category), str(space)]), "Name": name, "CreatedBy": get_email_from_history(history) if history else None, "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, @@ -479,7 +477,6 @@ def get_zone_data(ifc_file, element): space_name = val(space.Name) if space else None return { - "key": "-".join([str(name), str(category), str(space_name)]), "Name": name, "CreatedBy": get_created_by(zone), "CreatedOn": get_created_on(zone), @@ -588,7 +585,6 @@ def get_type_data(ifc_file, element): asset_type = "Moveable" return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -664,7 +660,6 @@ def get_component_data(ifc_file, element): asset_identifier = str(value) return { - "key": element.Name, "Name": element.Name, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -688,7 +683,6 @@ def get_system_data(ifc_file, element): category = get_category(system) component_name = val(component.Name) return { - "key": str(val(system.Name)) + str(category) + str(component_name), "Name": val(system.Name), "CreatedBy": get_created_by(system), "CreatedOn": get_created_on(system), @@ -723,7 +717,6 @@ def get_assembly_data(ifc_file, element): history = get_history(ifc_file) return { - "key": str(name) + str(sheet_name) + str(parent_name), "Name": name, "CreatedBy": get_email_from_history(history) if history else None, "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, @@ -749,7 +742,6 @@ def get_connection_data(ifc_file, element): row_name1 = val(ifcopenshell.util.system.get_port_element(element.RelatingPort).Name) row_name2 = val(ifcopenshell.util.system.get_port_element(element.RelatedPort).Name) return { - "key": str(name) + str(connection_type) + str(row_name1) + str(row_name2), "Name": name, "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -787,7 +779,6 @@ def get_spare_data(ifc_file, element): part_number = str(value) return { - "key": val(element), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -805,7 +796,6 @@ def get_spare_data(ifc_file, element): def get_resource_data(ifc_file, element): return { - "key": val(element.Name), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -866,7 +856,6 @@ def get_job_data(ifc_file, element): priors = ",".join(priors) if priors else task_number return { - "key": str(val(element.Name)) + str(type_name) + str(task_number), "Name": val(element.Name), "CreatedBy": get_created_by(element), "CreatedOn": get_created_on(element), @@ -908,7 +897,6 @@ def get_document_data(ifc_file, element): sheet_name = get_sheet_name(related_object) row_name = val(related_object.Name) return { - "key": str(name) + str(stage) + str(sheet_name) + str(row_name), "Name": name, "CreatedBy": get_created_by(rel), "CreatedOn": get_created_on(rel), @@ -961,28 +949,23 @@ def get_created_by(element): def get_email_from_history(element): pao = element.OwningUser if pao.is_a("IfcPersonAndOrganization"): - return get_email_from_pao(pao) + return get_email_from_pao(pao.ThePerson, pao.TheOrganization) + elif pao.is_a("IfcPerson"): + return get_email_from_pao(pao, None) + elif pao.is_a("IfcOrganization"): + return get_email_from_pao(None, pao) -def get_email_from_pao(pao): - for address in pao.ThePerson.Addresses or []: - if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: - return address.ElectronicMailAddresses[0] +def get_email_from_pao(person, organization): + if organization: + for address in organization.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] - for address in pao.TheOrganization.Addresses or []: - if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: - return address.ElectronicMailAddresses[0] - - person_id = getattr(pao.ThePerson, "Identification", getattr(pao.ThePerson, "Id", None)) - if person_id: - return person_id - - organization_id = getattr(pao.TheOrganization, "Identification", getattr(pao.TheOrganization, "Id", None)) - if organization_id: - return organization_id - - if pao.ThePerson.GivenName and pao.ThePerson.FamilyName and pao.TheOrganization.Name: - return pao.ThePerson.GivenName + pao.ThePerson.FamilyName + "@" + pao.TheOrganization.Name + ".com" + if person: + for address in person.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] def get_owner_name(element): @@ -1076,8 +1059,10 @@ def get_category(element): return val(getattr(element, "ObjectType", None)) -def get_pao_address(element, name): - for actor in [element.ThePerson, element.TheOrganization]: +def get_pao_address(person, organization, name): + for actor in [organization, person]: + if not actor: + continue for address in actor.Addresses or []: if hasattr(address, name) and getattr(address, name, None): result = getattr(address, name) @@ -1158,6 +1143,7 @@ config = { "bool_false": "No", "categories": { "Contact": { + "keys": ["Email"], "headers": [ "Email", "CreatedBy", @@ -1185,6 +1171,7 @@ config = { "get_element_data": get_contact_data, }, "Facility": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1215,6 +1202,7 @@ config = { "get_element_data": get_facility_data, }, "Floor": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1233,6 +1221,7 @@ config = { "get_element_data": get_floor_data, }, "Space": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1254,6 +1243,7 @@ config = { "get_element_data": get_space_data, }, "Zone": { + "keys": ["Name", "Category", "SpaceNames"], "headers": [ "Name", "CreatedBy", @@ -1271,6 +1261,7 @@ config = { "get_element_data": get_zone_data, }, "Type": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1314,6 +1305,7 @@ config = { "get_element_data": get_type_data, }, "Component": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1341,6 +1333,7 @@ config = { "get_element_data": get_component_data, }, "System": { + "keys": ["Name", "Category", "ComponentNames"], "headers": [ "Name", "CreatedBy", @@ -1358,6 +1351,7 @@ config = { "get_element_data": get_system_data, }, "Assembly": { # Note that this is technically "not required" + "keys": ["Name", "SheetName", "ParentName"], "headers": [ "Name", "CreatedBy", @@ -1377,6 +1371,7 @@ config = { "get_element_data": get_assembly_data, }, "Connection": { # Note that this is technically "not required" + "keys": ["Name", "ConnectionType", "RowName1", "RowName2"], "headers": [ "Name", "CreatedBy", @@ -1399,6 +1394,7 @@ config = { "get_element_data": get_connection_data, }, "Spare": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1419,6 +1415,7 @@ config = { "get_element_data": get_spare_data, }, "Resource": { + "keys": ["Name"], "headers": [ "Name", "CreatedBy", @@ -1435,6 +1432,7 @@ config = { "get_element_data": get_resource_data, }, "Job": { + "keys": ["Name", "TypeName", "TaskNumber"], "headers": [ "Name", "CreatedBy", @@ -1462,6 +1460,7 @@ config = { "get_element_data": get_job_data, }, "Document": { + "keys": ["Name", "Stage", "SheetName", "RowName"], "headers": [ "Name", "CreatedBy", @@ -1485,6 +1484,7 @@ config = { "get_element_data": get_document_data, }, "Attribute": { + "keys": ["Name", "SheetName", "RowName"], "headers": [ "Name", "CreatedBy", diff --git a/src/ifcfm/ifcfm/cobie24legacy.py b/src/ifcfm/ifcfm/cobie24legacy.py new file mode 100644 index 0000000000..0de1a783ad --- /dev/null +++ b/src/ifcfm/ifcfm/cobie24legacy.py @@ -0,0 +1,1524 @@ +# IfcFM - IFC for facility management +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcFM. +# +# IfcFM is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcFM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcFM. If not, see . + +import ifcopenshell +import ifcopenshell.util.fm +import ifcopenshell.util.date +import ifcopenshell.util.system +import ifcopenshell.util.placement +import ifcopenshell.util.classification + + +# The original BIMServer plugin has a function called ifcToCOBie: +# https://github.com/opensourceBIM/COBie-plugins/blob/master/COBieShared/src/org/bimserver/cobie/shared/serialization/COBieTabSerializer.java#L54 +# This calls various serialisers here: +# https://github.com/opensourceBIM/COBie-plugins/tree/master/COBieShared/src/org/bimserver/cobie/shared/serialization/util +# Some settings are also defined here: +# https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml +# Note that the following categories are not implemented in the BIMServer COBie-Plugins: +# Impact, Coordinate, Issue, Picklist + + +def get_contacts(ifc_file): + return ifc_file.by_type("IfcPersonAndOrganization") + + +def get_facilities(ifc_file): + return ifc_file.by_type("IfcBuilding") + + +def get_floors(ifc_file): + return [ + e + for e in ifc_file.by_type("IfcBuildingStorey") + if ifcopenshell.util.element.get_aggregate(e).is_a("IfcBuilding") + ] + + +def get_spaces(ifc_file): + return ifc_file.by_type("IfcSpace") + + +def get_zones(ifc_file): + results = [] + zones = ifc_file.by_type("IfcZone") + for zone in zones or []: + has_space = False + for rel in zone.IsGroupedBy: + items = [(zone, space) for space in rel.RelatedObjects if space.is_a("IfcSpace") and val(space.Name)] + if items: + results.extend(items) + has_space = True + if not has_space: + results.append((zone, None)) + if zones: + return results + + zone_spaces = {} + for space in ifc_file.by_type("IfcSpace"): + for _, props in ifcopenshell.util.element.get_psets(space).items(): + for name, value in props.items(): + if "ZoneName" in name: + zone_name = val(value) + space_name = val(space.Name) + category = name + zone_key = str(name) + "," + str(category) + zone_spaces.setdefault(zone_key, []) + if space_name not in zone_spaces[zone_key]: + zone_spaces[zone_key].append(space_name) + results.append(((zone_name, category), space_name)) + return results + + +def get_types(ifc_file): + return ifcopenshell.util.fm.get_cobie_types(ifc_file) + + +def get_components(ifc_file): + elements = set() + for element_type in get_types(ifc_file): + elements.update(ifcopenshell.util.element.get_types(element_type)) + return elements + + +def get_systems(ifc_file): + results = [] + components = get_components(ifc_file) + if ifc_file.schema == "IFC2X3": + systems = ifc_file.by_type("IfcSystem", include_subtypes=False) + else: + systems = ifc_file.by_type("IfcSystem", include_subtypes=False) + ifc_file.by_type("IfcDistributionSystem") + for system in systems: + for element in ifcopenshell.util.system.get_system_elements(system): + if element in components: + results.append((system, element)) + return results + + +def get_assemblies(ifc_file): + results = [] + layer_sets = ifc_file.by_type("IfcMaterialLayerSet") + layer_sets = [] # This is temporarily overridden because it is unclear exactly how this is stored in Type. + for layer_set in layer_sets: + for layer in layer_set.MaterialLayers: + results.append((None, layer_set, layer.Material)) + rels = ifc_file.by_type("IfcRelAggregates") + ifc_file.by_type("IfcRelNests") + types = get_types(ifc_file) + components = get_components(ifc_file) + for rel in rels: + if rel.RelatingObject.is_a("IfcSpace"): + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcSpace"): + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject.is_a("IfcZone"): + for related_object in rel.RelatedObjects: + if related_object.is_a("IfcZone"): + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject in types: + for related_object in rel.RelatedObjects: + if related_object in types: + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject in components: + for related_object in rel.RelatedObjects: + if related_object in components: + results.append((rel, rel.RelatingObject, related_object)) + elif rel.RelatingObject.is_a() in ("IfcSystem", "IfcDistributionSystem"): + for related_object in rel.RelatedObjects: + if related_object.is_a() in ("IfcSystem", "IfcDistributionSystem"): + results.append((rel, rel.RelatingObject, related_object)) + return results + + +def get_connections(ifc_file): + return ifc_file.by_type("IfcRelConnectsPorts") + + +def get_spares(ifc_file): + return ifc_file.by_type("IfcConstructionProductResource") + + +def get_resources(ifc_file): + return ifc_file.by_type("IfcConstructionEquipmentResource") + + +def get_jobs(ifc_file): + return ifc_file.by_type("IfcTask") + + +def get_documents(ifc_file): + # The original COBie-Plugins assumes a single related object per rel. I think this was wrong. + results = [] + for rel in ifc_file.by_type("IfcRelAssociatesDocument"): + doc = rel.RelatingDocument + if doc.is_a("IfcDocumentInformation"): + for related_object in rel.RelatedObjects: + results.append((rel, doc, related_object)) + elif doc.is_a("IfcDocumentReference") and doc.ReferencedDocument: + for related_object in rel.RelatedObjects: + results.append((rel, doc.ReferencedDocument, related_object)) + return results + + +def get_attributes(ifc_file): + results = [] + history = get_history(ifc_file) + created_by = get_email_from_history(history) if history else None + created_on = ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None + external_system = history.OwningApplication.ApplicationFullName if history else None + get_sheets = { + "Facility": get_facilities, + "Floor": get_floors, + "Space": get_spaces, + "Type": get_types, + "Component": get_components, + } + + # COBie-Plugins includes what seems like a whole bunch of arbitrary "at the + # time it seemed to help" exclusion names. I don't like that strategy. If + # you've got garbage in your model, clean it out first. + + # fmt: off + excluded_names = { + "Manufacturer", + "ModelNumber", "ArticleNumber", "ModelLabel", + "WarrantyGuarantorParts", "PointOfContact", + "WarrantyGuarantorLabor", "PointOfContact", + "WarrantyDescription", "WarrantyIdentifier", + "ReplacementCost", "Replacement Cost", "Replacement", "Cost", + "NominalLength", "OverallLength", + "NominalWidth", "Width", + "NominalHeight", "Height", + "ModelReference", "Reference", + "Shape", + "Size", + "Color", "Colour", + "Finish", + "Grade", + "Material", + "Constituents", "Parts", + "Features", + "AccessibilityPerformance", "Access", + "CodePerformance", "Regulation", + "SustainabilityPerformance", "Environmental", + "SerialNumber", "InstallationDate", "WarrantyStartDate", "TagNumber", "BarCode", "AssetIdentifier" + } + # fmt: on + + for sheet_name, get_sheet in get_sheets.items(): + for element in get_sheet(ifc_file): + for pset_name, props in ifcopenshell.util.element.get_psets(element).items(): + pset = ifc_file.by_id(props["id"]) + pset_created_by = get_created_by(pset) or created_by + pset_created_on = get_created_on(pset) or created_on + pset_external_system = get_external_system(element) or external_system + pset_description = val(pset.Description) or pset_name + category = get_category(pset) + for name, value in props.items(): + if value == "default" or not val(value): + continue + elif name in excluded_names: + continue + elif name == "id": + continue + + unit = None + if isinstance(value, (int, float)): + unit = get_property_unit(props["id"], name) + + allowed_values = None + if isinstance(value, (tuple, list)): + allowed_values = get_property_unit(props["id"], name) + + data = { + "key": str(val(name)) + str(sheet_name) + str(val(element.Name)), + "Name": val(name), + "CreatedBy": pset_created_by, + "CreatedOn": pset_created_on, + "Category": category, + "SheetName": sheet_name, + "RowName": val(element.Name), + "Value": value, + "Unit": unit, + "ExternalSystem": pset_external_system, + "ExternalObject": pset_name, + "ExternalIdentifier": pset.GlobalId, + "Description": pset_description, + "AllowedValues": allowed_values, + } + results.append(data) + return results + + +def get_contact_data(ifc_file, element): + email = get_email_from_pao(element) + + history = get_history(ifc_file) + + roles = [] + for actor in [element, element.ThePerson, element.TheOrganization]: + for role in actor.Roles or []: + if role.Role == "USERDEFINED": + if role.UserDefinedRole: + roles.append(role.UserDefinedRole) + else: + roles.append(role.Role) + + organization = element.TheOrganization + person = element.ThePerson + + department = get_pao_address(element, "InternalLocation") + if not department: + for rel in organization.Relates: + for org in rel.RelatedOrganizations: + if val(org.Name): + department = org.Name + + return { + "key": email, + "Email": email, + "CreatedBy": get_email_from_history(history) if history else None, + "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "Category": ",".join(roles), + "Company": getattr(organization, "Name", None), + "Phone": get_pao_address(element, "TelephoneNumbers"), + "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "ExternalObject": element.is_a(), + "ExternalIdentifier": email, + "Department": department, + "OrganizationCode": getattr(organization, "Id", getattr(organization, "Identification", None)) + or organization.Name, + "GivenName": getattr(person, "GivenName", None), + "FamilyName": getattr(person, "FamilyName", None), + "Street": get_pao_address(element, "AddressLines"), + "PostalBox": get_pao_address(element, "PostalBox"), + "Town": get_pao_address(element, "Town"), + "StateRegion": get_pao_address(element, "Region"), + "PostalCode": get_pao_address(element, "PostalCode"), + "Country": get_pao_address(element, "Country"), + } + + +def get_facility_data(ifc_file, element): + site = get_facility_parent(element, "IfcSite") + site_name = None + site_description = None + if site: + site_name = val(site.Name) or val(site.LongName) or site.GlobalId + site_description = val(site.Description) or val(site.LongName) or val(site.Name) + + project = None + project_name = None + project_description = None + try: + project = ifc_file.by_type("IfcProject")[0] + project_name = val(project.Name) or val(project.LongName) or project.GlobalId + project_description = val(project.Description) or val(project.LongName) or val(project.Name) + except: + pass + + name = val(element.Name) or val(element.LongName) + if not name: + name = val(project.Name) or val(project.LongName) + if not name and site: + name = val(site.Name) or val(site.LongName) + + return { + "key": name, + "Name": name, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "ProjectName": project_name, + "SiteName": site_name, + "LinearUnits": get_unit_type_name(ifc_file, "LENGTHUNIT"), + "AreaUnits": get_unit_type_name(ifc_file, "AREAUNIT"), + "VolumeUnits": get_unit_type_name(ifc_file, "VOLUMEUNIT"), + "CurrencyUnit": get_unit_type_name(ifc_file, "IfcMonetaryUnit"), + "AreaMeasurement": get_area_measurement(element), + "ExternalSystem": get_external_system(element), + "ExternalProjectObject": "IfcProject", + "ExternalProjectIdentifier": project.GlobalId if project else ifcopenshell.guid.new(), + "ExternalSiteObject": "IfcSite", + "ExternalSiteIdentifier": site.GlobalId if site else ifcopenshell.guid.new(), + "ExternalFacilityObject": "IfcBuilding", + "ExternalFacilityIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "ProjectDescription": project_description, + "SiteDescription": site_description, + "Phase": val(project.Phase) if project else None, + } + + +def get_floor_data(ifc_file, element): + external_object = element.is_a() + if element.ObjectType and element.ObjectType.lower() in ("site", "ifcsite"): + external_object = "IfcSite" + + height_names = { + "Height", + "NetHeight", + "GrossHeight", + "Net Height", + "Gross Height", + "StoreyHeight", + "Storey Height", + "FloorHeight", + "Floor Height", + } + + height = None + for _, props in ifcopenshell.util.element.get_psets(element).items(): + if height is not None: + break + for name, value in props.items(): + if name in height_names and val(value): + height = str(value) + break + + elevation = getattr(element, "Elevation", "") + elevation = "" if elevation is None else str(elevation) + + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "ExternalSystem": get_external_system(element), + "ExternalObject": external_object, + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "Elevation": val(elevation), + "Height": height, + } + + +def get_space_data(ifc_file, element): + floor_name = None + floor = ifcopenshell.util.element.get_aggregate(element) + if floor and floor.is_a("IfcBuildingStorey"): + floor_name = val(floor.Name) + + room_tag = None + room_tag_names = {"RoomTag", "Tag", "Room Tag"} + usable_height = None + usable_height_names = {"FinishCeilingHeight", "Height", "UsableHeight"} + gross_area = None + gross_area_names = {"GrossFloorArea", "GSA"} + net_area = None + net_area_names = {"NetFloorArea", "GSA"} + for _, props in ifcopenshell.util.element.get_psets(element).items(): + for name, value in props.items(): + if not room_tag and name in room_tag_names and val(value): + room_tag = str(value) + if not usable_height and name in usable_height_names and val(value): + usable_height = str(value) + if not gross_area and name in gross_area_names and val(value): + gross_area = str(value) + if not net_area and name in net_area_names and val(value): + net_area = str(value) + + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "FloorName": floor_name, + "Description": val(element.Description) or val(element.LongName) or val(element.Name), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "RoomTag": room_tag, + "UsableHeight": usable_height, + "GrossArea": gross_area, + "NetArea": net_area, + } + + +def get_zone_data(ifc_file, element): + zone, space = element + + if isinstance(zone, tuple): + name, category = zone + history = get_history(ifc_file) + return { + "key": "-".join([str(name), str(category), str(space)]), + "Name": name, + "CreatedBy": get_email_from_history(history) if history else None, + "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "Category": category, + "SpaceNames": space, + "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "ExternalObject": "IfcPropertySingleValue", + "ExternalIdentifier": None, + "Description": val(name) or val(category), + } + + name = zone.Name + parent = ifcopenshell.util.element.get_aggregate(zone) + if parent and val(parent.Name): + name = parent.Name + "-" + name + + category = get_category(zone) + space_name = val(space.Name) if space else None + + return { + "key": "-".join([str(name), str(category), str(space_name)]), + "Name": name, + "CreatedBy": get_created_by(zone), + "CreatedOn": get_created_on(zone), + "Category": category, + "SpaceNames": space_name, + "ExternalSystem": get_external_system(zone), + "ExternalObject": zone.is_a(), + "ExternalIdentifier": zone.GlobalId, + "Description": val(zone.Description) or zone.Name, + } + + +def get_type_data(ifc_file, element): + pset_metadata = {} + pset_mapping = { + "manufacturer": {"Manufacturer"}, + "model_number": {"ModelNumber", "ArticleNumber", "ModelReference"}, + "warranty_guarantor_parts": {"WarrantyGuarantorParts", "PointOfContact"}, + "warranty_guarantor_labor": {"WarrantyGuarantorLabor", "PointOfContact"}, + "warranty_description": {"WarrantyDescription", "WarrantyIdentifier"}, + "replacement_cost": {"ReplacementCost", "Replacement Cost", "Replacement", "Cost"}, + "nominal_length": {"NominalLength", "OverallLength", "Length"}, + "nominal_width": {"NominalWidth", "OverallWidth", "Width"}, + # https://github.com/opensourceBIM/COBie-plugins/blob/master/COBiePlugins/lib/IfcToCobieConfig.xml#L104 + "nominal_height": {"NominalHeight", "Height"}, # Original has a typo "Heght" + "model_reference": {"ModelLabel"}, # I believe this is what the intention was, not "ModelReference". + "shape": {"Shape"}, + "size": {"Size"}, + "color": {"Color", "Colour"}, + "finish": {"Finish"}, + "grade": {"Grade"}, + "material": {"Material"}, + "constituents": {"Constituents", "Parts"}, + "features": {"Features"}, + "accessibility_performance": {"AccessibilityPerformance", "Access"}, + "code_performance": {"CodePerformance", "Regulation"}, + "sustainability_performance": {"SustainabilityPerformance", "Environmental"}, + } + asset_type = None + asset_type_names = {"AssetType", "AssetAccountingType"} + warranty_duration_parts = None + warranty_duration_parts_names = {"WarrantyDurationParts", "WarrantyPeriod"} + warranty_duration_labor = None + warranty_duration_labor_names = {"WarrantyDurationLabor", "WarrantyPeriod"} + warranty_duration_unit = None + expected_life = None + expected_life_names = {"ExpectedLife", "Expected Life", "ServiceLifeDuration", "Expected"} + duration_unit = None + + for pset_name, props in ifcopenshell.util.element.get_psets(element).items(): + pset_warranty_type = None + if pset_name == "Pset_Warranty": + if "parts" in (props.get("WarrantyIdentifier", "") or "").lower(): + pset_warranty_type = "parts" + elif "labor" in (props.get("WarrantyIdentifier", "") or "").lower(): + pset_warranty_type = "labor" + + pset = ifc_file.by_id(props["id"]) + + for name, value in props.items(): + for key, prop_names in pset_mapping.items(): + if not pset_metadata.get(key, None) and name in prop_names and val(value): + pset_metadata[key] = str(value) + + if not asset_type and name in asset_type_names and val(value): + value = value.strip().lower() + if value in ("moveable", "nonfixed"): + asset_type = "Moveable" + elif value == "fixed": + asset_type = "Fixed" + if not warranty_duration_parts and name in warranty_duration_parts_names and val(value): + warranty_duration_parts = str(value) + if not warranty_duration_unit: + warranty_duration_unit = get_property_unit(pset, name) + if not warranty_duration_labor and name in warranty_duration_labor_names and val(value): + warranty_duration_labor = str(value) + if not warranty_duration_unit: + warranty_duration_unit = get_property_unit(pset, name) + if not expected_life and name in expected_life_names and val(value): + expected_life = str(value) + if not duration_unit: + duration_unit = get_property_unit(pset, name) + + if pset_warranty_type == "parts" and val(value): + if name == "PointOfContact": + # https://github.com/buildingSMART/IFC4.3.x-development/issues/698 + warranty_guarantor_parts = str(value) + elif name == "WarrantyPeriod": + warranty_duration_parts = str(value) + unit = get_property_unit(pset, name) + warranty_duration_unit = unit or warranty_duration_unit + elif pset_warranty_type == "labor" and val(value): + if name == "PointOfContact": + # https://github.com/buildingSMART/IFC4.3.x-development/issues/698 + warranty_guarantor_labor = str(value) + elif name == "WarrantyPeriod": + warranty_duration_labor = str(value) + unit = get_property_unit(pset, name) + warranty_duration_unit = unit or warranty_duration_unit + + if warranty_duration_parts or warranty_duration_labor: + if not warranty_duration_unit: + warranty_duration_unit = get_unit_type_name(ifc_file, "TIMEUNIT") + + if not asset_type and element.is_a("IfcFurnitureType"): + asset_type = "Moveable" + + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "Description": val(element.Description) or val(element.Name), + "AssetType": asset_type, + "Manufacturer": pset_metadata.get("manufacturer", None), + "ModelNumber": pset_metadata.get("model_number", None), + "WarrantyGuarantorParts": pset_metadata.get("warranty_guarantor_parts", None), + "WarrantyDurationParts": warranty_duration_parts, + "WarrantyGuarantorLabor": pset_metadata.get("warranty_guarantor_labor", None), + "WarrantyDurationLabor": warranty_duration_labor, + "WarrantyDurationUnit": warranty_duration_unit, + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "ReplacementCost": pset_metadata.get("replacement_cost", None), + "ExpectedLife": expected_life, + "DurationUnit": duration_unit, + "WarrantyDescription": pset_metadata.get("warranty_description", None), + "NominalLength": pset_metadata.get("nominal_length", None), + "NominalWidth": pset_metadata.get("nominal_width", None), + "NominalHeight": pset_metadata.get("nominal_height", None), + "ModelReference": pset_metadata.get("model_reference", None), + "Shape": pset_metadata.get("shape", None), + "Size": pset_metadata.get("size", None), + "Color": pset_metadata.get("color", None), + "Finish": pset_metadata.get("finish", None), + "Grade": pset_metadata.get("grade", None), + "Material": pset_metadata.get("material", None), + "Constituents": pset_metadata.get("constituents", None), + "Features": pset_metadata.get("features", None), + "AccessibilityPerformance": pset_metadata.get("accessibility_performance", None), + "CodePerformance": pset_metadata.get("code_performance", None), + "SustainabilityPerformance": pset_metadata.get("sustainability_performance", None), + } + + +def get_component_data(ifc_file, element): + space = ifcopenshell.util.element.get_container(element) + space_name = space.Name if space.is_a("IfcSpace") else None + systems = ifcopenshell.util.system.get_element_systems(element) + system = systems[0].Name if systems else None + + type_name = None + relating_type = ifcopenshell.util.element.get_type(element) + if relating_type and val(relating_type.Name): + type_name = relating_type.Name + else: + material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + type_name = getattr(material, "Name", None) or getattr(material, "LayerSetName", None) + + serial_number = None + installation_date = None + warranty_start_date = None + tag_number = None + bar_code = None + asset_identifier = None + + for _, props in ifcopenshell.util.element.get_psets(element).items(): + for name, value in props.items(): + if not serial_number and name == "SerialNumber" and val(value): + serial_number = str(value) + if not installation_date and name == "InstallationDate" and val(value): + installation_date = str(value) + if not warranty_start_date and name == "WarrantyStartDate" and val(value): + warranty_start_date = str(value) + if not tag_number and name == "TagNumber" and val(value): + tag_number = str(value) + if not bar_code and name == "BarCode" and val(value): + bar_code = str(value) + if not asset_identifier and name == "AssetIdentifier" and val(value): + asset_identifier = str(value) + + return { + "key": element.Name, + "Name": element.Name, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "TypeName": type_name, + "Space": space_name, + "Description": val(element.Description) or val(element.Name), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "SerialNumber": serial_number, + "InstallationDate": installation_date, + "WarrantyStartDate": warranty_start_date, + "TagNumber": tag_number, + "BarCode": bar_code, + "AssetIdentifier": asset_identifier, + } + + +def get_system_data(ifc_file, element): + system, component = element + category = get_category(system) + component_name = val(component.Name) + return { + "key": str(val(system.Name)) + str(category) + str(component_name), + "Name": val(system.Name), + "CreatedBy": get_created_by(system), + "CreatedOn": get_created_on(system), + "Category": get_category(system), + "ComponentNames": component_name, + "ExternalSystem": get_external_system(system), + "ExternalObject": system.is_a(), + "ExternalIdentifier": system.GlobalId, + "Description": val(system.Description) or val(system.Name), + } + + +def get_assembly_data(ifc_file, element): + rel, relating_object, related_object = element + + if relating_object.is_a("IfcMaterialLayerSet"): + name = val(relating_object.LayerSetName) + parent_name = name + if name: + name += " assembly" + assembly_type = "Layer" + sheet_name = "Type" + description = val(relating_object.LayerSetName) + else: + name = val(relating_object.Name) + parent_name = name + assembly_type = "Fixed" + sheet_name = get_sheet_name(relating_object) + description = val(rel.Description) or val(rel.Name) + + child_name = val(related_object.Name) + history = get_history(ifc_file) + + return { + "key": str(name) + str(sheet_name) + str(parent_name), + "Name": name, + "CreatedBy": get_email_from_history(history) if history else None, + "CreatedOn": ifcopenshell.util.date.ifc2datetime(history.CreationDate).isoformat() if history else None, + "SheetName": sheet_name, + "ParentName": parent_name, + "ChildNames": child_name, + "AssemblyType": assembly_type, + "ExternalSystem": history.OwningApplication.ApplicationFullName if history else None, + "ExternalObject": rel.is_a() if rel else relating_object.is_a(), + "ExternalIdentifier": rel.GlobalId if rel else None, + "Description": description, + } + + +def get_connection_data(ifc_file, element): + connection_type = ( + val(element.RelatingPort.ObjectType) + or val(element.RelatedPort.ObjectType) + or val(element.Description) + or val(element.Name) + ) + name = val(element.Name) + row_name1 = val(ifcopenshell.util.system.get_port_element(element.RelatingPort).Name) + row_name2 = val(ifcopenshell.util.system.get_port_element(element.RelatedPort).Name) + return { + "key": str(name) + str(connection_type) + str(row_name1) + str(row_name2), + "Name": name, + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "ConnectionType": connection_type, + "SheetName": "Component", + "RowName1": row_name1, + "RowName2": row_name2, + "RealizingElement": val(element.RealizingElement.Name) if element.RealizingElement else None, + "PortName1": val(element.RelatingPort.Name), + "PortName2": val(element.RelatedPort.Name), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.Name), + } + + +def get_spare_data(ifc_file, element): + type_name = None + for rel in element.ResourceOf or []: + for related_object in rel.RelatedObjects or []: + if val(related_object.Name): + type_name = val(related_object.Name) + + suppliers = None + set_number = None + part_number = None + for _, props in ifcopenshell.util.element.get_psets(element).items(): + for name, value in props.items(): + if name == "Suppliers" and val(value): + suppliers = str(value) + if name == "SetNumber" and val(value): + set_number = str(value) + if name == "PartNumber" and val(value): + part_number = str(value) + + return { + "key": val(element), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": get_category(element), + "TypeName": type_name, + "Suppliers": suppliers, + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.Name), + "SetNumber": set_number, + "PartNumber": part_number, + } + + +def get_resource_data(ifc_file, element): + return { + "key": val(element.Name), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": val(element.ObjectType), + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "Description": val(element.Description) or val(element.Name), + } + + +def get_job_data(ifc_file, element): + type_names = [] + resource_names = [] + for rel in element.OperatesOn or []: + for related_object in rel.RelatedObjects or []: + if not val(related_object.Name): + continue + if related_object.is_a("IfcTypeObject"): + type_names.append(related_object.Name) + elif related_object.is_a("IfcConstructionEquipmentResource"): + resource_names.append(related_object.Name) + type_name = ",".join(type_names) if type_names else None + resource_names = ",".join(resource_names) if resource_names else None + + duration = None + duration_unit = None + start = None + task_start_unit = None + frequency = None + frequency_unit = None + + for _, props in ifcopenshell.util.element.get_psets(element).items(): + pset = ifc_file.by_id(props["id"]) + for name, value in props.items(): + if not duration and name == "TaskDuration" and val(value): + duration = str(value) + if not duration_unit: + duration_unit = get_property_unit(pset, name) + if not start and name == "TaskStartDate" and val(value): + start = str(value) + if not task_start_unit: + task_start_unit = get_property_unit(pset, name) + if not frequency and name == "TaskInterval" and val(value): + frequency = str(value) + if not frequency_unit: + frequency_unit = get_property_unit(pset, name) + + task_number = val(getattr(element, "Id", None)) or val(getattr(element, "Identification", None)) + + priors = [] + for rel in element.IsSuccessorFrom or []: + if rel.RelatedProcess.is_a("IfcTask"): + prior_task = rel.RelatedProcess + prior_id = val(getattr(prior_task, "Id", None)) or val(getattr(prior_task, "Identification", None)) + if prior_id: + priors.append(prior_id) + priors = ",".join(priors) if priors else task_number + + return { + "key": str(val(element.Name)) + str(type_name) + str(task_number), + "Name": val(element.Name), + "CreatedBy": get_created_by(element), + "CreatedOn": get_created_on(element), + "Category": val(element.ObjectType), + "Status": val(element.Status), + "TypeName": type_name, + "Description": val(element.Description) or val(element.Name), + "Duration": duration, + "DurationUnit": duration_unit, + "Start": start, + "TaskStartUnit": task_start_unit, + "Frequency": frequency, + "FrequencyUnit": frequency_unit, + "ExternalSystem": get_external_system(element), + "ExternalObject": element.is_a(), + "ExternalIdentifier": element.GlobalId, + "TaskNumber": task_number, + "Priors": priors, + "ResourceNames": resource_names, + } + + +def get_document_data(ifc_file, element): + rel, doc, related_object = element + directory = getattr(doc, "Location", None) + file = None + if not directory: + references = getattr(doc, "DocumentReferences", []) or getattr(doc, "HasDocumentReferences", []) + for reference in references or []: + if val(reference.Location): + directory = reference.Location + identification = getattr(reference, "ItemReference", None) or getattr(reference, "Identification", None) + if val(reference.Name) and doc.Name != reference.Name: + file = reference.Name + elif val(identification): + file = identification + name = val(doc.Name) + stage = val(doc.Scope) or "Requirement" + sheet_name = get_sheet_name(related_object) + row_name = val(related_object.Name) + return { + "key": str(name) + str(stage) + str(sheet_name) + str(row_name), + "Name": name, + "CreatedBy": get_created_by(rel), + "CreatedOn": get_created_on(rel), + "Category": val(doc.Purpose), + "ApprovalBy": val(doc.IntendedUse) or "Information Only", + "Stage": stage, + "SheetName": sheet_name, + "RowName": row_name, + "Directory": directory, + "File": file, + "ExternalSystem": get_external_system(rel), + "ExternalObject": rel.is_a(), + "ExternalIdentifier": rel.GlobalId, + "Description": val(doc.Description), + "Reference": val(doc.Description) or val(doc.Name), + } + + +def get_attribute_data(ifc_file, element): + return element + + +def get_unit_type_name(ifc_file, unit_type): + for unit in ifc_file.by_type("IfcUnitAssignment")[0].Units: + if unit.is_a("IfcNamedUnit") and unit.UnitType == unit_type: + if unit.is_a("IfcSIUnit"): + prefix = (unit.Prefix or "").lower() + if unit_type == "LENGTHUNIT": + return f"{prefix}meters" + elif unit_type == "AREAUNIT": + return f"square {prefix}meters" + elif unit_type == "VOLUMEUNIT": + return f"cubic {prefix}meters" + else: + return val(unit.Name) + elif unit.is_a("IfcMonetaryUnit") and unit_type == "IfcMonetaryUnit": + return val(unit.Currency) + + +def get_unit_name(ifc_file, unit): + if unit.is_a("IfcNamedUnit"): + return val(unit.Name) + + +def get_created_by(element): + if getattr(element, "OwnerHistory", None): + return get_email_from_history(element.OwnerHistory) + + +def get_email_from_history(element): + pao = element.OwningUser + if pao.is_a("IfcPersonAndOrganization"): + return get_email_from_pao(pao) + + +def get_email_from_pao(pao): + for address in pao.ThePerson.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] + + for address in pao.TheOrganization.Addresses or []: + if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: + return address.ElectronicMailAddresses[0] + + person_id = getattr(pao.ThePerson, "Identification", getattr(pao.ThePerson, "Id", None)) + if person_id: + return person_id + + organization_id = getattr(pao.TheOrganization, "Identification", getattr(pao.TheOrganization, "Id", None)) + if organization_id: + return organization_id + + if pao.ThePerson.GivenName and pao.ThePerson.FamilyName and pao.TheOrganization.Name: + return pao.ThePerson.GivenName + pao.ThePerson.FamilyName + "@" + pao.TheOrganization.Name + ".com" + + +def get_owner_name(element): + if getattr(element, "OwnerHistory", None): + return element.OwnerHistory.OwningUser.TheOrganization.Name + + +def get_created_on(element): + if getattr(element, "OwnerHistory", None): + return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() + + +def get_external_system(element): + if getattr(element, "OwnerHistory", None): + return val(element.OwnerHistory.OwningApplication.ApplicationFullName) + + +def get_facility_parent(element, ifc_class): + parent = ifcopenshell.util.element.get_aggregate(element) + while parent: + if parent.is_a(ifc_class): + return parent + if parent.is_a("IfcProject"): + return + parent = ifcopenshell.util.element.get_aggregate(parent) + + +def val(x): + return x if x not in ("", "n/a") else None + + +def get_area_measurement(element): + for relationship in getattr(element, "IsDefinedBy", []) or []: + if relationship.is_a("IfcRelDefinesByProperties"): + definition = relationship.RelatingPropertyDefinition + if definition.is_a("IfcElementQuantity") and val(definition.MethodOfMeasurement): + return definition.MethodOfMeasurement + for rel in getattr(element, "IsDecomposedBy", []): + for related_object in rel.RelatedObjects: + result = get_area_measurement(related_object) + if result: + return result + + +def get_category(element): + references = list(ifcopenshell.util.classification.get_references(element)) + results = [] + for reference in references: + if reference.is_a("IfcClassification"): + results.append(reference.Name) + elif reference.is_a("IfcClassificationReference"): + identification = val(getattr(reference, "Identification", getattr(reference, "ItemReference", None))) + if val(reference.Name) and identification and val(reference.Name) != identification: + results.append(identification + " : " + val(reference.Name)) + elif val(reference.Name): + results.append(reference.Name) + elif identification: + results.append(identification) + elif reference.ReferencedSource and val(reference.ReferencedSource.Name): + results.append(reference.ReferencedSource.Name) + elif val(reference.Location): + results.append(reference.Location) + if results: + return ",".join(results) + + category_props = [ + ("Assembly Code", "Assembly Description"), + ("Category Code", "Category Description"), + ("Classification Code", "Classification Description"), + ("OmniClass Number", "OmniClass Title"), + ("Uniclass Code", "Uniclass Description"), + ] + + psets = ifcopenshell.util.element.get_psets(element) + properties = {} + if psets: + for _, props in psets.items(): + properties.update(props) + + for code, description in category_props: + code = val(properties.get(code, None)) + if code: + description = val(properties.get(description, None)) + if code and description: + results.append(code + " : " + description) + else: + results.append(code) + if results: + return ",".join(results) + + return val(getattr(element, "ObjectType", None)) + + +def get_pao_address(element, name): + for actor in [element.ThePerson, element.TheOrganization]: + for address in actor.Addresses or []: + if hasattr(address, name) and getattr(address, name, None): + result = getattr(address, name) + if isinstance(result, tuple): + if name == "AddressLines": + return " ".join(result) + return result[0] + return result + + +def get_property(psets, pset_name, prop_name, decimals=None): + if pset_name in psets: + result = psets[pset_name].get(prop_name, None) + if decimals is None or result is None: + return result + return round(result, decimals) + + +def get_history(ifc_file): + histories = ifc_file.by_type("IfcOwnerHistory") + if histories: + return sorted(histories, key=lambda x: x.id())[-1] + + +def get_property_unit(pset, prop_name): + for prop in getattr(pset, "HasProperties", []) or []: + if prop.Name == prop_name: + unit = getattr(prop, "Unit", None) + if unit: + return get_unit_name(unit) + + +def get_allowed_values(pset_id, prop_name): + pset = ifc_file.by_id(pset_id) + for prop in getattr(pset, "HasProperties", []) or []: + if prop.Name == prop_name: + if prop.is_a("IfcPropertyEnumeratedValue") and prop.EnumerationValues: + return ",".join([v.wrappedValue for v in prop.EnumerationValues]) + + +def get_sheet_name(element): + if element.is_a("IfcBuilding"): + return "Facility" + elif element.is_a("IfcBuildingStorey"): + return "Floor" + elif element.is_a("IfcSpace"): + return "Space" + elif element.is_a("IfcZone"): + return "Zone" + elif element.is_a("IfcSystem"): + return "System" + elif element.is_a("IfcElementType"): + return "Type" + elif element.is_a("IfcElement"): + return "Component" + elif element.is_a("IfcTask"): + return "Job" + elif element.is_a("IfcConstructionProductResource"): + return "Spare" + elif element.is_a("IfcConstructionEquipmentResource"): + return "Resource" + + +config = { + "colours": { + "h": "c0c0c0", # Header data + "r": "ffff99", # Required + "i": "ffcc99", # Internal reference + "e": "cc99ff", # External reference + "o": "ccffcc", # Optionally specified + "s": "c0c0c0", # Secondary product data + "b": "99ccff", # Bespoke data + "x": "000000", # Not in scope + }, + "null": "n/a", + "empty": "n/a", + "bool_true": "Yes", + "bool_false": "No", + "categories": { + "Contact": { + "keys": ["Email"], + "headers": [ + "Email", + "CreatedBy", + "CreatedOn", + "Category", + "Company", + "Phone", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Department", + "OrganizationCode", + "GivenName", + "FamilyName", + "Street", + "PostalBox", + "Town", + "StateRegion", + "PostalCode", + "Country", + ], + "colours": "rrrrrreeeoooooooooo", + "sort": [{"name": "Email", "order": "ASC"}], + "get_category_elements": get_contacts, + "get_element_data": get_contact_data, + }, + "Facility": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ProjectName", + "SiteName", + "LinearUnits", + "AreaUnits", + "VolumeUnits", + "CurrencyUnit", + "AreaMeasurement", + "ExternalSystem", + "ExternalProjectObject", + "ExternalProjectIdentifier", + "ExternalSiteObject", + "ExternalSiteIdentifier", + "ExternalFacilityObject", + "ExternalFacilityIdentifier", + "Description", + "ProjectDescription", + "SiteDescription", + "Phase", + ], + "colours": "ririrriiiireeeeeeeoooo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_facilities, + "get_element_data": get_facility_data, + }, + "Floor": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "Elevation", + "Height", + ], + "colours": "ririeeeooo", + "sort": [{"name": "Elevation", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_floors, + "get_element_data": get_floor_data, + }, + "Space": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "FloorName", + "Description", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "RoomTag", + "UsableHeight", + "GrossArea", + "NetArea", + ], + "colours": "ririrreeeoooo", + "sort": [{"name": "FloorName", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_spaces, + "get_element_data": get_space_data, + }, + "Zone": { + "keys": ["Name", "Category", "SpaceNames"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "SpaceNames", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririreeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_zones, + "get_element_data": get_zone_data, + }, + "Type": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "Description", + "AssetType", + "Manufacturer", + "ModelNumber", + "WarrantyGuarantorParts", + "WarrantyDurationParts", + "WarrantyGuarantorLabor", + "WarrantyDurationLabor", + "WarrantyDurationUnit", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "ReplacementCost", + "ExpectedLife", + "DurationUnit", + "WarrantyDescription", + "NominalLength", + "NominalWidth", + "NominalHeight", + "ModelReference", + "Shape", + "Size", + "Color", + "Finish", + "Grade", + "Material", + "Constituents", + "Features", + "AccessibilityPerformance", + "CodePerformance", + "SustainabilityPerformance", + ], + "colours": "riririiriririeeeooiorrroooooooooooo", + "sort": [{"name": "ExternalObject", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_types, + "get_element_data": get_type_data, + }, + "Component": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "TypeName", + "Space", + "Description", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "SerialNumber", + "InstallationDate", + "WarrantyStartDate", + "TagNumber", + "BarCode", + "AssetIdentifier", + ], + "colours": "ririireeeoooooo", + "sort": [ + {"name": "ExternalObject", "order": "ASC"}, + {"name": "TypeName", "order": "ASC"}, + {"name": "Name", "order": "ASC"}, + ], + "get_category_elements": get_components, + "get_element_data": get_component_data, + }, + "System": { + "keys": ["Name", "Category", "ComponentNames"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ComponentNames", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririieeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_systems, + "get_element_data": get_system_data, + }, + "Assembly": { # Note that this is technically "not required" + "keys": ["Name", "SheetName", "ParentName"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "SheetName", + "ParentName", + "ChildNames", + "AssemblyType", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririiiieeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_assemblies, + "get_element_data": get_assembly_data, + }, + "Connection": { # Note that this is technically "not required" + "keys": ["Name", "ConnectionType", "RowName1", "RowName2"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "ConnectionType", + "SheetName", + "RowName1", + "RowName2", + "RealizingElement", + "PortName1", + "PortName2", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririiiiiiieeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_connections, + "get_element_data": get_connection_data, + }, + "Spare": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "TypeName", + "Suppliers", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "SetNumber", + "PartNumber", + ], + "colours": "ririiieeeooo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_spares, + "get_element_data": get_spare_data, + }, + "Resource": { + "keys": ["Name"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + ], + "colours": "ririeeeo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_resources, + "get_element_data": get_resource_data, + }, + "Job": { + "keys": ["Name", "TypeName", "TaskNumber"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "Status", + "TypeName", + "Description", + "Duration", + "DurationUnit", + "Start", + "TaskStartUnit", + "Frequency", + "FrequencyUnit", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "TaskNumber", + "Priors", + "ResourceNames", + ], + "colours": "ririiirriririeeeoii", + "sort": [{"name": "TypeName", "order": "ASC"}, {"name": "TaskNumber", "order": "ASC"}], + "get_category_elements": get_jobs, + "get_element_data": get_job_data, + }, + "Document": { + "keys": ["Name", "Stage", "SheetName", "RowName"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "ApprovalBy", + "Stage", + "SheetName", + "RowName", + "Directory", + "File", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "Reference", + ], + "colours": "ririiiiirreeeoo", + "sort": [{"name": "Name", "order": "ASC"}], + "get_category_elements": get_documents, + "get_element_data": get_document_data, + }, + "Attribute": { + "keys": ["Name", "SheetName", "RowName"], + "headers": [ + "Name", + "CreatedBy", + "CreatedOn", + "Category", + "SheetName", + "RowName", + "Value", + "Unit", + "ExternalSystem", + "ExternalObject", + "ExternalIdentifier", + "Description", + "AllowedValues", + ], + "colours": "ririiirreeeoo", + "sort": [{"name": "Category", "order": "ASC"}, {"name": "Name", "order": "ASC"}], + "get_category_elements": get_attributes, + "get_element_data": get_attribute_data, + }, + }, +} diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py index 20380d9dc4..a39794a5d7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py @@ -82,8 +82,8 @@ class Usecase: # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) duct_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) + fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=fitting) + fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=fitting) # Connect the duct and fitting together. At this point, we have not # yet determined the direction of the flow, so we leave direction as diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index eddfae5f59..3782e8e27b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -52,8 +52,8 @@ class Usecase: # Create 2 ports, one for either end of both the duct and fitting. duct_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) duct_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=duct) - fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=duct) + fitting_port1 = ifcopenshell.api.run("system.add_port", model, element=fitting) + fitting_port2 = ifcopenshell.api.run("system.add_port", model, element=fitting) # Connect the duct and fitting together. At this point, we have not # yet determined the direction of the flow, so we leave direction as diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index ac84cc7046..4852ff067c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1022,7 +1022,13 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_ent elif exclude_callback and exclude_callback(attribute): pass else: - attribute = copy_deep(ifc_file, attribute, exclude=exclude, copied_entities=copied_entities) + attribute = copy_deep( + ifc_file, + attribute, + exclude=exclude, + copied_entities=copied_entities, + exclude_callback=exclude_callback, + ) elif isinstance(attribute, tuple) and attribute and isinstance(attribute[0], ifcopenshell.entity_instance): if exclude and any([attribute[0].is_a(e) for e in exclude]): pass