[FIX]hr_employee_stats_sheet:fix bug in _get_total_recovery_hours and _get_total_leave_hours for half-days request unit leaves

This commit is contained in:
2026-06-15 16:54:38 +02:00
parent b65b6431b6
commit 2f73c1b3a7
3 changed files with 196 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "hr_employee_stats_sheet",
"version": "16.0.3.2.0",
"version": "16.0.3.3.0",
"description": "Add global sheet for employee stats",
"summary": "Add global sheet for employee stats",
"author": "Nicolas JEUDY",

View File

@@ -121,14 +121,14 @@ class HrEmployeeStats(models.Model):
("request_date_to", ">=", self.date),
],
)
if recovery_ids:
for recovery_id in recovery_ids:
if recovery_id.request_unit_hours:
recovery_hours = recovery_id.number_of_hours_display
total_recovery_hours += min(recovery_hours,self._get_total_planned_hours())
elif recovery_id.request_unit_half:
total_recovery_hours += self._get_total_planned_hours() / 2
else :
if recovery_id.request_date_from == recovery_id.request_date_to:
# single-day request (hours or half-day):
# Odoo already computed the hours from the calendar
total_recovery_hours += recovery_id.number_of_hours_display
else:
# multi-day request: we take the planned hours
# of THAT specific day
total_recovery_hours += self._get_total_planned_hours()
return total_recovery_hours
@@ -147,14 +147,14 @@ class HrEmployeeStats(models.Model):
("active", '=', True),
],
)
if leave_ids:
for leave_id in leave_ids:
if leave_id.request_unit_hours:
leave_hours = leave_id.number_of_hours_display
total_leave_hours += min(leave_hours,self._get_total_planned_hours())
elif leave_id.request_unit_half:
total_leave_hours += self._get_total_planned_hours() / 2
else :
if leave_id.request_date_from == leave_id.request_date_to:
# single-day request (hours or half-day):
# Odoo already computed the hours from the calendar
total_leave_hours += leave_id.number_of_hours_display
else:
# multi-day request: we take the planned hours
# of THAT specific day
total_leave_hours += self._get_total_planned_hours()
return total_leave_hours

View File

@@ -394,7 +394,184 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
self.assertEqual(recovery_allocation.number_of_days,1.25, "The recovery allocation should be 1,25 days")
@tagged("post_install", "-at_install")
class TestHrEmployeeStatsLeaveAndRecoveryUnit(TransactionCase):
"""Check that _get_total_recovery_hours() and _get_total_leave_hours()
work whatever the request_unit of the leave type
('hour', 'half_day' or 'day'), both for a single-day request and for a
multi-day request."""
def setUp(self):
super().setUp()
self.employee = self.env['hr.employee'].create({
'name': 'Dominique',
'tz': 'Europe/Paris',
})
# Full-time calendar: Monday to Friday, 9am-12pm and 1pm-5pm (7h/day).
# The calendar timezone must match the employee's so that hour-based
# requests fall exactly on the working hours.
self.base_calendar = self.env['resource.calendar'].create({
'name': 'Default Calendar',
'tz': 'Europe/Paris',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
self.base_calendar.hours_per_day = 7
self.employee.resource_calendar_id = self.base_calendar
# hr.employee.calendar used by _get_total_planned_hours() (multi-day case)
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_calendar.id,
})
# Company recovery type (used by _get_total_recovery_hours)
self.recovery_type = self.env['hr.leave.type'].create({
'name': 'Recovery',
'request_unit': 'hour',
'requires_allocation': 'no',
'allows_negative': True,
'leave_validation_type': 'no_validation',
})
self.env.company.recovery_type_id = self.recovery_type
# Monday and Tuesday with no public holiday over the period
self.monday = Date.to_date("2025-03-03")
self.tuesday = Date.to_date("2025-03-04")
# --- Helpers ------------------------------------------------------------
def _create_leave_type(self, request_unit):
return self.env['hr.leave.type'].create({
'name': 'Leave %s' % request_unit,
'request_unit': request_unit,
'requires_allocation': 'no',
'allows_negative': True,
'leave_validation_type': 'no_validation',
})
def _create_leave(self, leave_type, date_from, date_to, half=False,
period='am', hours=False, hour_from=None, hour_to=None):
vals = {
'name': 'Leave',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': date_from,
'request_date_to': date_to,
}
if half:
vals['request_unit_half'] = True
vals['request_date_from_period'] = period
if hours:
vals['request_unit_hours'] = True
vals['request_hour_from'] = hour_from
vals['request_hour_to'] = hour_to
leave = self.env['hr.leave'].create(vals)
leave._compute_date_from_to()
leave.action_validate()
return leave
def _create_stat(self, day):
stat = self.env['hr.employee.stats'].create({
'employee_id': self.employee.id,
'date': day,
})
stat._compute_dayofweek()
stat._compute_hours()
return stat
# --- _get_total_recovery_hours -----------------------------------------
def test_recovery_hours_unit_day_single_day(self):
# 'day' type, a recovery taken on Monday only
self.recovery_type.request_unit = 'day'
self._create_leave(self.recovery_type, self.monday, self.monday)
stat = self._create_stat(self.monday)
# single-day request: Odoo already computed 7h from the calendar
self.assertEqual(stat.total_recovery_hours, 7, "total_recovery_hours should be 7")
def test_recovery_hours_unit_half_day(self):
# 'half_day' type, recovery taken on Monday morning (9am-12pm = 3h)
self.recovery_type.request_unit = 'half_day'
self._create_leave(self.recovery_type, self.monday, self.monday, half=True, period='am')
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_recovery_hours, 3, "total_recovery_hours should be 3 (morning 9am-12pm)")
def test_recovery_hours_unit_hour(self):
# 'hour' type, recovery taken from 9am to 12pm (3h)
self.recovery_type.request_unit = 'hour'
self._create_leave(self.recovery_type, self.monday, self.monday, hours=True, hour_from='9', hour_to='12')
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_recovery_hours, 3, "total_recovery_hours should be 3 (9am-12pm)")
def test_recovery_hours_unit_day_several_days(self):
# 'day' type, recovery taken on Monday AND Tuesday: for each day we
# fall back to the planned hours of THAT specific day (7h)
self.recovery_type.request_unit = 'day'
self._create_leave(self.recovery_type, self.monday, self.tuesday)
monday_stat = self._create_stat(self.monday)
tuesday_stat = self._create_stat(self.tuesday)
self.assertEqual(monday_stat.total_recovery_hours, 7, "total_recovery_hours should be 7 on monday")
self.assertEqual(tuesday_stat.total_recovery_hours, 7, "total_recovery_hours should be 7 on tuesday")
# --- _get_total_leave_hours --------------------------------------------
def test_leave_hours_unit_day_single_day(self):
leave_type = self._create_leave_type('day')
self._create_leave(leave_type, self.monday, self.monday)
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_leave_hours, 7, "total_leave_hours should be 7")
def test_leave_hours_unit_half_day(self):
leave_type = self._create_leave_type('half_day')
self._create_leave(leave_type, self.monday, self.monday, half=True, period='am')
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_leave_hours, 3, "total_leave_hours should be 3 (morning 9am-12pm)")
def test_leave_hours_unit_hour(self):
leave_type = self._create_leave_type('hour')
self._create_leave(leave_type, self.monday, self.monday, hours=True, hour_from='9', hour_to='12')
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_leave_hours, 3, "total_leave_hours should be 3 (9am-12pm)")
def test_leave_hours_unit_day_several_days(self):
leave_type = self._create_leave_type('day')
self._create_leave(leave_type, self.monday, self.tuesday)
monday_stat = self._create_stat(self.monday)
tuesday_stat = self._create_stat(self.tuesday)
self.assertEqual(monday_stat.total_leave_hours, 7, "total_leave_hours should be 7 on monday")
self.assertEqual(tuesday_stat.total_leave_hours, 7, "total_leave_hours should be 7 on tuesday")
def test_leave_hours_ignore_non_validated_leave(self):
# _get_total_leave_hours only counts validated and active leaves
leave_type = self._create_leave_type('day')
leave = self.env['hr.leave'].create({
'name': 'Draft leave',
'employee_id': self.employee.id,
'holiday_status_id': leave_type.id,
'request_date_from': self.monday,
'request_date_to': self.monday,
})
leave._compute_date_from_to() # left unvalidated
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_leave_hours, 0, "an unvalidated leave must not be counted")
def test_leave_hours_excludes_recovery_type(self):
# a "recovery" type leave must not be counted as a regular leave
self.recovery_type.request_unit = 'day'
self._create_leave(self.recovery_type, self.monday, self.monday)
stat = self._create_stat(self.monday)
self.assertEqual(stat.total_leave_hours, 0, "recovery must not count as a leave")
self.assertEqual(stat.total_recovery_hours, 7, "it must count as a recovery")