diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py
index c7ef36659ab..cb84b0eaca6 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr.py
@@ -229,6 +229,11 @@ class Employee(models.Model):
if self.user_id:
self.update(self._sync_user(self.user_id))
+ @api.onchange('user_id', 'resource_calendar_id')
+ def _onchange_timezone(self):
+ if self.user_id or self.resource_calendar_id:
+ self.tz = self.user_id.tz or self.resource_calendar_id.tz
+
def _sync_user(self, user):
return dict(
name=user.name,
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index 6011c5b1edb..94aa370d792 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -72,6 +72,7 @@
+
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 4884b481da9..7fca30f50f8 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -223,7 +223,7 @@ class HolidaysRequest(models.Model):
if employee_id:
employee = self.env['hr.employee'].browse(employee_id)
- return employee.get_work_days_count(from_dt, to_dt)
+ return employee.get_work_days_data(from_dt, to_dt)['days']
time_delta = to_dt - from_dt
return math.ceil(time_delta.days + float(time_delta.seconds) / 86400)
diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py
index f99591d51e1..19692265971 100644
--- a/addons/hr_payroll/models/hr_payslip.py
+++ b/addons/hr_payroll/models/hr_payslip.py
@@ -1,12 +1,10 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import time
-from datetime import datetime
-from datetime import time as datetime_time
-from dateutil import relativedelta
-
import babel
+from datetime import date, datetime, time
+from dateutil.relativedelta import relativedelta
+from pytz import timezone
from odoo import api, fields, models, tools, _
from odoo.addons import decimal_precision as dp
@@ -29,9 +27,9 @@ class HrPayslip(models.Model):
employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True,
states={'draft': [('readonly', False)]})
date_from = fields.Date(string='Date From', readonly=True, required=True,
- default=time.strftime('%Y-%m-01'), states={'draft': [('readonly', False)]})
+ default=lambda self: fields.Date.to_string(date.today().replace(day=1)), states={'draft': [('readonly', False)]})
date_to = fields.Date(string='Date To', readonly=True, required=True,
- default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
+ default=lambda self: fields.Date.to_string((datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()),
states={'draft': [('readonly', False)]})
# this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed)
state = fields.Selection([
@@ -170,28 +168,32 @@ class HrPayslip(models.Model):
res = []
# fill only if the contract as a working schedule linked
for contract in contracts.filtered(lambda contract: contract.resource_calendar_id):
- day_from = datetime.combine(fields.Date.from_string(date_from), datetime_time.min)
- day_to = datetime.combine(fields.Date.from_string(date_to), datetime_time.max)
+ day_from = datetime.combine(fields.Date.from_string(date_from), time.min)
+ day_to = datetime.combine(fields.Date.from_string(date_to), time.max)
# compute leave days
leaves = {}
- day_leave_intervals = contract.employee_id.iter_leaves(day_from, day_to, calendar=contract.resource_calendar_id)
- for day_intervals in day_leave_intervals:
- for interval in day_intervals:
- holiday = interval[2]['leaves'].holiday_id
- current_leave_struct = leaves.setdefault(holiday.holiday_status_id, {
- 'name': holiday.holiday_status_id.name,
- 'sequence': 5,
- 'code': holiday.holiday_status_id.name,
- 'number_of_days': 0.0,
- 'number_of_hours': 0.0,
- 'contract_id': contract.id,
- })
- leave_time = (interval[1] - interval[0]).seconds / 3600
- current_leave_struct['number_of_hours'] += leave_time
- work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id)
- if work_hours:
- current_leave_struct['number_of_days'] += leave_time / work_hours
+ calendar = contract.resource_calendar_id
+ tz = timezone(calendar.tz)
+ day_leave_intervals = contract.employee_id.list_leaves(day_from, day_to, calendar=contract.resource_calendar_id)
+ for day, hours, leave in day_leave_intervals:
+ holiday = leave.holiday_id
+ current_leave_struct = leaves.setdefault(holiday.holiday_status_id, {
+ 'name': holiday.holiday_status_id.name,
+ 'sequence': 5,
+ 'code': holiday.holiday_status_id.name,
+ 'number_of_days': 0.0,
+ 'number_of_hours': 0.0,
+ 'contract_id': contract.id,
+ })
+ current_leave_struct['number_of_hours'] += hours
+ work_hours = calendar.get_work_hours_count(
+ tz.localize(datetime.combine(day, time.min)),
+ tz.localize(datetime.combine(day, time.max)),
+ compute_leaves=False,
+ )
+ if work_hours:
+ current_leave_struct['number_of_days'] += hours / work_hours
# compute worked days
work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
@@ -394,7 +396,7 @@ class HrPayslip(models.Model):
}
if (not employee_id) or (not date_from) or (not date_to):
return res
- ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
+ ttyme = datetime.combine(fields.Date.from_string(date_from), time.min)
employee = self.env['hr.employee'].browse(employee_id)
locale = self.env.context.get('lang') or 'en_US'
res['value'].update({
@@ -446,7 +448,7 @@ class HrPayslip(models.Model):
date_to = self.date_to
contract_ids = []
- ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
+ ttyme = datetime.combine(fields.Date.from_string(date_from), time.min)
locale = self.env.context.get('lang') or 'en_US'
self.name = _('Salary Slip of %s for %s') % (employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale)))
self.company_id = employee.company_id
@@ -566,10 +568,10 @@ class HrPayslipRun(models.Model):
('close', 'Close'),
], string='Status', index=True, readonly=True, copy=False, default='draft')
date_start = fields.Date(string='Date From', required=True, readonly=True,
- states={'draft': [('readonly', False)]}, default=time.strftime('%Y-%m-01'))
+ states={'draft': [('readonly', False)]}, default=lambda self: fields.Date.to_string(date.today().replace(day=1)))
date_end = fields.Date(string='Date To', required=True, readonly=True,
states={'draft': [('readonly', False)]},
- default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10])
+ default=lambda self: fields.Date.to_string((datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()))
credit_note = fields.Boolean(string='Credit Note', readonly=True,
states={'draft': [('readonly', False)]},
help="If its checked, indicates that all payslips generated from here are refund payslips.")
diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py
index 9ec22553053..af78cad1ac9 100644
--- a/addons/mrp/models/mrp_workcenter.py
+++ b/addons/mrp/models/mrp_workcenter.py
@@ -212,7 +212,7 @@ class MrpWorkcenterProductivity(models.Model):
d2 = fields.Datetime.from_string(blocktime.date_end)
diff = d2 - d1
if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.resource_calendar_id:
- r = blocktime.workcenter_id.resource_calendar_id.get_work_hours_count(d1, d2, blocktime.workcenter_id.resource_id.id)
+ r = blocktime.workcenter_id.get_work_days_data(d1, d2)['hours']
blocktime.duration = round(r * 60, 2)
else:
blocktime.duration = round(diff.total_seconds() / 60.0, 2)
diff --git a/addons/mrp/tests/test_oee.py b/addons/mrp/tests/test_oee.py
index 3fb3ff3850b..4cf8a31a1a9 100644
--- a/addons/mrp/tests/test_oee.py
+++ b/addons/mrp/tests/test_oee.py
@@ -2,9 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta, time
+from pytz import timezone, utc
+from odoo import fields
from odoo.addons.mrp.tests.common import TestMrpCommon
-from odoo.addons.resource.models.resource import to_naive_utc
class TestOee(TestMrpCommon):
@@ -24,8 +25,12 @@ class TestOee(TestMrpCommon):
if day.weekday() in (5, 6):
day -= timedelta(days=2)
+ tz = timezone(self.workcenter_1.resource_calendar_id.tz)
+
def time_to_string_utc_datetime(time):
- return str(to_naive_utc(datetime.combine(day, time), self.workcenter_1.resource_id))
+ return fields.Datetime.to_string(
+ tz.localize(datetime.combine(day, time)).astimezone(utc)
+ )
start_time = time_to_string_utc_datetime(time(10, 43, 22))
end_time = time_to_string_utc_datetime(time(10, 56, 22))
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index cf2c6a941d4..d0564c78a28 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -546,13 +546,13 @@ class Task(models.Model):
if task.date_assign:
dt_date_assign = fields.Datetime.from_string(task.date_assign)
task.working_hours_open = task.project_id.resource_calendar_id.get_work_hours_count(
- dt_create_date, dt_date_assign, False, compute_leaves=True)
+ dt_create_date, dt_date_assign, compute_leaves=True)
task.working_days_open = task.working_hours_open / 24.0
if task.date_end:
dt_date_end = fields.Datetime.from_string(task.date_end)
task.working_hours_close = task.project_id.resource_calendar_id.get_work_hours_count(
- dt_create_date, dt_date_end, False, compute_leaves=True)
+ dt_create_date, dt_date_end, compute_leaves=True)
task.working_days_close = task.working_hours_close / 24.0
(self - task_linked_to_calendar).update(dict.fromkeys(
diff --git a/addons/project_timesheet_holidays/models/hr_holidays.py b/addons/project_timesheet_holidays/models/hr_holidays.py
index 3e4f3122c3c..75d4f47026b 100644
--- a/addons/project_timesheet_holidays/models/hr_holidays.py
+++ b/addons/project_timesheet_holidays/models/hr_holidays.py
@@ -56,7 +56,10 @@ class Holidays(models.Model):
holiday_project = holiday.holiday_status_id.timesheet_project_id
holiday_task = holiday.holiday_status_id.timesheet_task_id
- work_hours_data = [item for item in holiday.employee_id.iter_work_hours_count(fields.Datetime.from_string(holiday.date_from), fields.Datetime.from_string(holiday.date_to))]
+ work_hours_data = holiday.employee_id.list_work_time_per_day(
+ fields.Datetime.from_string(holiday.date_from),
+ fields.Datetime.from_string(holiday.date_to),
+ )
for index, (day_date, work_hours_count) in enumerate(work_hours_data):
self.env['account.analytic.line'].create({
'name': "%s (%s/%s)" % (holiday.name or '', index + 1, len(work_hours_data)),
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 0cc6406783a..85f0aac9c46 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -1,20 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import datetime
import math
-import pytz
-
-from collections import namedtuple
-from datetime import timedelta
-from dateutil import rrule
-from dateutil.relativedelta import relativedelta
-from operator import itemgetter
+from datetime import datetime, time, timedelta
+from dateutil.rrule import rrule, DAILY
+from functools import partial
+from itertools import chain
+from pytz import timezone, utc
from odoo import api, fields, models, _
from odoo.addons.base.models.res_partner import _tz_get
from odoo.exceptions import ValidationError
-from odoo.tools.float_utils import float_compare, float_round
+from odoo.tools.float_utils import float_round
# Default hour per day value. The one should
# only be used when the one from the calendar
@@ -22,25 +19,104 @@ from odoo.tools.float_utils import float_compare, float_round
HOURS_PER_DAY = 8
-def float_to_time(float_hour):
- return datetime.time(int(math.modf(float_hour)[1]), int(60 * math.modf(float_hour)[0]), 0)
+def string_to_datetime(value):
+ """ Convert the given string value to a datetime in UTC. """
+ return utc.localize(fields.Datetime.from_string(value))
-def to_naive_user_tz(datetime, record):
- tz_name = record._context.get('tz') or record.env.user.tz
- tz = tz_name and pytz.timezone(tz_name) or pytz.UTC
- return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(tz).replace(tzinfo=None)
+def datetime_to_string(dt):
+ """ Convert the given datetime (converted in UTC) to a string value. """
+ return fields.Datetime.to_string(dt.astimezone(utc))
-def to_naive_utc(datetime, record):
- tz_name = record._context.get('tz') or record.env.user.tz
- tz = tz_name and pytz.timezone(tz_name) or pytz.UTC
- return tz.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(pytz.UTC).replace(tzinfo=None)
+def float_to_time(hours):
+ """ Convert a number of hours into a time object. """
+ fractional, integral = math.modf(hours)
+ return time(int(integral), int(60 * fractional), 0)
-def to_tz(datetime, tz_name):
- tz = pytz.timezone(tz_name) if tz_name else pytz.UTC
- return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(tz).replace(tzinfo=None)
+def _boundaries(intervals, opening, closing):
+ """ Iterate on the boundaries of intervals. """
+ for start, stop, recs in intervals:
+ if start < stop:
+ yield (start, opening, recs)
+ yield (stop, closing, recs)
+
+
+class Intervals(object):
+ """ Collection of ordered disjoint intervals with some associated records.
+ Each interval is a triple ``(start, stop, records)``, where ``records``
+ is a recordset.
+ """
+ def __init__(self, intervals=()):
+ self._items = []
+ if intervals:
+ # normalize the representation of intervals
+ append = self._items.append
+ starts = []
+ recses = []
+ for value, flag, recs in sorted(_boundaries(intervals, 'start', 'stop')):
+ if flag == 'start':
+ starts.append(value)
+ recses.append(recs)
+ else:
+ start = starts.pop()
+ if not starts:
+ append((start, value, recses[0].union(*recses)))
+ recses.clear()
+
+ def __bool__(self):
+ return bool(self._items)
+
+ def __len__(self):
+ return len(self._items)
+
+ def __iter__(self):
+ return iter(self._items)
+
+ def __reversed__(self):
+ return reversed(self._items)
+
+ def __or__(self, other):
+ """ Return the union of two sets of intervals. """
+ return Intervals(chain(self._items, other._items))
+
+ def __and__(self, other):
+ """ Return the intersection of two sets of intervals. """
+ return self._merge(other, False)
+
+ def __sub__(self, other):
+ """ Return the difference of two sets of intervals. """
+ return self._merge(other, True)
+
+ def _merge(self, other, difference):
+ """ Return the difference or intersection of two sets of intervals. """
+ result = Intervals()
+ append = result._items.append
+
+ # using 'self' and 'other' below forces normalization
+ bounds1 = _boundaries(self, 'start', 'stop')
+ bounds2 = _boundaries(other, 'switch', 'switch')
+
+ start = None # set by start/stop
+ recs1 = None # set by start
+ enabled = difference # changed by switch
+ for value, flag, recs in sorted(chain(bounds1, bounds2)):
+ if flag == 'start':
+ start = value
+ recs1 = recs
+ elif flag == 'stop':
+ if enabled and start < value:
+ append((start, value, recs1))
+ start = None
+ else:
+ if not enabled and start is not None:
+ start = value
+ if enabled and start is not None and start < value:
+ append((start, value, recs1))
+ enabled = not enabled
+
+ return result
class ResourceCalendar(models.Model):
@@ -56,7 +132,6 @@ class ResourceCalendar(models.Model):
tuples, holding several intervals of work or leaves. """
_name = "resource.calendar"
_description = "Resource Calendar"
- _interval_obj = namedtuple('Interval', ('start_datetime', 'end_datetime', 'data'))
@api.model
def default_get(self, fields):
@@ -92,7 +167,12 @@ class ResourceCalendar(models.Model):
'resource.calendar.leaves', 'calendar_id', 'Global Leaves',
domain=[('resource_id', '=', False)]
)
- hours_per_day = fields.Float("Average hour per day", default=HOURS_PER_DAY, help="Average hours per day a resource is supposed to work with this calendar.")
+ hours_per_day = fields.Float("Average hour per day", default=HOURS_PER_DAY,
+ help="Average hours per day a resource is supposed to work with this calendar.")
+ tz = fields.Selection(
+ _tz_get, string='Timezone', required=True,
+ default=lambda self: self._context.get('tz') or self.env.user.tz,
+ help="This field is used in order to define in which timezone the resources will work.")
@api.onchange('attendance_ids')
def _onchange_hours_per_day(self):
@@ -103,550 +183,196 @@ class ResourceCalendar(models.Model):
self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2)
# --------------------------------------------------
- # Utility methods
+ # Computation API
# --------------------------------------------------
-
- def _merge_kw(self, kw, kw_ext):
- new_kw = dict(kw, **kw_ext)
- new_kw.update(
- attendances=kw.get('attendances', self.env['resource.calendar.attendance']) | kw_ext.get('attendances', self.env['resource.calendar.attendance']),
- leaves=kw.get('leaves', self.env['resource.calendar.leaves']) | kw_ext.get('leaves', self.env['resource.calendar.leaves'])
- )
- return new_kw
-
- def _interval_new(self, start_datetime, end_datetime, kw=None):
- kw = kw if kw is not None else dict()
- kw.setdefault('attendances', self.env['resource.calendar.attendance'])
- kw.setdefault('leaves', self.env['resource.calendar.leaves'])
- return self._interval_obj(start_datetime, end_datetime, kw)
-
- def _interval_exclude_left(self, interval, interval_dst):
- return self._interval_obj(
- interval.start_datetime > interval_dst.end_datetime and interval.start_datetime or interval_dst.end_datetime,
- interval.end_datetime,
- self._merge_kw(interval.data, interval_dst.data)
- )
-
- def _interval_exclude_right(self, interval, interval_dst):
- return self._interval_obj(
- interval.start_datetime,
- interval.end_datetime < interval_dst.start_datetime and interval.end_datetime or interval_dst.start_datetime,
- self._merge_kw(interval.data, interval_dst.data)
- )
-
- def _interval_or(self, interval, interval_dst):
- return self._interval_obj(
- interval.start_datetime < interval_dst.start_datetime and interval.start_datetime or interval_dst.start_datetime,
- interval.end_datetime > interval_dst.end_datetime and interval.end_datetime or interval_dst.end_datetime,
- self._merge_kw(interval.data, interval_dst.data)
- )
-
- def _interval_and(self, interval, interval_dst):
- if interval.start_datetime > interval_dst.end_datetime or interval.end_datetime < interval_dst.start_datetime:
- return None
- return self._interval_obj(
- interval.start_datetime > interval_dst.start_datetime and interval.start_datetime or interval_dst.start_datetime,
- interval.end_datetime < interval_dst.end_datetime and interval.end_datetime or interval_dst.end_datetime,
- self._merge_kw(interval.data, interval_dst.data)
- )
-
- def _interval_merge(self, intervals):
- """ Sort intervals based on starting datetime and merge overlapping intervals.
-
- :return list cleaned: sorted intervals merged without overlap """
- 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 = self._interval_new(*current_interval)
- elif working_interval[1] < current_interval[0]: # interval is disjoint
- cleaned.append(working_interval)
- working_interval = self._interval_new(*current_interval)
- elif working_interval[1] < current_interval[1]: # union of greater intervals
- working_interval = self._interval_or(working_interval, current_interval)
- if working_interval: # handle void lists
- cleaned.append(working_interval)
- return cleaned
-
- @api.model
- def _interval_remove_leaves(self, interval, leave_intervals):
- """ Remove leave intervals from a base interval
-
- :param tuple interval: an interval (see above) that is the base interval
- from which the leave intervals will be removed
- :param list leave_intervals: leave intervals to remove
- :return list intervals: ordered intervals with leaves removed """
- intervals = []
- leave_intervals = self._interval_merge(leave_intervals)
- current_interval = interval
- for leave in leave_intervals:
- # skip if ending before the current start datetime
- if leave[1] <= current_interval[0]:
- continue
- # skip if starting after current end datetime; break as leaves are ordered and
- # are therefore all out of range
- if leave[0] >= current_interval[1]:
- break
- # begins within current interval: close current interval and begin a new one
- # that begins at the leave end datetime
- if current_interval[0] < leave[0] < current_interval[1]:
- intervals.append(self._interval_exclude_right(current_interval, leave))
- current_interval = self._interval_exclude_left(interval, leave)
- # ends within current interval: set current start datetime as leave end datetime
- if current_interval[0] <= leave[1]:
- current_interval = self._interval_exclude_left(interval, leave)
- if current_interval and current_interval[0] < interval[1]: # remove intervals moved outside base interval due to leaves
- intervals.append(current_interval)
- return intervals
-
- @api.model
- def _interval_schedule_hours(self, intervals, hour, backwards=False):
- """ Schedule hours in intervals. The last matching interval is truncated
- to match the specified hours. This method can be applied backwards meaning
- scheduling hours going in the past. In that case truncating last interval
- is done accordingly. If number of hours to schedule is greater than possible
- scheduling in the given intervals, returned result equals intervals.
-
- :param list intervals: a list of time intervals
- :param int/float hours: number of hours to schedule. It will be converted
- into a timedelta, but should be submitted as an
- int or float
- :param boolean backwards: schedule starting from last hour
-
- :return list results: a list of time intervals """
- if backwards:
- intervals.reverse() # first interval is the last working interval of the day
- results = []
- res = timedelta()
- limit = timedelta(hours=hour)
- for interval in intervals:
- res += interval[1] - interval[0]
- if res > limit and not backwards:
- 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
- if backwards:
- results.reverse() # return interval with increasing starting times
- return results
-
- # --------------------------------------------------
- # Date and hours computation
- # --------------------------------------------------
-
- @api.multi
- def _get_day_attendances(self, day_date, start_time, end_time):
- """ Given a day date, return matching attendances. Those can be limited
- by starting and ending time objects. """
- self.ensure_one()
- weekday = day_date.weekday()
- attendances = self.env['resource.calendar.attendance']
-
- for attendance in self.attendance_ids.filtered(
- lambda att:
- int(att.dayofweek) == weekday and
- not (att.date_from and fields.Date.from_string(att.date_from) > day_date) and
- not (att.date_to and fields.Date.from_string(att.date_to) < day_date)):
- if start_time and float_to_time(attendance.hour_to) < start_time:
- continue
- if end_time and float_to_time(attendance.hour_from) > end_time:
- continue
- attendances |= attendance
- return attendances
-
- @api.multi
- def _get_weekdays(self):
- """ Return the list of weekdays that contain at least one working
- interval. """
- self.ensure_one()
- return list({int(d) for d in self.attendance_ids.mapped('dayofweek')})
-
- @api.multi
- def _get_next_work_day(self, day_date):
- """ Get following date of day_date, based on resource.calendar. """
- self.ensure_one()
- weekdays = self._get_weekdays()
- weekday = next((item for item in weekdays if item > day_date.weekday()), weekdays[0])
- days = weekday - day_date.weekday()
- if days < 0:
- days = 7 + days
-
- return day_date + relativedelta(days=days)
-
- @api.multi
- def _get_previous_work_day(self, day_date):
- """ Get previous date of day_date, based on resource.calendar. """
- self.ensure_one()
- weekdays = self._get_weekdays()
- weekdays.reverse()
- weekday = next((item for item in weekdays if item < day_date.weekday()), weekdays[0])
- days = weekday - day_date.weekday()
- if days > 0:
- days = days - 7
-
- return day_date + relativedelta(days=days)
-
- @api.multi
- def _get_leave_intervals(self, resource_id=None, start_datetime=None, end_datetime=None, domain=None):
- """Get the leaves of the calendar. Leaves can be filtered on the resource,
- and on a start and end datetime.
-
- Leaves are encoded from a given timezone given by their tz field. COnverting
- them in naive user timezone require to use the leave timezone, not the current
- user timezone. For example people managing leaves could be from different
- timezones and the correct one is the one used when encoding them.
-
- :return list leaves: list of time intervals """
- self.ensure_one()
- if resource_id:
- search_domain = ['|', ('resource_id', '=', resource_id), ('resource_id', '=', False)]
- else:
- search_domain = [('resource_id', '=', False)]
- if start_datetime:
- # domain += [('date_to', '>', fields.Datetime.to_string(to_naive_utc(start_datetime, self.env.user)))]
- search_domain += [('date_to', '>', fields.Datetime.to_string(start_datetime + timedelta(days=-1)))]
- if end_datetime:
- # domain += [('date_from', '<', fields.Datetime.to_string(to_naive_utc(end_datetime, self.env.user)))]
- search_domain += [('date_from', '<', fields.Datetime.to_string(end_datetime + timedelta(days=1)))]
- if domain:
- search_domain += domain
- if domain is None:
- search_domain += [('time_type', '=', 'leave')]
- leaves = self.env['resource.calendar.leaves'].search(search_domain + [('calendar_id', '=', self.id)])
-
- filtered_leaves = self.env['resource.calendar.leaves']
- for leave in leaves:
- if start_datetime:
- leave_date_to = to_tz(fields.Datetime.from_string(leave.date_to), leave.tz)
- if not leave_date_to >= start_datetime:
- continue
- if end_datetime:
- leave_date_from = to_tz(fields.Datetime.from_string(leave.date_from), leave.tz)
- if not leave_date_from <= end_datetime:
- continue
- filtered_leaves += leave
-
- return [self._interval_new(
- to_tz(fields.Datetime.from_string(leave.date_from), leave.tz),
- to_tz(fields.Datetime.from_string(leave.date_to), leave.tz),
- {'leaves': leave}) for leave in filtered_leaves]
-
- def _iter_day_attendance_intervals(self, day_date, start_time, end_time):
- """ Get an iterator of all interval of current day attendances. """
- for calendar_working_day in self._get_day_attendances(day_date, start_time, end_time):
- from_time = float_to_time(calendar_working_day.hour_from)
- to_time = float_to_time(calendar_working_day.hour_to)
-
- dt_f = datetime.datetime.combine(day_date, max(from_time, start_time))
- dt_t = datetime.datetime.combine(day_date, min(to_time, end_time))
-
- yield self._interval_new(dt_f, dt_t, {'attendances': calendar_working_day})
-
- @api.multi
- def _get_day_work_intervals(self, day_date, start_time=None, end_time=None, compute_leaves=False, resource_id=None, domain=None):
- """ Get the working intervals of the day given by day_date based on
- current calendar. Input should be given in current user timezone and
- output is given in naive UTC, ready to be used by the orm or webclient.
-
- :param time start_time: time object that is the beginning hours in user TZ
- :param time end_time: time object that is the ending hours in user TZ
- :param boolean compute_leaves: indicates whether to compute the
- leaves based on calendar and resource.
- :param int resource_id: the id of the resource to take into account when
- computing the work intervals. Leaves notably are
- filtered according to the resource.
-
- :return list intervals: list of time intervals in UTC """
- self.ensure_one()
-
- if not start_time:
- start_time = datetime.time.min
- if not end_time:
- end_time = datetime.time.max
-
- working_intervals = [att_interval for att_interval in self._iter_day_attendance_intervals(day_date, start_time, end_time)]
-
- # filter according to leaves
- if compute_leaves:
- leaves = self._get_leave_intervals(
- resource_id=resource_id,
- start_datetime=datetime.datetime.combine(day_date, start_time),
- end_datetime=datetime.datetime.combine(day_date, end_time),
- domain=domain)
- working_intervals = [
- sub_interval
- for interval in working_intervals
- for sub_interval in self._interval_remove_leaves(interval, leaves)]
-
- # adapt tz
- return [self._interval_new(
- to_naive_utc(interval[0], self.env.user),
- to_naive_utc(interval[1], self.env.user),
- interval[2]) for interval in working_intervals]
-
- def _get_day_leave_intervals(self, day_date, start_time, end_time, resource_id, domain=None):
- """ Get the leave intervals of the day given by day_date based on current
- calendar. Input should be given in current user timezone and
- output is given in naive UTC, ready to be used by the orm or webclient.
-
- :param time start_time: time object that is the beginning hours in user TZ
- :param time end_time: time object that is the ending hours in user TZ
- :param int resource_id: the id of the resource to take into account when
- computing the leaves.
-
- :return list intervals: list of time intervals in UTC """
- self.ensure_one()
-
- if not start_time:
- start_time = datetime.time.min
- if not end_time:
- end_time = datetime.time.max
-
- working_intervals = [att_interval for att_interval in self._iter_day_attendance_intervals(day_date, start_time, end_time)]
-
- leaves_intervals = self._get_leave_intervals(
- resource_id=resource_id,
- start_datetime=datetime.datetime.combine(day_date, start_time),
- end_datetime=datetime.datetime.combine(day_date, end_time),
- domain=domain)
-
- final_intervals = [i for i in
- [self._interval_and(leave_interval, work_interval)
- for leave_interval in leaves_intervals
- for work_interval in working_intervals] if i]
-
- # adapt tz
- return [self._interval_new(
- to_naive_utc(interval[0], self.env.user),
- to_naive_utc(interval[1], self.env.user),
- interval[2]) for interval in final_intervals]
-
- # --------------------------------------------------
- # Main computation API
- # --------------------------------------------------
-
- def _iter_work_intervals(self, start_dt, end_dt, resource_id, compute_leaves=True, domain=None):
- """ Lists the current resource's work intervals between the two provided
- datetimes (inclusive) expressed in UTC, for each worked day. """
- if not end_dt:
- end_dt = datetime.datetime.combine(start_dt.date(), datetime.time.max)
-
- start_dt = to_naive_user_tz(start_dt, self.env.user)
- end_dt = to_naive_user_tz(end_dt, self.env.user)
-
- for day in rrule.rrule(rrule.DAILY,
- dtstart=start_dt,
- until=end_dt,
- byweekday=self._get_weekdays()):
- start_time = datetime.time.min
- if day.date() == start_dt.date():
- start_time = start_dt.time()
- end_time = datetime.time.max
- if day.date() == end_dt.date() and end_dt.time() != datetime.time():
- end_time = end_dt.time()
-
- intervals = self._get_day_work_intervals(
- day.date(),
- start_time=start_time,
- end_time=end_time,
- compute_leaves=compute_leaves,
- resource_id=resource_id,
- domain=domain)
- if intervals:
- yield intervals
-
- def _iter_leave_intervals(self, start_dt, end_dt, resource_id, domain=None):
- """ Lists the current resource's leave intervals between the two provided
- datetimes (inclusive) expressed in UTC. """
- if not end_dt:
- end_dt = datetime.datetime.combine(start_dt.date(), datetime.time.max)
-
- start_dt = to_naive_user_tz(start_dt, self.env.user)
- end_dt = to_naive_user_tz(end_dt, self.env.user)
-
- for day in rrule.rrule(rrule.DAILY,
- dtstart=start_dt,
- until=end_dt,
- byweekday=self._get_weekdays()):
- start_time = datetime.time.min
- if day.date() == start_dt.date():
- start_time = start_dt.time()
- end_time = datetime.time.max
- if day.date() == end_dt.date() and end_dt.time() != datetime.time():
- end_time = end_dt.time()
-
- intervals = self._get_day_leave_intervals(
- day.date(),
- start_time,
- end_time,
- resource_id,
- domain=domain)
-
- if intervals:
- yield intervals
-
- def _iter_work_hours_count(self, from_datetime, to_datetime, resource_id, domain=None):
- """ Lists the current resource's work hours count between the two provided
- datetime expressed in naive UTC. """
-
- for interval in self._iter_work_intervals(from_datetime, to_datetime, resource_id, domain=domain):
- td = timedelta()
- for work_interval in interval:
- td += work_interval[1] - work_interval[0]
- yield (interval[0][0].date(), td.total_seconds() / 3600.0)
-
- def _iter_work_days(self, from_date, to_date, resource_id, domain=None):
- """ Lists the current resource's work days between the two provided
- dates (inclusive) expressed in naive UTC.
-
- 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)
+ def _attendance_intervals(self, start_dt, end_dt, resource=None):
+ """ Return the attendance intervals in the given datetime range.
+ The returned intervals are expressed in the resource's timezone.
"""
- for interval in self._iter_work_intervals(
- datetime.datetime(from_date.year, from_date.month, from_date.day),
- datetime.datetime(to_date.year, to_date.month, to_date.day),
- resource_id, domain=domain):
- yield interval[0][0].date()
+ assert start_dt.tzinfo and end_dt.tzinfo
+ combine = datetime.combine
- @api.multi
- def _is_work_day(self, date, resource_id):
- """ Whether the provided date is a work day for the subject resource.
+ # express all dates and times in the resource's timezone
+ tz = timezone((resource or self).tz)
+ start_dt = start_dt.astimezone(tz)
+ end_dt = end_dt.astimezone(tz)
- :type date: datetime.date
- :rtype: bool """
- return bool(next(self._iter_work_days(date, date, resource_id), False))
+ # for each attendance spec, generate the intervals in the date range
+ result = []
+ for attendance in self.attendance_ids:
+ start = start_dt.date()
+ if attendance.date_from:
+ start = max(start, attendance.date_from)
+ until = end_dt.date()
+ if attendance.date_to:
+ until = min(until, attendance.date_to)
+ weekday = int(attendance.dayofweek)
- @api.multi
- def get_work_hours_count(self, start_dt, end_dt, resource_id, compute_leaves=True, domain=None):
- """ Count number of work hours between two datetimes. For compute_leaves,
- resource_id: see _get_day_work_intervals. """
- res = timedelta()
- for intervals in self._iter_work_intervals(start_dt, end_dt, resource_id, compute_leaves=compute_leaves, domain=domain):
- for interval in intervals:
- res += interval[1] - interval[0]
- return res.total_seconds() / 3600.0
+ for day in rrule(DAILY, start, until=until, byweekday=weekday):
+ # attendance hours are interpreted in the resource's timezone
+ dt0 = tz.localize(combine(day, float_to_time(attendance.hour_from)))
+ dt1 = tz.localize(combine(day, float_to_time(attendance.hour_to)))
+ result.append((max(start_dt, dt0), min(end_dt, dt1), attendance))
- # --------------------------------------------------
- # Scheduling API
- # --------------------------------------------------
+ return Intervals(result)
- @api.multi
- def _schedule_hours(self, hours, day_dt, compute_leaves=False, resource_id=None, domain=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. For compute_leaves, resource_id:
- see _get_day_work_intervals. This method does not use rrule because
- rrule does not allow backwards computation.
-
- :param int hours: number of hours to schedule. Use a negative number to
- compute a backwards scheduling.
- :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.
-
- :return list intervals: list of time intervals in naive UTC """
+ def _leave_intervals(self, start_dt, end_dt, resource=None, domain=None):
+ """ Return the leave intervals in the given datetime range.
+ The returned intervals are expressed in the calendar's timezone.
+ """
+ assert start_dt.tzinfo and end_dt.tzinfo
self.ensure_one()
- backwards = (hours < 0)
- intervals = []
- remaining_hours, iterations = abs(hours * 1.0), 0
- day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
- current_datetime = day_dt_tz
+ # for the computation, express all datetimes in UTC
+ resource_ids = [resource.id, False] if resource else [False]
+ if domain is None:
+ domain = [('time_type', '=', 'leave')]
+ domain = domain + [
+ ('calendar_id', '=', self.id),
+ ('resource_id', 'in', resource_ids),
+ ('date_from', '<=', datetime_to_string(end_dt)),
+ ('date_to', '>=', datetime_to_string(start_dt)),
+ ]
- call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, domain=domain)
+ # retrieve leave intervals in (start_dt, end_dt)
+ tz = timezone((resource or self).tz)
+ start_dt = start_dt.astimezone(tz)
+ end_dt = end_dt.astimezone(tz)
+ result = []
+ for leave in self.env['resource.calendar.leaves'].search(domain):
+ dt0 = string_to_datetime(leave.date_from).astimezone(tz)
+ dt1 = string_to_datetime(leave.date_to).astimezone(tz)
+ result.append((max(start_dt, dt0), min(end_dt, dt1), leave))
- while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000:
- if backwards:
- call_args['end_time'] = current_datetime.time()
- else:
- call_args['start_time'] = current_datetime.time()
+ return Intervals(result)
- working_intervals = self._get_day_work_intervals(current_datetime.date(), **call_args)
+ def _work_intervals(self, start_dt, end_dt, resource=None, domain=None):
+ """ Return the effective work intervals between the given datetimes. """
+ return (self._attendance_intervals(start_dt, end_dt, resource) -
+ self._leave_intervals(start_dt, end_dt, resource, domain))
- if working_intervals:
- new_working_intervals = self._interval_schedule_hours(working_intervals, remaining_hours, backwards=backwards)
-
- res = timedelta()
- for interval in working_intervals:
- res += interval[1] - interval[0]
- remaining_hours -= res.total_seconds() / 3600.0
-
- intervals = intervals + new_working_intervals if not backwards else new_working_intervals + intervals
- # get next day
- if backwards:
- current_datetime = datetime.datetime.combine(self._get_previous_work_day(current_datetime), datetime.time(23, 59, 59))
- else:
- current_datetime = datetime.datetime.combine(self._get_next_work_day(current_datetime), datetime.time())
- # avoid infinite loops
- iterations += 1
-
- return intervals
+ # --------------------------------------------------
+ # External API
+ # --------------------------------------------------
@api.multi
- def plan_hours(self, hours, day_dt, compute_leaves=False, resource_id=None, domain=None):
- """ Return datetime after having planned hours """
- res = self._schedule_hours(hours, day_dt, compute_leaves, resource_id, domain)
- if res and hours < 0.0:
- return res[0][0]
- elif res:
- return res[-1][1]
- return False
+ def get_work_hours_count(self, start_dt, end_dt, compute_leaves=True, domain=None):
+ """
+ `compute_leaves` controls whether or not this method is taking into
+ account the global leaves.
+
+ `domain` controls the way leaves are recognized.
+ None means default value ('time_type', '=', 'leave')
+
+ Counts the number of work hours between two datetimes.
+ """
+ # Set timezone in UTC if no timezone is explicitly given
+ if not start_dt.tzinfo:
+ start_dt = start_dt.replace(tzinfo=utc)
+ if not end_dt.tzinfo:
+ end_dt = end_dt.replace(tzinfo=utc)
+
+ if compute_leaves:
+ intervals = self._work_intervals(start_dt, end_dt, domain=domain)
+ else:
+ intervals = self._attendance_intervals(start_dt, end_dt)
+
+ return sum(
+ (stop - start).total_seconds() / 3600
+ for start, stop, meta in intervals
+ )
@api.multi
- def _schedule_days(self, days, day_dt, compute_leaves=False, resource_id=None, domain=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. For compute_leaves, resource_id:
- see _get_day_work_intervals. This method does not use rrule because
- rrule does not allow backwards computation.
+ def plan_hours(self, hours, day_dt, compute_leaves=False, domain=None):
+ """
+ `compute_leaves` controls whether or not this method is taking into
+ account the global leaves.
- :param int days: number of days to schedule. Use a negative number to
- compute a backwards scheduling.
- :param date day_dt: reference datetime to compute working days. If days is > 0
- date is the starting date. If days is < 0 date is the
- ending date.
+ `domain` controls the way leaves are recognized.
+ None means default value ('time_type', '=', 'leave')
- :return list intervals: list of time intervals in naive UTC """
- backwards = (days < 0)
- intervals = []
- planned_days, iterations = 0, 0
+ Return datetime after having planned hours
+ """
+ if not day_dt.tzinfo:
+ day_dt = day_dt.replace(tzinfo=utc)
- day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
- current_datetime = day_dt_tz.replace(hour=0, minute=0, second=0, microsecond=0)
+ # which method to use for retrieving intervals
+ if compute_leaves:
+ get_intervals = partial(self._work_intervals, domain=domain)
+ else:
+ get_intervals = self._attendance_intervals
- while planned_days < abs(days) and iterations < 100:
- working_intervals = self._get_day_work_intervals(
- current_datetime.date(),
- compute_leaves=compute_leaves, resource_id=resource_id,
- domain=domain)
- if not self 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_work_day(current_datetime)
- else:
- current_datetime = self._get_next_work_day(current_datetime)
- # avoid infinite loops
- iterations += 1
+ if hours > 0:
+ delta = timedelta(days=14)
+ for n in range(100):
+ dt = day_dt + delta * n
+ for start, stop, meta in get_intervals(dt, dt + delta):
+ interval_hours = (stop - start).total_seconds() / 3600
+ if hours <= interval_hours:
+ return (start + timedelta(hours=hours)).astimezone(day_dt.tzinfo)
+ hours -= interval_hours
+ return False
- return intervals
+ elif hours < 0:
+ hours = abs(hours)
+ delta = timedelta(days=14)
+ for n in range(100):
+ dt = day_dt - delta * n
+ for start, stop, meta in reversed(get_intervals(dt - delta, dt)):
+ interval_hours = (stop - start).total_seconds() / 3600
+ if hours <= interval_hours:
+ return (stop - timedelta(hours=hours)).astimezone(day_dt.tzinfo)
+ hours -= interval_hours
+ return False
+
+ else:
+ return day_dt
@api.multi
- def plan_days(self, days, day_dt, compute_leaves=False, resource_id=None, domain=None):
- """ Returns the datetime of a days scheduling. """
- res = self._schedule_days(days, day_dt, compute_leaves, resource_id, domain)
- return res and res[-1][1] or False
+ def plan_days(self, days, day_dt, compute_leaves=False, domain=None):
+ """
+ `compute_leaves` controls whether or not this method is taking into
+ account the global leaves.
+
+ `domain` controls the way leaves are recognized.
+ None means default value ('time_type', '=', 'leave')
+
+ Returns the datetime of a days scheduling.
+ """
+ if not day_dt.tzinfo:
+ day_dt = day_dt.replace(tzinfo=utc)
+
+ # which method to use for retrieving intervals
+ if compute_leaves:
+ get_intervals = partial(self._work_intervals, domain=domain)
+ else:
+ get_intervals = self._attendance_intervals
+
+ if days > 0:
+ found = set()
+ delta = timedelta(days=14)
+ for n in range(100):
+ dt = day_dt + delta * n
+ for start, stop, meta in get_intervals(dt, dt + delta):
+ found.add(start.date())
+ if len(found) == days:
+ return stop.astimezone(day_dt.tzinfo)
+ return False
+
+ elif days < 0:
+ days = abs(days)
+ found = set()
+ delta = timedelta(days=14)
+ for n in range(100):
+ dt = day_dt - delta * n
+ for start, stop, meta in reversed(get_intervals(dt - delta, dt)):
+ found.add(start.date())
+ if len(found) == days:
+ return start.astimezone(day_dt.tzinfo)
+ return False
+
+ else:
+ return day_dt
class ResourceCalendarAttendance(models.Model):
@@ -712,6 +438,10 @@ class ResourceResource(models.Model):
default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id,
required=True,
help="Define the schedule of resource")
+ tz = fields.Selection(
+ _tz_get, string='Timezone', required=True,
+ default=lambda self: self._context.get('tz') or self.env.user.tz,
+ help="This field is used in order to define in which timezone the resources will work.")
_sql_constraints = [
('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'),
@@ -728,6 +458,12 @@ class ResourceResource(models.Model):
def create(self, values):
if values.get('company_id') and not values.get('calendar_id'):
values['calendar_id'] = self.env['res.company'].browse(values['company_id']).resource_calendar_id.id
+ if not values.get('tz'):
+ # retrieve timezone on user or calendar
+ tz = (self.env['res.users'].browse(values.get('user_id')).tz or
+ self.env['resource.calendar'].browse(values.get('calendar_id')).tz)
+ if tz:
+ values['tz'] = tz
return super(ResourceResource, self).create(values)
@api.multi
@@ -745,6 +481,11 @@ class ResourceResource(models.Model):
if self.company_id:
self.calendar_id = self.company_id.resource_calendar_id.id
+ @api.onchange('user_id')
+ def _onchange_user_id(self):
+ if self.user_id:
+ self.tz = self.user_id.tz
+
class ResourceCalendarLeaves(models.Model):
_name = "resource.calendar.leaves"
@@ -757,10 +498,6 @@ class ResourceCalendarLeaves(models.Model):
calendar_id = fields.Many2one('resource.calendar', 'Working Hours')
date_from = fields.Datetime('Start Date', required=True)
date_to = fields.Datetime('End Date', required=True)
- tz = fields.Selection(
- _tz_get, string='Timezone', default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC',
- help="Timezone used when encoding the leave. It is used to correctly "
- "localize leave hours when computing time intervals.")
resource_id = fields.Many2one(
"resource.resource", 'Resource',
help="If empty, this is a generic holiday for the company. If a resource is set, the holiday/leave is only for this resource")
diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index c08bd66d628..d5aa1cee51f 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -1,11 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from collections import defaultdict
from datetime import timedelta
+from pytz import utc
from odoo import api, fields, models
from odoo.tools import float_utils
+# This will generate quarter of days
+ROUNDING_FACTOR = 4
+
+
class ResourceMixin(models.AbstractModel):
_name = "resource.mixin"
_description = 'Resource Mixin'
@@ -21,13 +27,19 @@ class ResourceMixin(models.AbstractModel):
'resource.calendar', 'Working Hours',
default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id,
index=True, related='resource_id.calendar_id', store=True)
+ tz = fields.Selection(
+ string='Timezone', related='resource_id.tz',
+ help="This field is used in order to define in which timezone the resources will work.")
@api.model
def create(self, values):
if not values.get('resource_id'):
- resource = self.env['resource.resource'].create({
- 'name': values.get(self._rec_name)
- })
+ resource_vals = {'name': values.get(self._rec_name)}
+ tz = (values.pop('tz', False) or
+ self.env['resource.calendar'].browse(values.get('resource_calendar_id')).tz)
+ if tz:
+ resource_vals['tz'] = tz
+ resource = self.env['resource.resource'].create(resource_vals)
values['resource_id'] = resource.id
return super(ResourceMixin, self).create(values)
@@ -41,87 +53,150 @@ class ResourceMixin(models.AbstractModel):
default['resource_calendar_id'] = resource.calendar_id.id
return super(ResourceMixin, self).copy_data(default)
- def get_work_days_count(self, from_datetime, to_datetime, calendar=None, domain=None):
- """ Return the number of work days for the resource, taking into account
- leaves. An optional calendar can be given in case multiple calendars can
- be used on the resource. """
- return self.get_work_days_data(from_datetime, to_datetime, calendar=calendar, domain=domain)['days']
+ def get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None):
+ """
+ By default the resource calendar is used, but it can be
+ changed using the `calendar` argument.
- def get_work_hours_count(self, from_datetime, to_datetime, calendar=None, domain=None):
- """ Return the number of work hours for the resource, taking into account
- leaves. An optional calendar can be given in case multiple calendars can
- be used on the resource. """
- return self.get_work_days_data(from_datetime, to_datetime, calendar=calendar, domain=domain)['hours']
+ `domain` is used in order to recognise the leaves to take,
+ None means default value ('time_type', '=', 'leave')
- def get_work_days_data(self, from_datetime, to_datetime, calendar=None, domain=None):
- days_count = 0.0
- total_work_time = timedelta()
+ Returns a dict {'days': n, 'hours': h} containing the
+ quantity of working time expressed as days and as hours.
+ """
+ resource = self.resource_id
calendar = calendar or self.resource_calendar_id
- for day_intervals in calendar._iter_work_intervals(
- from_datetime, to_datetime, self.resource_id.id,
- compute_leaves=True, domain=domain):
- theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
- work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
- total_work_time += work_time
- if theoric_hours:
- days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
+
+ # naive datetimes are made explicit in UTC
+ if not from_datetime.tzinfo:
+ from_datetime = from_datetime.replace(tzinfo=utc)
+ if not to_datetime.tzinfo:
+ to_datetime = to_datetime.replace(tzinfo=utc)
+
+ # total hours per day: retrieve attendances with one extra day margin,
+ # in order to compute the total hours on the first and last days
+ from_full = from_datetime - timedelta(days=1)
+ to_full = to_datetime + timedelta(days=1)
+ intervals = calendar._attendance_intervals(from_full, to_full, resource)
+ day_total = defaultdict(float)
+ for start, stop, meta in intervals:
+ day_total[start.date()] += (stop - start).total_seconds() / 3600
+
+ # actual hours per day
+ if compute_leaves:
+ intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain)
+ else:
+ intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource)
+ day_hours = defaultdict(float)
+ for start, stop, meta in intervals:
+ day_hours[start.date()] += (stop - start).total_seconds() / 3600
+
+ # compute number of days as quarters
+ days = sum(
+ float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
+ for day in day_hours
+ )
return {
- 'days': days_count,
- 'hours': total_work_time.total_seconds() / 3600,
+ 'days': days,
+ 'hours': sum(day_hours.values()),
}
- def iter_works(self, from_datetime, to_datetime, calendar=None, domain=None):
+ def get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None):
+ """
+ By default the resource calendar is used, but it can be
+ changed using the `calendar` argument.
+
+ `domain` is used in order to recognise the leaves to take,
+ None means default value ('time_type', '=', 'leave')
+
+ Returns a dict {'days': n, 'hours': h} containing the number of leaves
+ expressed as days and as hours.
+ """
+ resource = self.resource_id
calendar = calendar or self.resource_calendar_id
- return calendar._iter_work_intervals(from_datetime, to_datetime, self.resource_id.id, domain=domain)
- def iter_work_hours_count(self, from_datetime, to_datetime, calendar=None, domain=None):
- calendar = calendar or self.resource_calendar_id
- return calendar._iter_work_hours_count(from_datetime, to_datetime, self.resource_id.id, domain=domain)
+ # naive datetimes are made explicit in UTC
+ if not from_datetime.tzinfo:
+ from_datetime = from_datetime.replace(tzinfo=utc)
+ if not to_datetime.tzinfo:
+ to_datetime = to_datetime.replace(tzinfo=utc)
- def get_leaves_day_count(self, from_datetime, to_datetime, calendar=None, domain=None):
- """ Return the number of leave days for the resource, taking into account
- attendances. An optional calendar can be given in case multiple calendars
- can be used on the resource. """
- return self.get_leaves_days_data(from_datetime, to_datetime, calendar=calendar, domain=domain)['days']
+ # total hours per day: retrieve attendances with one extra day margin,
+ # in order to compute the total hours on the first and last days
+ from_full = from_datetime - timedelta(days=1)
+ to_full = to_datetime + timedelta(days=1)
+ intervals = calendar._attendance_intervals(from_full, to_full, resource)
+ day_total = defaultdict(float)
+ for start, stop, meta in intervals:
+ day_total[start.date()] += (stop - start).total_seconds() / 3600
- def get_leaves_hours_count(self, from_datetime, to_datetime, calendar=None, domain=None):
- """ Return the number of leave hours for the resource, taking into account
- attendances. An optional calendar can be given in case multiple calendars can
- be used on the resource. """
- return self.get_leaves_days_data(from_datetime, to_datetime, calendar=calendar, domain=domain)['hours']
+ # compute actual hours per day
+ attendances = calendar._attendance_intervals(from_datetime, to_datetime, resource)
+ leaves = calendar._leave_intervals(from_datetime, to_datetime, resource, domain)
+ day_hours = defaultdict(float)
+ for start, stop, meta in (attendances & leaves):
+ day_hours[start.date()] += (stop - start).total_seconds() / 3600
- def get_leaves_days_data(self, from_datetime, to_datetime, calendar=None, domain=None):
- days_count = 0.0
- total_leave_time = timedelta()
- calendar = calendar or self.resource_calendar_id
- for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id, domain=domain):
- theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
- leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
- total_leave_time += leave_time
- if theoric_hours:
- days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
+ # compute number of days as quarters
+ days = sum(
+ float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
+ for day in day_hours
+ )
return {
- 'days': days_count,
- 'hours': total_leave_time.total_seconds() / 3600,
+ 'days': days,
+ 'hours': sum(day_hours.values()),
}
- def iter_leaves(self, from_datetime, to_datetime, calendar=None, domain=None):
- calendar = calendar or self.resource_calendar_id
- return calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id, domain=domain)
+ def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None):
+ """
+ By default the resource calendar is used, but it can be
+ changed using the `calendar` argument.
- def get_start_work_hour(self, day_dt, calendar=None, domain=None):
- calendar = calendar or self.resource_calendar_id
- work_intervals = calendar._get_day_work_intervals(day_dt, resource_id=self.resource_id.id, domain=domain)
- return work_intervals and work_intervals[0][0]
+ `domain` is used in order to recognise the leaves to take,
+ None means default value ('time_type', '=', 'leave')
- def get_end_work_hour(self, day_dt, calendar=None, domain=None):
+ Returns a list of tuples (day, hours) for each day
+ containing at least an attendance.
+ """
+ resource = self.resource_id
calendar = calendar or self.resource_calendar_id
- work_intervals = calendar._get_day_work_intervals(day_dt, resource_id=self.resource_id.id, domain=domain)
- return work_intervals and work_intervals[-1][1]
- def get_day_work_hours_count(self, day_date, calendar=None):
+ # naive datetimes are made explicit in UTC
+ if not from_datetime.tzinfo:
+ from_datetime = from_datetime.replace(tzinfo=utc)
+ if not to_datetime.tzinfo:
+ to_datetime = to_datetime.replace(tzinfo=utc)
+
+ intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain)
+ result = defaultdict(float)
+ for start, stop, meta in intervals:
+ result[start.date()] += (stop - start).total_seconds() / 3600
+ return sorted(result.items())
+
+ def list_leaves(self, from_datetime, to_datetime, calendar=None, domain=None):
+ """
+ By default the resource calendar is used, but it can be
+ changed using the `calendar` argument.
+
+ `domain` is used in order to recognise the leaves to take,
+ None means default value ('time_type', '=', 'leave')
+
+ Returns a list of tuples (day, hours, resource.calendar.leaves)
+ for each leave in the calendar.
+ """
+ resource = self.resource_id
calendar = calendar or self.resource_calendar_id
- attendances = calendar._get_day_attendances(day_date, False, False)
- if not attendances:
- return 0
- return sum(float(i.hour_to) - float(i.hour_from) for i in attendances)
+
+ # naive datetimes are made explicit in UTC
+ if not from_datetime.tzinfo:
+ from_datetime = from_datetime.replace(tzinfo=utc)
+ if not to_datetime.tzinfo:
+ to_datetime = to_datetime.replace(tzinfo=utc)
+
+ attendances = calendar._attendance_intervals(from_datetime, to_datetime, resource)
+ leaves = calendar._leave_intervals(from_datetime, to_datetime, resource, domain)
+ result = []
+ for start, stop, leave in (leaves & attendances):
+ hours = (stop - start).total_seconds() / 3600
+ result.append((start.date(), hours, leave))
+ return result
diff --git a/addons/resource/tests/common.py b/addons/resource/tests/common.py
index 0fd6f9f7d9b..fdcc6b80a10 100644
--- a/addons/resource/tests/common.py
+++ b/addons/resource/tests/common.py
@@ -1,74 +1,46 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.fields import Datetime
from odoo.tests.common import TransactionCase
class TestResourceCommon(TransactionCase):
+ def _define_calendar(self, name, attendances, tz):
+ return self.env['resource.calendar'].create({
+ 'name': name,
+ 'tz': tz,
+ 'attendance_ids': [
+ (0, 0, {
+ 'name': '%s_%d' % (name, index),
+ 'hour_from': att[0],
+ 'hour_to': att[1],
+ 'dayofweek': str(att[2]),
+ })
+ for index, att in enumerate(attendances)
+ ],
+ })
+
def setUp(self):
super(TestResourceCommon, self).setUp()
- self.env.user.tz = 'UTC'
- # Some demo data
- self.date1 = Datetime.from_string('2013-02-12 09:08:07') # weekday() returns 1, isoweekday() returns 2
- self.date2 = Datetime.from_string('2013-02-15 10:11:12') # weekday() returns 4, isoweekday() returns 5
+ # UTC+1 winter, UTC+2 summer
+ self.calendar_jean = self._define_calendar('40 Hours', [(8, 16, i) for i in range(5)], 'Europe/Brussels')
+ # UTC+6
+ self.calendar_patel = self._define_calendar('38 Hours', sum([((9, 12, i), (13, 17, i)) for i in range(5)], ()), 'Etc/GMT-6')
+ # UTC-8 winter, UTC-7 summer
+ self.calendar_john = self._define_calendar('8+12 Hours', [(8, 16, 1), (8, 13, 4), (16, 23, 4)], 'America/Los_Angeles')
- # Resource data
- # Calendar working days: 1 (8-16 -> 8hours), 4 (8-13, 16-23 -> 12hours)
- self.calendar = self.env['resource.calendar'].create({
- 'name': 'TestCalendar',
- 'attendance_ids': [(5, 0, 0)]
+ # Employee is linked to a resource.resource via resource.mixin
+ self.jean = self.env['resource.test'].create({
+ 'name': 'Jean',
+ 'resource_calendar_id': self.calendar_jean.id,
})
- self.att_1 = self.env['resource.calendar.attendance'].create({
- 'name': 'Att1',
- 'calendar_id': self.calendar.id,
- 'dayofweek': '1',
- 'hour_from': 8,
- 'hour_to': 16
+ self.patel = self.env['resource.test'].create({
+ 'name': 'Patel',
+ 'resource_calendar_id': self.calendar_patel.id,
})
- self.att_2 = self.env['resource.calendar.attendance'].create({
- 'name': 'Att2',
- 'calendar_id': self.calendar.id,
- 'dayofweek': '4',
- 'hour_from': 8,
- 'hour_to': 13
+ self.john = self.env['resource.test'].create({
+ 'name': 'John',
+ 'resource_calendar_id': self.calendar_john.id,
})
- self.att_3 = self.env['resource.calendar.attendance'].create({
- 'name': 'Att3',
- 'calendar_id': self.calendar.id,
- 'dayofweek': '4',
- 'hour_from': 16,
- 'hour_to': 23
- })
-
- self.resource1_id = self.env['resource.resource'].create(
- {
- 'name': 'TestResource1',
- 'resource_type': 'user',
- 'time_efficiency': 150.0,
- 'calendar_id': self.calendar.id,
- }
- ).id
-
- # Leave1: 19/02/2013, from 9 to 12, is a day 1
- self.leave1 = self.env['resource.calendar.leaves'].create({
- 'name': 'GenericLeave',
- 'calendar_id': self.calendar.id,
- 'date_from': Datetime.from_string('2013-02-19 09:00:00'),
- 'date_to': Datetime.from_string('2013-02-19 12:00:00')})
- # Leave2: 22/02/2013, from 9 to 15, is a day 4
- self.leave2 = self.env['resource.calendar.leaves'].create({
- 'name': 'ResourceLeave',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': Datetime.from_string('2013-02-22 09:00:00'),
- 'date_to': Datetime.from_string('2013-02-22 15:00:00')})
- # Leave3: 25/02/2013 (day0) -> 01/03/2013 (day4)
- self.leave3 = self.env['resource.calendar.leaves'].create({
- 'name': 'ResourceLeave2',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': Datetime.from_string('2013-02-25 13:00:00'),
- 'date_to': Datetime.from_string('2013-03-01 11:30:00')})
diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py
index 8e8c5b8015d..a190878e730 100644
--- a/addons/resource/tests/test_resource.py
+++ b/addons/resource/tests/test_resource.py
@@ -1,849 +1,876 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import babel.dates
+from datetime import date, datetime
+from pytz import timezone, utc
-from datetime import datetime, timedelta, date, time
-
-from dateutil import rrule
-from dateutil.relativedelta import relativedelta
-
-from odoo.fields import Date, Datetime
-from odoo.addons.resource.models.resource import to_naive_utc, to_naive_user_tz, to_tz
+from odoo import fields
+from odoo.exceptions import ValidationError
+from odoo.addons.resource.models.resource import Intervals
from odoo.addons.resource.tests.common import TestResourceCommon
+from odoo.tests.common import TransactionCase
-class TestIntervals(TestResourceCommon):
-
- def setUp(self):
- super(TestIntervals, self).setUp()
- # Some data intervals
- # - first one is included in second one
- # - second one is extended by third one
- # - sixth one is included in fourth one
- # - fifth one is prior to other one
- # Once cleaned: 1 interval 03/02 8-10), 2 intervals 04/02 (8-14 + 17-21)
- self.intervals = [
- self.calendar._interval_new(
- Datetime.from_string('2013-02-04 09:00:00'),
- Datetime.from_string('2013-02-04 11:00:00')
- ), self.calendar._interval_new(
- Datetime.from_string('2013-02-04 08:00:00'),
- Datetime.from_string('2013-02-04 12:00:00')
- ), self.calendar._interval_new(
- Datetime.from_string('2013-02-04 11:00:00'),
- Datetime.from_string('2013-02-04 14:00:00')
- ), self.calendar._interval_new(
- Datetime.from_string('2013-02-04 17:00:00'),
- Datetime.from_string('2013-02-04 21:00:00')
- ), self.calendar._interval_new(
- Datetime.from_string('2013-02-03 08:00:00'),
- Datetime.from_string('2013-02-03 10:00:00')
- ), self.calendar._interval_new(
- Datetime.from_string('2013-02-04 18:00:00'),
- Datetime.from_string('2013-02-04 19:00:00')
- )
- ]
-
- def test_interval_merge(self):
- cleaned_intervals = self.env['resource.calendar']._interval_merge(self.intervals)
- self.assertEqual(len(cleaned_intervals), 3)
- # First interval: 03, unchanged
- self.assertEqual(cleaned_intervals[0][:2], (Datetime.from_string('2013-02-03 08:00:00'), Datetime.from_string('2013-02-03 10:00:00')))
- # Second interval: 04, 08-14, combining 08-12 and 11-14, 09-11 being inside 08-12
- self.assertEqual(cleaned_intervals[1][:2], (Datetime.from_string('2013-02-04 08:00:00'), Datetime.from_string('2013-02-04 14:00:00')))
- # Third interval: 04, 17-21, 18-19 being inside 17-21
- self.assertEqual(cleaned_intervals[2][:2], (Datetime.from_string('2013-02-04 17:00:00'), Datetime.from_string('2013-02-04 21:00:00')))
-
- def test_interval_and(self):
- self.assertEqual(self.env['resource.calendar']._interval_and(self.intervals[0], self.intervals[1]),
- self.calendar._interval_new(Datetime.from_string('2013-02-04 09:00:00'), Datetime.from_string('2013-02-04 11:00:00')))
- self.assertEqual(self.env['resource.calendar']._interval_and(self.intervals[2], self.intervals[3]),
- None)
-
- def test_interval_remove(self):
- working_interval = self.calendar._interval_new(Datetime.from_string('2013-02-04 08:00:00'), Datetime.from_string('2013-02-04 18:00:00'))
- result = self.env['resource.calendar']._interval_remove_leaves(working_interval, self.intervals)
- self.assertEqual(len(result), 1)
- # First interval: 04, 14-17
- self.assertEqual(result[0][:2], (Datetime.from_string('2013-02-04 14:00:00'), Datetime.from_string('2013-02-04 17:00:00')))
-
- def test_interval_schedule_hours(self):
- cleaned_intervals = self.env['resource.calendar']._interval_merge(self.intervals)
- result = self.env['resource.calendar']._interval_schedule_hours(cleaned_intervals, 5.5)
- self.assertEqual(len(result), 2)
- # First interval: 03, 8-10 untouched
- self.assertEqual(result[0][:2], (Datetime.from_string('2013-02-03 08:00:00'), Datetime.from_string('2013-02-03 10:00:00')))
- # First interval: 04, 08-11:30
- self.assertEqual(result[1][:2], (Datetime.from_string('2013-02-04 08:00:00'), Datetime.from_string('2013-02-04 11:30:00')))
-
- def test_interval_schedule_hours_backwards(self):
- cleaned_intervals = self.env['resource.calendar']._interval_merge(self.intervals)
- result = self.env['resource.calendar']._interval_schedule_hours(cleaned_intervals, 5.5, backwards=True)
- self.assertEqual(len(result), 2)
- # First interval: 03, 8-10 untouched
- self.assertEqual(result[1][:2], (Datetime.from_string('2013-02-04 17:00:00'), Datetime.from_string('2013-02-04 21:00:00')))
- # First interval: 04, 08-11:30
- self.assertEqual(result[0][:2], (Datetime.from_string('2013-02-04 12:30:00'), Datetime.from_string('2013-02-04 14:00:00')))
+def datetime_tz(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None):
+ """ Return a `datetime` object with a given timezone (if given). """
+ dt = datetime(year, month, day, hour, minute, second, microsecond)
+ return timezone(tzinfo).localize(dt) if tzinfo else dt
-class TestCalendarBasics(TestResourceCommon):
+def datetime_str(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None):
+ """ Return a fields.Datetime value with the given timezone. """
+ dt = datetime(year, month, day, hour, minute, second, microsecond)
+ if tzinfo:
+ dt = timezone(tzinfo).localize(dt).astimezone(utc)
+ return fields.Datetime.to_string(dt)
- def test_calendar_weekdays(self):
- weekdays = self.calendar._get_weekdays()
- self.assertEqual(weekdays, [1, 4])
- def test_calendar_next_day(self):
- # Test: next day: next day after day1 is day4
- date = self.calendar._get_next_work_day(day_date=Date.from_string('2013-02-12'))
- self.assertEqual(date, self.date2.date())
+class TestIntervals(TransactionCase):
- # Test: next day: next day after day4 is (day1+7)
- date = self.calendar._get_next_work_day(day_date=Date.from_string('2013-02-15'))
- self.assertEqual(date, self.date1.date() + relativedelta(days=7))
+ def ints(self, pairs):
+ recs = self.env['base']
+ return [(a, b, recs) for a, b in pairs]
- # Test: next day: next day after day4+1 is (day1+7)
- date = self.calendar._get_next_work_day(day_date=Date.from_string('2013-02-15') + relativedelta(days=1))
- self.assertEqual(date, self.date1.date() + relativedelta(days=7))
+ def test_union(self):
+ def check(a, b):
+ a, b = self.ints(a), self.ints(b)
+ self.assertEqual(list(Intervals(a)), b)
- # Test: next day: next day after day1-1 is day1
- date = self.calendar._get_next_work_day(day_date=Date.from_string('2013-02-12') + relativedelta(days=-1))
- self.assertEqual(date, self.date1.date())
+ check([(1, 2), (3, 4)], [(1, 2), (3, 4)])
+ check([(1, 2), (2, 4)], [(1, 4)])
+ check([(1, 3), (2, 4)], [(1, 4)])
+ check([(1, 4), (2, 3)], [(1, 4)])
+ check([(3, 4), (1, 2)], [(1, 2), (3, 4)])
+ check([(2, 4), (1, 2)], [(1, 4)])
+ check([(2, 4), (1, 3)], [(1, 4)])
+ check([(2, 3), (1, 4)], [(1, 4)])
- def test_calendar_previous_day(self):
- # Test: previous day: previous day before day1 is (day4-7)
- date = self.calendar._get_previous_work_day(day_date=Date.from_string('2013-02-12'))
- self.assertEqual(date, self.date2.date() + relativedelta(days=-7))
+ def test_intersection(self):
+ def check(a, b, c):
+ a, b, c = self.ints(a), self.ints(b), self.ints(c)
+ self.assertEqual(list(Intervals(a) & Intervals(b)), c)
- # Test: previous day: previous day before day4 is day1
- date = self.calendar._get_previous_work_day(day_date=Date.from_string('2013-02-15'))
- self.assertEqual(date, self.date1.date())
-
- # Test: previous day: previous day before day4+1 is day4
- date = self.calendar._get_previous_work_day(day_date=Date.from_string('2013-02-15') + relativedelta(days=1))
- self.assertEqual(date, self.date2.date())
-
- # Test: previous day: previous day before day1-1 is (day4-7)
- date = self.calendar._get_previous_work_day(day_date=Date.from_string('2013-02-12') + relativedelta(days=-1))
- self.assertEqual(date, self.date2.date() + relativedelta(days=-7))
-
- def test_calendar_working_day_intervals_no_leaves(self):
- # Test: day0 without leaves: 1 interval
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-12'), start_time=time(9, 8, 7))
- self.assertEqual(len(intervals), 1)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-12 09:08:07'), Datetime.from_string('2013-02-12 16:00:00')))
- self.assertEqual(intervals[0][2]['attendances'], self.att_1)
-
- # Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-19'), start_time=time(10, 30, 0))
- self.assertEqual(len(intervals), 1)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-19 10:30:00'), Datetime.from_string('2013-02-19 16:00:00')))
- self.assertEqual(intervals[0][2]['attendances'], self.att_1)
-
- # Test: day3 without leaves: 2 interval
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-15'), start_time=time(10, 11, 12))
- self.assertEqual(len(intervals), 2)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
- self.assertEqual(intervals[1][:2], (Datetime.from_string('2013-02-15 16:00:00'), Datetime.from_string('2013-02-15 23:00:00')))
- self.assertEqual(intervals[0][2]['attendances'], self.att_2)
- self.assertEqual(intervals[1][2]['attendances'], self.att_3)
-
- def test_calendar_working_day_intervals_leaves_generic(self):
- # Test: day0 with leaves outside range: 1 interval
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-12'), start_time=time(7, 0, 0), compute_leaves=True)
- self.assertEqual(len(intervals), 1)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-12 08:00:00'), Datetime.from_string('2013-02-12 16:00:00')))
-
- # Test: day0 with leaves: 2 intervals because of leave between 9 and 12, ending at 15:45:30
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-19'),
- start_time=time(8, 15, 0),
- end_time=time(15, 45, 30),
- compute_leaves=True)
- self.assertEqual(len(intervals), 2)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-19 08:15:00'), Datetime.from_string('2013-02-19 09:00:00')))
- self.assertEqual(intervals[1][:2], (Datetime.from_string('2013-02-19 12:00:00'), Datetime.from_string('2013-02-19 15:45:30')))
- self.assertEqual(intervals[0][2]['attendances'], self.att_1)
- self.assertEqual(intervals[0][2]['leaves'], self.leave1)
- self.assertEqual(intervals[1][2]['attendances'], self.att_1)
- self.assertEqual(intervals[0][2]['leaves'], self.leave1)
-
- def test_calendar_working_day_intervals_leaves_resource(self):
- # Test: day1+14 on leave, with resource leave computation
- intervals = self.calendar._get_day_work_intervals(
- Date.from_string('2013-02-26'),
- start_time=time(7, 0, 0),
- compute_leaves=True,
- resource_id=self.resource1_id
+ check([(10, 20)], [(5, 8)], [])
+ check([(10, 20)], [(5, 10)], [])
+ check([(10, 20)], [(5, 15)], [(10, 15)])
+ check([(10, 20)], [(5, 20)], [(10, 20)])
+ check([(10, 20)], [(5, 25)], [(10, 20)])
+ check([(10, 20)], [(10, 15)], [(10, 15)])
+ check([(10, 20)], [(10, 20)], [(10, 20)])
+ check([(10, 20)], [(10, 25)], [(10, 20)])
+ check([(10, 20)], [(15, 18)], [(15, 18)])
+ check([(10, 20)], [(15, 20)], [(15, 20)])
+ check([(10, 20)], [(15, 25)], [(15, 20)])
+ check([(10, 20)], [(20, 25)], [])
+ check(
+ [(0, 5), (10, 15), (20, 25), (30, 35)],
+ [(6, 7), (9, 12), (13, 17), (22, 23), (24, 40)],
+ [(10, 12), (13, 15), (22, 23), (24, 25), (30, 35)],
)
- # Result: nothing, because on leave
- self.assertEqual(len(intervals), 0)
- def test_calendar_working_day_intervals_limited_attendances(self):
- """ Test attendances limited in time. """
- attendance = self.env['resource.calendar.attendance'].search(
- [('name', '=', 'Att3')])
- attendance.write({
- 'date_from': self.date2 + relativedelta(days=7),
- 'date_to': False,
+ def test_difference(self):
+ def check(a, b, c):
+ a, b, c = self.ints(a), self.ints(b), self.ints(c)
+ self.assertEqual(list(Intervals(a) - Intervals(b)), c)
+
+ check([(10, 20)], [(5, 8)], [(10, 20)])
+ check([(10, 20)], [(5, 10)], [(10, 20)])
+ check([(10, 20)], [(5, 15)], [(15, 20)])
+ check([(10, 20)], [(5, 20)], [])
+ check([(10, 20)], [(5, 25)], [])
+ check([(10, 20)], [(10, 15)], [(15, 20)])
+ check([(10, 20)], [(10, 20)], [])
+ check([(10, 20)], [(10, 25)], [])
+ check([(10, 20)], [(15, 18)], [(10, 15), (18, 20)])
+ check([(10, 20)], [(15, 20)], [(10, 15)])
+ check([(10, 20)], [(15, 25)], [(10, 15)])
+ check([(10, 20)], [(20, 25)], [(10, 20)])
+ check(
+ [(0, 5), (10, 15), (20, 25), (30, 35)],
+ [(6, 7), (9, 12), (13, 17), (22, 23), (24, 40)],
+ [(0, 5), (12, 13), (20, 22), (23, 24)],
+ )
+
+
+class TestErrors(TestResourceCommon):
+ def setUp(self):
+ super(TestErrors, self).setUp()
+
+ def test_create_negative_leave(self):
+ # from > to
+ with self.assertRaises(ValidationError):
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'error cannot return in the past',
+ 'resource_id': False,
+ 'calendar_id': self.calendar_jean.id,
+ 'date_from': datetime_str(2018, 4, 3, 20, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 3, 0, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ with self.assertRaises(ValidationError):
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'error caused by timezones',
+ 'resource_id': False,
+ 'calendar_id': self.calendar_jean.id,
+ 'date_from': datetime_str(2018, 4, 3, 10, 0, 0, tzinfo='UTC'),
+ 'date_to': datetime_str(2018, 4, 3, 12, 0, 0, tzinfo='Etc/GMT-6')
+ })
+
+
+class TestCalendar(TestResourceCommon):
+ def setUp(self):
+ super(TestCalendar, self).setUp()
+
+ def test_get_work_hours_count(self):
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'Global Leave',
+ 'resource_id': False,
+ 'calendar_id': self.calendar_jean.id,
+ 'date_from': datetime_str(2018, 4, 3, 0, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 3, 23, 59, 59, tzinfo=self.jean.tz),
})
- intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
- self.assertEqual(len(intervals), 1)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
- attendance.write({
- 'date_from': False,
- 'date_to': self.date2 - relativedelta(days=7),
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'leave for Jean',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 5, 0, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 5, 23, 59, 59, tzinfo=self.jean.tz),
})
- intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
- self.assertEqual(len(intervals), 1)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
- attendance.write({
- 'date_from': self.date2 + relativedelta(days=7),
- 'date_to': self.date2 - relativedelta(days=7),
+ hours = self.calendar_jean.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 59, 59, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(hours, 32)
+
+ hours = self.calendar_jean.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 59, 59, tzinfo=self.jean.tz),
+ compute_leaves=False,
+ )
+ self.assertEqual(hours, 40)
+
+ # leave of size 0
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'zero_length',
+ 'calendar_id': self.calendar_patel.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 3, 0, 0, 0, tzinfo=self.patel.tz),
+ 'date_to': datetime_str(2018, 4, 3, 0, 0, 0, tzinfo=self.patel.tz),
})
- intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
- self.assertEqual(len(intervals), 1)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
- attendance.write({
- 'date_from': self.date2,
- 'date_to': self.date2,
- })
- intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
- self.assertEqual(len(intervals), 2)
- self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
- self.assertEqual(intervals[1][:2], (Datetime.from_string('2013-02-15 16:00:00'), Datetime.from_string('2013-02-15 23:00:00')))
+ hours = self.calendar_patel.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 6, 23, 59, 59, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(hours, 35)
- def test_calendar_working_hours_of_date(self):
- # Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00
- wh = self.calendar.get_work_hours_count(Datetime.from_string('2013-02-19 10:30:00'), Datetime.from_string('2013-02-19 18:00:00'), self.resource1_id, compute_leaves=False)
- self.assertEqual(wh, 5.5)
-
-
-class ResourceWorkingHours(TestResourceCommon):
-
- def test_calendar_working_hours(self):
- # new API: resource without leaves
- # res: 2 weeks -> 40 hours
- res = self.calendar.get_work_hours_count(
- Datetime.from_string('2013-02-12 06:00:00'),
- Datetime.from_string('2013-02-22 23:00:00'),
- self.resource1_id,
- compute_leaves=False)
- self.assertEqual(res, 40.0)
-
- def test_calendar_working_hours_leaves(self):
- # new API: resource and leaves
- # res: 2 weeks -> 40 hours - (3+4) leave hours
- res = self.calendar.get_work_hours_count(
- Datetime.from_string('2013-02-12 06:00:00'),
- Datetime.from_string('2013-02-22 23:00:00'),
- self.resource1_id,
- compute_leaves=True)
- self.assertEqual(res, 33.0)
-
- def test_calendar_timezone(self):
- # user in timezone UTC-9 asks for work hours
- # Limits: between 2013-02-19 10:00:00 and 2013-02-26 15:30:00 (User TZ)
- # between 2013-02-19 19:00:00 and 2013-02-27 00:30:00 (UTC)
- # Leaves: between 2013-02-21 10:00:00 and 2013-02-26 12:00:00 (User TZ)
- # res: 19/02 (10-16 (beginning)) + 22/02 (0 (leave)) + 26/02 (12-15.30 (leave+ending))
- self.env.user.tz = 'US/Alaska'
- (self.leave1 | self.leave2 | self.leave3).unlink()
+ # leave of medium size
leave = self.env['resource.calendar.leaves'].create({
- 'name': 'Timezoned Leaves',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': to_naive_utc(Datetime.from_string('2013-02-21 10:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2013-02-26 12:00:00'), self.env.user)
+ 'name': 'zero_length',
+ 'calendar_id': self.calendar_patel.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 3, 9, 0, 0, tzinfo=self.patel.tz),
+ 'date_to': datetime_str(2018, 4, 3, 12, 0, 0, tzinfo=self.patel.tz),
})
- res = self.calendar.get_work_hours_count(
- to_naive_utc(Datetime.from_string('2013-02-19 10:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-26 15:30:00'), self.env.user),
- self.resource1_id,
- compute_leaves=True)
- self.assertEqual(res, 9.5)
- def test_calendar_leave_intervals_timezone(self):
- # _iter_leave_intervals takes UTC and outputs UTC
- # Leave is taken by user in US/Alaska timezone
- # It should be visible at the right time for every timezone
- self.env.user.tz = 'US/Alaska'
- (self.leave1 | self.leave2 | self.leave3).unlink()
+ hours = self.calendar_patel.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 6, 23, 59, 59, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(hours, 32)
+
+ leave.unlink()
+
+ # leave of very small size
leave = self.env['resource.calendar.leaves'].create({
- 'name': 'Timezoned Leaves',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': to_naive_utc(Datetime.from_string('2013-02-21 10:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2013-02-26 12:00:00'), self.env.user)
+ 'name': 'zero_length',
+ 'calendar_id': self.calendar_patel.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 3, 0, 0, 0, tzinfo=self.patel.tz),
+ 'date_to': datetime_str(2018, 4, 3, 0, 0, 10, tzinfo=self.patel.tz),
})
- def to_tuple(interval):
- return (interval.start_datetime, interval.end_datetime)
+ hours = self.calendar_patel.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 6, 23, 59, 59, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(hours, 35)
- # Checking for 'Europe/Brussels'
- self.env.user.tz = 'Europe/Brussels'
- leaves = list(self.calendar._iter_leave_intervals(
- to_naive_utc(Datetime.from_string('2013-02-26 14:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-26 15:30:00'), self.env.user),
- self.resource1_id))
- self.assertEqual(len(leaves), 0)
+ leave.unlink()
- leaves = list(self.calendar._iter_leave_intervals(
- to_naive_utc(Datetime.from_string('2013-02-26 08:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-26 11:00:00'), self.env.user),
- self.resource1_id))[0]
+ # no timezone given should be converted to UTC
+ # Should equal to a leave between 2018/04/03 10:00:00 and 2018/04/04 10:00:00
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'no timezone',
+ 'calendar_id': self.calendar_patel.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 3, 4, 0, 0),
+ 'date_to': datetime_str(2018, 4, 4, 4, 0, 0),
+ })
+
+ hours = self.calendar_patel.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 6, 23, 59, 59, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(hours, 28)
+
+ hours = self.calendar_patel.get_work_hours_count(
+ datetime_tz(2018, 4, 2, 23, 59, 59, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(hours, 0)
+
+ leave.unlink()
+
+ def test_plan_hours(self):
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'global',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 11, 0, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 11, 23, 59, 59, tzinfo=self.jean.tz),
+ })
+
+ time = self.calendar_jean.plan_hours(2, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=False)
+ self.assertEqual(time, datetime_tz(2018, 4, 10, 10, 0, 0, tzinfo=self.jean.tz))
+
+ time = self.calendar_jean.plan_hours(20, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=False)
+ self.assertEqual(time, datetime_tz(2018, 4, 12, 12, 0, 0, tzinfo=self.jean.tz))
+
+ time = self.calendar_jean.plan_hours(5, datetime_tz(2018, 4, 10, 15, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 4, 12, 12, 0, 0, tzinfo=self.jean.tz))
+
+ # negative planning
+ time = self.calendar_jean.plan_hours(-10, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 4, 6, 14, 0, 0, tzinfo=self.jean.tz))
+
+ # zero planning
+ time = self.calendar_jean.plan_hours(0, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz))
+
+ # very small planning
+ time = self.calendar_jean.plan_hours(0.0002, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 4, 10, 8, 0, 0, 720000, tzinfo=self.jean.tz))
+
+ # huge planning
+ time = self.calendar_jean.plan_hours(3000, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=False)
+ self.assertEqual(time, datetime_tz(2019, 9, 16, 16, 0, 0, tzinfo=self.jean.tz))
+
+ def test_plan_days(self):
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'global',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 11, 0, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 11, 23, 59, 59, tzinfo=self.jean.tz),
+ })
+
+ time = self.calendar_jean.plan_days(1, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=False)
+ self.assertEqual(time, datetime_tz(2018, 4, 10, 16, 0, 0, tzinfo=self.jean.tz))
+
+ time = self.calendar_jean.plan_days(3, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=False)
+ self.assertEqual(time, datetime_tz(2018, 4, 12, 16, 0, 0, tzinfo=self.jean.tz))
+
+ time = self.calendar_jean.plan_days(4, datetime_tz(2018, 4, 10, 16, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 4, 17, 16, 0, 0, tzinfo=self.jean.tz))
+
+ # negative planning
+ time = self.calendar_jean.plan_days(-10, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 3, 27, 8, 0, 0, tzinfo=self.jean.tz))
+
+ # zero planning
+ time = self.calendar_jean.plan_days(0, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz))
+
+ # very small planning returns False in this case
+ # TODO: decide if this behaviour is alright
+ time = self.calendar_jean.plan_days(0.0002, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=True)
+ self.assertEqual(time, False)
+
+ # huge planning
+ # TODO: Same as above
+ # NOTE: Maybe allow to set a max limit to the method
+ time = self.calendar_jean.plan_days(3000, datetime_tz(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz), compute_leaves=False)
+ self.assertEqual(time, False)
+
+
+class TestResMixin(TestResourceCommon):
+ def setUp(self):
+ super(TestResMixin, self).setUp()
+
+ def test_work_days_data(self):
+ # Looking at Jean's calendar
+
+ # Viewing it as Jean
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data, {'days': 5, 'hours': 40})
+
+ # Viewing it as Patel
+ # Views from 2018/04/01 20:00:00 to 2018/04/06 12:00:00
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(data, {'days': 4.5, 'hours': 36}) # We see only 36 hours
+
+ # Viewing it as John
+ # Views from 2018/04/02 09:00:00 to 2018/04/07 02:00:00
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 6, 16, 0, 0, tzinfo=self.john.tz),
+ )
+ # still showing as 5 days because of rounding, but we see only 39 hours
+ self.assertEqual(data, {'days': 5, 'hours': 39})
+
+ # Looking at John's calendar
+
+ # Viewing it as Jean
+ # Views from 2018/04/01 15:00:00 to 2018/04/06 14:00:00
+ data = self.john.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data, {'days': 1.5, 'hours': 13})
+
+ # Viewing it as Patel
+ # Views from 2018/04/01 11:00:00 to 2018/04/06 10:00:00
+ data = self.john.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(data, {'days': 1.25, 'hours': 10})
+
+ # Viewing it as John
+ data = self.john.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.john.tz),
+ )
+ self.assertEqual(data, {'days': 2, 'hours': 20})
+
+ # using Jean as a timezone reference
+ data = self.john.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.john.tz),
+ calendar=self.calendar_jean,
+ )
+ self.assertEqual(data, {'days': 5, 'hours': 40})
+
+ # half days
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'half',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 14, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data, {'days': 4.5, 'hours': 36})
+
+ # using John as a timezone reference, leaves are outside attendances
+ data = self.john.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.john.tz),
+ calendar=self.calendar_jean,
+ )
+ self.assertEqual(data, {'days': 5, 'hours': 40})
+
+ leave.unlink()
+
+ # leave size 0
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'zero',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data, {'days': 5, 'hours': 40})
+
+ leave.unlink()
+
+ # leave very small size
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'small',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 1, tzinfo=self.jean.tz),
+ })
+
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data['days'], 5)
+ self.assertAlmostEqual(data['hours'], 40, 2)
+
+ def test_leaves_days_data(self):
+ # Jean takes a leave
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'Jean is visiting India',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 10, 8, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 10, 16, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ # John takes a leave for Jean
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'Jean is comming in USA',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 12, 8, 0, 0, tzinfo=self.john.tz),
+ 'date_to': datetime_str(2018, 4, 12, 16, 0, 0, tzinfo=self.john.tz),
+ })
+
+ # Jean asks to see how much leave he has taken
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.jean.tz),
+ )
+ # Sees only 1 day and 8 hours because, as john is in UTC-7 the second leave is not in
+ # the attendances of Jean
+ self.assertEqual(data, {'days': 1, 'hours': 8})
+
+ # Patel Asks to see when Jean has taken some leaves
+ # Patel should see the same
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.patel.tz),
+ )
+ self.assertEqual(data, {'days': 1, 'hours': 8})
+
+ # use Patel as a resource, jean's leaves are not visible
+ datas = self.patel.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.patel.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.patel.tz),
+ calendar=self.calendar_jean,
+ )
+ self.assertEqual(datas['days'], 0)
+ self.assertEqual(datas['hours'], 0)
+
+ # Jean takes a leave for John
+ # Gives 3 hours (3/8 of a day)
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'John is sick',
+ 'calendar_id': self.john.resource_calendar_id.id,
+ 'resource_id': self.john.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 10, 20, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ # John takes a leave
+ # Gives all day (12 hours)
+ self.env['resource.calendar.leaves'].create({
+ 'name': 'John goes to holywood',
+ 'calendar_id': self.john.resource_calendar_id.id,
+ 'resource_id': self.john.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 13, 7, 0, 0, tzinfo=self.john.tz),
+ 'date_to': datetime_str(2018, 4, 13, 18, 0, 0, tzinfo=self.john.tz),
+ })
+
+ # John asks how much leaves he has
+ # He sees that he has only 15 hours of leave in his attendances
+ data = self.john.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.john.tz),
+ )
+ self.assertEqual(data, {'days': 1, 'hours': 10})
+
+ # half days
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'half',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 14, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data, {'days': 0.5, 'hours': 4})
+
+ leave.unlink()
+
+ # leave size 0
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'zero',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data, {'days': 0, 'hours': 0})
+
+ leave.unlink()
+
+ # leave very small size
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'small',
+ 'calendar_id': self.calendar_jean.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 1, tzinfo=self.jean.tz),
+ })
+
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(data['days'], 0)
+ self.assertAlmostEqual(data['hours'], 0, 2)
+
+ leave.unlink()
+
+ def test_list_leaves(self):
+ jean_leave = self.env['resource.calendar.leaves'].create({
+ 'name': "Jean's son is sick",
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': False,
+ 'date_from': datetime_str(2018, 4, 10, 0, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 10, 23, 59, 59, tzinfo=self.jean.tz),
+ })
+
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(leaves, [(date(2018, 4, 10), 8, jean_leave)])
+
+ # half days
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'half',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 14, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(leaves, [(date(2018, 4, 2), 4, leave)])
+
+ leave.unlink()
+
+ # very small size
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'small',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 1, tzinfo=self.jean.tz),
+ })
+
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
self.assertEqual(len(leaves), 1)
- self.assertEqual(to_tuple(leaves[0]), (Datetime.from_string('2013-02-26 07:00:00'), Datetime.from_string('2013-02-26 10:00:00')))
+ self.assertEqual(leaves[0][0], date(2018, 4, 2))
+ self.assertAlmostEqual(leaves[0][1], 0, 2)
+ self.assertEqual(leaves[0][2].id, leave.id)
- leaves = list(self.calendar._iter_leave_intervals(
- to_naive_utc(Datetime.from_string('2013-02-22 08:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-22 20:00:00'), self.env.user),
- self.resource1_id))[0]
- self.assertEqual(len(leaves), 2)
- self.assertEqual(to_tuple(leaves[0]), (Datetime.from_string('2013-02-22 07:00:00'), Datetime.from_string('2013-02-22 12:00:00')))
+ leave.unlink()
- # Checking for 'Japan'
- self.env.user.tz = 'Japan'
- leaves = list(self.calendar._iter_leave_intervals(
- to_naive_utc(Datetime.from_string('2013-02-26 14:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-26 15:30:00'), self.env.user),
- self.resource1_id))
- self.assertEqual(len(leaves), 0)
-
- leaves = list(self.calendar._iter_leave_intervals(
- to_naive_utc(Datetime.from_string('2013-02-26 08:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-26 11:00:00'), self.env.user),
- self.resource1_id))[0]
- self.assertEqual(len(leaves), 1)
- self.assertEqual(to_tuple(leaves[0]), (Datetime.from_string('2013-02-25 23:00:00'), Datetime.from_string('2013-02-26 02:00:00')))
-
- leaves = list(self.calendar._iter_leave_intervals(
- to_naive_utc(Datetime.from_string('2013-02-22 08:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-22 20:00:00'), self.env.user),
- self.resource1_id))[0]
- self.assertEqual(len(leaves), 2)
- self.assertEqual(to_tuple(leaves[0]), (Datetime.from_string('2013-02-21 23:00:00'), Datetime.from_string('2013-02-22 04:00:00')))
-
- def test_calendar_work_days_intervals_timezone(self):
- # _get_day_work_intervals converts from the timezone
- # of the user into UTC
- self.env.user.tz = 'US/Alaska'
- (self.leave1 | self.leave2 | self.leave3).unlink()
+ # size 0
leave = self.env['resource.calendar.leaves'].create({
- 'name': 'Timezoned Leaves',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': to_naive_utc(Datetime.from_string('2013-02-21 10:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2013-02-26 12:00:00'), self.env.user)
+ 'name': 'zero',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
})
- def to_tuple(interval):
- return (interval.start_datetime, interval.end_datetime)
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(leaves, [])
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-26'), time(10), time(18), False, self.resource1_id)
- self.assertEqual(to_tuple(intervals[0]), (Datetime.from_string('2013-02-26 19:00:00'), Datetime.from_string('2013-02-27 01:00:00')))
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-26'), time(10), time(18), True, self.resource1_id)
- self.assertEqual(to_tuple(intervals[0]), (Datetime.from_string('2013-02-26 21:00:00'), Datetime.from_string('2013-02-27 01:00:00')))
+ leave.unlink()
- self.env.user.tz = 'Japan'
+ def test_list_work_time_per_day(self):
+ working_time = self.john.list_work_time_per_day(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.john.tz),
+ )
+ self.assertEqual(working_time, [
+ (date(2018, 4, 10), 8),
+ (date(2018, 4, 13), 12),
+ ])
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-26'), time(10), time(18), False, self.resource1_id)
- self.assertEqual(to_tuple(intervals[0]), (Datetime.from_string('2013-02-26 01:00:00'), Datetime.from_string('2013-02-26 07:00:00')))
- intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-26'), time(10), time(18), True, self.resource1_id)
- self.assertEqual(to_tuple(intervals[0]), (Datetime.from_string('2013-02-26 03:00:00'), Datetime.from_string('2013-02-26 07:00:00')))
+ # change john's resource's timezone
+ self.john.resource_id.tz = 'Europe/Brussels'
+ self.assertEqual(self.john.tz, 'Europe/Brussels')
+ self.assertEqual(self.calendar_john.tz, 'America/Los_Angeles')
+ working_time = self.john.list_work_time_per_day(
+ datetime_tz(2018, 4, 9, 0, 0, 0, tzinfo=self.john.tz),
+ datetime_tz(2018, 4, 13, 23, 59, 59, tzinfo=self.john.tz),
+ )
+ self.assertEqual(working_time, [
+ (date(2018, 4, 10), 8),
+ (date(2018, 4, 13), 12),
+ ])
- def test_calendar_leave_days_intervals_timezone(self):
- # _get_day_leave_intervals converts from the timezone
- # of the user into UTC
- self.env.user.tz = 'US/Alaska'
- (self.leave1 | self.leave2 | self.leave3).unlink()
+ # half days
leave = self.env['resource.calendar.leaves'].create({
- 'name': 'Timezoned Leaves',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': to_naive_utc(Datetime.from_string('2013-02-21 10:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2013-02-26 12:00:00'), self.env.user)
+ 'name': 'small',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 14, 0, 0, tzinfo=self.jean.tz),
})
- def to_tuple(interval):
- return (interval.start_datetime, interval.end_datetime)
-
- intervals = self.calendar._get_day_leave_intervals(Date.from_string('2013-02-26'), time(10), time(18), self.resource1_id)
- self.assertEqual(to_tuple(intervals[0]), (Datetime.from_string('2013-02-26 19:00:00'), Datetime.from_string('2013-02-26 21:00:00')))
-
- self.env.user.tz = 'Japan'
-
- intervals = self.calendar._get_day_leave_intervals(Date.from_string('2013-02-26'), time(10), time(18), self.resource1_id)
- self.assertEqual(to_tuple(intervals[0]), (Datetime.from_string('2013-02-26 01:00:00'), Datetime.from_string('2013-02-26 03:00:00')))
-
- def test_calendar_hours_scheduling_backward(self):
- res = self.calendar._schedule_hours(-40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
- # current day, limited at 09:00 because of day_dt specified -> 1 hour
- self.assertEqual(res[-1][:2], (Datetime.from_string('2013-02-12 08:00:00'), Datetime.from_string('2013-02-12 09:00:00')))
- # previous days: 5+7 hours / 8 hours / 5+7 hours -> 32 hours
- self.assertEqual(res[-2][:2], (Datetime.from_string('2013-02-08 16:00:00'), Datetime.from_string('2013-02-08 23:00:00')))
- self.assertEqual(res[-3][:2], (Datetime.from_string('2013-02-08 08:00:00'), Datetime.from_string('2013-02-08 13:00:00')))
- self.assertEqual(res[-4][:2], (Datetime.from_string('2013-02-05 08:00:00'), Datetime.from_string('2013-02-05 16:00:00')))
- self.assertEqual(res[-5][:2], (Datetime.from_string('2013-02-01 16:00:00'), Datetime.from_string('2013-02-01 23:00:00')))
- self.assertEqual(res[-6][:2], (Datetime.from_string('2013-02-01 08:00:00'), Datetime.from_string('2013-02-01 13:00:00')))
- # 7 hours remaining
- self.assertEqual(res[-7][:2], (Datetime.from_string('2013-01-29 09:00:00'), Datetime.from_string('2013-01-29 16:00:00')))
-
- # Compute scheduled hours
- td = timedelta()
- for item in res:
- td += item[1] - item[0]
- self.assertEqual(td.total_seconds() / 3600.0, 40.0)
-
- res = self.calendar.plan_hours(-40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
- self.assertEqual(res, Datetime.from_string('2013-01-29 09:00:00'))
-
- def test_calendar_hours_scheduling_forward(self):
- res = self.calendar._schedule_hours(40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
- self.assertEqual(res[0][:2], (Datetime.from_string('2013-02-12 09:00:00'), Datetime.from_string('2013-02-12 16:00:00')))
- self.assertEqual(res[1][:2], (Datetime.from_string('2013-02-15 08:00:00'), Datetime.from_string('2013-02-15 13:00:00')))
- self.assertEqual(res[2][:2], (Datetime.from_string('2013-02-15 16:00:00'), Datetime.from_string('2013-02-15 23:00:00')))
- self.assertEqual(res[3][:2], (Datetime.from_string('2013-02-19 08:00:00'), Datetime.from_string('2013-02-19 16:00:00')))
- self.assertEqual(res[4][:2], (Datetime.from_string('2013-02-22 08:00:00'), Datetime.from_string('2013-02-22 13:00:00')))
- self.assertEqual(res[5][:2], (Datetime.from_string('2013-02-22 16:00:00'), Datetime.from_string('2013-02-22 23:00:00')))
- self.assertEqual(res[6][:2], (Datetime.from_string('2013-02-26 08:00:00'), Datetime.from_string('2013-02-26 09:00:00')))
-
- td = timedelta()
- for item in res:
- td += item[1] - item[0]
- self.assertEqual(td.total_seconds() / 3600.0, 40.0)
-
- res = self.calendar.plan_hours(40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
- self.assertEqual(res, Datetime.from_string('2013-02-26 09:00:00'))
-
- def test_calendar_hours_scheduling_timezone(self):
- # user in timezone UTC-9 asks for work hours
- self.env.user.tz = 'US/Alaska'
- res = self.calendar.plan_hours(
- 42,
- to_naive_utc(Datetime.from_string('2013-02-12 09:25:00'), self.env.user))
- self.assertEqual(res, to_naive_utc(Datetime.from_string('2013-02-26 11:25:00'), self.env.user))
-
- def test_calendar_hours_scheduling_timezone_2(self):
- # Call schedule_hours for a user in Autralia, Sydney (GMT+10)
- # Two cases:
- # - start at 2013-02-15 08:00:00 => 2013-02-14 21:00:00 in UTC
- # - start at 2013-02-15 11:00:00 => 2013-02-15 00:00:00 in UTC
- self.env.user.tz = 'Australia/Sydney'
- self.env['resource.calendar.attendance'].create({
- 'name': 'Day3 - 1',
- 'dayofweek': '3',
- 'hour_from': 8,
- 'hour_to': 12,
- 'calendar_id': self.calendar.id,
- })
- self.env['resource.calendar.attendance'].create({
- 'name': 'Day3 - 2',
- 'dayofweek': '3',
- 'hour_from': 13,
- 'hour_to': 17,
- 'calendar_id': self.calendar.id,
- })
- hours = 1.0/60.0
- for test_date in ['2013-02-15 08:00:00', '2013-02-15 11:00:00']:
- start_dt = Datetime.from_string(test_date)
- start_dt_utc = to_naive_utc(start_dt, self.env.user)
- res = self.calendar._schedule_hours(hours, start_dt_utc)
- self.assertEqual(
- [(start_dt_utc, start_dt_utc.replace(minute=1))], res,
- 'resource_calendar: wrong schedule_hours computation')
-
- def test_calendar_hours_scheduling_forward_leaves_resource(self):
- res = self.calendar._schedule_hours(
- 40, day_dt=Datetime.from_string('2013-02-12 09:00:00'),
- compute_leaves=True, resource_id=self.resource1_id
+ working_time = self.jean.list_work_time_per_day(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
)
- self.assertEqual(res[0][:2], (Datetime.from_string('2013-02-12 09:00:00'), Datetime.from_string('2013-02-12 16:00:00')))
- self.assertEqual(res[1][:2], (Datetime.from_string('2013-02-15 08:00:00'), Datetime.from_string('2013-02-15 13:00:00')))
- self.assertEqual(res[2][:2], (Datetime.from_string('2013-02-15 16:00:00'), Datetime.from_string('2013-02-15 23:00:00')))
- self.assertEqual(res[3][:2], (Datetime.from_string('2013-02-19 08:00:00'), Datetime.from_string('2013-02-19 09:00:00')))
- self.assertEqual(res[4][:2], (Datetime.from_string('2013-02-19 12:00:00'), Datetime.from_string('2013-02-19 16:00:00')))
- self.assertEqual(res[5][:2], (Datetime.from_string('2013-02-22 08:00:00'), Datetime.from_string('2013-02-22 09:00:00')))
- self.assertEqual(res[6][:2], (Datetime.from_string('2013-02-22 16:00:00'), Datetime.from_string('2013-02-22 23:00:00')))
- self.assertEqual(res[7][:2], (Datetime.from_string('2013-03-01 11:30:00'), Datetime.from_string('2013-03-01 13:00:00')))
- self.assertEqual(res[8][:2], (Datetime.from_string('2013-03-01 16:00:00'), Datetime.from_string('2013-03-01 22:30:00')))
+ self.assertEqual(working_time, [
+ (date(2018, 4, 2), 4),
+ (date(2018, 4, 3), 8),
+ (date(2018, 4, 4), 8),
+ (date(2018, 4, 5), 8),
+ (date(2018, 4, 6), 8),
+ ])
- td = timedelta()
- for item in res:
- td += item[1] - item[0]
- self.assertEqual(td.total_seconds() / 3600.0, 40.0)
+ leave.unlink()
- def test_calendar_days_scheduling(self):
- res = self.calendar.plan_days(5, Datetime.from_string('2013-02-12 09:08:07') )
- self.assertEqual(res.date(), Datetime.from_string('2013-02-26 00:00:00').date(), 'resource_calendar: wrong days scheduling')
- res = self.calendar.plan_days(-2, Datetime.from_string('2013-02-12 09:08:07') )
- self.assertEqual(res.date(), Datetime.from_string('2013-02-08 00:00:00').date(), 'resource_calendar: wrong days scheduling')
+ # very small size
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'small',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 1, tzinfo=self.jean.tz),
+ })
- res = self.calendar.plan_days(
- 5, Datetime.from_string('2013-02-12 09:08:07'),
- compute_leaves=True, resource_id=self.resource1_id)
- self.assertEqual(res.date(), Datetime.from_string('2013-03-01 00:00:00').date(), 'resource_calendar: wrong days scheduling')
+ working_time = self.jean.list_work_time_per_day(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(len(working_time), 5)
+ self.assertEqual(working_time[0][0], date(2018, 4, 2))
+ self.assertAlmostEqual(working_time[0][1], 8, 2)
- def test_calendar_days_scheduling_timezone(self):
- self.env.user.tz = 'US/Alaska'
- res = self.calendar.plan_days(5, to_naive_utc(Datetime.from_string('2013-02-12 09:08:07'), self.env.user))
- self.assertEqual(to_naive_user_tz(res, self.env.user).date(), Datetime.from_string('2013-02-26 00:00:00').date())
+ leave.unlink()
+
+ # size 0
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': 'zero',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ 'date_to': datetime_str(2018, 4, 2, 10, 0, 0, tzinfo=self.jean.tz),
+ })
+
+ working_time = self.jean.list_work_time_per_day(
+ datetime_tz(2018, 4, 2, 0, 0, 0, tzinfo=self.jean.tz),
+ datetime_tz(2018, 4, 6, 23, 0, 0, tzinfo=self.jean.tz),
+ )
+ self.assertEqual(working_time, [
+ (date(2018, 4, 2), 8),
+ (date(2018, 4, 3), 8),
+ (date(2018, 4, 4), 8),
+ (date(2018, 4, 5), 8),
+ (date(2018, 4, 6), 8),
+ ])
+
+ leave.unlink()
-WAR_START = date(1932, 11, 2)
-WAR_END = date(1932, 12, 10)
-
-
-class TestWorkDays(TestResourceCommon):
-
- 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
-
+class TestTimezones(TestResourceCommon):
def setUp(self):
- super(TestWorkDays, self).setUp()
- # trivial 5/7 9-17 resource calendar
- self.calendar.write({
- 'attendance_ids': [
- (0, 0, self._make_attendance(i))
- for i in range(5)
- ]
- })
+ super(TestTimezones, self).setUp()
- self._days = [dt.date() for dt in rrule.rrule(rrule.DAILY, dtstart=WAR_START, until=WAR_END)]
+ self.tz1 = 'Etc/GMT+6'
+ self.tz2 = 'Europe/Brussels'
+ self.tz3 = 'Etc/GMT-10'
+ self.tz4 = 'Etc/GMT+10'
- 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.calendar_id._iter_work_days(WAR_START, WAR_END, r.id))
+ def test_work_hours_count(self):
+ # When no timezone => UTC
+ count = self.calendar_jean.get_work_hours_count(
+ datetime_tz(2018, 4, 10, 8, 0, 0),
+ datetime_tz(2018, 4, 10, 12, 0, 0),
)
+ self.assertEqual(count, 4)
- def test_global_leaves(self):
+ # This timezone is not the same as the calendar's one
+ count = self.calendar_jean.get_work_hours_count(
+ datetime_tz(2018, 4, 10, 8, 0, 0, tzinfo=self.tz1),
+ datetime_tz(2018, 4, 10, 12, 0, 0, tzinfo=self.tz1),
+ )
+ self.assertEqual(count, 0)
+
+ # Using two different timezones
+ # 10-04-2018 06:00:00 - 10-04-2018 02:00:00
+ count = self.calendar_jean.get_work_hours_count(
+ datetime_tz(2018, 4, 10, 8, 0, 0, tzinfo=self.tz2),
+ datetime_tz(2018, 4, 10, 12, 0, 0, tzinfo=self.tz3),
+ )
+ self.assertEqual(count, 0)
+
+ # Using two different timezones
+ # 2018-4-10 06:00:00 - 2018-4-10 22:00:00
+ count = self.calendar_jean.get_work_hours_count(
+ datetime_tz(2018, 4, 10, 8, 0, 0, tzinfo=self.tz2),
+ datetime_tz(2018, 4, 10, 12, 0, 0, tzinfo=self.tz4),
+ )
+ self.assertEqual(count, 8)
+
+ def test_plan_hours(self):
+ dt = self.calendar_jean.plan_hours(10, datetime_tz(2018, 4, 10, 8, 0, 0))
+ self.assertEqual(dt, datetime_tz(2018, 4, 11, 12, 0, 0, tzinfo=self.tz2))
+
+ dt = self.calendar_jean.plan_hours(10, datetime_tz(2018, 4, 10, 8, 0, 0, tzinfo=self.tz4))
+ self.assertEqual(dt, datetime_tz(2018, 4, 12, 10, 0, 0, tzinfo=self.tz2))
+
+ def test_plan_days(self):
+ dt = self.calendar_jean.plan_days(2, datetime_tz(2018, 4, 10, 8, 0, 0))
+ self.assertEqual(dt, datetime_tz(2018, 4, 11, 16, 0, 0, tzinfo=self.tz2))
+
+ # We lose one day because of timezone
+ dt = self.calendar_jean.plan_days(2, datetime_tz(2018, 4, 10, 8, 0, 0, tzinfo=self.tz4))
+ self.assertEqual(dt, datetime_tz(2018, 4, 12, 16, 0, 0, tzinfo=self.tz2))
+
+ def test_work_data(self):
+ # 09-04-2018 10:00:00 - 13-04-2018 18:00:00
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 9, 8, 0, 0),
+ datetime_tz(2018, 4, 13, 16, 0, 0),
+ )
+ self.assertEqual(data, {'days': 4.75, 'hours': 38})
+
+ # 09-04-2018 00:00:00 - 13-04-2018 08:00:00
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz3),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz3),
+ )
+ self.assertEqual(data, {'days': 4, 'hours': 32})
+
+ # 09-04-2018 08:00:00 - 14-04-2018 12:00:00
+ data = self.jean.get_work_days_data(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz4),
+ )
+ self.assertEqual(data, {'days': 5, 'hours': 40})
+
+ def test_leave_data(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',
+ 'name': '',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2),
+ 'date_to': datetime_str(2018, 4, 9, 14, 0, 0, tzinfo=self.tz2),
})
- 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.calendar_id._iter_work_days(WAR_START, WAR_END, r1.id)))
- self.assertEqual(days, list(r2.calendar_id._iter_work_days(WAR_START, WAR_END, r2.id)))
-
- 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.calendar_id._iter_work_days(WAR_START, WAR_END, r1.id)))
- self.assertEqual([
- d for d in weekdays if d < date(1932, 11, 9) or d > date(1932, 11, 12)],
- list(r2.calendar_id._iter_work_days(WAR_START, WAR_END, r2.id))
+ # 09-04-2018 10:00:00 - 13-04-2018 18:00:00
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 8, 0, 0),
+ datetime_tz(2018, 4, 13, 16, 0, 0),
)
+ self.assertEqual(data, {'days': 0.5, 'hours': 4})
- 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.calendar_id._iter_work_days(WAR_START, WAR_END, r.id))
+ # 09-04-2018 00:00:00 - 13-04-2018 08:00:00
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz3),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz3),
)
+ self.assertEqual(data, {'days': 0.75, 'hours': 6})
- # _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.calendar_id._is_work_day(date(1932, 11, 8), r.id))
- self.assertTrue(r.calendar_id._is_work_day(date(1932, 11, 14), r.id))
- self.assertTrue(r.calendar_id._is_work_day(date(1932, 12, 1), r.id))
-
- self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 11), r.id)) # global leave
- self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 13), r.id)) # sun
- self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 19), r.id)) # sat
- self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 20), r.id)) # sun
- self.assertFalse(r.calendar_id._is_work_day(date(1932, 12, 6), r.id)) # personal leave
-
-
-class TestResourceMixin(TestResourceCommon):
-
- def setUp(self):
- super(TestResourceMixin, self).setUp()
- self.lost_user = self.env['res.users'].with_context(
- no_reset_password=True,
- mail_create_nosubscribe=True
- ).create({
- 'name': 'Désiré Boideladodo',
- 'login': 'desire',
- 'tz': 'Indian/Reunion',
- 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])]
- })
- self.test = self.env['resource.test'].with_context(default_resource_calendar_id=self.calendar.id).create({'name': 'Test'})
-
- def test_basics(self):
- self.assertEqual(self.env['resource.test'].create({'name': 'Test'}).resource_calendar_id, self.env.user.company_id.resource_calendar_id)
- self.assertEqual(self.test.resource_calendar_id, self.calendar)
-
- def test_work_days_count(self):
- # user in timezone UTC-9 asks for work hours
- self.env.user.tz = 'US/Alaska'
-
- res = self.test.get_work_days_count(
- to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-22 23:00:00'), self.env.user))
- self.assertEqual(res, 3.75) # generic leaves, 3 hours
-
- res = self.test.get_work_days_count(
- to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-22 20:00:00'), self.env.user))
- self.assertEqual(res, 3.5) # last day is truncated of 3 hours on 12)
-
- self.env['resource.calendar.leaves'].create({
- 'name': 'Timezoned Leaves',
- 'calendar_id': self.test.resource_calendar_id.id,
- 'resource_id': self.test.resource_id.id,
- 'date_from': to_naive_utc(Datetime.from_string('2013-02-13 10:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2013-02-17 12:00:00'), self.env.user)
- })
-
- res = self.test.get_work_days_count(
- to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-22 20:00:00'), self.env.user))
- self.assertEqual(res, 2.5) # one day is on leave and last day is truncated of 3 hours on 12)
-
- def test_work_days_count_timezones_ultra(self):
- # user in timezone UTC+4 is attached to the resource and create leaves
- self.test.resource_id.write({
- 'user_id': self.lost_user.id,
- })
- reunion_leave = self.env['resource.calendar.leaves'].sudo(self.lost_user).create({
- 'name': 'Timezoned Leaves',
- 'calendar_id': self.test.resource_calendar_id.id,
- 'resource_id': self.test.resource_id.id,
- 'date_from': to_naive_utc(Datetime.from_string('2013-02-12 10:00:00'), self.lost_user),
- 'date_to': to_naive_utc(Datetime.from_string('2013-02-12 12:00:00'), self.lost_user)
- })
- self.assertEqual(reunion_leave.tz, 'Indian/Reunion')
-
- # user in timezone UTC-9 read and manipulate leaves
- self.env.user.tz = 'US/Alaska'
- res = self.test.get_work_days_data(
- to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-12 20:00:00'), self.env.user))
- self.assertEqual(res['days'], 0.75)
- self.assertEqual(res['hours'], 6.0)
-
- # user in timezone UTC+4 read and manipulate leaves
- res = self.test.sudo(self.lost_user).get_work_days_data(
- to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2013-02-12 20:00:00'), self.env.user))
- self.assertEqual(res['days'], 0.75)
- self.assertEqual(res['hours'], 6.0)
-
- def test_days_count_with_domain(self):
- self.test.resource_id.write({
- 'user_id': self.lost_user.id,
- })
-
- self.env['resource.calendar.leaves'].sudo(self.lost_user).create({
- 'name': 'first',
- 'calendar_id': self.test.resource_calendar_id.id,
- 'resource_id': self.test.resource_id.id,
- 'date_from': to_naive_utc(Datetime.from_string('2018-02-05 08:00:00'), self.lost_user),
- 'date_to': to_naive_utc(Datetime.from_string('2018-02-09 18:00:00'), self.lost_user)
- })
-
- self.env['resource.calendar.leaves'].sudo(self.lost_user).create({
- 'name': 'second',
- 'calendar_id': self.test.resource_calendar_id.id,
- 'resource_id': self.test.resource_id.id,
- 'date_from': to_naive_utc(Datetime.from_string('2018-01-12 08:00:00'), self.lost_user),
- 'date_to': to_naive_utc(Datetime.from_string('2018-01-13 18:00:00'), self.lost_user)
- })
-
- res = self.test.get_work_days_count(
- to_naive_utc(Datetime.from_string('2018-02-04 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2018-02-14 20:00:00'), self.env.user),
- domain=[('name', '=', '')]
+ # 09-04-2018 08:00:00 - 14-04-2018 12:00:00
+ data = self.jean.get_leave_days_data(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz4),
)
- self.assertEqual(res, 3.0)
+ self.assertEqual(data, {'days': 0.75, 'hours': 6})
- res = self.test.get_work_days_count(
- to_naive_utc(Datetime.from_string('2018-02-04 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2018-02-14 20:00:00'), self.env.user),
- domain=[('name', '=', 'first')]
+ def test_leaves(self):
+ leave = self.env['resource.calendar.leaves'].create({
+ 'name': '',
+ 'calendar_id': self.jean.resource_calendar_id.id,
+ 'resource_id': self.jean.resource_id.id,
+ 'date_from': datetime_str(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2),
+ 'date_to': datetime_str(2018, 4, 9, 14, 0, 0, tzinfo=self.tz2),
+ })
+
+ # 09-04-2018 10:00:00 - 13-04-2018 18:00:00
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 9, 8, 0, 0),
+ datetime_tz(2018, 4, 13, 16, 0, 0),
)
- self.assertEqual(res, 1.0)
+ self.assertEqual(leaves, [(date(2018, 4, 9), 4, leave)])
- res = self.test.get_leaves_day_count(
- to_naive_utc(Datetime.from_string('2018-02-04 06:00:00'), self.env.user),
- to_naive_utc(Datetime.from_string('2018-02-14 20:00:00'), self.env.user),
- domain=[('name', '=', 'first')]
+ # 09-04-2018 00:00:00 - 13-04-2018 08:00:00
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz3),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz3),
)
- self.assertEqual(res, 2.0)
+ self.assertEqual(leaves, [(date(2018, 4, 9), 6, leave)])
-class TestGlobalLeaves(TestResourceCommon):
+ # 09-04-2018 08:00:00 - 14-04-2018 12:00:00
+ leaves = self.jean.list_leaves(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz4),
+ )
+ self.assertEqual(leaves, [(date(2018, 4, 9), 6, leave)])
- def setUp(self):
- super(TestGlobalLeaves, self).setUp()
+ def test_works(self):
+ work = self.jean.list_work_time_per_day(
+ datetime_tz(2018, 4, 9, 8, 0, 0),
+ datetime_tz(2018, 4, 13, 16, 0, 0),
+ )
+ self.assertEqual(work, [
+ (date(2018, 4, 9), 6),
+ (date(2018, 4, 10), 8),
+ (date(2018, 4, 11), 8),
+ (date(2018, 4, 12), 8),
+ (date(2018, 4, 13), 8),
+ ])
- self.Leave = self.env['resource.calendar.leaves']
+ work = self.jean.list_work_time_per_day(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz3),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz3),
+ )
+ self.assertEqual(len(work), 4)
+ self.assertEqual(work, [
+ (date(2018, 4, 9), 8),
+ (date(2018, 4, 10), 8),
+ (date(2018, 4, 11), 8),
+ (date(2018, 4, 12), 8),
+ ])
- # This leave has a timezone of False : UTC should be used as default
- self.global_no_tz = self.Leave.create({
- 'name': 'global no timezone',
- 'calendar_id': self.calendar.id,
- 'date_from': to_naive_utc(Datetime.from_string('2018-12-25 08:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2018-12-25 23:00:00'), self.env.user),
- 'tz': False,
- })
-
- # This leave has no timezone defined : should default to user timezone (here UTC)
- self.global_user_tz = self.Leave.create({
- 'name': 'global user timezone',
- 'calendar_id': self.calendar.id,
- 'date_from': to_naive_utc(Datetime.from_string('2018-05-01 08:00:00'), self.env.user),
- 'date_to': to_naive_utc(Datetime.from_string('2018-05-01 23:00:00'), self.env.user),
- })
-
- # This leave has been taken in Japan timezone
- # Leave from 8:00 to 13:00 but we have to adapt cause we define it in UTC
- self.global_spec_tz = self.Leave.create({
- 'name': 'global specific timezone',
- 'calendar_id': self.calendar.id,
- 'date_from': Datetime.from_string('2018-04-05 23:00:00'),
- 'date_to': Datetime.from_string('2018-04-06 04:00:00'),
- 'tz' : 'Japan', # UTC+9
- })
-
- def test_timezones(self):
- self.assertEqual(self.global_no_tz.tz, False)
- self.assertEqual(self.global_user_tz.tz, self.env.user.tz)
- self.assertEqual(self.global_spec_tz.tz, 'Japan')
-
- def test_leave(self):
- def to_tuple(interval):
- return (interval.start_datetime, interval.end_datetime)
-
- # Here we test if the leaves are correctly computed in the case our user is in UTC
- # NB: Beware of winter/spring time
- work_no_tz = [to_tuple(x) for x in self.calendar._get_day_leave_intervals(Date.from_string('2018-12-25'), time(0), time(23, 59, 59), self.resource1_id)]
- self.assertEqual(work_no_tz[0], (Datetime.from_string('2018-12-25 08:00:00'), Datetime.from_string('2018-12-25 16:00:00')))
-
- work_user_tz = [to_tuple(x) for x in self.calendar._get_day_leave_intervals(Date.from_string('2018-05-01'), time(0), time(23, 59, 59), self.resource1_id)]
- self.assertEqual(work_user_tz[0], (Datetime.from_string('2018-05-01 08:00:00'), Datetime.from_string('2018-05-01 16:00:00')))
-
- work_spec = [to_tuple(x) for x in self.calendar._get_day_leave_intervals(Date.from_string('2018-04-06'), time(0), time(23, 59, 59), self.resource1_id)]
- self.assertEqual(work_spec[0], (Datetime.from_string('2018-04-06 08:00:00'), Datetime.from_string('2018-04-06 13:00:00')))
-
- def test_leaves_timezones(self):
- self.env.user.tz = 'US/Alaska' # UTC-9
-
- def to_tuple(interval):
- return (interval.start_datetime, interval.end_datetime)
-
- # Here we test if the leaves are correctly computed in the case our user is in tz Alaska
- # NB: Beware of winter/spring time
- work_no_tz = [to_tuple(x) for x in self.calendar._get_day_leave_intervals(Date.from_string('2018-12-25'), time(0), time(23, 59, 59), self.resource1_id)]
- self.assertEqual(work_no_tz[0], (Datetime.from_string('2018-12-25 17:00:00'), Datetime.from_string('2018-12-26 01:00:00')))
-
- work_user_tz = [to_tuple(x) for x in self.calendar._get_day_leave_intervals(Date.from_string('2018-05-01'), time(0), time(23, 59, 59), self.resource1_id)]
- self.assertEqual(work_user_tz[0], (Datetime.from_string('2018-05-01 16:00:00'), Datetime.from_string('2018-05-02 00:00:00')))
-
- work_spec = [to_tuple(x) for x in self.calendar._get_day_leave_intervals(Date.from_string('2018-04-06'), time(0), time(23, 59, 59), self.resource1_id)]
- self.assertEqual(work_spec[0], (Datetime.from_string('2018-04-06 16:00:00'), Datetime.from_string('2018-04-06 21:00:00')))
-
-
-class TestTimeType(TestResourceCommon):
- def setUp(self):
- super(TestTimeType, self).setUp()
- Leave = self.env['resource.calendar.leaves']
-
- self.other1 = Leave.create({
- 'name': 'Formation',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': Datetime.from_string('2013-02-15 09:00:00'),
- 'date_to': Datetime.from_string('2013-02-15 19:00:00'),
- 'time_type': 'other',
- })
- self.other2 = Leave.create({
- 'name': 'Homeworking',
- 'calendar_id': self.calendar.id,
- 'resource_id': self.resource1_id,
- 'date_from': Datetime.from_string('2013-02-12 09:00:00'),
- 'date_to': Datetime.from_string('2013-02-12 15:00:00'),
- 'time_type': 'other',
- })
-
- def test_leaves_leaves(self):
- # Here we test if setting the value to leave has the effect intended
- leave = self.calendar._get_day_leave_intervals(Date.from_string('2013-02-26'), time(0), time(23, 59, 59), self.resource1_id, [('time_type', '=', 'leave')])
- self.assertEqual(len(leave), 1, "When asking for leave should compute the leaves")
- leave = self.calendar._get_day_leave_intervals(Date.from_string('2013-02-15'), time(0), time(23, 59, 59), self.resource1_id, [('time_type', '=', 'leave')])
- self.assertEqual(len(leave), 0, "When asking for leave should ignore others")
-
- def test_leaves_other(self):
- # Here we test if setting the value to other has the effect intended
- leave = self.calendar._get_day_leave_intervals(Date.from_string('2013-02-26'), time(0), time(23, 59, 59), self.resource1_id, [('time_type', '=', 'other')])
- self.assertEqual(len(leave), 0, "When asking for other should ignore leaves")
- leave = self.calendar._get_day_leave_intervals(Date.from_string('2013-02-15'), time(0), time(23, 59, 59), self.resource1_id, [('time_type', '=', 'other')])
- self.assertEqual(len(leave), 2, "When asking for other should compute others")
+ work = self.jean.list_work_time_per_day(
+ datetime_tz(2018, 4, 9, 8, 0, 0, tzinfo=self.tz2),
+ datetime_tz(2018, 4, 13, 16, 0, 0, tzinfo=self.tz4),
+ )
+ self.assertEqual(work, [
+ (date(2018, 4, 9), 8),
+ (date(2018, 4, 10), 8),
+ (date(2018, 4, 11), 8),
+ (date(2018, 4, 12), 8),
+ (date(2018, 4, 13), 8),
+ ])
diff --git a/addons/resource/tests/test_resource_model.py b/addons/resource/tests/test_resource_model.py
index dad7e3cb10a..d1379510ec6 100644
--- a/addons/resource/tests/test_resource_model.py
+++ b/addons/resource/tests/test_resource_model.py
@@ -4,7 +4,7 @@
from odoo import fields, models
-class MailTest(models.Model):
+class ResourceTest(models.Model):
_description = 'Test Resource Model'
_name = 'resource.test'
_inherit = ['resource.mixin']
diff --git a/addons/resource/views/resource_views.xml b/addons/resource/views/resource_views.xml
index dda1c55e11f..4d39092d801 100644
--- a/addons/resource/views/resource_views.xml
+++ b/addons/resource/views/resource_views.xml
@@ -38,6 +38,7 @@
+
@@ -250,6 +251,7 @@
+
@@ -261,7 +263,6 @@
-