[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:
Odoo's Mergebot
2021-11-30 17:07:26 +01:00
committed by GitHub
12 changed files with 429 additions and 34 deletions
+27
View File
@@ -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
+1
View File
@@ -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
+86
View File
@@ -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.")
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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")
+109
View File
@@ -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")
+69 -10
View File
@@ -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"
+25 -21
View File
@@ -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,
})
+70 -1
View File
@@ -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")