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>
172 lines
6.9 KiB
Python
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
|