From 353281fa4d98662183f0c68f24322cf1d5e63cc0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 May 2021 11:35:47 +1000 Subject: [PATCH] Updating a task duration now auto calculates the scheduled finish times based on the working calendar --- .../blenderbim/bim/module/sequence/helper.py | 80 ---------------- .../bim/module/sequence/operator.py | 15 ++- .../api/sequence/edit_task_time.py | 37 ++++++- .../ifcopenshell/util/sequence.py | 96 +++++++++++++++++++ src/ifcp6/ifcp6/p62ifc.py | 6 +- 5 files changed, 145 insertions(+), 89 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/util/sequence.py diff --git a/src/blenderbim/blenderbim/bim/module/sequence/helper.py b/src/blenderbim/blenderbim/bim/module/sequence/helper.py index 0bb980d09a..b0fed75c6f 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/helper.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/helper.py @@ -5,78 +5,6 @@ from dateutil import parser from ifcopenshell.api.sequence.data import Data -def count_working_days(start, finish, calendar): - result = 0 - current_date = datetime.date(start.year, start.month, start.day) - finish_date = datetime.date(finish.year, finish.month, finish.day) - while current_date <= finish_date: - if is_working_day(current_date, calendar): - result += 1 - current_date += datetime.timedelta(days=1) - return result - - -def is_working_day(day, calendar): - is_working_day = False - for work_time_id in calendar["WorkingTimes"] or []: - if is_work_time_applicable_to_day(Data.work_times[work_time_id], day): - is_working_day = True - break - if not is_working_day: - return is_working_day - for work_time_id in calendar["ExceptionTimes"] or []: - if is_work_time_applicable_to_day(Data.work_times[work_time_id], day): - is_working_day = False - break - return is_working_day - - -def is_work_time_applicable_to_day(work_time, day): - if work_time["Start"] and work_time["Start"] > day: - return False - - if work_time["Finish"] and work_time["Finish"] < day: - return False - - if not work_time["RecurrencePattern"]: - return True - - recurrence = Data.recurrence_patterns[work_time["RecurrencePattern"]] - - if recurrence["RecurrenceType"] == "DAILY": - if not recurrence["Interval"] and not recurrence["Occurrences"]: - return True - if not work_time["Start"]: - return False - return False # TODO - elif recurrence["RecurrenceType"] == "WEEKLY": - if not recurrence["Interval"] and not recurrence["Occurrences"]: - return (day.weekday() + 1) in recurrence["WeekdayComponent"] - if not work_time["Start"]: - return False - return False # TODO - elif recurrence["RecurrenceType"] == "MONTHLY_BY_DAY_OF_MONTH": - if not recurrence["Interval"] and not recurrence["Occurrences"]: - return day.day in recurrence["DayComponent"] - return False # TODO - elif recurrence["RecurrenceType"] == "MONTHLY_BY_POSITION": - if not recurrence["Interval"] and not recurrence["Occurrences"]: - return (day.weekday() + 1) in recurrence["WeekdayComponent"] and math.floor(day.day / 7) + 1 == recurrence[ - "Position" - ] - return False # TODO - elif recurrence["RecurrenceType"] == "YEARLY_BY_DAY_OF_MONTH": - if not recurrence["Interval"] and not recurrence["Occurrences"]: - return day.month in recurrence["MonthComponent"] and day.day in recurrence["DayComponent"] - return False # TODO - elif recurrence["RecurrenceType"] == "YEARLY_BY_POSITION": - if not recurrence["Interval"] and not recurrence["Occurrences"]: - return ( - day.month in recurrence["MonthComponent"] - and (day.weekday() + 1) in recurrence["WeekdayComponent"] - and math.floor(day.day / 7) + 1 == recurrence["Position"] - ) - return False # TODO def derive_date(ifc_definition_id, attribute_name, date=None, is_earliest=False, is_latest=False): @@ -96,14 +24,6 @@ def derive_date(ifc_definition_id, attribute_name, date=None, is_earliest=False, return date -def derive_calendar(ifc_definition_id): - task = Data.tasks[ifc_definition_id] - if task["HasAssignmentsWorkCalendar"]: - return Data.work_calendars[task["HasAssignmentsWorkCalendar"][0]] - if task["Nests"]: - return derive_calendar(task["Nests"][0]) - - def derive_duration(ifc_definition_id, attribute_name): task = Data.tasks[ifc_definition_id] if task["TaskTime"]: diff --git a/src/blenderbim/blenderbim/bim/module/sequence/operator.py b/src/blenderbim/blenderbim/bim/module/sequence/operator.py index d8b550d5e8..d4e8cb8118 100644 --- a/src/blenderbim/blenderbim/bim/module/sequence/operator.py +++ b/src/blenderbim/blenderbim/bim/module/sequence/operator.py @@ -8,6 +8,7 @@ import pystache import webbrowser import ifcopenshell.api import ifcopenshell.util.date +import ifcopenshell.util.sequence import blenderbim.bim.helper import blenderbim.bim.module.sequence.helper as helper from datetime import datetime @@ -275,6 +276,7 @@ class LoadTaskProperties(bpy.types.Operator): task: bpy.props.IntProperty() def execute(self, context): + self.file = IfcStore.get_file() self.props = context.scene.BIMWorkScheduleProperties self.tprops = context.scene.BIMTaskTreeProperties self.props.is_task_update_enabled = False @@ -292,7 +294,9 @@ class LoadTaskProperties(bpy.types.Operator): Data.sequences[r]["RelatingProcess"] for r in task["IsSuccessorFrom"] ] - calendar = helper.derive_calendar(item.ifc_definition_id) + calendar = ifcopenshell.util.sequence.derive_calendar(self.file.by_id(item.ifc_definition_id)) + if calendar: + calendar = Data.work_calendars[calendar.id()] if task["HasAssignmentsWorkCalendar"]: item.calendar = calendar["Name"] or "Unnamed" else: @@ -312,7 +316,9 @@ class LoadTaskProperties(bpy.types.Operator): item.derived_start = self.canonicalise_time(derived_start) if derived_start else "" item.derived_finish = self.canonicalise_time(derived_finish) if derived_finish else "" if derived_start and derived_finish and calendar: - derived_duration = helper.count_working_days(derived_start, derived_finish, calendar) + derived_duration = ifcopenshell.util.sequence.count_working_days( + derived_start, derived_finish, self.file.by_id(calendar["id"]) + ) item.derived_duration = f"P{derived_duration}D" item.start = "-" item.finish = "-" @@ -460,7 +466,6 @@ class EditTaskTime(bpy.types.Operator): def execute(self, context): props = context.scene.BIMWorkScheduleProperties attributes = blenderbim.bim.helper.export_attributes(props.task_time_attributes, self.export_attributes) - attributes = self.convert_strings_to_date_times(attributes) self.file = IfcStore.get_file() ifcopenshell.api.run( @@ -475,10 +480,10 @@ class EditTaskTime(bpy.types.Operator): def export_attributes(self, attributes, prop): if "Start" in prop.name or "Finish" in prop.name or prop.name == "StatusTime": - attributes[prop.name] = helper.parse_datetime(value) + attributes[prop.name] = helper.parse_datetime(prop.string_value) return True elif prop.name == "ScheduleDuration": - attributes[prop.name] = helper.parse_duration(value) + attributes[prop.name] = helper.parse_duration(prop.string_value) return True diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py index 1bede081db..daba1e2c22 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -1,4 +1,6 @@ +import datetime import ifcopenshell.util.date +import ifcopenshell.util.sequence class Usecase: @@ -9,10 +11,43 @@ class Usecase: self.settings[key] = value def execute(self): + # If the user specifies both an end date and a duration, the duration takes priority + if "ScheduleDuration" in self.settings["attributes"].keys(): + del self.settings["attributes"]["ScheduleFinish"] + if "ActualDuration" in self.settings["attributes"].keys(): + del self.settings["attributes"]["ActualFinish"] + for name, value in self.settings["attributes"].items(): if value: if "Start" in name or "Finish" in name or name == "StatusTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") - elif name == "ScheduleDuration": + elif name == "ScheduleDuration" or name == "ActualDuration" or name == "RemainingTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") setattr(self.settings["task_time"], name, value) + + if ( + "ScheduleDuration" in self.settings["attributes"].keys() + and self.settings["task_time"].ScheduleDuration + and self.settings["task_time"].ScheduleStart + ): + start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart) + current_date = datetime.date(start.year, start.month, start.day) + duration = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration).days + + task = [e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask")] + if not task: + return + else: + task = task[0] + + calendar = ifcopenshell.util.sequence.derive_calendar(task) + + while duration >= 0: + if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not calendar: + duration -= 1 + elif ifcopenshell.util.sequence.is_working_day(current_date, calendar): + duration -= 1 + current_date += datetime.timedelta(days=1) + + current_date -= datetime.timedelta(days=1) + self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(current_date, "IfcDateTime") diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py new file mode 100644 index 0000000000..91d96f1751 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -0,0 +1,96 @@ +import datetime +import ifcopenshell.util.date + + +def derive_calendar(task): + calendar = [ + rel.RelatingControl + for rel in task.HasAssignments or [] + if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkCalendar") + ] + if calendar: + return calendar[0] + for rel in task.Nests or []: + return derive_calendar(rel.RelatingObject) + + +def count_working_days(start, finish, calendar): + result = 0 + current_date = datetime.date(start.year, start.month, start.day) + finish_date = datetime.date(finish.year, finish.month, finish.day) + while current_date < finish_date: + if is_working_day(current_date, calendar): + result += 1 + current_date += datetime.timedelta(days=1) + return result + + +def is_working_day(day, calendar): + is_working_day = False + for work_time in calendar.WorkingTimes or []: + if is_work_time_applicable_to_day(work_time, day): + is_working_day = True + break + if not is_working_day: + return is_working_day + for work_time in calendar.ExceptionTimes or []: + if is_work_time_applicable_to_day(work_time, day): + is_working_day = False + break + print('is day working day ', day, is_working_day) + return is_working_day + + +def is_work_time_applicable_to_day(work_time, day): + start = None + finish = None + + if work_time.Start: + start = ifcopenshell.util.date.ifc2datetime(work_time.Start) + if start > day: + return False + + if work_time.Finish: + finish = ifcopenshell.util.date.ifc2datetime(work_time.Finish) + if finish < day: + return False + + if not work_time.RecurrencePattern: + return True + + recurrence = work_time.RecurrencePattern + + if recurrence.RecurrenceType == "DAILY": + if not recurrence.Interval and not recurrence.Occurrences: + return True + if not work_time.Start: + return False + return False # TODO + elif recurrence.RecurrenceType == "WEEKLY": + if not recurrence.Interval and not recurrence.Occurrences: + return (day.weekday() + 1) in recurrence.WeekdayComponent + if not work_time.Start: + return False + return False # TODO + elif recurrence.RecurrenceType == "MONTHLY_BY_DAY_OF_MONTH": + if not recurrence.Interval and not recurrence.Occurrences: + return day.day in recurrence.DayComponent + return False # TODO + elif recurrence.RecurrenceType == "MONTHLY_BY_POSITION": + if not recurrence.Interval and not recurrence.Occurrences: + return (day.weekday() + 1) in recurrence.WeekdayComponent and math.floor(day.day / 7) + 1 == recurrence[ + "Position" + ] + return False # TODO + elif recurrence.RecurrenceType == "YEARLY_BY_DAY_OF_MONTH": + if not recurrence.Interval and not recurrence.Occurrences: + return day.month in recurrence.MonthComponent and day.day in recurrence.DayComponent + return False # TODO + elif recurrence.RecurrenceType == "YEARLY_BY_POSITION": + if not recurrence.Interval and not recurrence.Occurrences: + return ( + day.month in recurrence.MonthComponent + and (day.weekday() + 1) in recurrence.WeekdayComponent + and math.floor(day.day / 7) + 1 == recurrence.Position + ) + return False # TODO diff --git a/src/ifcp6/ifcp6/p62ifc.py b/src/ifcp6/ifcp6/p62ifc.py index 9859d27399..be3052bceb 100644 --- a/src/ifcp6/ifcp6/p62ifc.py +++ b/src/ifcp6/ifcp6/p62ifc.py @@ -166,7 +166,7 @@ class P62Ifc: def process_working_week(self, week, calendar): for day in week: - if day["ifc"]: + if day["ifc"] or not day["WorkTimes"]: continue day["ifc"] = ifcopenshell.api.run( @@ -346,7 +346,7 @@ class P62Ifc: "ScheduleFinish": activity["FinishDate"], "DurationType": "WORKTIME" if activity["PlannedDuration"] else None, "ScheduleDuration": datetime.timedelta( - days=math.ceil(float(activity["PlannedDuration"]) / float(calendar["HoursPerDay"])) + days=float(activity["PlannedDuration"]) / float(calendar["HoursPerDay"]) ) or None if activity["PlannedDuration"] @@ -390,7 +390,7 @@ class P62Ifc: "sequence.assign_lag_time", self.file, rel_sequence=rel_sequence, - lag_value=datetime.timedelta(days=math.ceil(lag / float(calendar["HoursPerDay"]))), + lag_value=datetime.timedelta(days=lag / float(calendar["HoursPerDay"])), duration_type="WORKTIME", )