[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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user