[REF] resource: correctly take into account timezones and clean code
Currently timezones were not or incorrectly taken into account in resource computation. Indeed complexity comes from attendances being expressed in naive timezones valid for all users, whereas dates and leaves are computed into UTC. Computation should always be done in user timezone so that there are no overlapping days and all time intervals are correctly computed. A new timezone field is added on leaves to know in which timezone they were originally encoded. Indeed putting them in naive user timezone require its original timezone, not the current user timezone, as those can be different. This commit also contains code cleaning in resource to be a bit more pythonic, simplify method parameters and offer a more simple api to other modules. Eventually code field is removed on resource as it is not really necessary for resources. It is explicitly added on workcenter model who use it.
This commit is contained in:
@@ -334,7 +334,7 @@ class HrPayslip(models.Model):
|
|||||||
day_to = fields.Datetime.from_string(date_to)
|
day_to = fields.Datetime.from_string(date_to)
|
||||||
nb_of_days = (day_to - day_from).days + 1
|
nb_of_days = (day_to - day_from).days + 1
|
||||||
for day in range(0, nb_of_days):
|
for day in range(0, nb_of_days):
|
||||||
working_hours_on_day = contract.working_hours.get_work_hours_count(day_from + timedelta(days=day), False)
|
working_hours_on_day = contract.working_hours.get_work_hours_count(day_from + timedelta(days=day), False, contract.employee_id.resource_id.id)
|
||||||
if working_hours_on_day:
|
if working_hours_on_day:
|
||||||
#the employee had to work
|
#the employee had to work
|
||||||
leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day))
|
leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class MrpWorkcenter(models.Model):
|
|||||||
time_start = fields.Float('Time before prod.', help="Time in minutes for the setup.")
|
time_start = fields.Float('Time before prod.', help="Time in minutes for the setup.")
|
||||||
time_stop = fields.Float('Time after prod.', help="Time in minutes for the cleaning.")
|
time_stop = fields.Float('Time after prod.', help="Time in minutes for the cleaning.")
|
||||||
resource_id = fields.Many2one('resource.resource', 'Resource', ondelete='cascade', required=True)
|
resource_id = fields.Many2one('resource.resource', 'Resource', ondelete='cascade', required=True)
|
||||||
|
code = fields.Char('Code', copy=False)
|
||||||
routing_line_ids = fields.One2many('mrp.routing.workcenter', 'workcenter_id', "Routing Lines")
|
routing_line_ids = fields.One2many('mrp.routing.workcenter', 'workcenter_id', "Routing Lines")
|
||||||
|
|
||||||
order_ids = fields.One2many('mrp.workorder', 'workcenter_id', "Orders")
|
order_ids = fields.One2many('mrp.workorder', 'workcenter_id', "Orders")
|
||||||
@@ -206,7 +207,7 @@ class MrpWorkcenterProductivity(models.Model):
|
|||||||
d2 = fields.Datetime.from_string(blocktime.date_end)
|
d2 = fields.Datetime.from_string(blocktime.date_end)
|
||||||
diff = d2 - d1
|
diff = d2 - d1
|
||||||
if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.calendar_id:
|
if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.calendar_id:
|
||||||
r = blocktime.workcenter_id.calendar_id.get_work_hours_count(d1, d2, resource_id=blocktime.workcenter_id.resource_id.id)
|
r = blocktime.workcenter_id.calendar_id.get_work_hours_count(d1, d2, blocktime.workcenter_id.resource_id.id)
|
||||||
blocktime.duration = round(r * 60, 2)
|
blocktime.duration = round(r * 60, 2)
|
||||||
else:
|
else:
|
||||||
blocktime.duration = round(diff.total_seconds() / 60.0, 2)
|
blocktime.duration = round(diff.total_seconds() / 60.0, 2)
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ class ProjectIssue(models.Model):
|
|||||||
issue.day_open = (dt_date_open - dt_create_date).total_seconds() / (24.0 * 3600)
|
issue.day_open = (dt_date_open - dt_create_date).total_seconds() / (24.0 * 3600)
|
||||||
if issue.project_id.resource_calendar_id:
|
if issue.project_id.resource_calendar_id:
|
||||||
issue.working_hours_open = issue.project_id.resource_calendar_id.get_work_hours_count(
|
issue.working_hours_open = issue.project_id.resource_calendar_id.get_work_hours_count(
|
||||||
dt_create_date, dt_date_open,
|
dt_create_date, dt_date_open, False, compute_leaves=True)
|
||||||
compute_leaves=True, resource_id=False)
|
|
||||||
else:
|
else:
|
||||||
issue.working_hours_open = 0
|
issue.working_hours_open = 0
|
||||||
|
|
||||||
@@ -101,8 +100,7 @@ class ProjectIssue(models.Model):
|
|||||||
issue.day_close = (dt_date_closed - dt_create_date).total_seconds() / (24.0 * 3600)
|
issue.day_close = (dt_date_closed - dt_create_date).total_seconds() / (24.0 * 3600)
|
||||||
if issue.project_id.resource_calendar_id:
|
if issue.project_id.resource_calendar_id:
|
||||||
issue.working_hours_close = issue.project_id.resource_calendar_id.get_work_hours_count(
|
issue.working_hours_close = issue.project_id.resource_calendar_id.get_work_hours_count(
|
||||||
dt_create_date, dt_date_closed,
|
dt_create_date, dt_date_closed, False, compute_leaves=True)
|
||||||
compute_leaves=True, resource_id=False)
|
|
||||||
else:
|
else:
|
||||||
issue.working_hours_close = 0
|
issue.working_hours_close = 0
|
||||||
|
|
||||||
|
|||||||
+276
-255
@@ -2,7 +2,7 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import math
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@@ -12,13 +12,30 @@ from dateutil.relativedelta import relativedelta
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.addons.base.res.res_partner import _tz_get
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
from odoo.tools.float_utils import float_compare
|
from odoo.tools.float_utils import float_compare
|
||||||
|
|
||||||
|
|
||||||
def seconds(td):
|
def float_to_time(float_hour):
|
||||||
assert isinstance(td, timedelta)
|
return datetime.time(int(math.modf(float_hour)[1]), int(60 * math.modf(float_hour)[0]), 0)
|
||||||
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.**6
|
|
||||||
|
|
||||||
|
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 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 to_tz(datetime, tz_name):
|
||||||
|
tz = pytz.timezone(tz_name)
|
||||||
|
return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(tz).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
class ResourceCalendar(models.Model):
|
class ResourceCalendar(models.Model):
|
||||||
@@ -169,37 +186,36 @@ class ResourceCalendar(models.Model):
|
|||||||
return intervals
|
return intervals
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _interval_schedule_hours(self, intervals, hour, remove_at_end=True):
|
def _interval_schedule_hours(self, intervals, hour, backwards=False):
|
||||||
""" Schedule hours in intervals. The last matching interval is truncated
|
""" Schedule hours in intervals. The last matching interval is truncated
|
||||||
to match the specified hours.
|
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.
|
||||||
|
|
||||||
It is possible to truncate the last interval at its beginning or ending.
|
:param list intervals: a list of time intervals
|
||||||
However this does nothing on the given interval order that should be
|
|
||||||
submitted accordingly.
|
|
||||||
|
|
||||||
:param list intervals: a list of tuples (beginning datetime, ending datetime)
|
|
||||||
:param int/float hours: number of hours to schedule. It will be converted
|
:param int/float hours: number of hours to schedule. It will be converted
|
||||||
into a timedelta, but should be submitted as an
|
into a timedelta, but should be submitted as an
|
||||||
int or float.
|
int or float
|
||||||
:param boolean remove_at_end: remove extra hours at the end of the last
|
:param boolean backwards: schedule starting from last hour
|
||||||
matching interval. Otherwise, do it at the
|
|
||||||
beginning.
|
|
||||||
|
|
||||||
:return list results: a list of intervals. If the number of hours to schedule
|
:return list results: a list of time intervals """
|
||||||
is greater than the possible scheduling in the intervals, no extra-scheduling
|
if backwards:
|
||||||
is done, and results == intervals. """
|
intervals.reverse() # first interval is the last working interval of the day
|
||||||
results = []
|
results = []
|
||||||
res = timedelta()
|
res = timedelta()
|
||||||
limit = timedelta(hours=hour)
|
limit = timedelta(hours=hour)
|
||||||
for interval in intervals:
|
for interval in intervals:
|
||||||
res += interval[1] - interval[0]
|
res += interval[1] - interval[0]
|
||||||
if res > limit and remove_at_end:
|
if res > limit and not backwards:
|
||||||
interval = (interval[0], interval[1] + relativedelta(seconds=seconds(limit - res)))
|
interval = (interval[0], interval[1] + relativedelta(seconds=(limit - res).total_seconds()))
|
||||||
elif res > limit:
|
elif res > limit:
|
||||||
interval = (interval[0] + relativedelta(seconds=seconds(res - limit)), interval[1])
|
interval = (interval[0] + relativedelta(seconds=(res - limit).total_seconds()), interval[1])
|
||||||
results.append(interval)
|
results.append(interval)
|
||||||
if res > limit:
|
if res > limit:
|
||||||
break
|
break
|
||||||
|
if backwards:
|
||||||
|
results.reverse() # return interval with increasing starting times
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -207,17 +223,22 @@ class ResourceCalendar(models.Model):
|
|||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _get_day_attendances(self, day_dt):
|
def _get_day_attendances(self, day_date, start_time, end_time):
|
||||||
""" Given a day datetime, return matching attendances """
|
""" Given a day date, return matching attendances. Those can be limited
|
||||||
|
by starting and ending time objects. """
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
weekday = day_dt.weekday()
|
weekday = day_date.weekday()
|
||||||
attendances = self.env['resource.calendar.attendance']
|
attendances = self.env['resource.calendar.attendance']
|
||||||
|
|
||||||
for attendance in self.attendance_ids.filtered(
|
for attendance in self.attendance_ids.filtered(
|
||||||
lambda att:
|
lambda att:
|
||||||
int(att.dayofweek) == weekday and
|
int(att.dayofweek) == weekday and
|
||||||
not (att.date_from and fields.Date.from_string(att.date_from) > day_dt.date()) 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_dt.date())):
|
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
|
attendances |= attendance
|
||||||
return attendances
|
return attendances
|
||||||
|
|
||||||
@@ -226,28 +247,15 @@ class ResourceCalendar(models.Model):
|
|||||||
""" Return the list of weekdays that contain at least one working
|
""" Return the list of weekdays that contain at least one working
|
||||||
interval. """
|
interval. """
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
weekdays = set(map(int, (self.attendance_ids.mapped('dayofweek'))))
|
return list(set(map(int, (self.attendance_ids.mapped('dayofweek')))))
|
||||||
return list(weekdays)
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _get_next_work_day(self, day_date):
|
def _get_next_work_day(self, day_date):
|
||||||
""" Get following date of day_date, based on resource.calendar. If no
|
""" Get following date of day_date, based on resource.calendar. """
|
||||||
calendar is provided, just return the next day.
|
|
||||||
|
|
||||||
:param date day_date: current day as a date
|
|
||||||
|
|
||||||
:return date: next day of calendar, or just next day """
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
weekdays = self._get_weekdays()
|
weekdays = self._get_weekdays()
|
||||||
|
weekday = next((item for item in weekdays if item > day_date.weekday()), weekdays[0])
|
||||||
base_index = -1
|
days = weekday - day_date.weekday()
|
||||||
for weekday in weekdays:
|
|
||||||
if weekday > day_date.weekday():
|
|
||||||
break
|
|
||||||
base_index += 1
|
|
||||||
|
|
||||||
new_index = (base_index + 1) % len(weekdays)
|
|
||||||
days = (weekdays[new_index] - day_date.weekday())
|
|
||||||
if days < 0:
|
if days < 0:
|
||||||
days = 7 + days
|
days = 7 + days
|
||||||
|
|
||||||
@@ -255,224 +263,281 @@ class ResourceCalendar(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _get_previous_work_day(self, day_date):
|
def _get_previous_work_day(self, day_date):
|
||||||
""" Get previous date of day_date, based on resource.calendar. If no
|
""" Get previous date of day_date, based on resource.calendar. """
|
||||||
calendar is provided, just return the previous day.
|
|
||||||
|
|
||||||
:param date day_date: current day as a date
|
|
||||||
|
|
||||||
:return date: previous day of calendar, or just previous day """
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
weekdays = self._get_weekdays()
|
weekdays = self._get_weekdays()
|
||||||
weekdays.reverse()
|
weekdays.reverse()
|
||||||
|
weekday = next((item for item in weekdays if item < day_date.weekday()), weekdays[0])
|
||||||
base_index = -1
|
days = weekday - day_date.weekday()
|
||||||
for weekday in weekdays:
|
|
||||||
if weekday < day_date.weekday():
|
|
||||||
break
|
|
||||||
base_index += 1
|
|
||||||
|
|
||||||
new_index = (base_index + 1) % len(weekdays)
|
|
||||||
days = (weekdays[new_index] - day_date.weekday())
|
|
||||||
if days > 0:
|
if days > 0:
|
||||||
days = days - 7
|
days = days - 7
|
||||||
|
|
||||||
return day_date + relativedelta(days=days)
|
return day_date + relativedelta(days=days)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _get_leave_intervals(self, resource_id=None,
|
def _get_leave_intervals(self, resource_id=None, start_datetime=None, end_datetime=None):
|
||||||
start_datetime=None, end_datetime=None):
|
|
||||||
"""Get the leaves of the calendar. Leaves can be filtered on the resource,
|
"""Get the leaves of the calendar. Leaves can be filtered on the resource,
|
||||||
the start datetime or the end datetime.
|
and on a start and end datetime.
|
||||||
|
|
||||||
:param int resource_id: the id of the resource to take into account when
|
Leaves are encoded from a given timezone given by their tz field. COnverting
|
||||||
computing the leaves. If not set, only general
|
them in naive user timezone require to use the leave timezone, not the current
|
||||||
leaves are computed. If set, generic and
|
user timezone. For example people managing leaves could be from different
|
||||||
specific leaves are computed.
|
timezones and the correct one is the one used when encoding them.
|
||||||
:param datetime start_datetime: if provided, do not take into account leaves
|
|
||||||
ending before this date.
|
|
||||||
:param datetime end_datetime: if provided, do not take into account leaves
|
|
||||||
beginning after this date.
|
|
||||||
|
|
||||||
:return list leaves: list of tuples (start_datetime, end_datetime) of
|
:return list leaves: list of time intervals """
|
||||||
leave intervals
|
|
||||||
"""
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
leaves = []
|
if resource_id:
|
||||||
for leave in self.leave_ids:
|
domain = ['|', ('resource_id', '=', resource_id), ('resource_id', '=', False)]
|
||||||
if leave.resource_id and not resource_id == leave.resource_id.id:
|
else:
|
||||||
continue
|
domain = [('resource_id', '=', False)]
|
||||||
date_from = fields.Datetime.from_string(leave.date_from)
|
if start_datetime:
|
||||||
if end_datetime and date_from > end_datetime:
|
# domain += [('date_to', '>', fields.Datetime.to_string(to_naive_utc(start_datetime, self.env.user)))]
|
||||||
continue
|
domain += [('date_to', '>', fields.Datetime.to_string(start_datetime + timedelta(days=-1)))]
|
||||||
date_to = fields.Datetime.from_string(leave.date_to)
|
if end_datetime:
|
||||||
if start_datetime and date_to < start_datetime:
|
# domain += [('date_from', '<', fields.Datetime.to_string(to_naive_utc(end_datetime, self.env.user)))]
|
||||||
continue
|
domain += [('date_from', '<', fields.Datetime.to_string(start_datetime + timedelta(days=1)))]
|
||||||
leaves.append(self._interval_new(date_from, date_to, {'leaves': leave}))
|
leaves = self.env['resource.calendar.leaves'].search(domain + [('calendar_id', '=', self.id)])
|
||||||
return leaves
|
|
||||||
|
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
|
@api.multi
|
||||||
def _get_day_work_intervals(self, start_dt=None, end_dt=None,
|
def _get_day_work_intervals(self, day_date, start_time=None, end_time=None, compute_leaves=False, resource_id=None):
|
||||||
leaves=None, compute_leaves=False, resource_id=None):
|
""" Get the working intervals of the day given by day_date based on
|
||||||
""" Get the working intervals of the day based on calendar. This method
|
current calendar. Input should be given in current user timezone and
|
||||||
handle leaves that come directly from the leaves parameter or can be computed.
|
output is given in naive UTC, ready to be used by the orm or webclient.
|
||||||
|
|
||||||
:param datetime start_dt: datetime object that is the beginning hours
|
:param time start_time: time object that is the beginning hours in user TZ
|
||||||
for the working intervals computation; any
|
:param time end_time: time object that is the ending hours in user TZ
|
||||||
working interval beginning before start_dt
|
:param boolean compute_leaves: indicates whether to compute the
|
||||||
will be truncated. If not set, set to end_dt
|
|
||||||
or today() if no end_dt at 00.00.00.
|
|
||||||
:param datetime end_dt: datetime object that is the ending hour
|
|
||||||
for the working intervals computation; any
|
|
||||||
working interval ending after end_dt
|
|
||||||
will be truncated. If not set, set to start_dt()
|
|
||||||
at 23.59.59.
|
|
||||||
:param list leaves: a list of tuples(start_datetime, end_datetime) that
|
|
||||||
represent leaves.
|
|
||||||
:param boolean compute_leaves: if set and if leaves is None, compute the
|
|
||||||
leaves based on calendar and resource.
|
leaves based on calendar and resource.
|
||||||
If leaves is None and compute_leaves false
|
|
||||||
no leaves are taken into account.
|
|
||||||
:param int resource_id: the id of the resource to take into account when
|
:param int resource_id: the id of the resource to take into account when
|
||||||
computing the leaves. If not set, only general
|
computing the work intervals. Leaves notably are
|
||||||
leaves are computed. If set, generic and
|
filtered according to the resource.
|
||||||
specific leaves are computed.
|
|
||||||
|
|
||||||
:return list intervals: a list of tuples (start_datetime, end_datetime)
|
:return list intervals: list of time intervals in UTC """
|
||||||
of work intervals """
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
# Computes start_dt, end_dt (with default values if not set) + off-interval work limits
|
|
||||||
work_limits = []
|
|
||||||
if start_dt is None and end_dt is not None:
|
|
||||||
start_dt = end_dt.replace(hour=0, minute=0, second=0)
|
|
||||||
elif start_dt is None:
|
|
||||||
start_dt = datetime.datetime.now().replace(hour=0, minute=0, second=0)
|
|
||||||
else:
|
|
||||||
work_limits.append(self._interval_new(start_dt.replace(hour=0, minute=0, second=0), start_dt))
|
|
||||||
if end_dt is None:
|
|
||||||
end_dt = start_dt.replace(hour=23, minute=59, second=59)
|
|
||||||
else:
|
|
||||||
work_limits.append(self._interval_new(end_dt, end_dt.replace(hour=23, minute=59, second=59)))
|
|
||||||
assert start_dt.date() == end_dt.date(), '_get_day_work_intervals is restricted to one day'
|
|
||||||
|
|
||||||
work_dt = start_dt.replace(hour=0, minute=0, second=0)
|
if not start_time:
|
||||||
|
start_time = datetime.time.min
|
||||||
|
if not end_time:
|
||||||
|
end_time = datetime.time.max
|
||||||
|
|
||||||
working_intervals = []
|
working_intervals = [att_interval for att_interval in self._iter_day_attendance_intervals(day_date, start_time, end_time)]
|
||||||
tz_info = fields.Datetime.context_timestamp(self, work_dt).tzinfo
|
|
||||||
for calendar_working_day in self._get_day_attendances(start_dt):
|
|
||||||
dt_f = work_dt.replace(hour=0, minute=0, second=0) + timedelta(seconds=(calendar_working_day.hour_from * 3600))
|
|
||||||
dt_t = work_dt.replace(hour=0, minute=0, second=0) + timedelta(seconds=(calendar_working_day.hour_to * 3600))
|
|
||||||
|
|
||||||
# adapt tz
|
|
||||||
working_interval = self._interval_new(
|
|
||||||
dt_f.replace(tzinfo=tz_info).astimezone(pytz.UTC).replace(tzinfo=None),
|
|
||||||
dt_t.replace(tzinfo=tz_info).astimezone(pytz.UTC).replace(tzinfo=None),
|
|
||||||
{'attendances': calendar_working_day}
|
|
||||||
)
|
|
||||||
working_intervals += self._interval_remove_leaves(working_interval, work_limits)
|
|
||||||
|
|
||||||
# find leave intervals
|
|
||||||
if leaves is None and compute_leaves:
|
|
||||||
leaves = self._get_leave_intervals(resource_id=resource_id)
|
|
||||||
|
|
||||||
# filter according to leaves
|
# filter according to leaves
|
||||||
if leaves:
|
if compute_leaves:
|
||||||
return list(itertools.chain.from_iterable(map(lambda i: self._interval_remove_leaves(i, leaves), working_intervals)))
|
leaves = self._get_leave_intervals(
|
||||||
else:
|
resource_id=resource_id,
|
||||||
return working_intervals
|
start_datetime=datetime.datetime.combine(day_date, start_time),
|
||||||
|
end_datetime=datetime.datetime.combine(day_date, end_time))
|
||||||
|
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):
|
||||||
|
""" 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))
|
||||||
|
|
||||||
|
final_intervals = [
|
||||||
|
self._interval_and(leave_interval, work_interval)
|
||||||
|
for leave_interval in leaves_intervals
|
||||||
|
for work_interval in working_intervals]
|
||||||
|
|
||||||
|
# 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
|
# Main computation API
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
@api.multi
|
def _iter_work_intervals(self, start_dt, end_dt, resource_id, compute_leaves=True):
|
||||||
def get_work_hours_count(self, start_dt, end_dt, compute_leaves=False, resource_id=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:
|
if not end_dt:
|
||||||
end_dt = start_dt.replace(hour=23, minute=59, second=59)
|
end_dt = datetime.datetime.combine(start_dt.date(), datetime.time.max)
|
||||||
res = timedelta()
|
|
||||||
|
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,
|
for day in rrule.rrule(rrule.DAILY,
|
||||||
dtstart=start_dt,
|
dtstart=start_dt,
|
||||||
until=end_dt,
|
until=end_dt,
|
||||||
byweekday=self._get_weekdays()):
|
byweekday=self._get_weekdays()):
|
||||||
day_start_dt = day.replace(hour=0, minute=0, second=0)
|
start_time = day.date() == start_dt.date() and start_dt.time() or datetime.time.min
|
||||||
if start_dt and day.date() == start_dt.date():
|
end_time = day.date() == end_dt.date() and end_dt.time() or datetime.time.max
|
||||||
day_start_dt = start_dt
|
|
||||||
day_end_dt = day.replace(hour=23, minute=59, second=59)
|
|
||||||
if end_dt and day.date() == end_dt.date():
|
|
||||||
day_end_dt = end_dt
|
|
||||||
intervals = self._get_day_work_intervals(
|
intervals = self._get_day_work_intervals(
|
||||||
start_dt=day_start_dt, end_dt=day_end_dt,
|
day.date(),
|
||||||
compute_leaves=compute_leaves, resource_id=resource_id)
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
compute_leaves=compute_leaves,
|
||||||
|
resource_id=resource_id)
|
||||||
|
if intervals:
|
||||||
|
yield intervals
|
||||||
|
|
||||||
|
def _iter_leave_intervals(self, start_dt, end_dt, resource_id):
|
||||||
|
""" 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 = day.date() == start_dt.date() and start_dt.time() or datetime.time.min
|
||||||
|
end_time = day.date() == end_dt.date() and end_dt.time() or datetime.time.max
|
||||||
|
|
||||||
|
intervals = self._get_day_leave_intervals(
|
||||||
|
day.date(),
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
resource_id)
|
||||||
|
|
||||||
|
if intervals:
|
||||||
|
yield intervals
|
||||||
|
|
||||||
|
def _iter_work_days(self, from_date, to_date, resource_id):
|
||||||
|
""" 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)
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
yield interval[0][0].date()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _is_work_day(self, date, resource_id):
|
||||||
|
""" Whether the provided date is a work day for the subject resource.
|
||||||
|
|
||||||
|
:type date: datetime.date
|
||||||
|
:rtype: bool """
|
||||||
|
return bool(next(self._iter_work_days(date, date, resource_id), False))
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def get_work_hours_count(self, start_dt, end_dt, resource_id, compute_leaves=True):
|
||||||
|
""" 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):
|
||||||
for interval in intervals:
|
for interval in intervals:
|
||||||
res += interval[1] - interval[0]
|
res += interval[1] - interval[0]
|
||||||
|
return res.total_seconds() / 3600.0
|
||||||
return seconds(res) / 3600.0
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Scheduling API
|
# Scheduling API
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _schedule_hours(self, hours, day_dt=None,
|
def _schedule_hours(self, hours, day_dt, compute_leaves=False, resource_id=None):
|
||||||
compute_leaves=False, resource_id=None):
|
|
||||||
""" Schedule hours of work, using a calendar and an optional resource to
|
""" 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.
|
compute working and leave days. This method can be used backwards, i.e.
|
||||||
scheduling days before a deadline.
|
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
|
:param int hours: number of hours to schedule. Use a negative number to
|
||||||
compute a backwards scheduling.
|
compute a backwards scheduling.
|
||||||
:param datetime day_dt: reference date to compute working days. If days is
|
:param datetime day_dt: reference date to compute working days. If days is
|
||||||
> 0 date is the starting date. If days is < 0
|
> 0 date is the starting date. If days is < 0
|
||||||
date is the ending date.
|
date is the ending date.
|
||||||
:param boolean compute_leaves: if set, compute the leaves based on calendar
|
|
||||||
and resource. Otherwise no leaves are taken
|
|
||||||
into account.
|
|
||||||
:param int resource_id: the id of the resource to take into account when
|
|
||||||
computing the leaves. If not set, only general
|
|
||||||
leaves are computed. If set, generic and
|
|
||||||
specific leaves are computed.
|
|
||||||
|
|
||||||
:return tuple (datetime, intervals): datetime is the beginning/ending date
|
:return list intervals: list of time intervals in naive UTC """
|
||||||
of the schedulign; intervals are the
|
|
||||||
working intervals of the scheduling.
|
|
||||||
|
|
||||||
Note: Why not using rrule.rrule ? Because rrule does not seem to allow
|
|
||||||
getting back in time.
|
|
||||||
"""
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if day_dt is None:
|
|
||||||
day_dt = datetime.datetime.now()
|
|
||||||
backwards = (hours < 0)
|
backwards = (hours < 0)
|
||||||
hours = abs(hours)
|
|
||||||
intervals = []
|
intervals = []
|
||||||
remaining_hours = hours * 1.0
|
remaining_hours, iterations = abs(hours * 1.0), 0
|
||||||
iterations = 0
|
|
||||||
current_datetime = day_dt
|
current_datetime = day_dt
|
||||||
|
|
||||||
call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id)
|
call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id)
|
||||||
|
|
||||||
while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000:
|
while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000:
|
||||||
if backwards:
|
if backwards:
|
||||||
call_args['end_dt'] = current_datetime
|
call_args['end_time'] = current_datetime.time()
|
||||||
else:
|
else:
|
||||||
call_args['start_dt'] = current_datetime
|
call_args['start_time'] = current_datetime.time()
|
||||||
|
|
||||||
working_intervals = self._get_day_work_intervals(**call_args)
|
working_intervals = self._get_day_work_intervals(current_datetime.date(), **call_args)
|
||||||
|
|
||||||
if working_intervals:
|
if working_intervals:
|
||||||
if backwards:
|
new_working_intervals = self._interval_schedule_hours(working_intervals, remaining_hours, backwards=backwards)
|
||||||
working_intervals.reverse()
|
|
||||||
new_working_intervals = self._interval_schedule_hours(working_intervals, remaining_hours, not backwards)
|
|
||||||
if backwards:
|
|
||||||
new_working_intervals.reverse()
|
|
||||||
|
|
||||||
res = timedelta()
|
res = timedelta()
|
||||||
for interval in working_intervals:
|
for interval in working_intervals:
|
||||||
res += interval[1] - interval[0]
|
res += interval[1] - interval[0]
|
||||||
remaining_hours -= (seconds(res) / 3600.0)
|
remaining_hours -= res.total_seconds() / 3600.0
|
||||||
if backwards:
|
|
||||||
intervals = new_working_intervals + intervals
|
intervals = intervals + new_working_intervals if not backwards else new_working_intervals + intervals
|
||||||
else:
|
|
||||||
intervals = intervals + new_working_intervals
|
|
||||||
# get next day
|
# get next day
|
||||||
if backwards:
|
if backwards:
|
||||||
current_datetime = datetime.datetime.combine(self._get_previous_work_day(current_datetime), datetime.time(23, 59, 59))
|
current_datetime = datetime.datetime.combine(self._get_previous_work_day(current_datetime), datetime.time(23, 59, 59))
|
||||||
@@ -484,50 +549,34 @@ class ResourceCalendar(models.Model):
|
|||||||
return intervals
|
return intervals
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def plan_hours(self, hours, day_dt=None, compute_leaves=False, resource_id=None):
|
def plan_hours(self, hours, day_dt, compute_leaves=False, resource_id=None):
|
||||||
""" Return datetime after having planned hours """
|
""" Return datetime after having planned hours """
|
||||||
res = self._schedule_hours(hours, day_dt, compute_leaves, resource_id)
|
res = self._schedule_hours(hours, day_dt, compute_leaves, resource_id)
|
||||||
return res and res[0][0] or False
|
return res and res[0][0] or False
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _schedule_days(self, days, day_date=None, compute_leaves=False,
|
def _schedule_days(self, days, day_dt, compute_leaves=False, resource_id=None):
|
||||||
resource_id=None):
|
|
||||||
"""Schedule days of work, using a calendar and an optional resource to
|
"""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.
|
compute working and leave days. This method can be used backwards, i.e.
|
||||||
scheduling days before a deadline.
|
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 days: number of days to schedule. Use a negative number to
|
:param int days: number of days to schedule. Use a negative number to
|
||||||
compute a backwards scheduling.
|
compute a backwards scheduling.
|
||||||
:param date day_date: reference date to compute working days. If days is > 0
|
: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
|
date is the starting date. If days is < 0 date is the
|
||||||
ending date.
|
ending date.
|
||||||
:param boolean compute_leaves: if set, compute the leaves based on calendar
|
|
||||||
and resource. Otherwise no leaves are taken
|
|
||||||
into account.
|
|
||||||
:param int resource_id: the id of the resource to take into account when
|
|
||||||
computing the leaves. If not set, only general
|
|
||||||
leaves are computed. If set, generic and
|
|
||||||
specific leaves are computed.
|
|
||||||
|
|
||||||
:return tuple (datetime, intervals): datetime is the beginning/ending date
|
:return list intervals: list of time intervals in naive UTC """
|
||||||
of the schedulign; intervals are the
|
|
||||||
working intervals of the scheduling.
|
|
||||||
|
|
||||||
Implementation note: rrule.rrule is not used because rrule it des not seem
|
|
||||||
to allow getting back in time.
|
|
||||||
"""
|
|
||||||
if day_date is None:
|
|
||||||
day_date = datetime.datetime.now()
|
|
||||||
backwards = (days < 0)
|
backwards = (days < 0)
|
||||||
days = abs(days)
|
|
||||||
intervals = []
|
intervals = []
|
||||||
planned_days = 0
|
planned_days, iterations = 0, 0
|
||||||
iterations = 0
|
current_datetime = day_dt.replace(hour=0, minute=0, second=0)
|
||||||
current_datetime = day_date.replace(hour=0, minute=0, second=0)
|
|
||||||
|
|
||||||
while planned_days < days and iterations < 100:
|
while planned_days < abs(days) and iterations < 100:
|
||||||
working_intervals = self._get_day_work_intervals(
|
working_intervals = self._get_day_work_intervals(
|
||||||
current_datetime,
|
current_datetime.date(),
|
||||||
compute_leaves=compute_leaves, resource_id=resource_id)
|
compute_leaves=compute_leaves, resource_id=resource_id)
|
||||||
if not self or working_intervals: # no calendar -> no working hours, but day is considered as worked
|
if not self or working_intervals: # no calendar -> no working hours, but day is considered as worked
|
||||||
planned_days += 1
|
planned_days += 1
|
||||||
@@ -543,9 +592,9 @@ class ResourceCalendar(models.Model):
|
|||||||
return intervals
|
return intervals
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def plan_days(self, days, day_date=None, compute_leaves=False, resource_id=None):
|
def plan_days(self, days, day_dt, compute_leaves=False, resource_id=None):
|
||||||
""" Returns the datetime of a days scheduling. """
|
""" Returns the datetime of a days scheduling. """
|
||||||
res = self._schedule_days(days, day_date, compute_leaves, resource_id)
|
res = self._schedule_days(days, day_dt, compute_leaves, resource_id)
|
||||||
return res and res[-1][1] or False
|
return res and res[-1][1] or False
|
||||||
|
|
||||||
|
|
||||||
@@ -584,7 +633,6 @@ class ResourceResource(models.Model):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
name = fields.Char(required=True)
|
name = fields.Char(required=True)
|
||||||
code = fields.Char(copy=False)
|
|
||||||
active = fields.Boolean(
|
active = fields.Boolean(
|
||||||
'Active', default=True, track_visibility='onchange',
|
'Active', default=True, track_visibility='onchange',
|
||||||
help="If the active field is set to False, it will allow you to hide the resource record without removing it.")
|
help="If the active field is set to False, it will allow you to hide the resource record without removing it.")
|
||||||
@@ -623,37 +671,6 @@ class ResourceResource(models.Model):
|
|||||||
if self.company_id:
|
if self.company_id:
|
||||||
self.calendar_id = self.company_id.resource_calendar_id.id
|
self.calendar_id = self.company_id.resource_calendar_id.id
|
||||||
|
|
||||||
def _is_work_day(self, date):
|
|
||||||
""" Whether the provided date is a work day for the subject resource.
|
|
||||||
|
|
||||||
:type date: datetime.date
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
|
||||||
return bool(next(self._iter_work_days(date, date), False))
|
|
||||||
|
|
||||||
def _iter_work_days(self, from_date, to_date):
|
|
||||||
""" Lists the current resource's work days between the two provided
|
|
||||||
dates (inclusive).
|
|
||||||
|
|
||||||
Work days are the company or service's open days (as defined by the
|
|
||||||
resource.calendar) minus the resource's own leaves.
|
|
||||||
|
|
||||||
:param datetime.date from_date: start of the interval to check for
|
|
||||||
work days (inclusive)
|
|
||||||
:param datetime.date to_date: end of the interval to check for work
|
|
||||||
days (inclusive)
|
|
||||||
:rtype: list(datetime.date)
|
|
||||||
"""
|
|
||||||
working_intervals = self.calendar_id._get_day_work_intervals
|
|
||||||
# rrule coerces date inputs to datetimes (with time=0) and yields
|
|
||||||
# datetimes (with time=0 if freq >= daily)
|
|
||||||
for dt in rrule.rrule(rrule.DAILY, dtstart=from_date, until=to_date):
|
|
||||||
intervals = working_intervals(dt, compute_leaves=True, resource_id=self.id)
|
|
||||||
|
|
||||||
# FIXME: get_working_intervals is new-API mapped to return a list of lists of intervals
|
|
||||||
if intervals and intervals[0]:
|
|
||||||
yield dt.date()
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceCalendarLeaves(models.Model):
|
class ResourceCalendarLeaves(models.Model):
|
||||||
_name = "resource.calendar.leaves"
|
_name = "resource.calendar.leaves"
|
||||||
@@ -666,6 +683,10 @@ class ResourceCalendarLeaves(models.Model):
|
|||||||
calendar_id = fields.Many2one('resource.calendar', 'Working Hours')
|
calendar_id = fields.Many2one('resource.calendar', 'Working Hours')
|
||||||
date_from = fields.Datetime('Start Date', required=True)
|
date_from = fields.Datetime('Start Date', required=True)
|
||||||
date_to = fields.Datetime('End 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', self.env.user.tz),
|
||||||
|
help="Timezone used when encoding the leave. It is used to correctly"
|
||||||
|
"localize leave hours when computing time intervals.")
|
||||||
resource_id = fields.Many2one(
|
resource_id = fields.Many2one(
|
||||||
"resource.resource", 'Resource',
|
"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")
|
help="If empty, this is a generic holiday for the company. If a resource is set, the holiday/leave is only for this resource")
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
import babel.dates
|
import babel.dates
|
||||||
|
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date, time
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from odoo.fields import Date, Datetime
|
from odoo.fields import Date, Datetime
|
||||||
from odoo.addons.resource.models.resource import seconds
|
from odoo.addons.resource.models.resource import to_naive_utc, to_naive_user_tz
|
||||||
from odoo.addons.resource.tests.common import TestResourceCommon
|
from odoo.addons.resource.tests.common import TestResourceCommon
|
||||||
|
|
||||||
|
|
||||||
@@ -71,13 +71,12 @@ class TestIntervals(TestResourceCommon):
|
|||||||
|
|
||||||
def test_interval_schedule_hours_backwards(self):
|
def test_interval_schedule_hours_backwards(self):
|
||||||
cleaned_intervals = self.env['resource.calendar']._interval_merge(self.intervals)
|
cleaned_intervals = self.env['resource.calendar']._interval_merge(self.intervals)
|
||||||
cleaned_intervals.reverse()
|
result = self.env['resource.calendar']._interval_schedule_hours(cleaned_intervals, 5.5, backwards=True)
|
||||||
result = self.env['resource.calendar']._interval_schedule_hours(cleaned_intervals, 5.5, remove_at_end=False)
|
|
||||||
self.assertEqual(len(result), 2)
|
self.assertEqual(len(result), 2)
|
||||||
# First interval: 03, 8-10 untouched
|
# First interval: 03, 8-10 untouched
|
||||||
self.assertEqual(result[0][:2], (Datetime.from_string('2013-02-04 17:00:00'), Datetime.from_string('2013-02-04 21:00:00')))
|
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
|
# First interval: 04, 08-11:30
|
||||||
self.assertEqual(result[1][:2], (Datetime.from_string('2013-02-04 12:30:00'), Datetime.from_string('2013-02-04 14:00:00')))
|
self.assertEqual(result[0][:2], (Datetime.from_string('2013-02-04 12:30:00'), Datetime.from_string('2013-02-04 14:00:00')))
|
||||||
|
|
||||||
|
|
||||||
class TestCalendarBasics(TestResourceCommon):
|
class TestCalendarBasics(TestResourceCommon):
|
||||||
@@ -122,19 +121,19 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
|
|
||||||
def test_calendar_working_day_intervals_no_leaves(self):
|
def test_calendar_working_day_intervals_no_leaves(self):
|
||||||
# Test: day0 without leaves: 1 interval
|
# Test: day0 without leaves: 1 interval
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=Datetime.from_string('2013-02-12 09:08:07'))
|
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(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], (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)
|
self.assertEqual(intervals[0][2]['attendances'], self.att_1)
|
||||||
|
|
||||||
# Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00
|
# Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=Datetime.from_string('2013-02-19 10:30: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(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], (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)
|
self.assertEqual(intervals[0][2]['attendances'], self.att_1)
|
||||||
|
|
||||||
# Test: day3 without leaves: 2 interval
|
# Test: day3 without leaves: 2 interval
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=Datetime.from_string('2013-02-15 10:11:12'))
|
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(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[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[1][:2], (Datetime.from_string('2013-02-15 16:00:00'), Datetime.from_string('2013-02-15 23:00:00')))
|
||||||
@@ -143,14 +142,15 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
|
|
||||||
def test_calendar_working_day_intervals_leaves_generic(self):
|
def test_calendar_working_day_intervals_leaves_generic(self):
|
||||||
# Test: day0 with leaves outside range: 1 interval
|
# Test: day0 with leaves outside range: 1 interval
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=Datetime.from_string('2013-02-12 07:00:00'), compute_leaves=True)
|
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(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')))
|
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
|
# 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(start_dt=Datetime.from_string('2013-02-19 08:15:00'),
|
intervals = self.calendar._get_day_work_intervals(Date.from_string('2013-02-19'),
|
||||||
end_dt=Datetime.from_string('2013-02-19 15:45:30'),
|
start_time=time(8, 15, 0),
|
||||||
compute_leaves=True)
|
end_time=time(15, 45, 30),
|
||||||
|
compute_leaves=True)
|
||||||
self.assertEqual(len(intervals), 2)
|
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[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[1][:2], (Datetime.from_string('2013-02-19 12:00:00'), Datetime.from_string('2013-02-19 15:45:30')))
|
||||||
@@ -162,7 +162,8 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
def test_calendar_working_day_intervals_leaves_resource(self):
|
def test_calendar_working_day_intervals_leaves_resource(self):
|
||||||
# Test: day1+14 on leave, with resource leave computation
|
# Test: day1+14 on leave, with resource leave computation
|
||||||
intervals = self.calendar._get_day_work_intervals(
|
intervals = self.calendar._get_day_work_intervals(
|
||||||
Datetime.from_string('2013-02-26 07:00:00'),
|
Date.from_string('2013-02-26'),
|
||||||
|
start_time=time(7, 0, 0),
|
||||||
compute_leaves=True,
|
compute_leaves=True,
|
||||||
resource_id=self.resource1_id
|
resource_id=self.resource1_id
|
||||||
)
|
)
|
||||||
@@ -177,7 +178,7 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
'date_from': self.date2 + relativedelta(days=7),
|
'date_from': self.date2 + relativedelta(days=7),
|
||||||
'date_to': False,
|
'date_to': False,
|
||||||
})
|
})
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=self.date2)
|
intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
|
||||||
self.assertEqual(len(intervals), 1)
|
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')))
|
self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
'date_from': False,
|
'date_from': False,
|
||||||
'date_to': self.date2 - relativedelta(days=7),
|
'date_to': self.date2 - relativedelta(days=7),
|
||||||
})
|
})
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=self.date2)
|
intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
|
||||||
self.assertEqual(len(intervals), 1)
|
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')))
|
self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
'date_from': self.date2 + relativedelta(days=7),
|
'date_from': self.date2 + relativedelta(days=7),
|
||||||
'date_to': self.date2 - relativedelta(days=7),
|
'date_to': self.date2 - relativedelta(days=7),
|
||||||
})
|
})
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=self.date2)
|
intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
|
||||||
self.assertEqual(len(intervals), 1)
|
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')))
|
self.assertEqual(intervals[0][:2], (Datetime.from_string('2013-02-15 10:11:12'), Datetime.from_string('2013-02-15 13:00:00')))
|
||||||
|
|
||||||
@@ -201,14 +202,14 @@ class TestCalendarBasics(TestResourceCommon):
|
|||||||
'date_from': self.date2,
|
'date_from': self.date2,
|
||||||
'date_to': self.date2,
|
'date_to': self.date2,
|
||||||
})
|
})
|
||||||
intervals = self.calendar._get_day_work_intervals(start_dt=self.date2)
|
intervals = self.calendar._get_day_work_intervals(self.date2.date(), start_time=self.date2.time())
|
||||||
self.assertEqual(len(intervals), 2)
|
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[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[1][:2], (Datetime.from_string('2013-02-15 16:00:00'), Datetime.from_string('2013-02-15 23:00:00')))
|
||||||
|
|
||||||
def test_calendar_working_hours_of_date(self):
|
def test_calendar_working_hours_of_date(self):
|
||||||
# Test: day1, beginning at 10:30 -> work from 10:30 (arrival) until 16:00
|
# 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'))
|
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)
|
self.assertEqual(wh, 5.5)
|
||||||
|
|
||||||
|
|
||||||
@@ -220,7 +221,8 @@ class ResourceWorkingHours(TestResourceCommon):
|
|||||||
res = self.calendar.get_work_hours_count(
|
res = self.calendar.get_work_hours_count(
|
||||||
Datetime.from_string('2013-02-12 06:00:00'),
|
Datetime.from_string('2013-02-12 06:00:00'),
|
||||||
Datetime.from_string('2013-02-22 23:00:00'),
|
Datetime.from_string('2013-02-22 23:00:00'),
|
||||||
compute_leaves=False, resource_id=self.resource1_id)
|
self.resource1_id,
|
||||||
|
compute_leaves=False)
|
||||||
self.assertEqual(res, 40.0)
|
self.assertEqual(res, 40.0)
|
||||||
|
|
||||||
def test_calendar_working_hours_leaves(self):
|
def test_calendar_working_hours_leaves(self):
|
||||||
@@ -229,9 +231,32 @@ class ResourceWorkingHours(TestResourceCommon):
|
|||||||
res = self.calendar.get_work_hours_count(
|
res = self.calendar.get_work_hours_count(
|
||||||
Datetime.from_string('2013-02-12 06:00:00'),
|
Datetime.from_string('2013-02-12 06:00:00'),
|
||||||
Datetime.from_string('2013-02-22 23:00:00'),
|
Datetime.from_string('2013-02-22 23:00:00'),
|
||||||
compute_leaves=True, resource_id=self.resource1_id)
|
self.resource1_id,
|
||||||
|
compute_leaves=True)
|
||||||
self.assertEqual(res, 33.0)
|
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 = 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)
|
||||||
|
})
|
||||||
|
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_hours_scheduling_backward(self):
|
def test_calendar_hours_scheduling_backward(self):
|
||||||
res = self.calendar._schedule_hours(-40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
|
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
|
# current day, limited at 09:00 because of day_dt specified -> 1 hour
|
||||||
@@ -249,7 +274,7 @@ class ResourceWorkingHours(TestResourceCommon):
|
|||||||
td = timedelta()
|
td = timedelta()
|
||||||
for item in res:
|
for item in res:
|
||||||
td += item[1] - item[0]
|
td += item[1] - item[0]
|
||||||
self.assertEqual(seconds(td) / 3600.0, 40.0)
|
self.assertEqual(td.total_seconds() / 3600.0, 40.0)
|
||||||
|
|
||||||
def test_calendar_hours_scheduling_forward(self):
|
def test_calendar_hours_scheduling_forward(self):
|
||||||
res = self.calendar._schedule_hours(40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
|
res = self.calendar._schedule_hours(40, day_dt=Datetime.from_string('2013-02-12 09:00:00'))
|
||||||
@@ -264,7 +289,7 @@ class ResourceWorkingHours(TestResourceCommon):
|
|||||||
td = timedelta()
|
td = timedelta()
|
||||||
for item in res:
|
for item in res:
|
||||||
td += item[1] - item[0]
|
td += item[1] - item[0]
|
||||||
self.assertEqual(seconds(td) / 3600.0, 40.0)
|
self.assertEqual(td.total_seconds() / 3600.0, 40.0)
|
||||||
|
|
||||||
def test_calendar_hours_scheduling_forward_leaves_resource(self):
|
def test_calendar_hours_scheduling_forward_leaves_resource(self):
|
||||||
res = self.calendar._schedule_hours(
|
res = self.calendar._schedule_hours(
|
||||||
@@ -284,16 +309,16 @@ class ResourceWorkingHours(TestResourceCommon):
|
|||||||
td = timedelta()
|
td = timedelta()
|
||||||
for item in res:
|
for item in res:
|
||||||
td += item[1] - item[0]
|
td += item[1] - item[0]
|
||||||
self.assertEqual(seconds(td) / 3600.0, 40.0)
|
self.assertEqual(td.total_seconds() / 3600.0, 40.0)
|
||||||
|
|
||||||
def test_calendar_days_scheduling(self):
|
def test_calendar_days_scheduling(self):
|
||||||
res = self.calendar.plan_days(5, day_date=Datetime.from_string('2013-02-12 09:08:07') )
|
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')
|
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, day_date=Datetime.from_string('2013-02-12 09:08:07') )
|
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')
|
self.assertEqual(res.date(), Datetime.from_string('2013-02-08 00:00:00').date(), 'resource_calendar: wrong days scheduling')
|
||||||
|
|
||||||
res = self.calendar.plan_days(
|
res = self.calendar.plan_days(
|
||||||
5, day_date=Datetime.from_string('2013-02-12 09:08:07'),
|
5, Datetime.from_string('2013-02-12 09:08:07'),
|
||||||
compute_leaves=True, resource_id=self.resource1_id)
|
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')
|
self.assertEqual(res.date(), Datetime.from_string('2013-03-01 00:00:00').date(), 'resource_calendar: wrong days scheduling')
|
||||||
|
|
||||||
@@ -345,7 +370,7 @@ class TestWorkDays(TestResourceCommon):
|
|||||||
# saturday and sunday
|
# saturday and sunday
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[d for d in self._days if d.weekday() not in (5, 6)],
|
[d for d in self._days if d.weekday() not in (5, 6)],
|
||||||
list(r._iter_work_days(WAR_START, WAR_END))
|
list(r.calendar_id._iter_work_days(WAR_START, WAR_END, r.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_global_leaves(self):
|
def test_global_leaves(self):
|
||||||
@@ -369,8 +394,8 @@ class TestWorkDays(TestResourceCommon):
|
|||||||
if d.weekday() not in (5, 6)
|
if d.weekday() not in (5, 6)
|
||||||
if d < date(1932, 11, 9) or d > date(1932, 11, 12)
|
if d < date(1932, 11, 9) or d > date(1932, 11, 12)
|
||||||
]
|
]
|
||||||
self.assertEqual(days, list(r1._iter_work_days(WAR_START, WAR_END)))
|
self.assertEqual(days, list(r1.calendar_id._iter_work_days(WAR_START, WAR_END, r1.id)))
|
||||||
self.assertEqual(days, list(r2._iter_work_days(WAR_START, WAR_END)))
|
self.assertEqual(days, list(r2.calendar_id._iter_work_days(WAR_START, WAR_END, r2.id)))
|
||||||
|
|
||||||
def test_personal_leaves(self):
|
def test_personal_leaves(self):
|
||||||
""" Leaves with a resource_id apply only to that resource
|
""" Leaves with a resource_id apply only to that resource
|
||||||
@@ -391,10 +416,10 @@ class TestWorkDays(TestResourceCommon):
|
|||||||
})
|
})
|
||||||
|
|
||||||
weekdays = [d for d in self._days if d.weekday() not in (5, 6)]
|
weekdays = [d for d in self._days if d.weekday() not in (5, 6)]
|
||||||
self.assertEqual(weekdays, list(r1._iter_work_days(WAR_START, WAR_END)))
|
self.assertEqual(weekdays, list(r1.calendar_id._iter_work_days(WAR_START, WAR_END, r1.id)))
|
||||||
self.assertEqual([
|
self.assertEqual([
|
||||||
d for d in weekdays if d < date(1932, 11, 9) or d > date(1932, 11, 12)],
|
d for d in weekdays if d < date(1932, 11, 9) or d > date(1932, 11, 12)],
|
||||||
list(r2._iter_work_days(WAR_START, WAR_END))
|
list(r2.calendar_id._iter_work_days(WAR_START, WAR_END, r2.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_mixed_leaves(self):
|
def test_mixed_leaves(self):
|
||||||
@@ -419,17 +444,17 @@ class TestWorkDays(TestResourceCommon):
|
|||||||
if d.weekday() not in (5, 6)
|
if d.weekday() not in (5, 6)
|
||||||
if d < date(1932, 11, 9) or d > date(1932, 11, 12)
|
if d < date(1932, 11, 9) or d > date(1932, 11, 12)
|
||||||
if d < date(1932, 12, 2)],
|
if d < date(1932, 12, 2)],
|
||||||
list(r._iter_work_days(WAR_START, WAR_END))
|
list(r.calendar_id._iter_work_days(WAR_START, WAR_END, r.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
# _is_work_day is built on _iter_work_days, but it's probably a good
|
# _is_work_day is built on _iter_work_days, but it's probably a good
|
||||||
# idea to ensure it does do what it should
|
# idea to ensure it does do what it should
|
||||||
self.assertTrue(r._is_work_day(date(1932, 11, 8)))
|
self.assertTrue(r.calendar_id._is_work_day(date(1932, 11, 8), r.id))
|
||||||
self.assertTrue(r._is_work_day(date(1932, 11, 14)))
|
self.assertTrue(r.calendar_id._is_work_day(date(1932, 11, 14), r.id))
|
||||||
self.assertTrue(r._is_work_day(date(1932, 12, 1)))
|
self.assertTrue(r.calendar_id._is_work_day(date(1932, 12, 1), r.id))
|
||||||
|
|
||||||
self.assertFalse(r._is_work_day(date(1932, 11, 11))) # global leave
|
self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 11), r.id)) # global leave
|
||||||
self.assertFalse(r._is_work_day(date(1932, 11, 13))) # sun
|
self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 13), r.id)) # sun
|
||||||
self.assertFalse(r._is_work_day(date(1932, 11, 19))) # sat
|
self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 19), r.id)) # sat
|
||||||
self.assertFalse(r._is_work_day(date(1932, 11, 20))) # sun
|
self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 20), r.id)) # sun
|
||||||
self.assertFalse(r._is_work_day(date(1932, 12, 6))) # personal leave
|
self.assertFalse(r.calendar_id._is_work_day(date(1932, 12, 6), r.id)) # personal leave
|
||||||
|
|||||||
Reference in New Issue
Block a user