Skip to content

Commit ad85d57

Browse files
chrisduvivierOmar (OSAH)
authored andcommitted
[FIX] resource: fix flexible resource leave in planning gantt
Prior to this commit, flexible resources did not have their leaves reflected as gray cells in the planning gantt view. This was due to the work_intervals being ignored for the calculation of flexible resources availability. This commit adds measures to handle the leaves for flexible resources by setting a dummy attendance (which covers the whole length of the period in gantt interval), and then injects their leaves interval. To replicate: 1. set a timeoff to a flexible resource 2. open planning app 3. in the gantt view, the day in which the timeoff was set should be grayed. Ticket-id: 4492625 Enterprise: 79532 Part-of: odoo#198032 Related: odoo/enterprise#79532 Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
1 parent 6dcbe4b commit ad85d57

File tree

3 files changed

+54
-7
lines changed

3 files changed

+54
-7
lines changed

addons/hr_holidays/models/hr_leave_type.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,7 @@ def get_allocation_data(self, employees, target_date=None):
560560
datetime.combine(closest_expiration_date, time.max).replace(tzinfo=pytz.UTC),
561561
resources=employee.resource_id
562562
)
563-
closest_allocation_dict =\
564-
self.env['resource.calendar']._get_attendance_intervals_days_data(
565-
calendar_attendance[employee.resource_id.id])
563+
closest_allocation_dict = calendar._get_attendance_intervals_days_data(calendar_attendance[employee.resource_id.id])
566564
if leave_type.request_unit in ['hour']:
567565
closest_allocation_duration = closest_allocation_dict['hours']
568566
else:

addons/resource/models/resource_calendar.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import itertools
55

66
from collections import defaultdict
7-
from datetime import datetime, timedelta
7+
from datetime import datetime, timedelta, time
88
from functools import partial
99
from itertools import chain
1010

@@ -367,14 +367,25 @@ def _attendance_intervals_batch(self, start_dt, end_dt, resources=None, domain=N
367367
res = result_per_tz[tz]
368368
res_intervals = WorkIntervals(res)
369369
for resource in resources:
370-
if resource in per_resource_result:
370+
if resource and resource._is_flexible():
371+
# If the resource is flexible, return the whole period from start_dt to end_dt with a dummy attendance
372+
dummy_attendance = self.env['resource.calendar.attendance']
373+
result_per_resource_id[resource.id] = WorkIntervals([(start, end, dummy_attendance)])
374+
elif resource in per_resource_result:
371375
resource_specific_result = [(max(bounds_per_tz[tz][0], tz.localize(val[0])), min(bounds_per_tz[tz][1], tz.localize(val[1])), val[2])
372376
for val in per_resource_result[resource]]
373377
result_per_resource_id[resource.id] = WorkIntervals(itertools.chain(res, resource_specific_result))
374378
else:
375379
result_per_resource_id[resource.id] = res_intervals
376380
return result_per_resource_id
377381

382+
def _handle_flexible_leave_interval(self, dt0, dt1, leave):
383+
"""Hook method to handle flexible leave intervals. Can be overridden in other modules."""
384+
tz = dt0.tzinfo # Get the timezone information from dt0
385+
dt0 = datetime.combine(dt0.date(), time.min).replace(tzinfo=tz)
386+
dt1 = datetime.combine(dt1.date(), time.max).replace(tzinfo=tz)
387+
return dt0, dt1
388+
378389
def _leave_intervals(self, start_dt, end_dt, resource=None, domain=None, tz=None):
379390
if resource is None:
380391
resource = self.env['resource.resource']
@@ -431,6 +442,8 @@ def _leave_intervals_batch(self, start_dt, end_dt, resources=None, domain=None,
431442
tz_dates[(tz, end_dt)] = end
432443
dt0 = string_to_datetime(leave_date_from).astimezone(tz)
433444
dt1 = string_to_datetime(leave_date_to).astimezone(tz)
445+
if leave_resource and leave_resource._is_flexible():
446+
dt0, dt1 = self._handle_flexible_leave_interval(dt0, dt1, leave)
434447
result[resource.id].append((max(start, dt0), min(end, dt1), leave))
435448

436449
return {r.id: Intervals(result[r.id]) for r in resources_list}
@@ -472,7 +485,7 @@ def _unavailable_intervals_batch(self, start_dt, end_dt, resources=None, domain=
472485
resources_work_intervals = self._work_intervals_batch(start_dt, end_dt, resources, domain, tz)
473486
result = {}
474487
for resource in resources_list:
475-
if resource and resource._is_flexible():
488+
if resource and resource._is_fully_flexible():
476489
continue
477490
work_intervals = [(start, stop) for start, stop, meta in resources_work_intervals[resource.id]]
478491
# start + flatten(intervals) + end
@@ -505,7 +518,10 @@ def _get_attendance_intervals_days_data(self, attendance_intervals):
505518
# take durations in days proportionally to what is left of the interval.
506519
interval_hours = (stop - start).total_seconds() / 3600
507520
day_hours[start.date()] += interval_hours
508-
day_days[start.date()] += sum(meta.mapped('duration_days')) * interval_hours / sum(meta.mapped('duration_hours'))
521+
if len(self) == 1 and self.flexible_hours and self.hours_per_day:
522+
day_days[start.date()] += interval_hours / self.hours_per_day
523+
else:
524+
day_days[start.date()] += sum(meta.mapped('duration_days')) * interval_hours / sum(meta.mapped('duration_hours'))
509525

510526
return {
511527
# Round the number of days to the closest 16th of a day.

addons/test_resource/tests/test_resource.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,39 @@ def test_unavailable_intervals(self):
13221322
(datetime(2022, 9, 21, 15, 0, tzinfo=utc), datetime(2022, 9, 22, 0, 0, tzinfo=utc)),
13231323
])
13241324

1325+
def test_flexible_resource_leave_interval(self):
1326+
"""
1327+
Test whole day off for a flexible resource.
1328+
The standard 8 - 17 leave should be converted to a whole day leave interval for the flexible resource.
1329+
"""
1330+
1331+
flexible_calendar = self.env['resource.calendar'].create({
1332+
'name': 'Flex Calendar',
1333+
'tz': 'UTC',
1334+
'flexible_hours': True,
1335+
})
1336+
flex_resource = self.env['resource.resource'].create({
1337+
'name': 'Test FlexResource',
1338+
'calendar_id': flexible_calendar.id,
1339+
})
1340+
self.env['resource.calendar.leaves'].create({
1341+
'name': 'Standard Time Off',
1342+
'calendar_id': flexible_calendar.id,
1343+
'resource_id': flex_resource.id,
1344+
'date_from': '2025-03-07 08:00:00',
1345+
'date_to': '2025-03-07 17:00:00',
1346+
})
1347+
1348+
start_dt = datetime(2025, 3, 7, 0, 0, 0, tzinfo=utc)
1349+
end_dt = datetime(2025, 3, 7, 23, 59, 59, 999999, tzinfo=utc)
1350+
1351+
intervals = flexible_calendar._leave_intervals_batch(start_dt, end_dt, [flex_resource])
1352+
intervals_list = list(intervals[flex_resource.id])
1353+
self.assertEqual(len(intervals_list), 1, "There should be one leave interval")
1354+
interval = intervals_list[0]
1355+
self.assertEqual(interval[0], start_dt, "The start of the interval should be 00:00:00")
1356+
self.assertEqual(interval[1], end_dt, "The end of the interval should be 23:59:59.999999")
1357+
13251358
class TestResource(TestResourceCommon):
13261359

13271360
def test_calendars_validity_within_period(self):

0 commit comments

Comments
 (0)