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))