[IMP] project_timesheet_holidays: create timesheets for global leave wo calendar

Before this commit when user create a global without calendar then
it'll not create any timesheets for any employee but global leave
without calendar is taken as leave global leave for all calendar
in that company's calendar and should create timesheet for all
calendars employee in company.

This commit create timesheets for all calendars employee in that
company if global leave does not have specific calendar set on it.

task-2969791

Part-of: odoo/odoo#100141
This commit is contained in:
Kartik Chavda
2023-04-03 17:07:07 +02:00
parent 2989ef360a
commit fdb692c25b
3 changed files with 193 additions and 67 deletions
@@ -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,
@@ -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
@@ -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)