From af79a26a1f5f85b760f8b1cbab169acaa8bd4e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 30 Aug 2013 15:32:50 +0200 Subject: [PATCH] [IMP] resource: overall fix of hours/days computation + scheduling + added tests bzr revid: tde@openerp.com-20130830133250-ixehp7imac0duygd --- addons/resource/resource.py | 473 ++++++++++++++++--------- addons/resource/test/resource.yml | 2 +- addons/resource/tests/common.py | 10 +- addons/resource/tests/test_resource.py | 346 ++++++++++++++---- 4 files changed, 588 insertions(+), 243 deletions(-) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index dd87157aeb0..c8635a01d0c 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -19,13 +19,14 @@ # ############################################################################## -import pytz -from datetime import date, datetime, timedelta +import datetime +# from datetime import datetime, timedelta from dateutil import rrule from dateutil.relativedelta import relativedelta import itertools import math from operator import itemgetter +import pytz from faces import * from openerp import tools @@ -38,10 +39,10 @@ class resource_calendar(osv.osv): _name = "resource.calendar" _description = "Resource Calendar" _columns = { - 'name' : fields.char("Name", size=64, required=True), - 'company_id' : fields.many2one('res.company', 'Company', required=False), - 'attendance_ids' : fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'), - 'manager' : fields.many2one('res.users', 'Workgroup Manager'), + 'name': fields.char("Name", size=64, required=True), + 'company_id': fields.many2one('res.company', 'Company', required=False), + 'attendance_ids': fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'), + 'manager': fields.many2one('res.users', 'Workgroup Manager'), 'leave_ids': fields.one2many( 'resource.calendar.leaves', 'calendar_id', 'Leaves', help='' @@ -56,27 +57,29 @@ class resource_calendar(osv.osv): # -------------------------------------------------- def interval_clean(self, intervals): - """ Utility method that removes overlapping inside datetime intervals. + """ Utility method that sorts and removes overlapping inside datetime + intervals. The intervals are sorted based on increasing starting datetime. + Overlapping intervals are merged into a single one. - :param list intervals: list of datetime intervals. Each interval is a - tuple(datetime_from, datetime_to) - :return list final_list: list of intervals without overlap + :param list intervals: list of intervals; each interval is a tuple + (datetime_from, datetime_to) + :return list cleaned: list of sorted intervals without overlap """ - intervals = sorted(intervals) # TODO: check sorted method - final_list = [] + intervals = sorted(intervals, key=itemgetter(0)) # sort on first datetime + cleaned = [] working_interval = None while intervals: current_interval = intervals.pop(0) if not working_interval: # init working_interval = [current_interval[0], current_interval[1]] elif working_interval[1] < current_interval[0]: # interval is disjoint - final_list.append(tuple(working_interval)) + cleaned.append(tuple(working_interval)) working_interval = [current_interval[0], current_interval[1]] elif working_interval[1] < current_interval[1]: # union of greater intervals working_interval[1] = current_interval[1] if working_interval: # handle void lists - final_list.append(tuple(working_interval)) - return final_list + cleaned.append(tuple(working_interval)) + return cleaned def interval_remove_leaves(self, interval, leave_intervals): """ Utility method that remove leave intervals from a base interval: @@ -98,87 +101,120 @@ class resource_calendar(osv.osv): :param tuple interval: a tuple (beginning datetime, ending datetime) that is the base interval from which the leave intervals will be removed - :param list interval: a list of tuples (beginning datetime, ending datetime) - that are intervals to remove from the base interval - :return list final_list: a list of tuples (begin datetime, end datetime) - that are the remaining valid intervals + :param list leave_intervals: a list of tuples (beginning datetime, ending datetime) + that are intervals to remove from the base interval + :return list intervals: a list of tuples (begin datetime, end datetime) + that are the remaining valid intervals """ if not interval: return interval if leave_intervals is None: leave_intervals = [] - final_list = [] + intervals = [] leave_intervals = self.interval_clean(leave_intervals) current_interval = [interval[0], interval[1]] - # print '\tcurrent_intreval', current_interval for leave in leave_intervals: - # print '\thandling leave', leave if leave[1] <= current_interval[0]: - # print '\t\tbefore, skipping' continue if leave[0] >= current_interval[1]: - # print '\t\tafter, abort' break if current_interval[0] < leave[0] < current_interval[1]: - # print '\t\tbeginning inside' current_interval[1] = leave[0] - final_list.append((current_interval[0], current_interval[1])) + intervals.append((current_interval[0], current_interval[1])) current_interval = [leave[1], interval[1]] - if current_interval[0] <= leave[1] <= current_interval[1]: - # print '\t\tending inside' + # if current_interval[0] <= leave[1] <= current_interval[1]: + if current_interval[0] <= leave[1]: current_interval[0] = leave[1] if current_interval and current_interval[0] < interval[1]: # remove intervals moved outside base interval due to leaves - final_list.append((current_interval[0], current_interval[1])) - return final_list + intervals.append((current_interval[0], current_interval[1])) + return intervals + + def interval_schedule_hours(self, intervals, hour, remove_at_end=True): + """ Schedule hours in intervals. The last matching interval is truncated + to match the specified hours. + + It is possible to truncate the last interval at its beginning or ending. + However this does nothing on the given interval order that should be + submitted accordingly. + + :param list intervals: a list of tuples (beginning datetime, ending datetime) + :param int/float hours: number of hours to schedule + :param boolean remove_at_end: remove extra hours at the end of the last + matching interval. Otherwise, do it at the + beginning. + + :return list results: a list of intervals + """ + results = [] + res = datetime.timedelta() + limit = datetime.timedelta(hours=hour) + for interval in intervals: + res += interval[1] - interval[0] + if res > limit and remove_at_end: + interval = (interval[0], interval[1] + relativedelta(seconds=(limit-res).total_seconds())) + elif res > limit: + interval = (interval[0] + relativedelta(seconds=(res-limit).total_seconds()), interval[1]) + results.append(interval) + if res > limit: + break + return results # -------------------------------------------------- # Date and hours computation # -------------------------------------------------- - def get_next_day(self, cr, uid, id, day_date, context=None): - if id is None: - return day_date + relativedelta(days=1) + def get_attendances_for_weekdays(self, cr, uid, id, weekdays, context=None): + """ Given a list of weekdays, return matching resource.calendar.attendance""" + calendar = self.browse(cr, uid, id, context=None) + return [att for att in calendar.attendance_ids if int(att.dayofweek) in weekdays] + + def get_weekdays(self, cr, uid, id, context=None): + """ Return the list of weekdays that contain at least one working interval """ calendar = self.browse(cr, uid, id, context=None) weekdays = set() for attendance in calendar.attendance_ids: weekdays.add(int(attendance.dayofweek)) - weekdays = list(weekdays) + return list(weekdays) - if day_date.weekday() in weekdays: - base_index = weekdays.index(day_date.weekday()) - else: - base_index = -1 - for weekday in weekdays: - if weekday > day_date.weekday(): - break - base_index += 1 + def get_next_day(self, cr, uid, id, day_date, context=None): + """ Get following date of day_date, based on resource.calendar. If no + calendar is provided, just return the next day. + + :param date day_date: current day as a date + """ + if id is None: + return day_date + relativedelta(days=1) + weekdays = self.get_weekdays(cr, uid, id, context) + + base_index = -1 + for weekday in weekdays: + if weekday > day_date.weekday(): + break + base_index += 1 new_index = (base_index + 1) % len(weekdays) days = (weekdays[new_index] - day_date.weekday()) - if days < 0: days = 7 + days return day_date + relativedelta(days=days) def get_previous_day(self, cr, uid, id, day_date, context=None): + """ Get previous date of day_date, based on resource.calendar. If no + calendar is provided, just return the previous day. + + :param date day_date: current day as a date + """ if id is None: return day_date + relativedelta(days=-1) - calendar = self.browse(cr, uid, id, context=None) - weekdays = set() - for attendance in calendar.attendance_ids: - weekdays.add(int(attendance.dayofweek)) - weekdays = list(weekdays) + weekdays = self.get_weekdays(cr, uid, id, context) weekdays.reverse() - if day_date.weekday() in weekdays: - base_index = weekdays.index(day_date.weekday()) - else: - base_index = -1 - for weekday in weekdays: - if weekday < day_date.weekday(): - break - base_index += 1 + base_index = -1 + for weekday in weekdays: + if weekday < day_date.weekday(): + break + base_index += 1 new_index = (base_index + 1) % len(weekdays) days = (weekdays[new_index] - day_date.weekday()) @@ -187,35 +223,55 @@ class resource_calendar(osv.osv): return day_date + relativedelta(days=days) - def _get_leave_intervals(self, cr, uid, id, resource_id=None, start_datetime=None, end_datetime=None, context=None): + def get_leave_intervals(self, cr, uid, id, resource_id=None, + start_datetime=None, end_datetime=None, + context=None): """Get the leaves of the calendar. Leaves can be filtered on the resource, the start datetime or the end datetime. - :param int resource_id: if set, global + specific leaves will be taken - into account - TODO: COMPLETE ME + :param int resource_id: the id of the resource to take into account when + computing the leaves. If not set, only general + leaves are computed. If set, generic and + specific leaves are computed. + :param datetime start_datetime: if provided, do not take into account leaves + ending before this date. + :param datetime end_datetime: if provided, do not take into account leaves + beginning after this date. + + :return list leaves: list of tuples (start_datetime, end_datetime) of + leave intervals """ resource_calendar = self.browse(cr, uid, id, context=context) leaves = [] for leave in resource_calendar.leave_ids: - if resource_id and leave.resource_id and not resource_id == leave.resource_id.id: + if leave.resource_id and not resource_id == leave.resource_id.id: continue - date_from = datetime.strptime(leave.date_from, tools.DEFAULT_SERVER_DATETIME_FORMAT) - if start_datetime and date_from < start_datetime: + date_from = datetime.datetime.strptime(leave.date_from, tools.DEFAULT_SERVER_DATETIME_FORMAT) + if end_datetime and date_from > end_datetime: continue - if end_datetime and date_end > end_datetime: + date_to = datetime.datetime.strptime(leave.date_to, tools.DEFAULT_SERVER_DATETIME_FORMAT) + if start_datetime and date_to < start_datetime: continue - date_to = datetime.strptime(leave.date_to, tools.DEFAULT_SERVER_DATETIME_FORMAT) leaves.append((date_from, date_to)) return leaves - def get_working_intervals_of_day(self, cr, uid, id, day_date=None, leaves=None, compute_leaves=False, resource_id=None, context=None): + def get_working_intervals_of_day(self, cr, uid, id, start_dt=None, end_dt=None, + leaves=None, compute_leaves=False, resource_id=None, + context=None): """Get the working intervals of the day based on calendar. This method handle leaves that come directly from the leaves parameter or can be computed. :param int id: resource.calendar id; take the first one if is a list - :param date day_date: date object that is the day for which this method - computes the working intervals; is None, set to today + :param datetime start_dt: datetime object that is the beginning hours + for the working intervals computation; any + working interval beginning before start_dt + will be truncated. If not set, set to end_dt + or today() if no end_dt at 00.00.00. + :param datetime end_dt: datetime object that is the ending hour + for the working intervals computation; any + working interval ending after end_dt + will be truncated. If not set, set to start_dt() + at 23.59.59. :param list leaves: a list of tuples(start_datetime, end_datetime) that represent leaves. :param boolean compute_leaves: if set and if leaves is None, compute the @@ -224,40 +280,45 @@ class resource_calendar(osv.osv): no leaves are taken into account. :param int resource_id: the id of the resource to take into account when computing the leaves. If not set, only general - leaves will be computed. + leaves are computed. If set, generic and + specific leaves are computed. - :returns list intervals: a list of tuples (start_datetime, end_datetime) - that are intervals of work + :return list intervals: a list of tuples (start_datetime, end_datetime) + of work intervals """ if id is None: return [] if isinstance(id, (list, tuple)): id = id[0] - if day_date is None: - day_date = date.today() - resource_calendar = self.browse(cr, uid, id, context=context) - intervals = [] - # find working intervals - date_dict = { - 'Y': day_date.year, - 'm': day_date.month, - 'd': day_date.day, - } + # Computes start_dt, end_dt (with default values if not set) + off-interval work limits + work_limits = [] + if start_dt is None and end_dt is not None: + start_dt = end_dt.replace(hour=0, minute=0, second=0) + elif start_dt is None: + start_dt = datetime.datetime.now().replace(hour=0, minute=0, second=0) + else: + work_limits.append((start_dt.replace(hour=0, minute=0, second=0), start_dt)) + if end_dt is None: + end_dt = start_dt.replace(hour=23, minute=59, second=59) + else: + work_limits.append((end_dt, end_dt.replace(hour=23, minute=59, second=59))) + assert start_dt.date() == end_dt.date(), 'get_working_intervals_of_day is restricted to one day' + + intervals = [] + work_dt = start_dt.replace(hour=0, minute=0, second=0) + working_intervals = [] - for calendar_working_day in resource_calendar.attendance_ids: - if int(calendar_working_day.dayofweek) == day_date.weekday(): - date_dict.update({ - 'HF': calendar_working_day.hour_from, - 'HT': calendar_working_day.hour_to, - }) - date_from = datetime.strptime('%(Y)04d-%(m)02d-%(d)02d %(HF)02d:00:00' % date_dict, '%Y-%m-%d %H:%M:%S') - date_to = datetime.strptime('%(Y)04d-%(m)02d-%(d)02d %(HT)02d:00:00' % date_dict, '%Y-%m-%d %H:%M:%S') - working_intervals.append((date_from, date_to)) + for calendar_working_day in self.get_attendances_for_weekdays(cr, uid, id, [start_dt.weekday()], context): + working_interval = ( + work_dt.replace(hour=int(calendar_working_day.hour_from)), + work_dt.replace(hour=int(calendar_working_day.hour_to)) + ) + working_intervals += self.interval_remove_leaves(working_interval, work_limits) # find leave intervals if leaves is None and compute_leaves: - leaves = self._get_leave_intervals(cr, uid, id, resource_id=resource_id, context=None) + leaves = self.get_leave_intervals(cr, uid, id, resource_id=resource_id, context=None) # filter according to leaves for interval in working_intervals: @@ -266,70 +327,135 @@ class resource_calendar(osv.osv): return intervals - def get_working_hours_of_date(self, cr, uid, id, day_date=None, leaves=None, compute_leaves=False, resource_id=None, context=None): + def get_working_hours_of_date(self, cr, uid, id, start_dt=None, end_dt=None, + leaves=None, compute_leaves=False, resource_id=None, + context=None): """Get the working hours of the day based on calendar. This method uses get_working_intervals_of_day to have the work intervals of the day. It then calculates the number of hours contained in those intervals. """ - res = timedelta() - intervals = self.get_working_intervals_of_day(cr, uid, id, day_date, leaves, compute_leaves, resource_id, context) + res = datetime.timedelta() + intervals = self.get_working_intervals_of_day(cr, uid, id, start_dt, end_dt, leaves, compute_leaves, resource_id, context) for interval in intervals: res += interval[1] - interval[0] return (res.total_seconds() / 3600.0) - def schedule_hours(self, cr, uid, id, hours, start_datetime=None, end_datetime=None, compute_leaves=False, resource_id=None, context=None): + def _schedule_hours(self, cr, uid, id, hours, day_dt=None, + compute_leaves=False, resource_id=None, + context=None): + """Schedule hours of work, using a calendar and an optional resource to + compute working and leave days. This method can be used backwards, i.e. + scheduling days before a deadline. + + :param timedelta hours: number of hours to schedule + :param datetime day_dt: reference date to compute working days. If days is + > 0 date is the starting date. If days is < 0 + date is the ending date. + :param boolean compute_leaves: if set, compute the leaves based on calendar + and resource. Otherwise no leaves are taken + into account. + :param int resource_id: the id of the resource to take into account when + computing the leaves. If not set, only general + leaves are computed. If set, generic and + specific leaves are computed. + + :return tuple (datetime, intervals): datetime is the beginning/ending date + of the schedulign; intervals are the + working intervals of the scheduling. + + Note: Why not using rrule.rrule ? Because rrule does not seem to allow + getting back in time. """ - """ - if start_datetime is None: - start_datetime = datetime.now() - work_hours = 0 + if day_dt is None: + day_dt = datetime.datetime.now() + backwards = (hours < 0) + hours = abs(hours) + intervals = [] + remaining_hours = hours * 1.0 iterations = 0 - final_intervals = [] + current_datetime = day_dt - # compute work days - work_days = set() - resource_calendar = self.browse(cr, uid, id, context=context) - for attendance in resource_calendar.attendance_ids: - work_days.add(int(attendance.dayofweek)) + call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, context=context) - # prepare rrule arguments - rrule_args = { - 'byweekday': work_days, - 'dtstart': start_datetime, - } - if end_date: - rrule_args['until'] = end_datetime - else: - rrule_args['count'] = 1024 + while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000: + if backwards: + call_args['end_dt'] = current_datetime + else: + call_args['start_dt'] = current_datetime - for day in rrule.rrule(rrule.DAILY, **rrule_args): - working_intervals = self.get_working_intervals_of_day(cr, uid, id, day_date=day, compute_leaves=compute_leaves, resource_id=resource_id, context=context) - if not working_intervals: - continue - # Compute worked hours, compare to requested number of hours - res = timedelta() - for interval in working_intervals: - res += interval[1] - interval[0] - work_hours += (res.total_seconds() / 3600.0) - final_intervals += working_intervals - if float_compare(work_hours, hours * 1.0, precision_digits=2) in (0, 1) or (iterations >= 50): - break + working_intervals = self.get_working_intervals_of_day(cr, uid, id, **call_args) + + if id is None: # no calendar -> consider 8 working hours + remaining_hours -= 8.0 + elif working_intervals: + if backwards: + working_intervals.reverse() + new_working_intervals = self.interval_schedule_hours(working_intervals, remaining_hours, not backwards) + if backwards: + new_working_intervals.reverse() + + res = datetime.timedelta() + for interval in working_intervals: + res += interval[1] - interval[0] + remaining_hours -= (res.total_seconds() / 3600.0) + if backwards: + intervals = new_working_intervals + intervals + else: + intervals = intervals + new_working_intervals + # get next day + if backwards: + current_datetime = datetime.datetime.combine(self.get_previous_day(cr, uid, id, current_datetime, context), datetime.time(23, 59, 59)) + else: + current_datetime = datetime.datetime.combine(self.get_next_day(cr, uid, id, current_datetime, context), datetime.time()) + # avoid infinite loops iterations += 1 - return final_intervals + return intervals - def _schedule_days(self, cr, uid, id, days, date=None, compute_leaves=False, resource_id=None, context=None): - """Schedule days of work. + def schedule_hours_get_date(self, cr, uid, id, hours, day_dt=None, compute_leaves=False, resource_id=None, context=None): + """Wrapper on _schedule_hours: return the beginning/ending datetime of + an hours scheduling. """ + res = self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, context) + return res[0][0] - This method can be used backwards, i.e. scheduling days before a deadline. + def schedule_hours(self, cr, uid, id, hours, day_dt=None, compute_leaves=False, resource_id=None, context=None): + """Wrapper on _schedule_hours: return the working intervals of an hours + scheduling. """ + return self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, context) + + def _schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False, resource_id=None, context=None): + """Schedule days of work, using a calendar and an optional resource to + compute working and leave days. This method can be used backwards, i.e. + scheduling days before a deadline. + + :param date day_date: reference date to compute working days. If days is > 0 + date is the starting date. If days is < 0 date is the + ending date. + :param boolean compute_leaves: if set, compute the leaves based on calendar + and resource. Otherwise no leaves are taken + into account. + :param int resource_id: the id of the resource to take into account when + computing the leaves. If not set, only general + leaves are computed. If set, generic and + specific leaves are computed. + + :return tuple (datetime, intervals): datetime is the beginning/ending date + of the schedulign; intervals are the + working intervals of the scheduling. + + TDE NOTE: Why not using rrule.rrule ? Because rrule does not seem to + allow getting back in time. """ - backwards = False - if days < 0: - backwards = True - days = abs(days) + if day_date is None: + day_date = datetime.datetime.now() + backwards = (days < 0) + days = abs(days) intervals = [] planned_days = 0 iterations = 0 - current_datetime = date + if backwards: + current_datetime = day_date.replace(hour=23, minute=59, second=59) + else: + current_datetime = day_date.replace(hour=0, minute=0, second=0) while planned_days < days and iterations < 1000: working_intervals = self.get_working_intervals_of_day(cr, uid, id, current_datetime, compute_leaves=compute_leaves, resource_id=resource_id, context=context) @@ -338,25 +464,38 @@ class resource_calendar(osv.osv): intervals += working_intervals # get next day if backwards: - current_datetime = self.get_previous_day(cr, uid, id, current_datetime) + current_datetime = self.get_previous_day(cr, uid, id, current_datetime, context) else: - current_datetime = self.get_next_day(cr, uid, id, current_datetime) + current_datetime = self.get_next_day(cr, uid, id, current_datetime, context) + # avoid infinite loops + iterations += 1 - return (current_datetime, intervals) + return intervals - def schedule_days(self, cr, uid, id, days, date=None, compute_leaves=False, resource_id=None, context=None): - res = self._schedule_days(cr, uid, id, days, date, compute_leaves, resource_id, context) - return res[0] + def schedule_days_get_date(self, cr, uid, id, days, day_date=None, compute_leaves=False, resource_id=None, context=None): + """Wrapper on _schedule_days: return the beginning/ending datetime of + a days scheduling. """ + res = self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, context) + return res[-1][1].date() + + def schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False, resource_id=None, context=None): + """Wrapper on _schedule_days: return the working intervals of a days + scheduling. """ + return self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, context) # -------------------------------------------------- # Compaqtibility / to clean / to remove # -------------------------------------------------- def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None): - """ Compatibility method - will be removed for OpenERP v8 + """ Compatibility method - will be removed for OpenERP v8. Computation + was done for the whole day, therefore setting start_dt at the beginning + of the day. TDE TODO: hr_payroll/hr_payroll.py """ - return self.get_working_hours_of_date(cr, uid, resource_calendar_id.id, day_date=day, context=None) + if isinstance(day, datetime.datetime): + day = day.replace(hour=0, minute=0) + return self.get_working_hours_of_date(cr, uid, resource_calendar_id.id, start_dt=day, context=None) def _get_leaves(self, cr, uid, id, resource): """Private Method to Calculate resource Leaves days @@ -366,6 +505,7 @@ class resource_calendar(osv.osv): @return : returns the list of dates, where resource on leave in resource.calendar.leaves object (e.g.['%Y-%m-%d', '%Y-%m-%d']) + TDE TODO: internal only """ resource_cal_leaves = self.pool.get('resource.calendar.leaves') dt_leave = [] @@ -374,10 +514,10 @@ class resource_calendar(osv.osv): res_leaves = resource_cal_leaves.browse(cr, uid, resource_leave_ids) for leave in res_leaves: - dtf = datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S') - dtt = datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S') + dtf = datetime.datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S') + dtt = datetime.datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S') no = dtt - dtf - [dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))] + [dt_leave.append((dtf + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))] dt_leave.sort() return dt_leave @@ -399,10 +539,11 @@ class resource_calendar(osv.osv): params TDE TODO: used in mrp_operations/mrp_operations.py + TDE NOTE: do not count leave hours, a leave is considered all-day """ if not id: td = int(hours)*3 - return [(dt_from - timedelta(hours=td), dt_from)] + return [(dt_from - datetime.timedelta(hours=td), dt_from)] dt_leave = self._get_leaves(cr, uid, id, resource) dt_leave.reverse() todo = hours @@ -420,17 +561,17 @@ class resource_calendar(osv.osv): dt_check = dt_from.strftime('%Y-%m-%d') for leave in dt_leave: if dt_check == leave: - dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1) + dt_check = datetime.datetime.strptime(dt_check, '%Y-%m-%d') + datetime.timedelta(days=1) leave_flag = True if leave_flag: break else: - d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60)) - d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60)) + d1 = datetime.datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60)) + d2 = datetime.datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60)) result.append((d1, d2)) current_hour = hour_from todo -= (m-hour_from) - dt_from -= timedelta(days=1) + dt_from -= datetime.timedelta(days=1) current_hour = 24 maxrecur -= 1 result.reverse() @@ -438,7 +579,10 @@ class resource_calendar(osv.osv): # def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True): def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True): - """ TDE NOTE: used in mrp_operations/mrp_operations.py and in interval_get() """ + """ TDE NOTE: used in mrp_operations/mrp_operations.py (default parameters) and in interval_get() + TDE NOTE: byday is not used in this method... + TDE NOTE: do not count leave hours, a leave is considered all-day + """ def group(lst, key): lst.sort(key=itemgetter(key)) grouped = itertools.groupby(lst, itemgetter(key)) @@ -452,10 +596,10 @@ class resource_calendar(osv.osv): results = {} for d, hours, id in date_and_hours_by_cal: - dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S') + dt_from = datetime.datetime.strptime(d, '%Y-%m-%d %H:%M:%S') if not id: td = int(hours)*3 - results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))] + results[(d, hours, id)] = [(dt_from, dt_from + datetime.timedelta(hours=td))] continue dt_leave = self._get_leaves(cr, uid, id, resource) @@ -473,17 +617,17 @@ class resource_calendar(osv.osv): dt_check = dt_from.strftime('%Y-%m-%d') for leave in dt_leave: if dt_check == leave: - dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1) + dt_check = datetime.datetime.strptime(dt_check, '%Y-%m-%d') + datetime.timedelta(days=1) leave_flag = True if leave_flag: break else: - d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60)) - d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60)) + d1 = datetime.datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60)) + d2 = datetime.datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60)) result.append((d1, d2)) current_hour = hour_to todo -= (hour_to - m) - dt_from += timedelta(days=1) + dt_from += datetime.timedelta(days=1) current_hour = 0 maxrecur -= 1 results[(d, hours, id)] = result @@ -500,7 +644,7 @@ class resource_calendar(osv.osv): @return : list of scheduled working timing based on resource calendar. - TDE NOTE: mrp_operations/mrp_operations.py, crm/crm_lead.py + TDE NOTE: mrp_operations/mrp_operations.py, crm/crm_lead.py (res given) """ res = self.interval_get_multi(cr, uid, [(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)], resource, byday)[(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)] return res @@ -538,6 +682,7 @@ class resource_calendar(osv.osv): resource if supplied. TDE NOTE: used in project_issue/project_issue.py + TDE NOTE: day-long leaves """ utc_tz = pytz.timezone('UTC') local_tz = utc_tz @@ -591,7 +736,7 @@ class resource_calendar(osv.osv): interval_start = utc_to_local_zone(dt_from) interval_end = utc_to_local_zone(dt_to) - hours_timedelta = timedelta() + hours_timedelta = datetime.timedelta() # Get leaves for requested resource dt_leaves = set([]) @@ -599,7 +744,7 @@ class resource_calendar(osv.osv): dt_leaves = set(self._get_leaves(cr, uid, id, resource=resource_id)) for day in rrule.rrule(rrule.DAILY, dtstart=interval_start, - until=interval_end+timedelta(days=1), + until=interval_end+datetime.timedelta(days=1), byweekday=hours_range_per_weekday.keys()): if exclude_leaves and day.strftime('%Y-%m-%d') in dt_leaves: # XXX: futher improve leave management to allow for partial day leave @@ -724,10 +869,10 @@ class resource_resource(osv.osv): ], context=context) leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context) for i in range(len(leaves)): - dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S') - dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S') + dt_start = datetime.datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S') + dt_end = datetime.datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S') no = dt_end - dt_start - [leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))] + [leave_list.append((dt_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))] leave_list.sort() return leave_list diff --git a/addons/resource/test/resource.yml b/addons/resource/test/resource.yml index 28604518b7b..b085c0734c4 100644 --- a/addons/resource/test/resource.yml +++ b/addons/resource/test/resource.yml @@ -26,7 +26,7 @@ dt = now - timedelta(days=now.weekday()) for resource in resources: result = calendar_pool.working_hours_on_day(cr, uid, resource.calendar_id, dt, context) - assert result == 9.0, 'Wrong calculation of day work hour availability of the Resource.' + assert result == 9.0, 'Wrong calculation of day work hour availability of the Resource (found %d).' % result - Now, resource "Developer" drafted leave on Thursday in this week. - diff --git a/addons/resource/tests/common.py b/addons/resource/tests/common.py index 6dffafd8179..e5d17bc7455 100644 --- a/addons/resource/tests/common.py +++ b/addons/resource/tests/common.py @@ -39,15 +39,15 @@ class TestResourceCommon(common.TransactionCase): self.resource_leaves = self.registry('resource.calendar.leaves') # Some demo data - self.date1 = datetime.strptime('2013-02-12 09:22:13', '%Y-%m-%d %H:%M:%S') # weekday() returns 1, isoweekday() returns 2 - self.date2 = datetime.strptime('2013-02-15 09:22:13', '%Y-%m-%d %H:%M:%S') # weekday() returns 4, isoweekday() returns 5 - # Leave1: 19/02/2013, from 9 to 12, is a day 0 + self.date1 = datetime.strptime('2013-02-12 09:08:07', '%Y-%m-%d %H:%M:%S') # weekday() returns 1, isoweekday() returns 2 + self.date2 = datetime.strptime('2013-02-15 10:11:12', '%Y-%m-%d %H:%M:%S') # weekday() returns 4, isoweekday() returns 5 + # Leave1: 19/02/2013, from 9 to 12, is a day 1 self.leave1_start = datetime.strptime('2013-02-19 09:00:00', '%Y-%m-%d %H:%M:%S') self.leave1_end = datetime.strptime('2013-02-19 12:00:00', '%Y-%m-%d %H:%M:%S') - # Leave2: 22/02/2013, from 9 to 15, is a day 3 + # Leave2: 22/02/2013, from 9 to 15, is a day 4 self.leave2_start = datetime.strptime('2013-02-22 09:00:00', '%Y-%m-%d %H:%M:%S') self.leave2_end = datetime.strptime('2013-02-22 15:00:00', '%Y-%m-%d %H:%M:%S') - # Leave3: 25/02/2013 (day6) -> 01/03/2013 (day3) + # Leave3: 25/02/2013 (day0) -> 01/03/2013 (day4) self.leave3_start = datetime.strptime('2013-02-25 13:00:00', '%Y-%m-%d %H:%M:%S') self.leave3_end = datetime.strptime('2013-03-01 11:30:00', '%Y-%m-%d %H:%M:%S') diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index fa29de91b3d..7ae4343a21f 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -19,7 +19,7 @@ # ############################################################################## -from datetime import datetime +from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from openerp.addons.resource.tests.common import TestResourceCommon @@ -51,17 +51,17 @@ class TestResource(TestResourceCommon): ] # Test: interval cleaning - result = self.resource_calendar.interval_clean(intervals) - self.assertEqual(len(result), 3, 'resource_calendar: wrong interval cleaning') + cleaned_intervals = self.resource_calendar.interval_clean(intervals) + self.assertEqual(len(cleaned_intervals), 3, 'resource_calendar: wrong interval cleaning') # First interval: 03, unchanged - self.assertEqual(result[0][0], datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') - self.assertEqual(result[0][1], datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') + self.assertEqual(cleaned_intervals[0][0], datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') + self.assertEqual(cleaned_intervals[0][1], datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') # Second intreval: 04, 08-14, combining 08-12 and 11-14, 09-11 being inside 08-12 - self.assertEqual(result[1][0], datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') - self.assertEqual(result[1][1], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') + self.assertEqual(cleaned_intervals[1][0], datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') + self.assertEqual(cleaned_intervals[1][1], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') # Third interval: 04, 17-21, 18-19 being inside 17-21 - self.assertEqual(result[2][0], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') - self.assertEqual(result[2][1], datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') + self.assertEqual(cleaned_intervals[2][0], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') + self.assertEqual(cleaned_intervals[2][1], datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong interval cleaning') # Test: disjoint removal working_interval = (datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), datetime.strptime('2013-02-04 18:00:00', '%Y-%m-%d %H:%M:%S')) @@ -71,6 +71,27 @@ class TestResource(TestResourceCommon): self.assertEqual(result[0][0], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') self.assertEqual(result[0][1], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + # Test: schedule hours on intervals + result = self.resource_calendar.interval_schedule_hours(cleaned_intervals, 5.5) + self.assertEqual(len(result), 2, 'resource_calendar: wrong hours scheduling in interval') + # First interval: 03, 8-10 untouches + self.assertEqual(result[0][0], datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + self.assertEqual(result[0][1], datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + # First interval: 04, 08-11:30 + self.assertEqual(result[1][0], datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + self.assertEqual(result[1][1], datetime.strptime('2013-02-04 11:30:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + + # Test: schedule hours on intervals, backwards + cleaned_intervals.reverse() + result = self.resource_calendar.interval_schedule_hours(cleaned_intervals, 5.5, remove_at_end=False) + self.assertEqual(len(result), 2, 'resource_calendar: wrong hours scheduling in interval') + # First interval: 03, 8-10 untouches + self.assertEqual(result[0][0], datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + self.assertEqual(result[0][1], datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + # First interval: 04, 08-11:30 + self.assertEqual(result[1][0], datetime.strptime('2013-02-04 12:30:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + self.assertEqual(result[1][1], datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong leave removal from interval') + def test_10_calendar_basics(self): """ Testing basic method of resource.calendar """ cr, uid = self.cr, self.uid @@ -80,110 +101,289 @@ class TestResource(TestResourceCommon): # -------------------------------------------------- # Test: next day: next day after day1 is day4 - date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date1) - self.assertEqual(date, self.date2, 'resource_calendar: wrong next day computing') + date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date1.date()) + self.assertEqual(date, self.date2.date(), 'resource_calendar: wrong next day computing') # Test: next day: next day after day4 is (day1+7) - date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date2) - self.assertEqual(date, self.date1 + relativedelta(days=7), 'resource_calendar: wrong next day computing') + date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date2.date()) + self.assertEqual(date, self.date1.date() + relativedelta(days=7), 'resource_calendar: wrong next day computing') # Test: next day: next day after day4+1 is (day1+7) - date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date2 + relativedelta(days=1)) - self.assertEqual(date, self.date1 + relativedelta(days=7), 'resource_calendar: wrong next day computing') + date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date2.date() + relativedelta(days=1)) + self.assertEqual(date, self.date1.date() + relativedelta(days=7), 'resource_calendar: wrong next day computing') # Test: next day: next day after day1-1 is day1 - date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date1 + relativedelta(days=-1)) - self.assertEqual(date, self.date1, 'resource_calendar: wrong next day computing') + date = self.resource_calendar.get_next_day(cr, uid, self.calendar_id, day_date=self.date1.date() + relativedelta(days=-1)) + self.assertEqual(date, self.date1.date(), 'resource_calendar: wrong next day computing') # -------------------------------------------------- # Test2: get_previous_day # -------------------------------------------------- # Test: previous day: previous day before day1 is (day4-7) - date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date1) - self.assertEqual(date, self.date2 + relativedelta(days=-7), 'resource_calendar: wrong previous day computing') + date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date1.date()) + self.assertEqual(date, self.date2.date() + relativedelta(days=-7), 'resource_calendar: wrong previous day computing') # Test: previous day: previous day before day4 is day1 - date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date2) - self.assertEqual(date, self.date1, 'resource_calendar: wrong previous day computing') + date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date2.date()) + self.assertEqual(date, self.date1.date(), 'resource_calendar: wrong previous day computing') # Test: previous day: previous day before day4+1 is day4 - date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date2 + relativedelta(days=1)) - self.assertEqual(date, self.date2, 'resource_calendar: wrong previous day computing') + date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date2.date() + relativedelta(days=1)) + self.assertEqual(date, self.date2.date(), 'resource_calendar: wrong previous day computing') # Test: previous day: previous day before day1-1 is (day4-7) - date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date1 + relativedelta(days=-1)) - self.assertEqual(date, self.date2 + relativedelta(days=-7), 'resource_calendar: wrong previous day computing') + date = self.resource_calendar.get_previous_day(cr, uid, self.calendar_id, day_date=self.date1.date() + relativedelta(days=-1)) + self.assertEqual(date, self.date2.date() + relativedelta(days=-7), 'resource_calendar: wrong previous day computing') + + # -------------------------------------------------- + # Test3: misc + # -------------------------------------------------- + + weekdays = self.resource_calendar.get_weekdays(cr, uid, self.calendar_id) + self.assertEqual(weekdays, [1, 4], 'resource_calendar: wrong weekdays computing') + + attendances = self.resource_calendar.get_attendances_for_weekdays(cr, uid, self.calendar_id, [2, 3, 4, 5]) + self.assertEqual(set([att.id for att in attendances]), set([self.att2_id, self.att3_id]), + 'resource_calendar: wrong attendances filtering by weekdays computing') def test_20_calendar_working_intervals(self): """ Testing working intervals computing method of resource.calendar """ cr, uid = self.cr, self.uid + _format = '%Y-%m-%d %H:%M:%S' # Test: day0 without leaves: 1 interval - intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, day_date=self.date1) + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date1) self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 09:08:07', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong working intervals') # Test: day3 without leaves: 2 interval - intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, day_date=self.date2) + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date2) self.assertEqual(len(intervals), 2, 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][0], datetime.strptime('2013-02-15 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][1], datetime.strptime('2013-02-15 13:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[1][0], datetime.strptime('2013-02-15 16:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[1][1], datetime.strptime('2013-02-15 23:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-15 10:11:12', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-15 13:00:00', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[1][0], datetime.strptime('2013-02-15 16:00:00', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[1][1], datetime.strptime('2013-02-15 23:00:00', _format), 'resource_calendar: wrong working intervals') # Test: day0 with leaves outside range: 1 interval - intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, day_date=self.date1, compute_leaves=True) + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date1.replace(hour=0), compute_leaves=True) self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 08:00:00', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong working intervals') - # Test: day0 with leaves: 2 intrevals because of leave between 9 ans 12 - intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, day_date=self.date1 + relativedelta(days=7), compute_leaves=True) + # Test: day0 with leaves: 2 intervals because of leave between 9 ans 12, ending at 15:45:30 + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, + start_dt=self.date1.replace(hour=8) + relativedelta(days=7), + end_dt=self.date1.replace(hour=15, minute=45, second=30) + relativedelta(days=7), + compute_leaves=True) self.assertEqual(len(intervals), 2, 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 09:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[1][0], datetime.strptime('2013-02-19 12:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') - self.assertEqual(intervals[1][1], datetime.strptime('2013-02-19 16:00:00', '%Y-%m-%d %H:%M:%S'), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:08:07', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 09:00:00', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[1][0], datetime.strptime('2013-02-19 12:00:00', _format), 'resource_calendar: wrong working intervals') + self.assertEqual(intervals[1][1], datetime.strptime('2013-02-19 15:45:30', _format), 'resource_calendar: wrong working intervals') + + def test_30_calendar_working_days(self): + """ Testing calendar hours computation on a working day """ + cr, uid = self.cr, self.uid + _format = '%Y-%m-%d %H:%M:%S' + + # Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00 + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, start_dt=self.date1.replace(hour=10, minute=30, second=0)) + self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-12 10:30:00', _format), 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong working interval / day computing') + # Test: hour computation for same interval, should give 5.5 + wh = self.resource_calendar.get_working_hours_of_date(cr, uid, self.calendar_id, start_dt=self.date1.replace(hour=10, minute=30, second=0)) + self.assertEqual(wh, 5.5, 'resource_calendar: wrong working interval / day time computing') + + # Test: day1+7 on leave, without leave computation + intervals = self.resource_calendar.get_working_intervals_of_day( + cr, uid, self.calendar_id, + start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=7) + ) + # Result: day1 (08->16) + self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working interval/day computing') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong working interval / day computing') + + # Test: day1+7 on leave, with generic leave computation + intervals = self.resource_calendar.get_working_intervals_of_day( + cr, uid, self.calendar_id, + start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=7), + compute_leaves=True + ) + # Result: day1 (08->09 + 12->16) + self.assertEqual(len(intervals), 2, 'resource_calendar: wrong working interval/day computing') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-19 09:00:00', _format), 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[1][0], datetime.strptime('2013-02-19 12:00:00', _format), 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[1][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong working interval / day computing') + + # Test: day1+14 on leave, with generic leave computation + intervals = self.resource_calendar.get_working_intervals_of_day( + cr, uid, self.calendar_id, + start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=14), + compute_leaves=True + ) + # Result: day1 (08->16) + self.assertEqual(len(intervals), 1, 'resource_calendar: wrong working interval/day computing') + self.assertEqual(intervals[0][0], datetime.strptime('2013-02-26 08:00:00', _format), 'resource_calendar: wrong working interval / day computing') + self.assertEqual(intervals[0][1], datetime.strptime('2013-02-26 16:00:00', _format), 'resource_calendar: wrong working interval / day computing') + + # Test: day1+14 on leave, with resource leave computation + intervals = self.resource_calendar.get_working_intervals_of_day( + cr, uid, self.calendar_id, + start_dt=self.date1.replace(hour=7, minute=0, second=0) + relativedelta(days=14), + compute_leaves=True, + resource_id=self.resource1_id + ) + # Result: nothing, because on leave + self.assertEqual(len(intervals), 0, 'resource_calendar: wrong working interval/day computing') + + def test_40_calendar_hours_scheduling(self): + """ Testing calendar hours scheduling """ + cr, uid = self.cr, self.uid + _format = '%Y-%m-%d %H:%M:%S' + + # -------------------------------------------------- + # Test0: schedule hours backwards (old interval_min_get) + # Done without calendar + # -------------------------------------------------- + + # Done without calendar + # res = self.resource_calendar.interval_min_get(cr, uid, None, self.date1, 40, resource=False) + # res: (datetime.datetime(2013, 2, 7, 9, 8, 7), datetime.datetime(2013, 2, 12, 9, 8, 7)) + + # -------------------------------------------------- + # Test1: schedule hours backwards (old interval_min_get) + # -------------------------------------------------- + + # res = self.resource_calendar.interval_min_get(cr, uid, self.calendar_id, self.date1, 40, resource=False) + # (datetime.datetime(2013, 1, 29, 9, 0), datetime.datetime(2013, 1, 29, 16, 0)) + # (datetime.datetime(2013, 2, 1, 8, 0), datetime.datetime(2013, 2, 1, 13, 0)) + # (datetime.datetime(2013, 2, 1, 16, 0), datetime.datetime(2013, 2, 1, 23, 0)) + # (datetime.datetime(2013, 2, 5, 8, 0), datetime.datetime(2013, 2, 5, 16, 0)) + # (datetime.datetime(2013, 2, 8, 8, 0), datetime.datetime(2013, 2, 8, 13, 0)) + # (datetime.datetime(2013, 2, 8, 16, 0), datetime.datetime(2013, 2, 8, 23, 0)) + # (datetime.datetime(2013, 2, 12, 8, 0), datetime.datetime(2013, 2, 12, 9, 0)) + + res = self.resource_calendar.schedule_hours(cr, uid, self.calendar_id, -40, day_dt=self.date1.replace(minute=0, second=0)) + # current day, limited at 09:00 because of day_dt specified -> 1 hour + self.assertEqual(res[-1][0], datetime.strptime('2013-02-12 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-1][1], datetime.strptime('2013-02-12 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + # previous days: 5+7 hours / 8 hours / 5+7 hours -> 32 hours + self.assertEqual(res[-2][0], datetime.strptime('2013-02-08 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-2][1], datetime.strptime('2013-02-08 23:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-3][0], datetime.strptime('2013-02-08 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-3][1], datetime.strptime('2013-02-08 13:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-4][0], datetime.strptime('2013-02-05 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-4][1], datetime.strptime('2013-02-05 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-5][0], datetime.strptime('2013-02-01 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-5][1], datetime.strptime('2013-02-01 23:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-6][0], datetime.strptime('2013-02-01 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-6][1], datetime.strptime('2013-02-01 13:00:00', _format), 'resource_calendar: wrong hours scheduling') + # 7 hours remaining + self.assertEqual(res[-7][0], datetime.strptime('2013-01-29 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[-7][1], datetime.strptime('2013-01-29 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + # Compute scheduled hours + td = timedelta() + for item in res: + td += item[1] - item[0] + self.assertEqual(td.total_seconds() / 3600.0, 40.0, 'resource_calendar: wrong hours scheduling') + + # -------------------------------------------------- + # Test1: schedule hours forward (old interval_get) + # -------------------------------------------------- + + # res = self.resource_calendar.interval_get(cr, uid, self.calendar_id, self.date1, 40, resource=False, byday=True) + # (datetime.datetime(2013, 2, 12, 9, 0), datetime.datetime(2013, 2, 12, 16, 0)) + # (datetime.datetime(2013, 2, 15, 8, 0), datetime.datetime(2013, 2, 15, 13, 0)) + # (datetime.datetime(2013, 2, 15, 16, 0), datetime.datetime(2013, 2, 15, 23, 0)) + # (datetime.datetime(2013, 2, 22, 8, 0), datetime.datetime(2013, 2, 22, 13, 0)) + # (datetime.datetime(2013, 2, 22, 16, 0), datetime.datetime(2013, 2, 22, 23, 0)) + # (datetime.datetime(2013, 2, 26, 8, 0), datetime.datetime(2013, 2, 26, 16, 0)) + # (datetime.datetime(2013, 3, 1, 8, 0), datetime.datetime(2013, 3, 1, 9, 0)) + + res = self.resource_calendar.schedule_hours( + cr, uid, self.calendar_id, 40, + day_dt=self.date1.replace(minute=0, second=0) + ) + self.assertEqual(res[0][0], datetime.strptime('2013-02-12 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[1][0], datetime.strptime('2013-02-15 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[1][1], datetime.strptime('2013-02-15 13:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[2][0], datetime.strptime('2013-02-15 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[2][1], datetime.strptime('2013-02-15 23:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[3][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[3][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[4][0], datetime.strptime('2013-02-22 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[4][1], datetime.strptime('2013-02-22 13:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[5][0], datetime.strptime('2013-02-22 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[5][1], datetime.strptime('2013-02-22 23:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[6][0], datetime.strptime('2013-02-26 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[6][1], datetime.strptime('2013-02-26 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + td = timedelta() + for item in res: + td += item[1] - item[0] + self.assertEqual(td.total_seconds() / 3600.0, 40.0, 'resource_calendar: wrong hours scheduling') + + # res = self.resource_calendar.interval_get(cr, uid, self.calendar_id, self.date1, 40, resource=self.resource1_id, byday=True) + # (datetime.datetime(2013, 2, 12, 9, 0), datetime.datetime(2013, 2, 12, 16, 0)) + # (datetime.datetime(2013, 2, 15, 8, 0), datetime.datetime(2013, 2, 15, 13, 0)) + # (datetime.datetime(2013, 2, 15, 16, 0), datetime.datetime(2013, 2, 15, 23, 0)) + # (datetime.datetime(2013, 3, 1, 8, 0), datetime.datetime(2013, 3, 1, 13, 0)) + # (datetime.datetime(2013, 3, 1, 16, 0), datetime.datetime(2013, 3, 1, 23, 0)) + # (datetime.datetime(2013, 3, 5, 8, 0), datetime.datetime(2013, 3, 5, 16, 0)) + # (datetime.datetime(2013, 3, 8, 8, 0), datetime.datetime(2013, 3, 8, 9, 0)) + + res = self.resource_calendar.schedule_hours( + cr, uid, self.calendar_id, 40, + day_dt=self.date1.replace(minute=0, second=0), + compute_leaves=True, + resource_id=self.resource1_id + ) + self.assertEqual(res[0][0], datetime.strptime('2013-02-12 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[0][1], datetime.strptime('2013-02-12 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[1][0], datetime.strptime('2013-02-15 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[1][1], datetime.strptime('2013-02-15 13:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[2][0], datetime.strptime('2013-02-15 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[2][1], datetime.strptime('2013-02-15 23:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[3][0], datetime.strptime('2013-02-19 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[3][1], datetime.strptime('2013-02-19 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[4][0], datetime.strptime('2013-02-19 12:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[4][1], datetime.strptime('2013-02-19 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[5][0], datetime.strptime('2013-02-22 08:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[5][1], datetime.strptime('2013-02-22 09:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[6][0], datetime.strptime('2013-02-22 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[6][1], datetime.strptime('2013-02-22 23:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[7][0], datetime.strptime('2013-03-01 11:30:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[7][1], datetime.strptime('2013-03-01 13:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[8][0], datetime.strptime('2013-03-01 16:00:00', _format), 'resource_calendar: wrong hours scheduling') + self.assertEqual(res[8][1], datetime.strptime('2013-03-01 22:30:00', _format), 'resource_calendar: wrong hours scheduling') + td = timedelta() + for item in res: + td += item[1] - item[0] + self.assertEqual(td.total_seconds() / 3600.0, 40.0, 'resource_calendar: wrong hours scheduling') def test_40_calendar_schedule_days(self): """ Testing calendar days scheduling """ cr, uid = self.cr, self.uid + _format = '%Y-%m-%d %H:%M:%S' - print '---------------' - res = self.resource_calendar.schedule_days(cr, uid, self.calendar_id, 5, date=self.date1) - print res + res = self.resource_calendar.schedule_days_get_date(cr, uid, self.calendar_id, 5, day_date=self.date1) + self.assertEqual(res, datetime.strptime('2013-02-26 00:0:00', _format).date(), 'resource_calendar: wrong days scheduling') + + res = self.resource_calendar.schedule_days_get_date( + cr, uid, self.calendar_id, 5, day_date=self.date1, + compute_leaves=True, resource_id=self.resource1_id) + self.assertEqual(res, datetime.strptime('2013-03-01 00:0:00', _format).date(), 'resource_calendar: wrong days scheduling') # -------------------------------------------------- # Misc # -------------------------------------------------- - # Without calendar, should only count days - print '---------------' - res = self.resource_calendar.schedule_days(cr, uid, None, 5, date=self.date1) - print res - - # @mute_logger('openerp.addons.base.ir.ir_model', 'openerp.osv.orm') - # def test_20_calendar(self): - # """ Testing calendar and time computation """ - # cr, uid = self.cr, self.uid - - # wh = self.resource_calendar.get_working_hours_of_date(cr, uid, self.calendar_id, day_date=self.date1) - # self.assertEqual(wh, 8, 'cacamou') - - # wh = self.resource_calendar.get_working_hours_of_date(cr, uid, self.calendar_id, day_date=self.date2+relativedelta(days=7)) - # self.assertEqual(wh, 12, 'cacamou') - - # # print '---------------------' - # # print self.date1 - # # res = self.resource_calendar.interval_min_get(cr, uid, self.calendar_id, self.date1, 40, resource=False) - # # print res - - # print '----------------------' - # res = self.resource_calendar.schedule_hours(cr, uid, self.calendar_id, 40, start_datetime=self.date1) - # print res - # print '----------------------' - # # print self.date1 - # # res = self.resource_calendar.interval_get(cr, uid, self.calendar_id, self.date1, 40, resource=False, byday=True) - # # print res + # # Without calendar, should only count days + # print '---------------' + # res = self.resource_calendar.schedule_days_get_date(cr, uid, None, 5, day_date=self.date1) + # print res