Files
odoo_source/addons/resource/models/resource_mixin.py
T
Lucas Lefèvre b674e4401d [FIX] resource: Adjust time span to calendar
The method `_get_work_interval` has several problems:

1) The name is not self explanatory in any way

2) It should be defined on the `resource.mixin`

3) It simply does not work (as described in its docstring).
   Here is a simplified example (only concerned about hours):
   Currently, given two attendances: 8-12 and 13-17,
   with parameters start=9 and end=18 it returns (9, 17)
   while it should return (8, 17).

   This leads to strange behaviors in the planning app:
   Given employee A with calendar 8-17 and employee B 8-16.
   Create a planning slot and assign employee A: the start and end times
   are set to 8-17.
   Then assign employee B, the start and end times are correcly set to 8-16.
   Now reassign employee A: the start and end times are not set to 8-17.

Task 2229296

closes odoo/odoo#50804

X-original-commit: 7dfab89bc0025cf6424f00b6992fef1f42463ffe
Related: odoo/enterprise#10426
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Signed-off-by: lul-odoo <LucasLefevre@users.noreply.github.com>
2020-05-07 08:22:22 +00:00

172 lines
6.9 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from pytz import utc
from odoo import api, fields, models
def timezone_datetime(time):
if not time.tzinfo:
time = time.replace(tzinfo=utc)
return time
class ResourceMixin(models.AbstractModel):
_name = "resource.mixin"
_description = 'Resource Mixin'
resource_id = fields.Many2one(
'resource.resource', 'Resource',
auto_join=True, index=True, ondelete='restrict', required=True)
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env.company,
index=True, related='resource_id.company_id', store=True, readonly=False)
resource_calendar_id = fields.Many2one(
'resource.calendar', 'Working Hours',
default=lambda self: self.env.company.resource_calendar_id,
index=True, related='resource_id.calendar_id', store=True, readonly=False)
tz = fields.Selection(
string='Timezone', related='resource_id.tz', readonly=False,
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_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)
def copy_data(self, default=None):
if default is None:
default = {}
resource = self.resource_id.copy()
default['resource_id'] = resource.id
default['company_id'] = resource.company_id.id
default['resource_calendar_id'] = resource.calendar_id.id
return super(ResourceMixin, self).copy_data(default)
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.
`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
quantity of working time expressed as days and as hours.
"""
resource = self.resource_id
calendar = calendar or self.resource_calendar_id
# naive datetimes are made explicit in UTC
from_datetime = timezone_datetime(from_datetime)
to_datetime = timezone_datetime(to_datetime)
day_total = calendar._get_day_total(from_datetime, to_datetime, resource)
# 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)
return calendar._get_days_data(intervals, day_total)
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
# naive datetimes are made explicit in UTC
from_datetime = timezone_datetime(from_datetime)
to_datetime = timezone_datetime(to_datetime)
day_total = calendar._get_day_total(from_datetime, to_datetime, resource)
# compute actual hours per day
attendances = calendar._attendance_intervals(from_datetime, to_datetime, resource)
leaves = calendar._leave_intervals(from_datetime, to_datetime, resource, domain)
return calendar._get_days_data(attendances & leaves, day_total)
def _adjust_to_calendar(self, start, end):
resource_results = self.resource_id._adjust_to_calendar(start, end)
# change dict keys from resources to associated records.
return {
record: resource_results[record.resource_id]
for record in self
}
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.
`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) for each day
containing at least an attendance.
"""
resource = self.resource_id
calendar = calendar or self.resource_calendar_id
# 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
# 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