diff --git a/addons/project_timesheet_holidays/models/hr_employee.py b/addons/project_timesheet_holidays/models/hr_employee.py index c09ed520f68..be70eb8eab4 100644 --- a/addons/project_timesheet_holidays/models/hr_employee.py +++ b/addons/project_timesheet_holidays/models/hr_employee.py @@ -34,14 +34,16 @@ class Employee(models.Model): def _create_future_public_holidays_timesheets(self, employees): lines_vals = [] + today = fields.Datetime.today() + global_leaves_wo_calendar = self.env['resource.calendar.leaves'].search([('calendar_id', '=', False), ('date_from', '>=', today)]) for employee in employees: if not employee.active: continue # First we look for the global time off that are already planned after today - global_leaves = employee.resource_calendar_id.global_leave_ids.filtered(lambda l: l.date_from >= fields.Datetime.today()) + global_leaves = employee.resource_calendar_id.global_leave_ids.filtered(lambda l: l.date_from >= today) + global_leaves_wo_calendar work_hours_data = global_leaves._work_time_per_day() for global_time_off in global_leaves: - for index, (day_date, work_hours_count) in enumerate(work_hours_data[global_time_off.id]): + for index, (day_date, work_hours_count) in enumerate(work_hours_data[employee.resource_calendar_id.id][global_time_off.id]): lines_vals.append( global_time_off._timesheet_prepare_line_values( index, diff --git a/addons/project_timesheet_holidays/models/resource_calendar_leaves.py b/addons/project_timesheet_holidays/models/resource_calendar_leaves.py index 5d002d80370..a9d378d9af1 100644 --- a/addons/project_timesheet_holidays/models/resource_calendar_leaves.py +++ b/addons/project_timesheet_holidays/models/resource_calendar_leaves.py @@ -12,7 +12,15 @@ class ResourceCalendarLeaves(models.Model): timesheet_ids = fields.One2many('account.analytic.line', 'global_leave_id', string="Analytic Lines") - def _work_time_per_day(self): + def _get_resource_calendars(self): + leaves_with_calendar = self.filtered('calendar_id') + calendars = leaves_with_calendar.calendar_id + leaves_wo_calendar = self - leaves_with_calendar + if leaves_wo_calendar: + calendars += self.env['resource.calendar'].search([('company_id', 'in', leaves_wo_calendar.company_id.ids)]) + return calendars + + def _work_time_per_day(self, resource_calendars=False): """ Get work time per day based on the calendar and its attendances 1) Gets all calendars with their characteristics (i.e. @@ -32,6 +40,7 @@ class ResourceCalendarLeaves(models.Model): } } """ + resource_calendars = resource_calendars or self._get_resource_calendars() leaves_read_group = self.env['resource.calendar.leaves']._read_group( [('id', 'in', self.ids)], ['calendar_id', 'ids:array_agg(id)', 'resource_ids:array_agg(resource_id)', 'min_date_from:min(date_from)', 'max_date_to:max(date_to)'], @@ -39,21 +48,27 @@ class ResourceCalendarLeaves(models.Model): ) # dict of keys: calendar_id # and values : { 'date_from': datetime, 'date_to': datetime, resources: self.env['resource.resource'] } - cal_attendance_intervals_dict = { - res['calendar_id'][0]: { + cal_attendance_intervals_dict = {} + for res in leaves_read_group: + calendar_data = { 'date_from': utc.localize(res['min_date_from']), 'date_to': utc.localize(res['max_date_to']), 'resources': self.env['resource.resource'].browse(res['resource_ids'] if res['resource_ids'] and res['resource_ids'][0] else []), 'leaves': self.env['resource.calendar.leaves'].browse(res['ids']), - } for res in leaves_read_group - } + } + if not res.get('calendar_id', False): + for calendar_id in resource_calendars.ids: + cal_attendance_intervals_dict[calendar_id] = calendar_data + else: + cal_attendance_intervals_dict[res['calendar_id'][0]] = calendar_data # to easily find the calendar with its id. - calendars_dict = {calendar.id: calendar for calendar in self.calendar_id} + calendars_dict = {calendar.id: calendar for calendar in resource_calendars} - # dict of keys: leave.id - # and values: a dict of keys: date - # and values: number of days - results = defaultdict(lambda: defaultdict(float)) + # dict of keys: calendar_id + # and values: a dict of keys: leave.id + # and values: a dict of keys: date + # and values: number of days + results = defaultdict(lambda: defaultdict(lambda: defaultdict(float))) for calendar_id, cal_attendance_intervals_params_entry in cal_attendance_intervals_dict.items(): calendar = calendars_dict[calendar_id] work_hours_intervals = calendar._attendance_intervals_batch( @@ -69,8 +84,8 @@ class ResourceCalendarLeaves(models.Model): if date_to > utc.localize(leave.date_from) and date_from < utc.localize(leave.date_to): tmp_start = max(date_from, utc.localize(leave.date_from)) tmp_end = min(date_to, utc.localize(leave.date_to)) - results[leave.id][tmp_start.date()] += (tmp_end - tmp_start).total_seconds() / 3600 - results[leave.id] = sorted(results[leave.id].items()) + results[calendar_id][leave.id][tmp_start.date()] += (tmp_end - tmp_start).total_seconds() / 3600 + results[calendar_id][leave.id] = sorted(results[calendar_id][leave.id].items()) return results def _timesheet_create_lines(self): @@ -78,9 +93,10 @@ class ResourceCalendarLeaves(models.Model): If the employee has already a time off in the same day then no timesheet should be created. """ - work_hours_data = self._work_time_per_day() + resource_calendars = self._get_resource_calendars() + work_hours_data = self._work_time_per_day(resource_calendars) employees_groups = self.env['hr.employee']._read_group( - [('resource_calendar_id', 'in', self.calendar_id.ids)], + [('resource_calendar_id', 'in', resource_calendars.ids)], ['resource_calendar_id', 'ids:array_agg(id)'], ['resource_calendar_id']) mapped_employee = { @@ -91,13 +107,14 @@ class ResourceCalendarLeaves(models.Model): employee_ids_set.update(*[line['ids'] for line in employees_groups]) min_date = max_date = None for values in work_hours_data.values(): - for d, dummy in values: - if not min_date and not max_date: - min_date = max_date = d - elif d < min_date: - min_date = d - elif d > max_date: - max_date = d + for vals in values.values(): + for d, dummy in vals: + if not min_date and not max_date: + min_date = max_date = d + elif d < min_date: + min_date = d + elif d > max_date: + max_date = d holidays_read_group = self.env['hr.leave']._read_group([ ('employee_id', 'in', list(employee_ids_set)), @@ -111,10 +128,10 @@ class ResourceCalendarLeaves(models.Model): ] for line in holidays_read_group } vals_list = [] - for leave in self: - for employee in mapped_employee.get(leave.calendar_id.id, self.env['hr.employee']): + + def get_timesheets_data(employees, work_hours_list, vals_list): + for employee in employees: holidays = holidays_by_employee.get(employee.id) - work_hours_list = work_hours_data[leave.id] for index, (day_date, work_hours_count) in enumerate(work_hours_list): if not holidays or all(not (date_from <= day_date and date_to >= day_date) for date_from, date_to in holidays): vals_list.append( @@ -126,6 +143,18 @@ class ResourceCalendarLeaves(models.Model): work_hours_count ) ) + return vals_list + + for leave in self: + if not leave.calendar_id: + for calendar_id, calendar_employees in mapped_employee.items(): + work_hours_list = work_hours_data[calendar_id][leave.id] + vals_list = get_timesheets_data(calendar_employees, work_hours_list, vals_list) + else: + employees = mapped_employee.get(leave.calendar_id.id, self.env['hr.employee']) + work_hours_list = work_hours_data[leave.calendar_id.id][leave.id] + vals_list = get_timesheets_data(employees, work_hours_list, vals_list) + return self.env['account.analytic.line'].sudo().create(vals_list) def _timesheet_prepare_line_values(self, index, employee_id, work_hours_data, day_date, work_hours_count): @@ -143,24 +172,26 @@ class ResourceCalendarLeaves(models.Model): 'company_id': employee_id.company_id.id, } + def _generate_timesheeets(self): + results_with_leave_timesheet = self.filtered(lambda r: not r.resource_id and r.company_id.internal_project_id and r.company_id.leave_timesheet_task_id) + if results_with_leave_timesheet: + results_with_leave_timesheet._timesheet_create_lines() + @api.model_create_multi def create(self, vals_list): results = super(ResourceCalendarLeaves, self).create(vals_list) - results_with_leave_timesheet = results.filtered(lambda r: not r.resource_id.id and r.calendar_id.company_id.internal_project_id and r.calendar_id.company_id.leave_timesheet_task_id) - results_with_leave_timesheet and results_with_leave_timesheet._timesheet_create_lines() + results._generate_timesheeets() return results def write(self, vals): date_from, date_to, calendar_id = vals.get('date_from'), vals.get('date_to'), vals.get('calendar_id') global_time_off_updated = self.env['resource.calendar.leaves'] if date_from or date_to or 'calendar_id' in vals: - global_time_off_updated = self.filtered(lambda r: (date_from is not None and r.date_from != date_from) or (date_to is not None and r.date_to != date_to) or (calendar_id is not None and r.calendar_id.id != calendar_id)) + global_time_off_updated = self.filtered(lambda r: (date_from is not None and r.date_from != date_from) or (date_to is not None and r.date_to != date_to) or (calendar_id is None or r.calendar_id.id != calendar_id)) timesheets = global_time_off_updated.sudo().timesheet_ids if timesheets: timesheets.write({'global_leave_id': False}) timesheets.unlink() result = super(ResourceCalendarLeaves, self).write(vals) - if global_time_off_updated: - global_time_offs_with_leave_timesheet = global_time_off_updated.filtered(lambda r: not r.resource_id and r.calendar_id.company_id.internal_project_id and r.calendar_id.company_id.leave_timesheet_task_id) - global_time_offs_with_leave_timesheet.sudo()._timesheet_create_lines() + global_time_off_updated and global_time_off_updated.sudo()._generate_timesheeets() return result 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 ca255df3c14..d19d3c8a8ff 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 @@ -35,30 +35,45 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase): (0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}) ] - self.part_time_calendar = self.env['resource.calendar'].create({ - 'name': 'Part Time Calendar', - 'company_id': self.test_company.id, - 'hours_per_day': 6, - 'attendance_ids': attendance_ids, - }) + self.part_time_calendar, self.part_time_calendar2 = self.env['resource.calendar'].create([ + { + 'name': 'Part Time Calendar', + 'company_id': self.test_company.id, + 'hours_per_day': 6, + 'attendance_ids': attendance_ids, + }, { + 'name': 'Night Watch', + 'company_id': self.test_company.id, + 'hours_per_day': 6, + 'attendance_ids': attendance_ids, + } + ]) + self.full_time_employee, self.full_time_employee_2,\ + self.part_time_employee, self.part_time_employee2 = self.env['hr.employee'].create([{ + 'name': 'John Doe', + 'company_id': self.test_company.id, + 'resource_calendar_id': self.test_company.resource_calendar_id.id, + }, { + 'name': 'John Smith', + 'company_id': self.test_company.id, + 'resource_calendar_id': self.test_company.resource_calendar_id.id, + }, { + 'name': 'Jane Doe', + 'company_id': self.test_company.id, + 'resource_calendar_id': self.part_time_calendar.id, + }, { + 'name': 'Jon Show', + 'company_id': self.test_company.id, + 'resource_calendar_id': self.part_time_calendar2.id, + }, + ]) - self.full_time_employee = self.env['hr.employee'].create({ - 'name': 'John Doe', - 'company_id': self.test_company.id, - 'resource_calendar_id': self.test_company.resource_calendar_id.id, - }) - - self.full_time_employee_2 = self.env['hr.employee'].create({ - 'name': 'John Smith', - 'company_id': self.test_company.id, - 'resource_calendar_id': self.test_company.resource_calendar_id.id, - }) - - self.part_time_employee = self.env['hr.employee'].create({ - 'name': 'Jane Doe', - 'company_id': self.test_company.id, - 'resource_calendar_id': self.part_time_calendar.id, - }) + def _get_timesheets_by_employee(self, leave_task): + timesheets_by_read_dict = self.env['account.analytic.line']._read_group([('task_id', '=', leave_task.id)], ['employee_id'], ['employee_id']) + timesheets_by_employee = {} + for timesheet in timesheets_by_read_dict: + timesheets_by_employee[timesheet['employee_id'][0]] = timesheet['employee_id_count'] + return timesheets_by_employee # This tests that timesheets are created for every employee with the same calendar # when a global time off is created. @@ -78,12 +93,10 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase): # but none for part_time_employee leave_task = self.test_company.leave_timesheet_task_id - timesheets_by_employee = defaultdict(lambda: self.env['account.analytic.line']) - for timesheet in leave_task.timesheet_ids: - timesheets_by_employee[timesheet.employee_id] |= timesheet - self.assertFalse(timesheets_by_employee.get(self.part_time_employee, False)) - self.assertEqual(len(timesheets_by_employee.get(self.full_time_employee)), 5) - self.assertEqual(len(timesheets_by_employee.get(self.full_time_employee_2)), 5) + timesheets_by_employee = self._get_timesheets_by_employee(leave_task) + self.assertFalse(timesheets_by_employee.get(self.part_time_employee.id, False)) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee.id), 5) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee_2.id), 5) # The standard calendar is for 8 hours/day from 8 to 12 and from 13 to 17. # So we need to check that the timesheets don't have more than 8 hours per day. @@ -169,12 +182,10 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase): # Now we reset the calendar_id. The timesheets should be created and have the right value. global_time_off.calendar_id = self.test_company.resource_calendar_id.id - timesheets_by_employee = defaultdict(lambda: self.env['account.analytic.line']) - for timesheet in leave_task.timesheet_ids: - timesheets_by_employee[timesheet.employee_id] |= timesheet - self.assertFalse(timesheets_by_employee.get(self.part_time_employee, False)) - self.assertEqual(len(timesheets_by_employee.get(self.full_time_employee)), 5) - self.assertEqual(len(timesheets_by_employee.get(self.full_time_employee_2)), 5) + timesheets_by_employee = self._get_timesheets_by_employee(leave_task) + self.assertFalse(timesheets_by_employee.get(self.part_time_employee.id, False)) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee.id), 5) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee_2.id), 5) # The standard calendar is for 8 hours/day from 8 to 12 and from 13 to 17. # So we need to check that the timesheets don't have more than 8 hours per day. @@ -188,3 +199,85 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase): leaves_types_with_task_id.write({'timesheet_task_id': False}) self.env['project.task'].search([('is_timeoff_task', '!=', False)]) + + def test_timesheet_creation_for_global_time_off_wo_calendar(self): + leave_start_datetime = datetime(2021, 1, 4, 7, 0) # This is a monday + leave_end_datetime = datetime(2021, 1, 8, 18, 0) # This is a friday + + global_time_off = self.env['resource.calendar.leaves'].with_company(self.test_company).create({ + 'name': 'Test', + 'calendar_id': False, + 'date_from': leave_start_datetime, + 'date_to': leave_end_datetime, + }) + + leave_task = self.test_company.leave_timesheet_task_id + timesheets_by_employee = self._get_timesheets_by_employee(leave_task) + # 5 Timesheets for full time employees and 4 Timesheets for part time employees should have been created + self.assertEqual(timesheets_by_employee.get(self.part_time_employee.id), 4) + self.assertEqual(timesheets_by_employee.get(self.part_time_employee2.id), 4) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee.id), 5) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee_2.id), 5) + # 8 hours/day for full time calendar employees and 6 hours/day for part time calendar employees. + # So it should add to 2(full time employees)*5(leave days)*8(hours per day) + 2(part time employees)*4(leave days)*6(hours per day). + self.assertEqual(leave_task.effective_hours, 128) + + + # Now we set the calendar_id. The timesheets should be deleted from other calendars. + global_time_off.calendar_id = self.test_company.resource_calendar_id.id + timesheets_by_employee = self._get_timesheets_by_employee(leave_task) + + self.assertFalse(timesheets_by_employee.get(self.part_time_employee.id, False)) + self.assertFalse(timesheets_by_employee.get(self.part_time_employee2.id, False)) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee.id), 5) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee_2.id), 5) + self.assertEqual(leave_task.effective_hours, 80) + + def test_timesheet_creation_for_global_time_off_in_differant_company(self): + leave_start_datetime = datetime(2021, 1, 4, 7, 0) # This is a monday + leave_end_datetime = datetime(2021, 1, 8, 18, 0) # This is a friday + + new_company = self.env['res.company'].create({ + 'name': 'Winterfell', + }) + + self.env['resource.calendar.leaves'].with_company(new_company).create({ + 'name': 'Test', + 'calendar_id': False, + 'date_from': leave_start_datetime, + 'date_to': leave_end_datetime, + }) + + leave_task = self.test_company.leave_timesheet_task_id + timesheets_by_employee = self._get_timesheets_by_employee(leave_task) + # Should no create timesheet if leave is in differant company then employees + self.assertFalse(timesheets_by_employee.get(self.part_time_employee, False)) + self.assertFalse(timesheets_by_employee.get(self.full_time_employee, False)) + # Should not add any timsheets in other companies + self.assertEqual(leave_task.effective_hours, 0) + + def test_timesheet_creation_for_global_time_off_wo_calendar_in_batch(self): + self.env['resource.calendar.leaves'].with_company(self.test_company).create([{ + 'name': "Easter Monday", + 'calendar_id': False, + 'date_from': datetime(2022, 4, 18, 5, 0, 0), + 'date_to': datetime(2022, 4, 18, 18, 0, 0), + 'resource_id': False, + 'time_type': "leave", + }, { + 'name': "Ascension Day", + 'calendar_id': False, + 'date_from': datetime(2022, 4, 26, 5, 0, 0), + 'date_to': datetime(2022, 4, 26, 18, 0, 0), + }]) + + # 2 Timesheets for 2 global leaves should have been created for current companies all calendar employees + leave_task = self.test_company.leave_timesheet_task_id + timesheets_by_employee = self._get_timesheets_by_employee(leave_task) + + self.assertEqual(timesheets_by_employee.get(self.part_time_employee.id), 2) + self.assertEqual(timesheets_by_employee.get(self.part_time_employee2.id), 2) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee.id), 2) + self.assertEqual(timesheets_by_employee.get(self.full_time_employee_2.id), 2) + # Total hours should be 2(part time employees)*6(hour per day)*2(leaves days) + 2(full time employees)*8(hour per day)*2(leaves days) + self.assertEqual(leave_task.effective_hours, 56)