From 8d9f027f3ead5dc76271e616bb5ab79b54317ccb Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sun, 12 Jul 2026 19:01:22 +0300 Subject: [PATCH] ifc4d: tolerate activities without a CalendarObjectId in P6 import (#5617) Importing a Primavera P6 XML crashed with `AttributeError: 'NoneType' object has no attribute 'text'` in P62Ifc.parse_activity_xml, which read activity.find("pr:CalendarObjectId").text unconditionally. CalendarObjectId is optional on a P6 Activity; when omitted, the activity inherits the project's ActivityDefaultCalendarObjectId. Capture the project default in parse_xml and fall back to it when an activity has no CalendarObjectId (`calendar_id or self.default_calendar_id`). Verified on the reporter's attached file (20241021 Cronograma.xml): 3 of 14 activities lack a CalendarObjectId and reproduced the exact crash on v0.8.0; after the fix parse_xml completes and those activities resolve to the project default calendar "2" (a valid calendar in the file). An activity with an explicit CalendarObjectId keeps its own value. Fixes the P6 re-import crash reported in #5617 (that issue tracks several Gantt items; this addresses the import AttributeError). Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 (cherry picked from commit 694a44e638548c7c714d34d036ab1280408d5436) --- src/ifc4d/ifc4d/p62ifc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ifc4d/ifc4d/p62ifc.py b/src/ifc4d/ifc4d/p62ifc.py index 7d4881161a..aa6aafc8dd 100644 --- a/src/ifc4d/ifc4d/p62ifc.py +++ b/src/ifc4d/ifc4d/p62ifc.py @@ -28,6 +28,7 @@ class P62Ifc: self.file = None self.work_plan = None self.project = {} + self.default_calendar_id = None self.calendars = {} self.wbs = {} self.root_activites = [] @@ -89,6 +90,7 @@ class P62Ifc: self.ns = {"pr": root.tag[1:].partition("}")[0]} project = root.find("pr:Project", self.ns) self.project["Name"] = project.findtext("pr:Name") or "Unnamed" + self.default_calendar_id = project.findtext("pr:ActivityDefaultCalendarObjectId", namespaces=self.ns) self.parse_calendar_xml(root) self.parse_calendar_xml(project) self.parse_wbs_xml(project) @@ -174,6 +176,9 @@ class P62Ifc: self.wbs[wbs_id]["activities"].append(activity_id) else: self.root_activites.append(activity_id) + # CalendarObjectId is optional in the P6 schema: an activity without one + # inherits the project's ActivityDefaultCalendarObjectId. + calendar_id = activity.findtext("pr:CalendarObjectId", namespaces=self.ns) self.activities[activity_id] = { "Name": activity.find("pr:Name", self.ns).text, "Identification": activity.find("pr:Id", self.ns).text, @@ -181,7 +186,7 @@ class P62Ifc: "FinishDate": datetime.datetime.fromisoformat(activity.find("pr:FinishDate", self.ns).text), "PlannedDuration": activity.find("pr:PlannedDuration", self.ns).text, "Status": activity.find("pr:Status", self.ns).text, - "CalendarObjectId": activity.find("pr:CalendarObjectId", self.ns).text, + "CalendarObjectId": calendar_id or self.default_calendar_id, "ifc": None, }