From 5eb6054f1071f7fdc4e13982f017abf72089f830 Mon Sep 17 00:00:00 2001 From: "Simon Goffaux (sigo)" Date: Wed, 13 Dec 2023 07:28:42 +0000 Subject: [PATCH] [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) --- .../models/hr_holidays.py | 18 +++ .../models/resource_calendar_leaves.py | 34 +++++ .../tests/test_timesheet_global_time_off.py | 117 ++++++++++++++++++ 3 files changed, 169 insertions(+) diff --git a/addons/project_timesheet_holidays/models/hr_holidays.py b/addons/project_timesheet_holidays/models/hr_holidays.py index 740d06d71d8..f5fd4dd1eda 100644 --- a/addons/project_timesheet_holidays/models/hr_holidays.py +++ b/addons/project_timesheet_holidays/models/hr_holidays.py @@ -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 diff --git a/addons/project_timesheet_holidays/models/resource_calendar_leaves.py b/addons/project_timesheet_holidays/models/resource_calendar_leaves.py index 43ca8af55d6..0f555618e34 100644 --- a/addons/project_timesheet_holidays/models/resource_calendar_leaves.py +++ b/addons/project_timesheet_holidays/models/resource_calendar_leaves.py @@ -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) diff --git a/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py b/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py index b6a68725d01..b5b85f9b33d 100644 --- a/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py +++ b/addons/project_timesheet_holidays/tests/test_timesheet_global_time_off.py @@ -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))