[MERGE] resource: get work intervals based on resource settings/contracts
The False key is always added in attendance and leave intervals batch but is not added in the work_intervals batch. To give a more smooth API, this commit aligns work_intervals behavior with the two other methods behavior, regarding the added False key. task-2646630 -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#77362 Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
@@ -1,10 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from pytz import utc, timezone
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
|
||||
|
||||
class ResourceResource(models.Model):
|
||||
_inherit = "resource.resource"
|
||||
|
||||
user_id = fields.Many2one(copy=False)
|
||||
employee_id = fields.One2many('hr.employee', 'resource_id', domain="[('company_id', '=', company_id)]")
|
||||
|
||||
def _get_calendars_validity_within_period(self, start, end, default_company=None):
|
||||
"""
|
||||
Returns a dict of dict with resource's id as first key and resource's calendar as secondary key
|
||||
The value is the validity interval of the calendar for the given resource.
|
||||
|
||||
The validity interval of the employee resource calendar is the lifetime of the employee, from creation to departure.
|
||||
"""
|
||||
assert start.tzinfo and end.tzinfo
|
||||
calendars_within_period_per_resource = super()._get_calendars_validity_within_period(start, end, default_company=default_company)
|
||||
for resource in self:
|
||||
if not resource.employee_id:
|
||||
continue
|
||||
create_date = max(start, utc.localize(resource.employee_id.create_date))
|
||||
if resource.employee_id.departure_date and resource.employee_id.departure_date <= end.date():
|
||||
departure_datetime = timezone(resource.tz).localize(datetime.combine(resource.employee_id.departure_date, datetime.max.time()))
|
||||
departure_datetime = min(departure_datetime, end)
|
||||
else:
|
||||
departure_datetime = end
|
||||
interval = Intervals([(create_date, departure_datetime, self.env['resource.calendar.attendance'])])
|
||||
for calendar in calendars_within_period_per_resource[resource.id]:
|
||||
calendars_within_period_per_resource[resource.id][calendar] &= interval
|
||||
return calendars_within_period_per_resource
|
||||
|
||||
@@ -6,3 +6,4 @@ from . import test_channel
|
||||
from . import test_hr_flow
|
||||
from . import test_self_user_access
|
||||
from . import test_multi_company
|
||||
from . import test_resource
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime
|
||||
from pytz import timezone, utc
|
||||
|
||||
from odoo.addons.resource.models.resource import Intervals, sum_intervals
|
||||
|
||||
from .common import TestHrCommon
|
||||
|
||||
|
||||
class TestResource(TestHrCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestResource, cls).setUpClass()
|
||||
cls.calendar_40h = cls.env['resource.calendar'].create({'name': 'Default calendar'})
|
||||
cls.employee_niv = cls.env['hr.employee'].create({
|
||||
'name': 'Sharlene Rhodes',
|
||||
'departure_date': '2022-06-01',
|
||||
'resource_calendar_id': cls.calendar_40h.id,
|
||||
})
|
||||
cls.employee_niv_create_date = '2021-01-01 10:00:00'
|
||||
cls.env.cr.execute("UPDATE hr_employee SET create_date=%s WHERE id=%s",
|
||||
(cls.employee_niv_create_date, cls.employee_niv.id))
|
||||
|
||||
def test_calendars_validity_within_period_default(self):
|
||||
calendars = self.employee_niv.resource_id._get_calendars_validity_within_period(
|
||||
utc.localize(datetime(2021, 7, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2021, 7, 30, 17, 0, 0)),
|
||||
)
|
||||
interval = Intervals([(
|
||||
utc.localize(datetime(2021, 7, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2021, 7, 30, 17, 0, 0)),
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
|
||||
self.assertEqual(1, len(calendars), "The dict returned by calendars validity should only have 1 entry")
|
||||
self.assertEqual(1, len(calendars[self.employee_niv.resource_id.id]), "Niv should only have one calendar")
|
||||
niv_entry = calendars[self.employee_niv.resource_id.id]
|
||||
niv_calendar = next(iter(niv_entry))
|
||||
self.assertEqual(niv_calendar, self.calendar_40h, "It should be Niv's Calendar")
|
||||
self.assertFalse(niv_entry[niv_calendar] - interval, "Interval should cover all calendar's validity")
|
||||
self.assertFalse(interval - niv_entry[niv_calendar], "Calendar validity should cover all interval")
|
||||
|
||||
def test_calendars_validity_within_period_creation(self):
|
||||
calendars = self.employee_niv.resource_id._get_calendars_validity_within_period(
|
||||
utc.localize(datetime(2020, 12, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2021, 1, 31, 17, 0, 0)),
|
||||
)
|
||||
interval = Intervals([(
|
||||
utc.localize(datetime(2021, 1, 1, 10, 0, 0)),
|
||||
utc.localize(datetime(2021, 1, 31, 17, 0, 0)),
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
niv_entry = calendars[self.employee_niv.resource_id.id]
|
||||
self.assertFalse(niv_entry[self.calendar_40h] - interval, "Interval should cover all calendar's validity")
|
||||
self.assertFalse(interval - niv_entry[self.calendar_40h], "Calendar validity should cover all interval")
|
||||
|
||||
def test_calendars_validity_within_period_before_creation(self):
|
||||
calendars = self.employee_niv.resource_id._get_calendars_validity_within_period(
|
||||
utc.localize(datetime(2020, 12, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2020, 12, 31, 17, 0, 0)),
|
||||
)
|
||||
niv_entry = calendars[self.employee_niv.resource_id.id]
|
||||
self.assertFalse(niv_entry[self.calendar_40h], "Interval should be empty")
|
||||
|
||||
def test_calendars_validity_within_period_before_departure(self):
|
||||
calendars = self.employee_niv.resource_id._get_calendars_validity_within_period(
|
||||
utc.localize(datetime(2022, 5, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2022, 6, 30, 17, 0, 0)),
|
||||
)
|
||||
interval = Intervals([(
|
||||
utc.localize(datetime(2022, 5, 1, 8, 0, 0)),
|
||||
timezone(self.employee_niv.tz).localize(datetime(2022, 6, 1, 23, 59, 59, 999999)),
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
niv_entry = calendars[self.employee_niv.resource_id.id]
|
||||
self.assertFalse(niv_entry[self.calendar_40h] - interval, "Interval should cover all calendar's validity")
|
||||
self.assertFalse(interval - niv_entry[self.calendar_40h], "Calendar validity should cover all interval")
|
||||
|
||||
def test_get_valid_work_intervals(self):
|
||||
start = utc.localize(datetime(2022, 5, 29, 0, 0, 0))
|
||||
end = utc.localize(datetime(2022, 6, 4, 23, 59, 59))
|
||||
work_intervals, _ = self.employee_niv.resource_id._get_valid_work_intervals(start, end)
|
||||
sum_work_intervals = sum_intervals(work_intervals[self.employee_niv.resource_id.id])
|
||||
self.assertEqual(24, sum_work_intervals, "Sum of the work intervals for the resource niv should be 24h as he left his work after wednesday.")
|
||||
@@ -6,4 +6,5 @@ from . import hr_employee_public
|
||||
from . import hr_contract
|
||||
from . import res_users
|
||||
from . import resource
|
||||
from . import resource_resource
|
||||
from . import hr_contract_type
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pytz import timezone
|
||||
|
||||
from odoo import models
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
|
||||
class ResourceResource(models.Model):
|
||||
_inherit = 'resource.resource'
|
||||
|
||||
def _get_calendars_validity_within_period(self, start, end, default_company=None):
|
||||
assert start.tzinfo and end.tzinfo
|
||||
if not self:
|
||||
return super()._get_calendars_validity_within_period(start, end, default_company=default_company)
|
||||
calendars_within_period_per_resource = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']]
|
||||
resource_without_contract = self.filtered(lambda r: not r.employee_id or r.employee_id.employee_type not in ['employee', 'student'])
|
||||
if resource_without_contract:
|
||||
calendars_within_period_per_resource.update(
|
||||
super(ResourceResource, resource_without_contract)._get_calendars_validity_within_period(start, end, default_company=default_company)
|
||||
)
|
||||
resource_with_contract = self - resource_without_contract
|
||||
if not resource_with_contract:
|
||||
return calendars_within_period_per_resource
|
||||
timezones = {resource.tz for resource in resource_with_contract}
|
||||
date_start = min(start.astimezone(timezone(tz)).date() for tz in timezones)
|
||||
date_end = max(end.astimezone(timezone(tz)).date() for tz in timezones)
|
||||
contracts = resource_with_contract.employee_id._get_contracts(
|
||||
date_start, date_end, states=['open', 'draft', 'close']
|
||||
).filtered(lambda c: c.state in ['open', 'close'] or c.kanban_state == 'done')
|
||||
for contract in contracts:
|
||||
tz = timezone(contract.employee_id.tz)
|
||||
calendars_within_period_per_resource[contract.employee_id.resource_id.id][contract.resource_calendar_id] |= Intervals([(
|
||||
tz.localize(datetime.combine(contract.date_start, datetime.min.time())) if contract.date_start > start.astimezone(tz).date() else start,
|
||||
tz.localize(datetime.combine(contract.date_end, datetime.max.time())) if contract.date_end and contract.date_end < end.astimezone(tz).date() else end,
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
return calendars_within_period_per_resource
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import test_auto_status
|
||||
from . import test_contract
|
||||
from . import test_calendar_sync
|
||||
from . import test_resource
|
||||
|
||||
@@ -66,4 +66,4 @@ class TestContractCalendars(TestContractCommon):
|
||||
# Transfer global leaves
|
||||
self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=None, from_date=Date.to_date('2015-11-21'))
|
||||
|
||||
self.assertEqual(leave3.calendar_id, self.calendar_35h, "Global leave should be transfered")
|
||||
self.assertEqual(leave3.calendar_id, self.calendar_35h, "Global leave should be transfered")
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import date, datetime
|
||||
from pytz import utc, timezone
|
||||
|
||||
from odoo.addons.resource.models.resource import Intervals, sum_intervals
|
||||
from odoo.fields import Date
|
||||
|
||||
from .common import TestContractCommon
|
||||
|
||||
class TestResource(TestContractCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestResource, cls).setUpClass()
|
||||
cls.calendar_richard = cls.env['resource.calendar'].create({'name': 'Calendar of Richard'})
|
||||
cls.employee.resource_calendar_id = cls.calendar_richard
|
||||
|
||||
cls.calendar_35h = cls.env['resource.calendar'].create({
|
||||
'name': '35h calendar',
|
||||
'attendance_ids': [
|
||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
|
||||
],
|
||||
})
|
||||
cls.calendar_35h._onchange_hours_per_day() # update hours/day
|
||||
|
||||
cls.contract_cdd = cls.env['hr.contract'].create({
|
||||
'date_start': Date.to_date('2021-09-01'),
|
||||
'date_end': Date.to_date('2021-10-31'),
|
||||
'name': 'First CDD Contract for Richard',
|
||||
'resource_calendar_id': cls.calendar_35h.id,
|
||||
'wage': 5000.0,
|
||||
'employee_id': cls.employee.id,
|
||||
'state': 'open',
|
||||
})
|
||||
cls.contract_cdi = cls.env['hr.contract'].create({
|
||||
'date_start': Date.to_date('2021-11-01'),
|
||||
'name': 'CDI Contract for Richard',
|
||||
'resource_calendar_id': cls.calendar_richard.id,
|
||||
'wage': 5000.0,
|
||||
'employee_id': cls.employee.id,
|
||||
'state': 'draft',
|
||||
'kanban_state': 'done',
|
||||
})
|
||||
|
||||
def test_calendars_validity_within_period(self):
|
||||
tz = timezone(self.employee.tz)
|
||||
calendars = self.employee.resource_id._get_calendars_validity_within_period(
|
||||
tz.localize(datetime(2021, 10, 1, 0, 0, 0)),
|
||||
tz.localize(datetime(2021, 12, 1, 0, 0, 0)),
|
||||
)
|
||||
interval_35h = Intervals([(
|
||||
tz.localize(datetime(2021, 10, 1, 0, 0, 0)),
|
||||
tz.localize(datetime.combine(date(2021, 10, 31), datetime.max.time())),
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
interval_40h = Intervals([(
|
||||
tz.localize(datetime(2021, 11, 1, 0, 0, 0)),
|
||||
tz.localize(datetime(2021, 12, 1, 0, 0, 0)),
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
|
||||
self.assertEqual(1, len(calendars), "The dict returned by calendars validity should only have 1 entry")
|
||||
self.assertEqual(2, len(calendars[self.employee.resource_id.id]), "Jean should only have one calendar")
|
||||
richard_entries = calendars[self.employee.resource_id.id]
|
||||
for calendar in richard_entries:
|
||||
self.assertTrue(calendar in (self.calendar_35h | self.calendar_richard), "Each calendar should be listed")
|
||||
if calendar == self.calendar_35h:
|
||||
self.assertFalse(richard_entries[calendar] - interval_35h, "Interval 35h should cover all calendar 35h validity")
|
||||
self.assertFalse(interval_35h - richard_entries[calendar], "Calendar 35h validity should cover all interval 35h")
|
||||
elif calendar == self.calendar_richard:
|
||||
self.assertFalse(richard_entries[calendar] - interval_40h, "Interval 40h should cover all calendar 40h validity")
|
||||
self.assertFalse(interval_40h - richard_entries[calendar], "Calendar 40h validity should cover all interval 40h")
|
||||
|
||||
def test_queries(self):
|
||||
employees_test = self.env['hr.employee'].create([{
|
||||
'name': 'Employee ' + str(i),
|
||||
} for i in range(0, 50)])
|
||||
for emp in employees_test:
|
||||
new_contract = self.contract_cdd.copy()
|
||||
new_contract.employee_id = emp
|
||||
new_contract.state = 'open'
|
||||
new_contract = self.contract_cdi.copy()
|
||||
new_contract.employee_id = emp
|
||||
new_contract.state = 'draft'
|
||||
new_contract.kanban_state = 'done'
|
||||
|
||||
start = utc.localize(datetime(2021, 9, 1, 0, 0, 0))
|
||||
end = utc.localize(datetime(2021, 11, 30, 23, 59, 59))
|
||||
with self.assertQueryCount(14):
|
||||
work_intervals, _ = (employees_test | self.employee).resource_id._get_valid_work_intervals(start, end)
|
||||
|
||||
self.assertEqual(len(work_intervals), 51)
|
||||
|
||||
def test_get_valid_work_intervals(self):
|
||||
start = timezone(self.employee.tz).localize(datetime(2021, 10, 24, 2, 0, 0))
|
||||
end = timezone(self.employee.tz).localize(datetime(2021, 11, 6, 23, 59, 59))
|
||||
work_intervals, _ = self.employee.resource_id._get_valid_work_intervals(start, end)
|
||||
sum_work_intervals = sum_intervals(work_intervals[self.employee.resource_id.id])
|
||||
self.assertEqual(75, sum_work_intervals, "Sum of the work intervals for the employee should be 35h+40h = 75h")
|
||||
@@ -138,6 +138,12 @@ class Intervals(object):
|
||||
|
||||
return result
|
||||
|
||||
def sum_intervals(intervals):
|
||||
""" Sum the intervals duration (unit : hour)"""
|
||||
return sum(
|
||||
(stop - start).total_seconds() / 3600
|
||||
for start, stop, meta in intervals
|
||||
)
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
""" Calendar model for a resource. It has
|
||||
@@ -382,13 +388,15 @@ class ResourceCalendar(models.Model):
|
||||
""" Return the attendance intervals in the given datetime range.
|
||||
The returned intervals are expressed in specified tz or in the resource's timezone.
|
||||
"""
|
||||
self.ensure_one()
|
||||
resources = self.env['resource.resource'] if not resources else resources
|
||||
assert start_dt.tzinfo and end_dt.tzinfo
|
||||
self.ensure_one()
|
||||
combine = datetime.combine
|
||||
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
if not resources:
|
||||
resources = self.env['resource.resource']
|
||||
resources_list = [resources]
|
||||
else:
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
resource_ids = [r.id for r in resources_list]
|
||||
domain = domain if domain is not None else []
|
||||
domain = expression.AND([domain, [
|
||||
@@ -472,15 +480,18 @@ class ResourceCalendar(models.Model):
|
||||
""" Return the leave intervals in the given datetime range.
|
||||
The returned intervals are expressed in specified tz or in the calendar's timezone.
|
||||
"""
|
||||
resources = self.env['resource.resource'] if not resources else resources
|
||||
assert start_dt.tzinfo and end_dt.tzinfo
|
||||
self.ensure_one()
|
||||
|
||||
# for the computation, express all datetimes in UTC
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
if not resources:
|
||||
resources = self.env['resource.resource']
|
||||
resources_list = [resources]
|
||||
else:
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
resource_ids = [r.id for r in resources_list]
|
||||
if domain is None:
|
||||
domain = [('time_type', '=', 'leave')]
|
||||
# for the computation, express all datetimes in UTC
|
||||
domain = domain + [
|
||||
('calendar_id', 'in', [False, self.id]),
|
||||
('resource_id', 'in', resource_ids),
|
||||
@@ -518,7 +529,7 @@ class ResourceCalendar(models.Model):
|
||||
resources = self.env['resource.resource']
|
||||
resources_list = [resources]
|
||||
else:
|
||||
resources_list = list(resources)
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
|
||||
attendance_intervals = self._attendance_intervals_batch(start_dt, end_dt, resources, tz=tz)
|
||||
if compute_leaves:
|
||||
@@ -588,8 +599,11 @@ class ResourceCalendar(models.Model):
|
||||
@return dict with hours of attendance in each day between `from_datetime` and `to_datetime`
|
||||
"""
|
||||
self.ensure_one()
|
||||
resources = self.env['resource.resource'] if not resources else resources
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
if not resources:
|
||||
resources = self.env['resource.resource']
|
||||
resources_list = [resources]
|
||||
else:
|
||||
resources_list = list(resources) + [self.env['resource.resource']]
|
||||
# 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)
|
||||
@@ -994,7 +1008,6 @@ class ResourceResource(models.Model):
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _get_unavailable_intervals(self, start, end):
|
||||
""" Compute the intervals during which employee is unavailable with hour granularity between start and end
|
||||
Note: this method is used in enterprise (forecast and planning)
|
||||
@@ -1012,6 +1025,52 @@ class ResourceResource(models.Model):
|
||||
resource_mapping.update(resources_unavailable_intervals)
|
||||
return resource_mapping
|
||||
|
||||
def _get_calendars_validity_within_period(self, start, end, default_company=None):
|
||||
""" Gets a dict of dict with resource's id as first key and resource's calendar as secondary key
|
||||
The value is the validity interval of the calendar for the given resource.
|
||||
|
||||
Here the validity interval for each calendar is the whole interval but it's meant to be overriden in further modules
|
||||
handling resource's employee contracts.
|
||||
"""
|
||||
assert start.tzinfo and end.tzinfo
|
||||
resource_calendars_within_period = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']]
|
||||
default_calendar = default_company and default_company.resource_calendar_id or self.env.company.resource_calendar_id
|
||||
if not self:
|
||||
# if no resource, add the company resource calendar.
|
||||
resource_calendars_within_period[False][default_calendar] = Intervals([(start, end, self.env['resource.calendar.attendance'])])
|
||||
for resource in self:
|
||||
calendar = resource.calendar_id or resource.company_id.resource_calendar_id or default_calendar
|
||||
resource_calendars_within_period[resource.id][calendar] = Intervals([(start, end, self.env['resource.calendar.attendance'])])
|
||||
return resource_calendars_within_period
|
||||
|
||||
def _get_valid_work_intervals(self, start, end, calendars=None):
|
||||
""" Gets the valid work intervals of the resource following their calendars between ``start`` and ``end``
|
||||
|
||||
This methods handle the eventuality of a resource having multiple resource calendars, see _get_calendars_validity_within_period method
|
||||
for further explanation.
|
||||
"""
|
||||
assert start.tzinfo and end.tzinfo
|
||||
resource_calendar_validity_intervals = {}
|
||||
calendar_resources = defaultdict(lambda: self.env['resource.resource'])
|
||||
resource_work_intervals = defaultdict(Intervals)
|
||||
calendar_work_intervals = dict()
|
||||
|
||||
resource_calendar_validity_intervals = self._get_calendars_validity_within_period(start, end)
|
||||
for resource in self:
|
||||
# For each resource, retrieve its calendar and their validity intervals
|
||||
for calendar in resource_calendar_validity_intervals[resource.id]:
|
||||
calendar_resources[calendar] |= resource
|
||||
for calendar in (calendars or []):
|
||||
calendar_resources[calendar] |= self.env['resource.resource']
|
||||
for calendar, resources in calendar_resources.items():
|
||||
# For each calendar used by the resources, retrieve the work intervals for every resources using it
|
||||
work_intervals_batch = calendar._work_intervals_batch(start, end, resources=resources)
|
||||
for resource in resources:
|
||||
# Make the conjunction between work intervals and calendar validity
|
||||
resource_work_intervals[resource.id] |= work_intervals_batch[resource.id] & resource_calendar_validity_intervals[resource.id][calendar]
|
||||
calendar_work_intervals[calendar.id] = work_intervals_batch[False]
|
||||
|
||||
return resource_work_intervals, calendar_work_intervals
|
||||
|
||||
class ResourceCalendarLeaves(models.Model):
|
||||
_name = "resource.calendar.leaves"
|
||||
|
||||
@@ -6,8 +6,9 @@ from odoo.tests.common import TransactionCase
|
||||
|
||||
class TestResourceCommon(TransactionCase):
|
||||
|
||||
def _define_calendar(self, name, attendances, tz):
|
||||
return self.env['resource.calendar'].create({
|
||||
@classmethod
|
||||
def _define_calendar(cls, name, attendances, tz):
|
||||
return cls.env['resource.calendar'].create({
|
||||
'name': name,
|
||||
'tz': tz,
|
||||
'attendance_ids': [
|
||||
@@ -20,8 +21,10 @@ class TestResourceCommon(TransactionCase):
|
||||
for index, att in enumerate(attendances)
|
||||
],
|
||||
})
|
||||
def _define_calendar_2_weeks(self, name, attendances, tz):
|
||||
return self.env['resource.calendar'].create({
|
||||
|
||||
@classmethod
|
||||
def _define_calendar_2_weeks(cls, name, attendances, tz):
|
||||
return cls.env['resource.calendar'].create({
|
||||
'name': name,
|
||||
'tz': tz,
|
||||
'two_weeks_calendar': True,
|
||||
@@ -39,42 +42,43 @@ class TestResourceCommon(TransactionCase):
|
||||
],
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceCommon, self).setUp()
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestResourceCommon, cls).setUpClass()
|
||||
|
||||
# UTC+1 winter, UTC+2 summer
|
||||
self.calendar_jean = self._define_calendar('40 Hours', [(8, 16, i) for i in range(5)], 'Europe/Brussels')
|
||||
cls.calendar_jean = cls._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')
|
||||
cls.calendar_patel = cls._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')
|
||||
cls.calendar_john = cls._define_calendar('8+12 Hours', [(8, 16, 1), (8, 13, 4), (16, 23, 4)], 'America/Los_Angeles')
|
||||
# UTC+1 winter, UTC+2 summer
|
||||
self.calendar_jules = self._define_calendar_2_weeks('Week 1: 30 Hours - Week 2: 16 Hours', [
|
||||
cls.calendar_jules = cls._define_calendar_2_weeks('Week 1: 30 Hours - Week 2: 16 Hours', [
|
||||
(0, 0, 0, '0', 'line_section', 0), (8, 16, 0, '0', False, 1), (9, 17, 1, '0', False, 2),
|
||||
(0, 0, 0, '1', 'line_section', 10), (8, 16, 0, '1', False, 11), (7, 15, 2, '1', False, 12),
|
||||
(8, 16, 3, '1', False, 13), (10, 16, 4, '1', False, 14)], 'Europe/Brussels')
|
||||
|
||||
self.calendar_paul = self._define_calendar('Morning and evening shifts', sum([((2, 7, i), (10, 16, i)) for i in range(5)], ()), 'Brazil/DeNoronha')
|
||||
cls.calendar_paul = cls._define_calendar('Morning and evening shifts', sum([((2, 7, i), (10, 16, i)) for i in range(5)], ()), 'Brazil/DeNoronha')
|
||||
|
||||
# Employee is linked to a resource.resource via resource.mixin
|
||||
self.jean = self.env['resource.test'].create({
|
||||
cls.jean = cls.env['resource.test'].create({
|
||||
'name': 'Jean',
|
||||
'resource_calendar_id': self.calendar_jean.id,
|
||||
'resource_calendar_id': cls.calendar_jean.id,
|
||||
})
|
||||
self.patel = self.env['resource.test'].create({
|
||||
cls.patel = cls.env['resource.test'].create({
|
||||
'name': 'Patel',
|
||||
'resource_calendar_id': self.calendar_patel.id,
|
||||
'resource_calendar_id': cls.calendar_patel.id,
|
||||
})
|
||||
self.john = self.env['resource.test'].create({
|
||||
cls.john = cls.env['resource.test'].create({
|
||||
'name': 'John',
|
||||
'resource_calendar_id': self.calendar_john.id,
|
||||
'resource_calendar_id': cls.calendar_john.id,
|
||||
})
|
||||
self.jules = self.env['resource.test'].create({
|
||||
cls.jules = cls.env['resource.test'].create({
|
||||
'name': 'Jules',
|
||||
'resource_calendar_id': self.calendar_jules.id,
|
||||
'resource_calendar_id': cls.calendar_jules.id,
|
||||
})
|
||||
|
||||
self.paul = self.env['resource.test'].create({
|
||||
cls.paul = cls.env['resource.test'].create({
|
||||
'name': 'Paul',
|
||||
'resource_calendar_id': self.calendar_paul.id,
|
||||
'resource_calendar_id': cls.calendar_paul.id,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ from pytz import timezone, utc
|
||||
|
||||
from odoo import fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
from odoo.addons.resource.models.resource import Intervals, sum_intervals
|
||||
from odoo.addons.resource.tests.common import TestResourceCommon
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
@@ -1148,3 +1148,72 @@ class TestTimezones(TestResourceCommon):
|
||||
(date(2018, 4, 12), 8),
|
||||
(date(2018, 4, 13), 8),
|
||||
])
|
||||
|
||||
class TestResource(TestResourceCommon):
|
||||
|
||||
def test_calendars_validity_within_period(self):
|
||||
calendars = self.jean.resource_id._get_calendars_validity_within_period(
|
||||
utc.localize(datetime(2021, 7, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2021, 7, 30, 17, 0, 0)),
|
||||
)
|
||||
interval = Intervals([(
|
||||
utc.localize(datetime(2021, 7, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2021, 7, 30, 17, 0, 0)),
|
||||
self.env['resource.calendar.attendance']
|
||||
)])
|
||||
|
||||
self.assertEqual(1, len(calendars), "The dict returned by calendars validity should only have 1 entry")
|
||||
self.assertEqual(1, len(calendars[self.jean.resource_id.id]), "Jean should only have one calendar")
|
||||
jean_entry = calendars[self.jean.resource_id.id]
|
||||
jean_calendar = next(iter(jean_entry))
|
||||
self.assertEqual(self.jean.resource_calendar_id, jean_calendar, "It should be Jean's Calendar")
|
||||
self.assertFalse(jean_entry[jean_calendar] - interval, "Interval should cover all calendar's validity")
|
||||
self.assertFalse(interval - jean_entry[jean_calendar], "Calendar validity should cover all interval")
|
||||
|
||||
calendars = self.env['resource.resource']._get_calendars_validity_within_period(
|
||||
utc.localize(datetime(2021, 7, 1, 8, 0, 0)),
|
||||
utc.localize(datetime(2021, 7, 30, 17, 0, 0)),
|
||||
)
|
||||
self.assertEqual(1, len(calendars), "The dict returned by calendars validity should only have 1 entry")
|
||||
self.assertEqual(1, len(calendars[False]), "False (default) should only have one calendar")
|
||||
false_entry = calendars[False]
|
||||
false_calendar = next(iter(false_entry))
|
||||
self.assertEqual(self.env.company.resource_calendar_id, false_calendar, "It should be company calendar Calendar")
|
||||
self.assertFalse(false_entry[false_calendar] - interval, "Interval should cover all calendar's validity")
|
||||
self.assertFalse(interval - false_entry[false_calendar], "Calendar validity should cover all interval")
|
||||
|
||||
def test_performance(self):
|
||||
calendars = [self.calendar_jean, self.calendar_john, self.calendar_jules, self.calendar_patel]
|
||||
calendars_len = len(calendars)
|
||||
self.resources_test = self.env['resource.test'].create([{
|
||||
'name': 'resource ' + str(i),
|
||||
'resource_calendar_id': calendars[i % calendars_len].id,
|
||||
} for i in range(0, 50)])
|
||||
|
||||
start = utc.localize(datetime(2021, 7, 7, 12, 0, 0))
|
||||
end = utc.localize(datetime(2021, 7, 16, 23, 59, 59))
|
||||
with self.assertQueryCount(12):
|
||||
work_intervals, _ = self.resources_test.resource_id._get_valid_work_intervals(start, end)
|
||||
|
||||
self.assertEqual(len(work_intervals), 50)
|
||||
|
||||
def test_get_valid_work_intervals(self):
|
||||
start = utc.localize(datetime(2021, 7, 7, 12, 0, 0))
|
||||
end = utc.localize(datetime(2021, 7, 16, 23, 59, 59))
|
||||
work_intervals, _ = self.jean.resource_id._get_valid_work_intervals(start, end)
|
||||
sum_work_intervals = sum_intervals(work_intervals[self.jean.resource_id.id])
|
||||
self.assertEqual(58, sum_work_intervals, "Sum of the work intervals for the resource jean should be 40h+18h = 58h")
|
||||
|
||||
def test_get_valid_work_intervals_calendars_only(self):
|
||||
calendars = [self.calendar_jean, self.calendar_john, self.calendar_jules, self.calendar_patel]
|
||||
start = utc.localize(datetime(2021, 7, 7, 12, 0, 0))
|
||||
end = utc.localize(datetime(2021, 7, 16, 23, 59, 59))
|
||||
_, calendars_intervals = self.env['resource.resource']._get_valid_work_intervals(start, end, calendars)
|
||||
sum_work_intervals_jean = sum_intervals(calendars_intervals[self.calendar_jean.id])
|
||||
self.assertEqual(58, sum_work_intervals_jean, "Sum of the work intervals for the calendar of jean should be 40h+18h = 58h")
|
||||
sum_work_intervals_john = sum_intervals(calendars_intervals[self.calendar_john.id])
|
||||
self.assertEqual(26 - 1 / 3600, sum_work_intervals_john, "Sum of the work intervals for the calendar of john should be 20h+6h-1s = 25h59m59s")
|
||||
sum_work_intervals_jules = sum_intervals(calendars_intervals[self.calendar_jules.id])
|
||||
self.assertEqual(31, sum_work_intervals_jules, "Sum of the work intervals for the calendar of jules should be Wodd:15h+Wpair:16h = 31h")
|
||||
sum_work_intervals_patel = sum_intervals(calendars_intervals[self.calendar_patel.id])
|
||||
self.assertEqual(49, sum_work_intervals_patel, "Sum of the work intervals for the calendar of patel should be 14+35h = 49h")
|
||||
|
||||
Reference in New Issue
Block a user