diff --git a/addons/resource/resource.py b/addons/resource/resource.py index 449fa422ab4..dd87157aeb0 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -20,17 +20,19 @@ ############################################################################## import pytz -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from dateutil import rrule +from dateutil.relativedelta import relativedelta +import itertools import math +from operator import itemgetter + from faces import * +from openerp import tools from openerp.osv import fields, osv from openerp.tools.float_utils import float_compare from openerp.tools.translate import _ -from itertools import groupby -from operator import itemgetter - class resource_calendar(osv.osv): _name = "resource.calendar" @@ -40,25 +42,321 @@ class resource_calendar(osv.osv): '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='' + ), } _defaults = { 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context) } - def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None): - """Calculates the Working Total Hours based on Resource Calendar and - given working day (datetime object). + # -------------------------------------------------- + # Utility methods + # -------------------------------------------------- - @param resource_calendar_id: resource.calendar browse record - @param day: datetime object + def interval_clean(self, intervals): + """ Utility method that removes overlapping inside datetime intervals. - @return: returns the working hours (as float) men should work on the given day if is in the attendance_ids of the resource_calendar_id (i.e if that day is a working day), returns 0.0 otherwise + :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 """ - res = 0.0 - for working_day in resource_calendar_id.attendance_ids: - if (int(working_day.dayofweek) + 1) == day.isoweekday(): - res += working_day.hour_to - working_day.hour_from - return res + intervals = sorted(intervals) # TODO: check sorted method + final_list = [] + 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)) + 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 + + def interval_remove_leaves(self, interval, leave_intervals): + """ Utility method that remove leave intervals from a base interval: + + - clean the leave intrevals, to have an ordered list of not-overlapping + intervals + - initiate the current interval to be the base interval + - for each leave interval: + + - finishing before the current interval: skip, go to next + - beginning after the current interval: skip and get out of the loop + because we are outside range (leaves are ordered) + - beginning within the current interval: close the current interval + and begin a new current interval that begins at the end of the leave + interval + - ending within the current interval: update the current interval begin + to match the leave interval ending + + :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 + """ + if not interval: + return interval + if leave_intervals is None: + leave_intervals = [] + final_list = [] + 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])) + current_interval = [leave[1], interval[1]] + if current_interval[0] <= leave[1] <= current_interval[1]: + # print '\t\tending inside' + 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 + + # -------------------------------------------------- + # 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) + calendar = self.browse(cr, uid, id, context=None) + weekdays = set() + for attendance in calendar.attendance_ids: + weekdays.add(int(attendance.dayofweek)) + weekdays = 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 + + 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): + 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.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 + + new_index = (base_index + 1) % len(weekdays) + days = (weekdays[new_index] - day_date.weekday()) + if days > 0: + days = days - 7 + + 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): + """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 + """ + 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: + continue + date_from = datetime.strptime(leave.date_from, tools.DEFAULT_SERVER_DATETIME_FORMAT) + if start_datetime and date_from < start_datetime: + continue + if end_datetime and date_end > end_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): + """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 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 + leaves based on calendar and resource. + If leaves is None and compute_leaves false + 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. + + :returns list intervals: a list of tuples (start_datetime, end_datetime) + that are intervals of work + """ + 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, + } + 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)) + + # find leave intervals + if leaves is None and compute_leaves: + leaves = self._get_leave_intervals(cr, uid, id, resource_id=resource_id, context=None) + + # filter according to leaves + for interval in working_intervals: + work_intervals = self.interval_remove_leaves(interval, leaves) + intervals += work_intervals + + 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): + """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) + 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): + """ + """ + if start_datetime is None: + start_datetime = datetime.now() + work_hours = 0 + iterations = 0 + final_intervals = [] + + # 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)) + + # prepare rrule arguments + rrule_args = { + 'byweekday': work_days, + 'dtstart': start_datetime, + } + if end_date: + rrule_args['until'] = end_datetime + else: + rrule_args['count'] = 1024 + + 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 + iterations += 1 + + return final_intervals + + def _schedule_days(self, cr, uid, id, days, date=None, compute_leaves=False, resource_id=None, context=None): + """Schedule days of work. + + This method can be used backwards, i.e. scheduling days before a deadline. + """ + backwards = False + if days < 0: + backwards = True + days = abs(days) + intervals = [] + planned_days = 0 + iterations = 0 + current_datetime = date + + 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) + if id is None or working_intervals: # no calendar -> no working hours, but day is considered as worked + planned_days += 1 + intervals += working_intervals + # get next day + if backwards: + current_datetime = self.get_previous_day(cr, uid, id, current_datetime) + else: + current_datetime = self.get_next_day(cr, uid, id, current_datetime) + + return (current_datetime, 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] + + # -------------------------------------------------- + # 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 + 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) def _get_leaves(self, cr, uid, id, resource): """Private Method to Calculate resource Leaves days @@ -99,6 +397,8 @@ class resource_calendar(osv.osv): schedule. @return : List datetime object of working schedule based on supplies params + + TDE TODO: used in mrp_operations/mrp_operations.py """ if not id: td = int(hours)*3 @@ -138,9 +438,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() """ def group(lst, key): lst.sort(key=itemgetter(key)) - grouped = groupby(lst, itemgetter(key)) + grouped = itertools.groupby(lst, itemgetter(key)) return dict([(k, [v for v in itr]) for k, itr in grouped]) # END group @@ -198,6 +499,8 @@ class resource_calendar(osv.osv): @param byday: boolean flag bit enforce day wise scheduling @return : list of scheduled working timing based on resource calendar. + + TDE NOTE: mrp_operations/mrp_operations.py, crm/crm_lead.py """ 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 @@ -233,6 +536,8 @@ class resource_calendar(osv.osv): @param context: current request context @return : Total number of working hours based dt_from and dt_end and resource if supplied. + + TDE NOTE: used in project_issue/project_issue.py """ utc_tz = pytz.timezone('UTC') local_tz = utc_tz @@ -374,6 +679,8 @@ class resource_resource(osv.osv): def generate_resources(self, cr, uid, user_ids, calendar_id, context=None): """ Return a list of Resource Class objects for the resources allocated to the phase. + + TDE NOTE: used in project/project.py """ resource_objs = {} user_pool = self.pool.get('res.users') @@ -401,6 +708,8 @@ class resource_resource(osv.osv): @param calendar_id : working calendar of the project @param resource_id : resource working on phase/task @param resource_calendar : working calendar of the resource + + TDE NOTE: used in project/project.py, and in generate_resources """ resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves') leave_list = [] @@ -426,6 +735,8 @@ 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 + + TDE NOTE: used in project/project.py """ if not calendar_id: # Calendar is not specified: working days: 24/7 diff --git a/addons/resource/resource_view.xml b/addons/resource/resource_view.xml index 3c9f83905a9..dcd7e1a67c0 100644 --- a/addons/resource/resource_view.xml +++ b/addons/resource/resource_view.xml @@ -253,5 +253,6 @@ + diff --git a/addons/resource/tests/__init__.py b/addons/resource/tests/__init__.py new file mode 100644 index 00000000000..f22158c04b8 --- /dev/null +++ b/addons/resource/tests/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2013-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.addons.resource.tests import test_resource + +checks = [ + test_resource, +] + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/resource/tests/common.py b/addons/resource/tests/common.py new file mode 100644 index 00000000000..6dffafd8179 --- /dev/null +++ b/addons/resource/tests/common.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2013-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from datetime import datetime + +from openerp.tests import common + + +class TestResourceCommon(common.TransactionCase): + + def setUp(self): + super(TestResourceCommon, self).setUp() + cr, uid = self.cr, self.uid + if not hasattr(self, 'context'): + self.context = {} + + # Usefull models + self.resource_resource = self.registry('resource.resource') + self.resource_calendar = self.registry('resource.calendar') + self.resource_attendance = self.registry('resource.calendar.attendance') + 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.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 + 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) + 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') + + # Resource data + # Calendar working days: 1 (8-16), 4 (8-13, 21-22) + self.calendar_id = self.resource_calendar.create( + cr, uid, { + 'name': 'TestCalendar', + } + ) + self.att1_id = self.resource_attendance.create( + cr, uid, { + 'name': 'Att1', + 'dayofweek': '1', + 'hour_from': 8, + 'hour_to': 16, + 'calendar_id': self.calendar_id, + } + ) + self.att2_id = self.resource_attendance.create( + cr, uid, { + 'name': 'Att2', + 'dayofweek': '4', + 'hour_from': 8, + 'hour_to': 13, + 'calendar_id': self.calendar_id, + } + ) + self.att3_id = self.resource_attendance.create( + cr, uid, { + 'name': 'Att3', + 'dayofweek': '4', + 'hour_from': 16, + 'hour_to': 23, + 'calendar_id': self.calendar_id, + } + ) + self.resource1_id = self.resource_resource.create( + cr, uid, { + 'name': 'TestResource1', + 'resource_type': 'user', + 'time_efficiency': 150.0, + 'calendar_id': self.calendar_id, + } + ) + self.leave1_id = self.resource_leaves.create( + cr, uid, { + 'name': 'GenericLeave', + 'calendar_id': self.calendar_id, + 'date_from': self.leave1_start, + 'date_to': self.leave1_end, + } + ) + self.leave2_id = self.resource_leaves.create( + cr, uid, { + 'name': 'ResourceLeave', + 'calendar_id': self.calendar_id, + 'resource_id': self.resource1_id, + 'date_from': self.leave2_start, + 'date_to': self.leave2_end, + } + ) + self.leave3_id = self.resource_leaves.create( + cr, uid, { + 'name': 'ResourceLeave2', + 'calendar_id': self.calendar_id, + 'resource_id': self.resource1_id, + 'date_from': self.leave3_start, + 'date_to': self.leave3_end, + } + ) + # Some browse data + self.calendar = self.resource_calendar.browse(cr, uid, self.calendar_id) diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py new file mode 100644 index 00000000000..fa29de91b3d --- /dev/null +++ b/addons/resource/tests/test_resource.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2013-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from openerp.addons.resource.tests.common import TestResourceCommon + + +class TestResource(TestResourceCommon): + + def test_00_intervals(self): + intervals = [ + ( + datetime.strptime('2013-02-04 09:00:00', '%Y-%m-%d %H:%M:%S'), + datetime.strptime('2013-02-04 11:00:00', '%Y-%m-%d %H:%M:%S') + ), ( + datetime.strptime('2013-02-04 08:00:00', '%Y-%m-%d %H:%M:%S'), + datetime.strptime('2013-02-04 12:00:00', '%Y-%m-%d %H:%M:%S') + ), ( + datetime.strptime('2013-02-04 11:00:00', '%Y-%m-%d %H:%M:%S'), + datetime.strptime('2013-02-04 14:00:00', '%Y-%m-%d %H:%M:%S') + ), ( + datetime.strptime('2013-02-04 17:00:00', '%Y-%m-%d %H:%M:%S'), + datetime.strptime('2013-02-04 21:00:00', '%Y-%m-%d %H:%M:%S') + ), ( + datetime.strptime('2013-02-03 08:00:00', '%Y-%m-%d %H:%M:%S'), + datetime.strptime('2013-02-03 10:00:00', '%Y-%m-%d %H:%M:%S') + ), ( + datetime.strptime('2013-02-04 18:00:00', '%Y-%m-%d %H:%M:%S'), + datetime.strptime('2013-02-04 19:00:00', '%Y-%m-%d %H:%M:%S') + ) + ] + + # Test: interval cleaning + result = self.resource_calendar.interval_clean(intervals) + self.assertEqual(len(result), 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') + # 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') + # 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') + + # 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')) + result = self.resource_calendar.interval_remove_leaves(working_interval, intervals) + self.assertEqual(len(result), 1, 'resource_calendar: wrong leave removal from interval') + # First interval: 04, 14-17 + 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') + + def test_10_calendar_basics(self): + """ Testing basic method of resource.calendar """ + cr, uid = self.cr, self.uid + + # -------------------------------------------------- + # Test1: get_next_day + # -------------------------------------------------- + + # 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') + + # 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') + + # 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') + + # 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') + + # -------------------------------------------------- + # 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') + + # 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') + + # 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') + + # 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') + + def test_20_calendar_working_intervals(self): + """ Testing working intervals computing method of resource.calendar """ + cr, uid = self.cr, self.uid + + # Test: day0 without leaves: 1 interval + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, day_date=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') + + # Test: day3 without leaves: 2 interval + intervals = self.resource_calendar.get_working_intervals_of_day(cr, uid, self.calendar_id, day_date=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') + + # 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) + 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') + + # 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) + 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') + + def test_40_calendar_schedule_days(self): + """ Testing calendar days scheduling """ + cr, uid = self.cr, self.uid + + print '---------------' + res = self.resource_calendar.schedule_days(cr, uid, self.calendar_id, 5, date=self.date1) + print res + + # -------------------------------------------------- + # 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