From 28c4e5530f062cc7c618c67810220e5126330d5e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Fri, 23 Mar 2012 15:21:46 +1000 Subject: [PATCH 1/6] faces: Fix 24-hour and end-at-midnight timespans In cases where a working day either: - Spans 24 hours or - ends at midnight the time span calculation code incorrectly calculates the time of that day, either stating that the day is 0 minutes long, or worse, is negative. The following patch addresses this. Non-working days are specified using the Python 'None', or 'False' values, rather than specifying the same start and end time, as the latter will now be interpreted as a 24-hour period. bzr revid: me@vk4msl.yi.org-20120323052146-vosvz50lg12n7e1i --- addons/resource/faces/pcalendar.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/resource/faces/pcalendar.py b/addons/resource/faces/pcalendar.py index c82c49d4816..4b023911b1e 100644 --- a/addons/resource/faces/pcalendar.py +++ b/addons/resource/faces/pcalendar.py @@ -895,7 +895,16 @@ class Calendar(object): def _recalc_working_time(self): def slot_sum_time(day): slots = self.working_times.get(day, DEFAULT_WORKING_DAYS[day]) - return sum(map(lambda slot: slot[1] - slot[0], slots)) + def time_diff(times): + (start, end) = times + if end == start: + return 24*60 # 24 hours + + diff = end - start + if end < start: + diff += (24*60) + return diff + return sum(map(time_diff, slots)) self.day_times = map(slot_sum_time, range(0, 7)) self.week_time = sum(self.day_times) From 5c4dba40f74f09d5ff0856fb9904f61ff792656e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Fri, 23 Mar 2012 15:22:50 +1000 Subject: [PATCH 2/6] resource: Convert working hours to UTC Faces assumes that all times are given in the same timezone. OpenERP uses UTC internally, and uses UTC when communicating task and project start/end times to faces. It unfortunately gives the working hours in local time. The following converts the time zone to UTC, assuming that the working hours are in the time zone of the currently logged-in user. (This will have to be fixed to convert each resources' calendar from the resource's local time zone to UTC.) bzr revid: me@vk4msl.yi.org-20120323052250-m03bth2dy755hw1p --- addons/resource/resource.py | 145 ++++++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 46 deletions(-) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index fb3d8c0980e..b3bfcea9664 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -19,7 +19,7 @@ # ############################################################################## -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time, date import math from faces import * from osv import fields, osv @@ -28,6 +28,7 @@ from tools.translate import _ from itertools import groupby from operator import itemgetter +import pytz class resource_calendar(osv.osv): _name = "resource.calendar" @@ -279,8 +280,9 @@ def convert_timeformat(time_string): hour_part = split_list[0] mins_part = split_list[1] round_mins = int(round(float(mins_part) * 60,-2)) - converted_string = hour_part + ':' + str(round_mins)[0:2] - return converted_string + return time(int(hour_part), round_mins) + #converted_string = hour_part + ':' + str(round_mins)[0:2] + #return converted_string class resource_resource(osv.osv): _name = "resource.resource" @@ -366,51 +368,102 @@ class resource_resource(osv.osv): Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format. @param calendar_id : working calendar of the project """ + tz = pytz.utc + if context and ('tz' in context): + tz = pytz.timezone(context['tz']) + + wktime_local = None + week_days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] if not calendar_id: - # Calendar is not specified: working days: 24/7 - return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'), - ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')] - resource_attendance_pool = self.pool.get('resource.calendar.attendance') - time_range = "8:00-8:00" - non_working = "" - week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"} - wk_days = {} - wk_time = {} - wktime_list = [] + # Calendar is not specified: working days: some sane default + wktime_local = [ + (0, time(8,0), time(12,0)), + (0, time(13,0), time(17,0)), + (1, time(8,0), time(12,0)), + (1, time(13,0), time(17,0)), + (2, time(8,0), time(12,0)), + (2, time(13,0), time(17,0)), + (3, time(8,0), time(12,0)), + (3, time(13,0), time(17,0)), + (4, time(8,0), time(12,0)), + (4, time(13,0), time(17,0)), + ] + #return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'), + # ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')] + else: + resource_attendance_pool = self.pool.get('resource.calendar.attendance') + non_working = "" + wktime_local = [] + + week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context) + weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context) + # Convert time formats into appropriate format required + # and create lists inside wktime_local. + for week in weeks: + res_str = "" + day = None + if week['dayofweek']: + day = int(week['dayofweek']) + else: + raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!')) + hour_from = convert_timeformat(week['hour_from']) + hour_to = convert_timeformat(week['hour_to']) + wktime_local.append((day, hour_from, hour_to)) + + # We now have working hours _in local time_. Non-working days are an + # empty list, while working days are a list of tuples, consisting of a + # start time and an end time. We will convert these to UTC for time + # calculation purposes. + + # We need to get this into a dict + # which will be in the following format: + # { 'day': [(time(9,0), time(17,0)), ...], ... } + wktime_utc = {} + + # NOTE: This may break with regards to DST! + for (day, start, end) in wktime_local: + # Convert start time to UTC + start_dt_local = datetime.combine(date.today(), start.replace(tzinfo=tz)) + start_dt_utc = start_dt_local.astimezone(pytz.utc) + start_dt_day = (day + (start_dt_utc.date() - start_dt_local.date()).days) % 7 + + # Convert end time to UTC + end_dt_local = datetime.combine(date.today(), end.replace(tzinfo=tz)) + end_dt_utc = end_dt_local.astimezone(pytz.utc) + end_dt_day = (day + (end_dt_utc.date() - end_dt_local.date()).days) % 7 + + # Are start and end still on the same day? + if start_dt_day == end_dt_day: + day_name = week_days[start_dt_day] + if day_name not in wktime_utc: + wktime_utc[day_name] = [] + wktime_utc[day_name].append((start_dt_utc.time(), end_dt_utc.time())) + else: + day_start_name = week_days[start_dt_day] + if day_start_name not in wktime_utc: + wktime_utc[day_start_name] = [] + # We go until midnight that day + wktime_utc[day_start_name].append((start_dt_utc.time(), time(0,0))) + + day_end_name = week_days[end_dt_day] + if day_end_name not in wktime_utc: + wktime_utc[day_end_name] = [] + # Then resume from midnight that day + wktime_utc[day_end_name].append((time(0,0), end_dt_utc.time())) + + # Now having gotten a list of times together, generate the final output wktime_cal = [] - week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context) - weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context) - # Convert time formats into appropriate format required - # and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')] - for week in weeks: - res_str = "" - day = None - if week_days.get(week['dayofweek'],False): - day = week_days[week['dayofweek']] - wk_days[week['dayofweek']] = week_days[week['dayofweek']] - else: - raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!')) - hour_from_str = convert_timeformat(week['hour_from']) - hour_to_str = convert_timeformat(week['hour_to']) - res_str = hour_from_str + '-' + hour_to_str - wktime_list.append((day, res_str)) - # Convert into format like [('mon', '8:00-12:00', '13:00-18:00')] - for item in wktime_list: - if wk_time.has_key(item[0]): - wk_time[item[0]].append(item[1]) - else: - wk_time[item[0]] = [item[0]] - wk_time[item[0]].append(item[1]) - for k,v in wk_time.items(): - wktime_cal.append(tuple(v)) - # Add for the non-working days like: [('sat, sun', '8:00-8:00')] - for k, v in wk_days.items(): - if week_days.has_key(k): - week_days.pop(k) - for v in week_days.itervalues(): - non_working += v + ',' - if non_working: - wktime_cal.append((non_working[:-1], time_range)) + for day, times in wktime_utc.iteritems(): + # Sort the times + times.sort() + wktime = ['{0}-{1}'.format(s.strftime('%H:%M'), e.strftime('%H:%M')) for (s, e) in times] + wktime.insert(0, day) + wktime_cal.append(tuple(wktime)) + # Finally, add in non-working days + for day in week_days: + if day not in wktime_utc: + wktime_cal.append((day, None)) + return wktime_cal resource_resource() From 5f933e0188f2bc69f4e2b394844667d187cda26f Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 7 Dec 2012 11:23:21 +0100 Subject: [PATCH 3/6] [IMP] resource: small improvement of Stuart's time_diff function bzr revid: rco@openerp.com-20121207102321-3e4rp9562on0zspg --- addons/resource/faces/pcalendar.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/addons/resource/faces/pcalendar.py b/addons/resource/faces/pcalendar.py index 4b023911b1e..de7bf251c1b 100644 --- a/addons/resource/faces/pcalendar.py +++ b/addons/resource/faces/pcalendar.py @@ -893,17 +893,13 @@ class Calendar(object): #@-node:_build_mapping #@+node:_recalc_working_time def _recalc_working_time(self): + def time_diff(times): + diff = times[1] - times[0] + if diff <= 0: # happens when times span across midnight + diff += 24*60 + return diff def slot_sum_time(day): slots = self.working_times.get(day, DEFAULT_WORKING_DAYS[day]) - def time_diff(times): - (start, end) = times - if end == start: - return 24*60 # 24 hours - - diff = end - start - if end < start: - diff += (24*60) - return diff return sum(map(time_diff, slots)) self.day_times = map(slot_sum_time, range(0, 7)) From ecd2cd1e05657cd2927821a82308cd1f32370ba4 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 7 Dec 2012 11:27:18 +0100 Subject: [PATCH 4/6] [IMP] resource: improve conversion function hours (float) -> datetime.time bzr revid: rco@openerp.com-20121207102718-jxojya78wnb8jmgc --- addons/resource/resource.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index b3bfcea9664..25d5af09d90 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -275,14 +275,10 @@ class resource_calendar_attendance(osv.osv): } resource_calendar_attendance() -def convert_timeformat(time_string): - split_list = str(time_string).split('.') - hour_part = split_list[0] - mins_part = split_list[1] - round_mins = int(round(float(mins_part) * 60,-2)) - return time(int(hour_part), round_mins) - #converted_string = hour_part + ':' + str(round_mins)[0:2] - #return converted_string +def float_time(hours): + """ convert a number of hours (float) into a datetime.time """ + minutes = int(round(hours * 60)) + return time(minutes / 60, minutes % 60) class resource_resource(osv.osv): _name = "resource.resource" @@ -406,8 +402,8 @@ class resource_resource(osv.osv): day = int(week['dayofweek']) else: raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!')) - hour_from = convert_timeformat(week['hour_from']) - hour_to = convert_timeformat(week['hour_to']) + hour_from = float_time(week['hour_from']) + hour_to = float_time(week['hour_to']) wktime_local.append((day, hour_from, hour_to)) # We now have working hours _in local time_. Non-working days are an From 6ed367c09386236941a46c913ff8d1ff1eb1a564 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 7 Dec 2012 13:19:25 +0100 Subject: [PATCH 5/6] [IMP] resource: small code improvements bzr revid: rco@openerp.com-20121207121925-ju6mdv66k4zsw5yk --- addons/resource/resource.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index 25d5af09d90..1effc9ced2b 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -373,35 +373,27 @@ class resource_resource(osv.osv): if not calendar_id: # Calendar is not specified: working days: some sane default wktime_local = [ - (0, time(8,0), time(12,0)), - (0, time(13,0), time(17,0)), - (1, time(8,0), time(12,0)), - (1, time(13,0), time(17,0)), - (2, time(8,0), time(12,0)), - (2, time(13,0), time(17,0)), - (3, time(8,0), time(12,0)), - (3, time(13,0), time(17,0)), - (4, time(8,0), time(12,0)), - (4, time(13,0), time(17,0)), + (0, time( 8, 0), time(12, 0)), + (0, time(13, 0), time(17, 0)), + (1, time( 8, 0), time(12, 0)), + (1, time(13, 0), time(17, 0)), + (2, time( 8, 0), time(12, 0)), + (2, time(13, 0), time(17, 0)), + (3, time( 8, 0), time(12, 0)), + (3, time(13, 0), time(17, 0)), + (4, time( 8, 0), time(12, 0)), + (4, time(13, 0), time(17, 0)), ] - #return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'), - # ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')] else: resource_attendance_pool = self.pool.get('resource.calendar.attendance') - non_working = "" - wktime_local = [] - week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context) weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context) # Convert time formats into appropriate format required # and create lists inside wktime_local. + wktime_local = [] for week in weeks: - res_str = "" - day = None - if week['dayofweek']: - day = int(week['dayofweek']) - else: - raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!')) + day = int(week['dayofweek']) + assert day in xrange(7) hour_from = float_time(week['hour_from']) hour_to = float_time(week['hour_to']) wktime_local.append((day, hour_from, hour_to)) From aff07dfa3b1af822536a88f5feb5be6046f29696 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Fri, 7 Dec 2012 13:23:51 +0100 Subject: [PATCH 6/6] [IMP] resource: improve code that computes working hours in local times bzr revid: rco@openerp.com-20121207122351-eitc7zej6ss2035j --- addons/resource/resource.py | 54 +++++++++++++++---------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index 1effc9ced2b..29909476511 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -403,54 +403,44 @@ class resource_resource(osv.osv): # start time and an end time. We will convert these to UTC for time # calculation purposes. - # We need to get this into a dict - # which will be in the following format: - # { 'day': [(time(9,0), time(17,0)), ...], ... } - wktime_utc = {} + # We need to get this into a dict which will be in the following format: + # {0: [(time(9, 0), time(17, 0)), ...], ...} + wktime_utc = dict((day, []) for day in range(0, 7)) # NOTE: This may break with regards to DST! for (day, start, end) in wktime_local: # Convert start time to UTC start_dt_local = datetime.combine(date.today(), start.replace(tzinfo=tz)) start_dt_utc = start_dt_local.astimezone(pytz.utc) - start_dt_day = (day + (start_dt_utc.date() - start_dt_local.date()).days) % 7 + start_day = (day + (start_dt_utc.date() - start_dt_local.date()).days) % 7 # Convert end time to UTC end_dt_local = datetime.combine(date.today(), end.replace(tzinfo=tz)) end_dt_utc = end_dt_local.astimezone(pytz.utc) - end_dt_day = (day + (end_dt_utc.date() - end_dt_local.date()).days) % 7 + end_day = (day + (end_dt_utc.date() - end_dt_local.date()).days) % 7 # Are start and end still on the same day? - if start_dt_day == end_dt_day: - day_name = week_days[start_dt_day] - if day_name not in wktime_utc: - wktime_utc[day_name] = [] - wktime_utc[day_name].append((start_dt_utc.time(), end_dt_utc.time())) + if start_day == end_day: + wktime_utc[start_day].append((start_dt_utc.time(), end_dt_utc.time())) else: - day_start_name = week_days[start_dt_day] - if day_start_name not in wktime_utc: - wktime_utc[day_start_name] = [] - # We go until midnight that day - wktime_utc[day_start_name].append((start_dt_utc.time(), time(0,0))) + # We go until midnight on the start day + wktime_utc[start_day].append((start_dt_utc.time(), time(0, 0))) + # Then resume from midnight on the end day + wktime_utc[end_day].append((time(0, 0), end_dt_utc.time())) - day_end_name = week_days[end_dt_day] - if day_end_name not in wktime_utc: - wktime_utc[day_end_name] = [] - # Then resume from midnight that day - wktime_utc[day_end_name].append((time(0,0), end_dt_utc.time())) - - # Now having gotten a list of times together, generate the final output + # Now having gotten a list of times together, generate the final output: + # [('mon', '08:00-12:00', '13:00-17:00', ...), ...] wktime_cal = [] - for day, times in wktime_utc.iteritems(): - # Sort the times - times.sort() - wktime = ['{0}-{1}'.format(s.strftime('%H:%M'), e.strftime('%H:%M')) for (s, e) in times] - wktime.insert(0, day) + for day, day_name in enumerate(week_days): + # retrieve the times and sort them + times = sorted(wktime_utc[day]) + wktime = [day_name] + if times: + for s, e in times: + wktime.append('{0}-{1}'.format(s.strftime('%H:%M'), e.strftime('%H:%M'))) + else: + wktime.append(None) wktime_cal.append(tuple(wktime)) - # Finally, add in non-working days - for day in week_days: - if day not in wktime_utc: - wktime_cal.append((day, None)) return wktime_cal