From 4a6adff30d3f11013cf0463a2c47afeeb8040a7f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 7 Jun 2016 17:27:44 +0200 Subject: [PATCH] [ADD] resource: work day helpers Add methods to query work days (entire) for resources for systems working daily insted of hourly. --- addons/resource/resource.py | 31 +++++ addons/resource/tests/test_resource.py | 155 ++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 1 deletion(-) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index 1a0ea76c71f..a604764fcfb 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime +import functools from datetime import timedelta from dateutil import rrule from dateutil.relativedelta import relativedelta @@ -686,6 +687,36 @@ class resource_resource(osv.osv): default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name)) return super(resource_resource, self).copy(cr, uid, id, default, context) + def _is_work_day(self, date): + """ Whether the provided date is a work day for the subject resource. + + :type date: datetime.date + :rtype: bool + """ + return bool(next(self._iter_work_days(date, date), False)) + + def _iter_work_days(self, from_date, to_date): + """ Lists the current resource's work days between the two provided + dates (inclusive). + + Work days are the company or service's open days (as defined by the + resource.calendar) minus the resource's own leaves. + + :param datetime.date from_date: start of the interval to check for + work days (inclusive) + :param datetime.date to_date: end of the interval to check for work + days (inclusive) + :rtype: list(datetime.date) + """ + working_intervals = self.calendar_id.get_working_intervals_of_day + # rrule coerces date inputs to datetimes (with time=0) and yields + # datetimes (with time=0 if freq >= daily) + for dt in rrule.rrule(rrule.DAILY, dtstart=from_date, until=to_date): + intervals = working_intervals(dt, compute_leaves=True, resource_id=self.id) + + # FIXME: get_working_intervals is new-API mapped to return a list of lists of intervals + if intervals and intervals[0]: + yield dt.date() class resource_calendar_leaves(osv.osv): _name = "resource.calendar.leaves" diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index 029c001f81a..208166a2108 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date + +import babel.dates from dateutil.relativedelta import relativedelta from openerp.addons.resource.tests.common import TestResourceCommon +from openerp.tests import TransactionCase + class TestResource(TestResourceCommon): @@ -461,6 +465,155 @@ class TestResource(TestResourceCommon): res = self.resource_calendar.schedule_days_get_date(cr, uid, None, 5, day_date=self.date1, default_interval=(8, 16), context={'tz': 'UTC'}) self.assertEqual(res, datetime.strptime('2013-02-16 16:00:00', _format), 'resource_calendar: wrong days scheduling') +WAR_START = date(1932, 11, 2) +WAR_END = date(1932, 12, 10) +class TestWorkDays(TransactionCase): + def _make_attendance(self, weekday, **kw): + data = { + 'name': babel.dates.get_day_names()[weekday], + 'dayofweek': str(weekday), + 'hour_from': 9, + 'hour_to': 17, + } + data.update(kw) + return data + def setUp(self): + super(TestWorkDays, self).setUp() + # trivial 5/7 9-17 resource calendar + self._calendar = self.env['resource.calendar'].create({ + 'name': "Trivial Calendar", + 'attendance_ids': [ + (0, 0, self._make_attendance(i)) + for i in range(5) + ] + }) + + self._days = [ + date.fromordinal(o) + for o in xrange( + WAR_START.toordinal(), + WAR_END.toordinal() + 1 + ) + ] + + def test_no_calendar(self): + """ + If a resource has no resource calendar, they don't work + """ + r = self.env['resource.resource'].create({ + 'name': "NoCalendar" + }) + + self.assertEqual( + [], + list(r._iter_work_days(WAR_START, WAR_END)), + ) + + def test_trivial_calendar_no_leaves(self): + """ If leaves are not involved, only calendar attendances (basic + company configuration) are taken in account + """ + r = self.env['resource.resource'].create({ + 'name': "Trivial Calendar", + 'calendar_id': self._calendar.id + }) + + # with the trivial calendar, all days are work days except for + # saturday and sunday + self.assertEqual( + [d for d in self._days if d.weekday() not in (5, 6)], + list(r._iter_work_days(WAR_START, WAR_END)) + ) + + def test_global_leaves(self): + self.env['resource.calendar.leaves'].create({ + 'calendar_id': self._calendar.id, + 'date_from': '1932-11-09 00:00:00', + 'date_to': '1932-11-12 23:59:59', + }) + + r1 = self.env['resource.resource'].create({ + 'name': "Resource 1", + 'calendar_id': self._calendar.id + }) + r2 = self.env['resource.resource'].create({ + 'name': "Resource 2", + 'calendar_id': self._calendar.id + }) + + days = [ + d for d in self._days + if d.weekday() not in (5, 6) + if d < date(1932, 11, 9) or d > date(1932, 11, 12) + ] + self.assertEqual(days, list(r1._iter_work_days(WAR_START, WAR_END))) + self.assertEqual(days, list(r2._iter_work_days(WAR_START, WAR_END))) + + def test_personal_leaves(self): + """ Leaves with a resource_id apply only to that resource + """ + r1 = self.env['resource.resource'].create({ + 'name': "Resource 1", + 'calendar_id': self._calendar.id + }) + r2 = self.env['resource.resource'].create({ + 'name': "Resource 2", + 'calendar_id': self._calendar.id + }) + self.env['resource.calendar.leaves'].create({ + 'calendar_id': self._calendar.id, + 'date_from': '1932-11-09 00:00:00', + 'date_to': '1932-11-12 23:59:59', + 'resource_id': r2.id + }) + + weekdays = [d for d in self._days if d.weekday() not in (5, 6)] + self.assertEqual(weekdays, list(r1._iter_work_days(WAR_START, WAR_END))) + self.assertEqual( + [d for d in weekdays + if d < date(1932, 11, 9) or d > date(1932, 11, 12) + ], + list(r2._iter_work_days(WAR_START, WAR_END)) + ) + + def test_mixed_leaves(self): + r = self.env['resource.resource'].create({ + 'name': "Resource 1", + 'calendar_id': self._calendar.id + }) + self.env['resource.calendar.leaves'].create({ + 'calendar_id': self._calendar.id, + 'date_from': '1932-11-09 00:00:00', + 'date_to': '1932-11-12 23:59:59', + }) + self.env['resource.calendar.leaves'].create({ + 'calendar_id': self._calendar.id, + 'date_from': '1932-12-02 00:00:00', + 'date_to': '1932-12-31 23:59:59', + 'resource_id': r.id + }) + + self.assertEqual( + [d for d in self._days + if d.weekday() not in (5, 6) + if d < date(1932, 11, 9) or d > date(1932, 11, 12) + if d < date(1932, 12, 2) + ], + list(r._iter_work_days(WAR_START, WAR_END)) + ) + + # _is_work_day is built on _iter_work_days, but it's probably a good + # idea to ensure it does do what it should + self.assertTrue(r._is_work_day(date(1932, 11, 8))) + self.assertTrue(r._is_work_day(date(1932, 11, 14))) + self.assertTrue(r._is_work_day(date(1932, 12, 1))) + + self.assertFalse(r._is_work_day(date(1932, 11, 11))) # global leave + self.assertFalse(r._is_work_day(date(1932, 11, 13))) # sun + self.assertFalse(r._is_work_day(date(1932, 11, 19))) # sat + self.assertFalse(r._is_work_day(date(1932, 11, 20))) # sun + self.assertFalse(r._is_work_day(date(1932, 12, 6))) # personal leave + def seconds(td): assert isinstance(td, timedelta)