[FIX] project_timesheet_holidays: restore public holiday timesheets after refusal

Before this commit, if a public holiday was created on dates overlapping
an existing validated leave, the timesheet entries for the public
holiday would not be created as they already existed for the leave. If
the leave was then refused or cancelled, the timesheet records linked to
it would be removed and the employee would have missing timesheet
entries for the public holiday.

This commit fixes this behavior by recreating the public holiday
timesheet records for the affected employees when refusing their leave.

opw-3550523

linked to com: https://github.com/odoo/odoo/pull/145522

closes odoo/odoo#153118

X-original-commit: 47b4763ad948686e0e7155b3c0b3d0dc4d970963
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
Simon Goffaux (sigo)
2024-02-08 12:29:22 +00:00
parent ed959cdcbd
commit 5eb6054f10
3 changed files with 169 additions and 0 deletions
@@ -112,12 +112,29 @@ class Holidays(models.Model):
'company_id': task.sudo().company_id.id or project.sudo().company_id.id,
}
def _check_missing_global_leave_timesheets(self):
if not self:
return
min_date = min([leave.date_from for leave in self])
max_date = max([leave.date_to for leave in self])
global_leaves = self.env['resource.calendar.leaves'].search([
("resource_id", "=", False),
("date_to", ">=", min_date),
("date_from", "<=", max_date),
("company_id.internal_project_id", "!=", False),
("company_id.leave_timesheet_task_id", "!=", False),
])
if global_leaves:
global_leaves._generate_public_time_off_timesheets(self.employee_ids)
def action_refuse(self):
""" Remove the timesheets linked to the refused holidays """
result = super(Holidays, self).action_refuse()
timesheets = self.sudo().mapped('timesheet_ids')
timesheets.write({'holiday_id': False})
timesheets.unlink()
self._check_missing_global_leave_timesheets()
return result
def _action_user_cancel(self, reason):
@@ -125,4 +142,5 @@ class Holidays(models.Model):
timesheets = self.sudo().timesheet_ids
timesheets.write({'holiday_id': False})
timesheets.unlink()
self._check_missing_global_leave_timesheets()
return res
@@ -199,6 +199,40 @@ class ResourceCalendarLeaves(models.Model):
if results_with_leave_timesheet:
results_with_leave_timesheet._timesheet_create_lines()
def _generate_public_time_off_timesheets(self, employees):
timesheet_vals_list = []
resource_calendars = self._get_resource_calendars()
work_hours_data = self._work_time_per_day(resource_calendars)
timesheet_read_group = self.env['account.analytic.line']._read_group(
[('global_leave_id', 'in', self.ids), ('employee_id', 'in', employees.ids)],
['employee_id'],
['date:array_agg']
)
timesheet_dates_per_employee_id = {
employee.id: date
for employee, date in timesheet_read_group
}
for leave in self:
for employee in employees:
if leave.calendar_id and employee.resource_calendar_id != leave.calendar_id:
continue
calendar = leave.calendar_id or employee.resource_calendar_id
work_hours_list = work_hours_data[calendar.id][leave.id]
timesheet_dates = timesheet_dates_per_employee_id.get(employee.id, [])
for index, (day_date, work_hours_count) in enumerate(work_hours_list):
generate_timesheet = day_date not in timesheet_dates
if not generate_timesheet:
continue
timesheet_vals = leave._timesheet_prepare_line_values(
index,
employee,
work_hours_list,
day_date,
work_hours_count
)
timesheet_vals_list.append(timesheet_vals)
return self.env['account.analytic.line'].sudo().create(timesheet_vals_list)
@api.model_create_multi
def create(self, vals_list):
results = super(ResourceCalendarLeaves, self).create(vals_list)
@@ -392,3 +392,120 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
self.assertIn(gto_09_04, global_leaves_ids_35h)
self.assertIn(gto_11_13, global_leaves_ids_35h)
self.assertNotIn(gto_09_11, global_leaves_ids_35h)
def test_global_time_off_timesheet_creation_after_leave_refusal(self):
""" When a global time off is created and an employee already has a
validated leave at that date, a timesheet is not created for the
global time off.
We make sure that the global time off timesheet is restored if the
leave is refused.
"""
test_user = self.env['res.users'].with_company(self.test_company).create({
'name': 'Jonathan Doe',
'login': 'jdoe@example.com',
})
test_user.with_company(self.test_company).action_create_employee()
test_user.employee_id.write({
'resource_calendar_id': self.test_company.resource_calendar_id.id,
})
# needed for cancelled leave chatter message
test_user.partner_id.write({
'email': 'jdoe@example.com',
})
# employee leave dates: from monday to friday
today = datetime.today()
next_monday = today.date() + timedelta(days=-today.weekday(), weeks=1)
hr_leave_start_datetime = datetime(next_monday.year, next_monday.month, next_monday.day, 8, 0, 0) # monday next week
hr_leave_end_datetime = hr_leave_start_datetime + timedelta(days=4, hours=9) # friday next week
self.env.company = self.test_company
internal_project = self.test_company.internal_project_id
internal_task_leaves = self.test_company.leave_timesheet_task_id
hr_leave_type_with_ts = self.env['hr.leave.type'].create({
'name': 'Leave Type with timesheet generation',
'requires_allocation': 'no',
'timesheet_generate': True,
'timesheet_project_id': internal_project.id,
'timesheet_task_id': internal_task_leaves.id,
})
# create and validate a leave for full time employee
HrLeave = self.env['hr.leave'].with_context(mail_create_nolog=True, mail_notrack=True)
holiday = HrLeave.with_user(test_user).create({
'name': 'Leave 1',
'employee_id': test_user.employee_id.id,
'holiday_status_id': hr_leave_type_with_ts.id,
'request_date_from': hr_leave_start_datetime,
'request_date_to': hr_leave_end_datetime,
})
holiday.sudo().action_validate()
self.assertEqual(len(holiday.timesheet_ids), 5)
# create overlapping global time off
global_leave_start_datetime = hr_leave_start_datetime + timedelta(days=2)
global_leave_end_datetime = global_leave_start_datetime + timedelta(hours=9)
global_time_off = self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
'calendar_id': self.test_company.resource_calendar_id.id,
'date_from': global_leave_start_datetime,
'date_to': global_leave_end_datetime,
})
gto_without_calendar = self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday without calendar',
'date_from': global_leave_start_datetime + timedelta(days=1), # still within the hr.leave being refused
'date_to': global_leave_end_datetime + timedelta(days=1),
})
# timesheets linked to global time offs should not exist as one already exists for the leave
self.assertFalse(global_time_off.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
self.assertFalse(gto_without_calendar.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
# refuse original leave
holiday.sudo().action_refuse()
self.assertFalse(holiday.timesheet_ids)
# timesheets linked to global time offs should be restored as the existing leave timesheets were unlinked after refusal
self.assertTrue(global_time_off.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
self.assertTrue(gto_without_calendar.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
# remove global time offs to remove the timesheet so we can test cancelling the leave
global_time_off.unlink()
gto_without_calendar.unlink()
# create a new leave at same dates
holiday2 = HrLeave.with_user(test_user).create({
'name': 'Leave 2',
'employee_id': test_user.employee_id.id,
'holiday_status_id': hr_leave_type_with_ts.id,
'request_date_from': hr_leave_start_datetime,
'request_date_to': hr_leave_end_datetime,
})
holiday2.sudo().action_validate()
# recreate the global time off
global_time_off = self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
'calendar_id': self.test_company.resource_calendar_id.id,
'date_from': global_leave_start_datetime,
'date_to': global_leave_end_datetime,
})
gto_without_calendar = self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday without calendar',
'date_from': global_leave_start_datetime + timedelta(days=1), # still within the hr.leave being cancelled
'date_to': global_leave_end_datetime + timedelta(days=1),
})
# timesheets linked to global time offs should not exist as one already exists for the leave
self.assertFalse(global_time_off.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
self.assertFalse(gto_without_calendar.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
# cancel the leave
holiday2.with_user(test_user)._action_user_cancel('User cancelled leave')
self.assertFalse(holiday2.timesheet_ids)
self.assertTrue(global_time_off.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
self.assertTrue(gto_without_calendar.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))