diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index f48d1c26066..059b7541a33 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -377,19 +377,18 @@ class HolidaysRequest(models.Model):
if (holiday.request_unit_half or holiday.request_unit_hours) and holiday.request_date_to != holiday.request_date_from:
holiday.request_date_to = holiday.request_date_from
- attendance_from, attendance_to = holiday._get_attendances(holiday.request_date_from, holiday.request_date_to)
+
+ day_period = {
+ 'am': 'morning',
+ 'pm': 'afternoon'
+ }.get(holiday.request_date_from_period, None) if holiday.request_unit_half else None
+
+ attendance_from, attendance_to = holiday._get_attendances(holiday.request_date_from, holiday.request_date_to, day_period=day_period)
compensated_request_date_from = holiday.request_date_from
compensated_request_date_to = holiday.request_date_to
- if holiday.request_unit_half:
- if holiday.request_date_from_period == 'am':
- hour_from = attendance_from.hour_from
- hour_to = attendance_from.hour_to
- else:
- hour_from = attendance_to.hour_from
- hour_to = attendance_to.hour_to
- elif holiday.request_unit_hours:
+ if holiday.request_unit_hours:
hour_from = holiday.request_hour_from
hour_to = holiday.request_hour_to
else:
@@ -546,8 +545,6 @@ class HolidaysRequest(models.Model):
False)
hours = resource_calendar.get_work_hours_count(self.date_from, self.date_to)
days = hours / (today_hours or HOURS_PER_DAY)
- if self.request_unit_half and hours > 0 and hours < HOURS_PER_DAY:
- days = 0.5
return (days, hours)
@@ -1593,13 +1590,15 @@ class HolidaysRequest(models.Model):
holiday_tz = timezone(resource.tz or self.env.user.tz or 'UTC')
return holiday_tz.localize(datetime.combine(date, hour)).astimezone(UTC).replace(tzinfo=None)
- def _get_attendances(self, request_date_from, request_date_to):
+ def _get_attendances(self, request_date_from, request_date_to, day_period=None):
self.ensure_one()
domain = [
('calendar_id', '=', self.resource_calendar_id.id),
('display_type', '=', False),
('day_period', '!=', 'lunch'),
]
+ if day_period:
+ domain.append(('day_period', '=', day_period))
attendances = self.env['resource.calendar.attendance']._read_group(domain,
['week_type', 'dayofweek', 'day_period'],
['hour_from:min', 'hour_to:max'])
diff --git a/addons/hr_holidays/tests/test_automatic_leave_dates.py b/addons/hr_holidays/tests/test_automatic_leave_dates.py
index 78991b18b3b..23cf3d1e4d4 100644
--- a/addons/hr_holidays/tests/test_automatic_leave_dates.py
+++ b/addons/hr_holidays/tests/test_automatic_leave_dates.py
@@ -93,6 +93,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'hour_to': 10,
'day_period': 'morning',
'dayofweek': '0',
+ 'duration_days': 0.25,
}),
(0, 0, {
'name': 'monday morning 2',
@@ -100,6 +101,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'hour_to': 12.25,
'day_period': 'morning',
'dayofweek': '0',
+ 'duration_days': 0.25,
}),
(0, 0, {
'name': 'monday lunch',
@@ -114,6 +116,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'hour_to': 17,
'day_period': 'afternoon',
'dayofweek': '0',
+ 'duration_days': 0.5,
})]
})
employee = self.employee_emp
@@ -135,9 +138,6 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
self.assertEqual(leave_form.number_of_hours_text, '4 Hours')
def test_attendance_on_morning(self):
- # TODO: temporarily fixed this test. The behaviour of half-day leaves
- # currently not very well defined. This will be handled in an
- # upcoming task.
calendar = self.env['resource.calendar'].create({
'name': 'Morning only',
'attendance_ids': [(5, 0, 0),
@@ -165,8 +165,8 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
# Ask for afternoon
leave_form.request_date_from_period = 'pm'
- self.assertEqual(leave_form.number_of_days_display, 1)
- self.assertEqual(leave_form.number_of_hours_text, '8 Hours')
+ self.assertEqual(leave_form.number_of_days_display, 0)
+ self.assertEqual(leave_form.number_of_hours_text, '0 Hours')
def test_attendance_next_day(self):
self.env.user.tz = 'Europe/Brussels'
@@ -241,6 +241,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'day_period': 'morning',
'dayofweek': '0',
'week_type': '0',
+ 'duration_days': 0.5,
}),
(0, 0, {
'name': 'monday morning even week',
@@ -249,6 +250,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
'day_period': 'morning',
'dayofweek': '0',
'week_type': '1',
+ 'duration_days': 0.25
})]
})
employee = self.employee_emp
@@ -262,7 +264,7 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon):
leave_form.request_unit_half = True
leave_form.request_date_from_period = 'am'
- self.assertEqual(leave_form.number_of_days_display, 0.5)
+ self.assertEqual(leave_form.number_of_days_display, 0.25)
self.assertEqual(leave_form.number_of_hours_text, '2 Hours')
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 8, 0, 0))
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
diff --git a/addons/resource/models/resource_calendar.py b/addons/resource/models/resource_calendar.py
index 9855616d792..45fe7c437b3 100644
--- a/addons/resource/models/resource_calendar.py
+++ b/addons/resource/models/resource_calendar.py
@@ -166,11 +166,12 @@ class ResourceCalendar(models.Model):
and not attendance.resource_id and not attendance.display_type)
def _get_hours_per_day(self, attendances):
+ """
+ Calculate the average hours worked per workday.
+ """
if not attendances:
return 0
- attendances = attendances.filtered(lambda a: a.day_period != 'lunch')
-
hour_count = 0.0
for attendance in attendances:
hour_count += attendance.hour_to - attendance.hour_from
@@ -470,6 +471,31 @@ class ResourceCalendar(models.Model):
# Private Methods / Helpers
# --------------------------------------------------
+ def _get_attendance_intervals_days_data(self, attendance_intervals):
+ """
+ helper function to compute duration of `intervals` that have
+ 'resource.calendar.attendance' records as payload (3rd element in tuple).
+ expressed in days and hours.
+
+ resource.calendar.attendance records have durations associated
+ with them so this method merely calculates the proportion that is
+ covered by the intervals.
+ """
+ day_hours = defaultdict(float)
+ day_days = defaultdict(float)
+ for start, stop, meta in attendance_intervals:
+ # If the interval covers only a part of the original attendance, we
+ # take durations in days proportionally to what is left of the interval.
+ interval_hours = (stop - start).total_seconds() / 3600
+ day_hours[start.date()] += interval_hours
+ day_days[start.date()] += meta.duration_days * interval_hours / meta.duration_hours
+
+ return {
+ # Round the number of days to the closest 16th of a day.
+ 'days': sum(float_utils.round(ROUNDING_FACTOR * day_days[day]) / ROUNDING_FACTOR for day in day_days),
+ 'hours': sum(day_hours.values()),
+ }
+
def _get_days_data(self, intervals, day_total):
"""
helper function to compute duration of `intervals`
@@ -607,15 +633,13 @@ class ResourceCalendar(models.Model):
from_datetime, dummy = make_aware(from_datetime)
to_datetime, dummy = make_aware(to_datetime)
- day_total = self._get_resources_day_total(from_datetime, to_datetime)[False]
-
# actual hours per day
if compute_leaves:
intervals = self._work_intervals_batch(from_datetime, to_datetime, domain=domain)[False]
else:
intervals = self._attendance_intervals_batch(from_datetime, to_datetime, domain=domain)[False]
- return self._get_days_data(intervals, day_total)
+ return self._get_attendance_intervals_days_data(intervals)
def plan_hours(self, hours, day_dt, compute_leaves=False, domain=None, resource=None):
"""
diff --git a/addons/resource/models/resource_calendar_attendance.py b/addons/resource/models/resource_calendar_attendance.py
index c9d6e5b4e13..8e6ea708718 100644
--- a/addons/resource/models/resource_calendar_attendance.py
+++ b/addons/resource/models/resource_calendar_attendance.py
@@ -9,7 +9,7 @@ from odoo import api, fields, models, _
class ResourceCalendarAttendance(models.Model):
_name = "resource.calendar.attendance"
_description = "Work Detail"
- _order = 'week_type, dayofweek, hour_from'
+ _order = 'sequence, week_type, dayofweek, hour_from'
name = fields.Char(required=True)
dayofweek = fields.Selection([
@@ -27,10 +27,15 @@ class ResourceCalendarAttendance(models.Model):
help="Start and End time of working.\n"
"A specific value of 24:00 is interpreted as 23:59:59.999999.")
hour_to = fields.Float(string='Work to', required=True)
+ # For the hour duration, the compute function is used to compute the value
+ # unambiguously, while the duration in days is computed for the default
+ # value based on the day_period but can be manually overridden.
+ duration_hours = fields.Float(compute='_compute_duration_hours', string='Duration (hours)')
+ duration_days = fields.Float(compute='_compute_duration_days', string='Duration (days)', store=True, readonly=False)
calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade')
day_period = fields.Selection([
('morning', 'Morning'),
- ('lunch', 'Lunch'),
+ ('lunch', 'Break'),
('afternoon', 'Afternoon')], required=True, default='morning')
resource_id = fields.Many2one('resource.resource', 'Resource')
week_type = fields.Selection([
@@ -64,6 +69,19 @@ class ResourceCalendarAttendance(models.Model):
# some years have 53 weeks. Therefore, two consecutive odd week number follow each other (53 --> 1).
return int(math.floor((date.toordinal() - 1) / 7) % 2)
+ @api.depends('hour_from', 'hour_to')
+ def _compute_duration_hours(self):
+ for attendance in self:
+ attendance.duration_hours = (attendance.hour_to - attendance.hour_from) if attendance.day_period != 'lunch' else 0
+
+ @api.depends('day_period', 'hour_from', 'hour_to')
+ def _compute_duration_days(self):
+ for attendance in self:
+ if attendance.day_period == 'lunch':
+ attendance.duration_days = 0
+ else:
+ attendance.duration_days = 0.5 if attendance.duration_hours <= attendance.calendar_id.hours_per_day * 3 / 4 else 1
+
@api.depends('week_type')
def _compute_display_name(self):
super()._compute_display_name()
diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index c64e1a8b664..f8d2c411961 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -104,7 +104,6 @@ class ResourceMixin(models.AbstractModel):
for calendar_resource in calendar_resources:
result[calendar_resource.id] = {'days': 0, 'hours': 0}
continue
- day_total = calendar._get_resources_day_total(from_datetime, to_datetime, calendar_resources)
# actual hours per day
if compute_leaves:
@@ -113,7 +112,7 @@ class ResourceMixin(models.AbstractModel):
intervals = calendar._attendance_intervals_batch(from_datetime, to_datetime, calendar_resources)
for calendar_resource in calendar_resources:
- result[calendar_resource.id] = calendar._get_days_data(intervals[calendar_resource.id], day_total[calendar_resource.id])
+ result[calendar_resource.id] = calendar._get_attendance_intervals_days_data(intervals[calendar_resource.id])
# convert "resource: result" into "employee: result"
return {mapped_employees[r.id]: result[r.id] for r in resources}
@@ -142,16 +141,13 @@ class ResourceMixin(models.AbstractModel):
mapped_resources[calendar or record.resource_calendar_id] |= record.resource_id
for calendar, calendar_resources in mapped_resources.items():
- day_total = calendar._get_resources_day_total(from_datetime, to_datetime, calendar_resources)
-
# compute actual hours per day
attendances = calendar._attendance_intervals_batch(from_datetime, to_datetime, calendar_resources)
leaves = calendar._leave_intervals_batch(from_datetime, to_datetime, calendar_resources, domain)
for calendar_resource in calendar_resources:
- result[calendar_resource.id] = calendar._get_days_data(
- attendances[calendar_resource.id] & leaves[calendar_resource.id],
- day_total[calendar_resource.id]
+ result[calendar_resource.id] = calendar._get_attendance_intervals_days_data(
+ attendances[calendar_resource.id] & leaves[calendar_resource.id]
)
# convert "resource: result" into "employee: result"
diff --git a/addons/resource/views/resource_calendar_attendance_views.xml b/addons/resource/views/resource_calendar_attendance_views.xml
index 73a9b4678cb..fca2233ea0b 100644
--- a/addons/resource/views/resource_calendar_attendance_views.xml
+++ b/addons/resource/views/resource_calendar_attendance_views.xml
@@ -4,7 +4,7 @@
resource.calendar.attendance.tree
resource.calendar.attendance
-
+
@@ -13,6 +13,7 @@
+
@@ -37,6 +38,7 @@
+
diff --git a/addons/test_resource/tests/common.py b/addons/test_resource/tests/common.py
index 94fd46fa9e6..a4f28f07a6d 100644
--- a/addons/test_resource/tests/common.py
+++ b/addons/test_resource/tests/common.py
@@ -17,6 +17,7 @@ class TestResourceCommon(TransactionCase):
'hour_from': att[0],
'hour_to': att[1],
'dayofweek': str(att[2]),
+ 'duration_days': att[3],
})
for index, att in enumerate(attendances)
],
@@ -47,18 +48,18 @@ class TestResourceCommon(TransactionCase):
super(TestResourceCommon, cls).setUpClass()
# UTC+1 winter, UTC+2 summer
- cls.calendar_jean = cls._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, 1) for i in range(5)], 'Europe/Brussels')
# UTC+6
- cls.calendar_patel = cls._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, 3/7), (13, 17, i, 4/7)) for i in range(5)], ()), 'Etc/GMT-6')
# UTC-8 winter, UTC-7 summer
- cls.calendar_john = cls._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, 1), (8, 13, 4, 5/12), (16, 23, 4, 7/12)], 'America/Los_Angeles')
# UTC+1 winter, UTC+2 summer
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')
- cls.calendar_paul = cls._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, 0.5), (10, 16, i, 0.5)) for i in range(5)], ()), 'Brazil/DeNoronha')
# Employee is linked to a resource.resource via resource.mixin
cls.jean = cls.env['resource.test'].create({