19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:56 +01:00
parent a2f74aefd8
commit 4a4d12c333
844 changed files with 212348 additions and 270090 deletions

View file

@ -35,7 +35,7 @@ class TestCancelTimeOff(TransactionCase):
})
cls.generic_time_off_type = cls.env['hr.leave.type'].create({
'name': 'Generic Time Off',
'requires_allocation': 'no',
'requires_allocation': False,
'leave_validation_type': 'both',
'company_id': cls.company.id,
})
@ -54,14 +54,15 @@ class TestCancelTimeOff(TransactionCase):
"""
time_off = self.env['hr.leave'].create({
'name': 'Test Time Off',
'holiday_type': 'employee',
'holiday_status_id': self.generic_time_off_type.id,
'employee_id': self.employee.id,
'date_from': '2020-01-07 08:00:00',
'date_to': '2020-01-09 17:00:00',
})
time_off.action_validate()
time_off.action_approve()
self.assertEqual(time_off.state, 'validate')
HrHolidaysCancelLeave = self.env[
'hr.holidays.cancel.leave'].with_user(self.employee_user).with_company(self.company.id)
HrHolidaysCancelLeave.create({
'leave_id': time_off.id, 'reason': 'Test Reason'}).action_cancel_leave()
self.assertEqual(time_off.state, 'cancel')

View file

@ -33,11 +33,32 @@ class TestEmployee(TransactionCase):
2) Check the timesheets representing the time off of this new employee
is correctly generated
"""
existing_employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'company_id': self.company.id,
'resource_calendar_id': self.company.resource_calendar_id.id,
})
resource_leave = self.env['resource.calendar.leaves'].with_company(self.company).create({
'name': 'Future resource specific leave without a calendar',
'date_from': '2020-01-02 00:00:00',
'date_to': '2020-01-02 23:59:59',
'calendar_id': False,
'resource_id': existing_employee.resource_id.id,
'company_id': self.company.id,
})
employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'company_id': self.company.id,
'resource_calendar_id': self.company.resource_calendar_id.id,
})
# Check resource-specific leave does not create a timesheet
resource_timesheet = self.env['account.analytic.line'].search([
('employee_id', '=', employee.id),
('global_leave_id', '=', resource_leave.id),
])
self.assertFalse(resource_timesheet, 'No timesheet should be created for resource-specific leaves')
timesheet = self.env['account.analytic.line'].search([
('employee_id', '=', employee.id),
('global_leave_id', '=', self.global_leave.id),
@ -103,6 +124,14 @@ class TestEmployee(TransactionCase):
self.assertEqual(str(timesheet.date), '2020-01-01', 'The timesheet should be created for the correct date')
self.assertEqual(timesheet.unit_amount, 8, 'The timesheet should be created for the correct duration')
# test unarchiving on an already active employee does not create duplicate public leaves
employee.write({'active': True})
timesheet = self.env['account.analytic.line'].search([
('employee_id', '=', employee.id),
('global_leave_id', '=', self.global_leave.id),
])
self.assertEqual(len(timesheet), 1, 'We should not have created duplicate public holiday leaves')
# simulate the company of the employee updated is not in the allowed_company_ids of the current user
employee.with_company(self.env.company).write({'resource_calendar_id': self.company.resource_calendar_id.id})
timesheet = self.env['account.analytic.line'].search([

View file

@ -7,12 +7,12 @@ from freezegun import freeze_time
from odoo import Command
from odoo.tests import common
from odoo.exceptions import UserError
class TestTimesheetGlobalTimeOff(common.TransactionCase):
def setUp(self):
super(TestTimesheetGlobalTimeOff, self).setUp()
super().setUp()
# Creates 1 test company and a calendar for employees that
# work part time. Then creates an employee per calendar (one
# for the standard calendar and one for the one we created)
@ -21,40 +21,64 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
})
attendance_ids = [
Command.create({'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
Command.create({'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
Command.create({'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
Command.create({'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
Command.create({'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
Command.create({'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
Command.create({'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
Command.create({'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Lunch', 'dayofweek': '0', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Lunch', 'dayofweek': '1', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Lunch', 'dayofweek': '3', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Friday Lunch', 'dayofweek': '4', 'hour_from': 12, 'hour_to': 13, 'day_period': 'lunch'}),
(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,
},
])
# Create a 2nd company
self.test_company_2 = self.env['res.company'].create({
'name': 'My Test Company 2',
})
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'], ['__count'])
return {
timesheet.id: count
for timesheet, count in timesheets_by_read_dict
}
# This tests that timesheets are created for every employee with the same calendar
# when a global time off is created.
@ -74,12 +98,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.
@ -104,19 +126,38 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
'date_to': leave_end_datetime,
})
# 5 Timesheets should have been created for full_time_employee
# Create a global time-off in the same company (not specific a to calendar)
# this should be added to calendar's leaves
self.env['resource.calendar.leaves'].with_company(self.test_company).create({
'name': 'Global leave',
'calendar_id': False,
'date_from': (leave_start_datetime + timedelta(weeks=1)).replace(hour=0, minute=0, second=0),
'date_to': (leave_start_datetime + timedelta(weeks=1, days=1)).replace(hour=23, minute=59, second=59),
})
# Create a global time-off in another company which should not be
# taken into account when creating/unarchiving employee
self.env['resource.calendar.leaves'].with_company(self.test_company_2).create({
'name': 'Global leave in another company',
'calendar_id': False,
# Monday in two weeks
'date_from': (leave_start_datetime + timedelta(weeks=2)).replace(hour=0, minute=0, second=0),
'date_to': (leave_start_datetime + timedelta(weeks=2)).replace(hour=23, minute=59, second=59),
})
# 7 Timesheets should have been created for full_time_employee
timesheets_full_time_employee = self.env['account.analytic.line'].search([('employee_id', '=', self.full_time_employee.id)])
self.assertEqual(len(timesheets_full_time_employee), 5)
self.assertEqual(len(timesheets_full_time_employee), 7)
# All timesheets should have been deleted for full_time_employee when he is archived
self.full_time_employee.active = False
timesheets_full_time_employee = self.env['account.analytic.line'].search([('employee_id', '=', self.full_time_employee.id)])
self.assertEqual(len(timesheets_full_time_employee), 0)
# 5 Timesheets should have been created for full_time_employee when he is unarchived
# 7 Timesheets should have been created for full_time_employee when he is unarchived
self.full_time_employee.active = True
timesheets_full_time_employee = self.env['account.analytic.line'].search([('employee_id', '=', self.full_time_employee.id)])
self.assertEqual(len(timesheets_full_time_employee), 5)
self.assertEqual(len(timesheets_full_time_employee), 7)
# This tests that no timesheet are created for days when the employee is not supposed to work
def test_no_timesheet_on_off_days(self):
@ -165,25 +206,111 @@ 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.
self.assertEqual(leave_task.effective_hours, 80)
def test_search_is_timeoff_task(self):
""" Test the search method on is_timeoff_task
with and without any hr.leave.type with timesheet_task_id defined"""
leaves_types_with_task_id = self.env['hr.leave.type'].search([('timesheet_task_id', '!=', False)])
self.env['project.task'].search([('is_timeoff_task', '!=', False)])
def test_timeoff_task_creation_with_global_leave(self):
""" Test the search method on is_timeoff_task"""
task_count = self.env['project.task'].search_count([('is_timeoff_task', '!=', False)])
leaves_types_with_task_id.write({'timesheet_task_id': False})
self.env['project.task'].search([('is_timeoff_task', '!=', False)])
# Create a leave and validate it
self.env['resource.calendar.leaves'].create({
'name': 'Test',
'calendar_id': self.test_company.resource_calendar_id.id,
'date_from': datetime(2021, 1, 4, 7, 0, 0, 0),
'date_to': datetime(2021, 1, 8, 18, 0, 0, 0),
})
new_task_count = self.env['project.task'].search_count([('is_timeoff_task', '!=', False)])
self.assertEqual(task_count + 1, new_task_count)
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)
def test_timesheet_creation_and_deletion_for_calendar_update(self):
"""
@ -282,6 +409,7 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
test_user = self.env['res.users'].with_company(self.test_company).create({
'name': 'Jonathan Doe',
'login': 'jdoe@example.com',
'group_ids': self.env.ref('hr_timesheet.group_hr_timesheet_user'),
})
test_user.with_company(self.test_company).action_create_employee()
test_user.employee_id.write({
@ -298,17 +426,14 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
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
self.env = self.env(context=dict(self.env.context, allowed_company_ids=self.test_company.ids))
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,
'requires_allocation': False,
})
# create and validate a leave for full time employee
@ -317,15 +442,15 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
'name': 'Leave 1',
'employee_id': test_user.employee_id.id,
'holiday_status_id': hr_leave_type_with_ts.id,
'date_from': hr_leave_start_datetime,
'date_to': hr_leave_end_datetime,
'request_date_from': hr_leave_start_datetime,
'request_date_to': hr_leave_end_datetime,
})
holiday.sudo().action_validate()
holiday.sudo().action_approve()
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)
# create overlapping global time off, with some margin over working day to account for different timezones
global_leave_start_datetime = hr_leave_start_datetime + timedelta(days=2, hours=-3)
global_leave_end_datetime = global_leave_start_datetime + timedelta(hours=12)
global_time_off = self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
@ -333,23 +458,37 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
'date_from': global_leave_start_datetime,
'date_to': global_leave_end_datetime,
})
# timesheet linked to global time off should not exist as one already exists for the leave
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)
# timesheet linked to global time off should be restored as the existing leave timesheets were unlinked after refusal
# 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 off to remove the timesheet so we can test cancelling the leave
# remove global time offs to remove the timesheet so we can test cancelling the leave
global_time_off.unlink()
gto_without_calendar.unlink()
# restore the leave to validated to re-create the associated timesheets
holiday.sudo().action_draft()
holiday.sudo().action_confirm()
holiday.sudo().action_validate()
# 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_approve()
# recreate the global time off
global_time_off = self.env['resource.calendar.leaves'].create({
@ -358,62 +497,96 @@ class TestTimesheetGlobalTimeOff(common.TransactionCase):
'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),
})
# timesheet linked to global time off should not exist as one already exists for the leave
# 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
holiday.with_user(test_user)._action_user_cancel('User cancelled leave')
self.assertFalse(holiday.timesheet_ids)
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))
def test_global_time_off_timesheet_creation_without_calendar(self):
""" Ensure that a global time off without a calendar does not get restored if it overlaps with a refused leave. """
# 5 day leave
hr_leave_start_datetime = datetime(2023, 12, 25, 7, 0, 0, 0)
hr_leave_end_datetime = datetime(2023, 12, 29, 18, 0, 0, 0)
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(self.full_time_employee.user_id).create({
'name': 'Leave 1',
'employee_id': self.full_time_employee.id,
# create a new leave at same dates
holiday3 = HrLeave.with_user(test_user).create({
'name': 'Leave 3',
'employee_id': test_user.employee_id.id,
'holiday_status_id': hr_leave_type_with_ts.id,
'date_from': hr_leave_start_datetime,
'date_to': hr_leave_end_datetime,
'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)
holiday3.sudo().action_approve()
# overlapping leave without calendar
global_leave_start_datetime = datetime(2023, 12, 27, 7, 0, 0, 0)
global_leave_end_datetime = datetime(2023, 12, 27, 18, 0, 0, 0)
self.assertEqual(len(holiday3.timesheet_ids), 3)
global_time_off.unlink()
self.assertEqual(len(holiday3.timesheet_ids), 4)
gto_without_calendar.calendar_id = self.part_time_calendar
self.assertFalse(gto_without_calendar.timesheet_ids.filtered(lambda r: r.employee_id == test_user.employee_id))
self.assertEqual(len(holiday3.timesheet_ids), 5)
self.assertEqual(sum(holiday3.timesheet_ids.mapped('unit_amount')), 40)
gto_without_calendar = self.env['resource.calendar.leaves'].create({
'name': '2 days afer Christmas',
'date_from': global_leave_start_datetime,
'date_to': global_leave_end_datetime,
def test_unlink_timesheet_with_global_time_off(self):
leave_start = datetime(2025, 1, 1, 7, 0)
leave_end = datetime(2025, 1, 1, 18, 0)
global_time_off = self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
'calendar_id': self.test_company.resource_calendar_id.id,
'date_from': leave_start,
'date_to': leave_end,
})
# ensure timesheets are not created for a global time off without a calendar
self.assertFalse(gto_without_calendar.timesheet_ids)
timesheet = self.env['account.analytic.line'].search([
('global_leave_id', '=', global_time_off.id),
('employee_id', '=', self.full_time_employee.id)
])
# refuse the leave
holiday.sudo().action_refuse()
self.assertFalse(holiday.timesheet_ids)
with self.assertRaises(UserError):
timesheet.unlink()
# timesheets should not be restored for a global time off without a calendar
self.assertFalse(gto_without_calendar.timesheet_ids)
def test_timesheet_generation_on_public_holiday_creation_with_global_working_schedule(self):
""" Test that public holidays are included in the global working schedule (company should be False)
when a global time off is created.
"""
self.part_time_calendar.company_id = False
self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
'date_from': datetime(2021, 1, 4, 0, 0, 0),
'date_to': datetime(2021, 1, 4, 23, 59, 59),
})
timesheet_count = self.env['account.analytic.line'].search_count([('employee_id', '=', self.part_time_employee.id)])
self.assertEqual(timesheet_count, 1, "A timesheet should have been generated for the employee with a global working "
"schedule when a new public holiday is created")
def test_timesheet_generation_on_public_holiday_creation_with_flexible_hours(self):
""" Test that public holidays timesheet duration match the hours per days value for flexible
"""
self.flexible_calendar = self.env['resource.calendar'].create({
'name': 'Flexible Calendar',
'hours_per_day': 7.0,
'hours_per_week': 7.0,
'full_time_required_hours': 7.0,
'flexible_hours': True,
})
self.flexible_employee = self.env['hr.employee'].create({
'name': 'Flexible',
'company_id': self.test_company.id,
'resource_calendar_id': self.flexible_calendar.id,
})
self.env['resource.calendar.leaves'].create({
'name': 'Public Holiday',
'date_from': datetime(2021, 1, 4, 0, 0, 0),
'date_to': datetime(2021, 1, 4, 23, 59, 59),
})
timesheet = self.env['account.analytic.line'].search([('employee_id', '=', self.flexible_employee.id)])
self.assertEqual(timesheet.unit_amount, self.flexible_calendar.hours_per_day)

View file

@ -15,17 +15,6 @@ import time
class TestTimesheetHolidaysCreate(common.TransactionCase):
def test_status_create(self):
"""Ensure that when a status is created, it fullfills the project and task constrains"""
status = self.env['hr.leave.type'].create({
'name': 'A nice Leave Type',
'requires_allocation': 'no'
})
company = self.env.company
self.assertEqual(status.timesheet_project_id, company.internal_project_id, 'The default project linked to the status should be the same as the company')
self.assertEqual(status.timesheet_task_id, company.leave_timesheet_task_id, 'The default task linked to the status should be the same as the company')
def test_company_create(self):
main_company = self.env.ref('base.main_company')
user = new_test_user(self.env, login='fru',
@ -41,32 +30,28 @@ class TestTimesheetHolidaysCreate(common.TransactionCase):
class TestTimesheetHolidays(TestCommonTimesheet):
def setUp(self):
super(TestTimesheetHolidays, self).setUp()
super().setUp()
self.employee_working_calendar = self.empl_employee.resource_calendar_id
# leave dates : from next monday to next wednesday (to avoid crashing tests on weekend, when
# there is no work days in working calendar)
# NOTE: second and millisecond can add a working days
self.leave_start_datetime = datetime(2018, 2, 5, 7, 0, 0, 0) # this is monday
self.leave_end_datetime = self.leave_start_datetime + relativedelta(days=3)
self.leave_start_datetime = datetime(2018, 2, 5) # this is monday
self.leave_end_datetime = self.leave_start_datetime + relativedelta(days=2)
# all company have those internal project/task (created by default)
self.internal_project = self.env.company.internal_project_id
self.internal_task_leaves = self.env.company.leave_timesheet_task_id
self.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': self.internal_project.id,
'timesheet_task_id': self.internal_task_leaves.id,
self.hr_leave_type_with_ts = self.env['hr.leave.type'].sudo().create({
'name': 'Time Off Type with timesheet generation (absence)',
'requires_allocation': False,
})
self.hr_leave_type_no_ts = self.env['hr.leave.type'].create({
'name': 'Leave Type without timesheet generation',
'requires_allocation': 'no',
'timesheet_generate': False,
'timesheet_project_id': False,
'timesheet_task_id': False,
self.hr_leave_type_worked = self.env['hr.leave.type'].sudo().create({
'name': 'Time Off Type (worked time)',
'requires_allocation': False,
'time_type': 'other',
})
# HR Officer allocates some leaves to the employee 1
@ -81,73 +66,80 @@ class TestTimesheetHolidays(TestCommonTimesheet):
'date_from': time.strftime('%Y-01-01'),
'date_to': time.strftime('%Y-12-31'),
})
self.hr_leave_allocation_with_ts.action_validate()
self.hr_leave_allocation_no_ts = self.Allocations.sudo().create({
'name': 'Days for limited category without timesheet',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_no_ts.id,
'number_of_days': 10,
'state': 'confirm',
'date_from': time.strftime('%Y-01-01'),
'date_to': time.strftime('%Y-12-31'),
})
self.hr_leave_allocation_no_ts.action_validate()
def test_validate_with_timesheet(self):
# employee creates a leave request
number_of_days = (self.leave_end_datetime - self.leave_start_datetime).days
holiday = self.Requests.with_user(self.user_employee).create({
'name': 'Leave 1',
'name': 'Time Off 1',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'date_from': self.leave_start_datetime,
'date_to': self.leave_end_datetime,
'number_of_days': number_of_days,
'request_date_from': self.leave_start_datetime,
'request_date_to': self.leave_end_datetime,
})
holiday.with_user(SUPERUSER_ID).action_validate()
holiday.with_user(SUPERUSER_ID).action_approve()
# The leave type and timesheet are linked to the same project and task'
# The leave type and timesheet are linked to the same project and task of hr_leave_type_with_ts as the company is set
self.assertEqual(holiday.timesheet_ids.project_id.id, self.internal_project.id)
self.assertEqual(holiday.timesheet_ids.task_id.id, self.internal_task_leaves.id)
self.assertEqual(len(holiday.timesheet_ids), number_of_days, 'Number of generated timesheets should be the same as the leave duration (1 per day between %s and %s)' % (fields.Datetime.to_string(self.leave_start_datetime), fields.Datetime.to_string(self.leave_end_datetime)))
self.assertEqual(len(holiday.timesheet_ids), holiday.number_of_days, 'Number of generated timesheets should be the same as the leave duration (1 per day between %s and %s)' % (fields.Datetime.to_string(self.leave_start_datetime), fields.Datetime.to_string(self.leave_end_datetime)))
# manager refuse the leave
holiday.with_user(SUPERUSER_ID).action_refuse()
self.assertEqual(len(holiday.timesheet_ids), 0, 'Number of linked timesheets should be zero, since the leave is refused.')
def test_validate_without_timesheet(self):
# employee creates a leave request
number_of_days = (self.leave_end_datetime - self.leave_start_datetime).days
holiday = self.Requests.with_user(self.user_employee).create({
'name': 'Leave 1',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_no_ts.id,
'date_from': self.leave_start_datetime,
'date_to': self.leave_end_datetime,
'number_of_days': number_of_days,
company = self.env['res.company'].create({"name": "new company"})
self.empl_employee.write({
"company_id": company.id,
})
holiday.with_user(SUPERUSER_ID).action_validate()
self.assertEqual(len(holiday.timesheet_ids), 0, 'Number of generated timesheets should be zero since the leave type does not generate timesheet')
hr_leave_type_with_ts_without_company = self.hr_leave_type_with_ts.copy()
hr_leave_type_with_ts_without_company.write({
'company_id': False,
})
holiday = self.Requests.create({
'name': 'Time Off 2',
'employee_id': self.empl_employee.id,
'holiday_status_id': hr_leave_type_with_ts_without_company.id,
'request_date_from': self.leave_start_datetime,
'request_date_to': self.leave_end_datetime,
})
holiday.with_user(SUPERUSER_ID).action_approve()
# The leave type and timesheet are linked to the same project and task of the employee company as the company is not set
self.assertEqual(holiday.timesheet_ids.project_id.id, company.internal_project_id.id)
self.assertEqual(holiday.timesheet_ids.task_id.id, company.leave_timesheet_task_id.id)
def test_validate_worked_leave(self):
# employee creates a leave request of worked time type
holiday = self.Requests.with_user(self.user_employee).create({
'name': 'Time Off 3',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_worked.id,
'request_date_from': self.leave_start_datetime,
'request_date_to': self.leave_end_datetime,
})
holiday.with_user(SUPERUSER_ID).action_approve()
self.assertEqual(len(holiday.timesheet_ids), 0, 'No timesheet should be created for a leave of worked time type')
@freeze_time('2018-02-05') # useful to be able to cancel the validated time off
def test_cancel_validate_holidays(self):
number_of_days = (self.leave_end_datetime - self.leave_start_datetime).days
holiday = self.Requests.with_user(self.user_employee).create({
'name': 'Leave 1',
'name': 'Time Off 1',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'date_from': self.leave_start_datetime,
'date_to': self.leave_end_datetime,
'number_of_days': number_of_days,
'request_date_from': self.leave_start_datetime,
'request_date_to': self.leave_end_datetime,
})
holiday.with_user(self.env.user).action_validate()
self.assertEqual(len(holiday.timesheet_ids), number_of_days, 'Number of generated timesheets should be the same as the leave duration (1 per day between %s and %s)' % (fields.Datetime.to_string(self.leave_start_datetime), fields.Datetime.to_string(self.leave_end_datetime)))
holiday.with_user(self.env.user).action_approve()
self.assertEqual(len(holiday.timesheet_ids), holiday.number_of_days, 'Number of generated timesheets should be the same as the leave duration (1 per day between %s and %s)' % (fields.Datetime.to_string(self.leave_start_datetime), fields.Datetime.to_string(self.leave_end_datetime)))
self.env['hr.holidays.cancel.leave'].with_user(self.user_employee).with_context(default_leave_id=holiday.id) \
.new({'reason': 'Test remove holiday'}) \
.action_cancel_leave()
self.assertFalse(holiday.active, 'The time off should be archived')
self.assertEqual(holiday.state, 'cancel', 'The time off should be archived')
self.assertEqual(len(holiday.timesheet_ids), 0, 'The timesheets generated should be unlink.')
def test_timesheet_time_off_including_public_holiday(self):
@ -173,13 +165,13 @@ class TestTimesheetHolidays(TestCommonTimesheet):
})
holiday = self.Requests.with_user(self.user_employee).create({
'name': 'Leave 1',
'name': 'Time Off 1',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'date_from': leave_start_datetime,
'date_to': leave_end_datetime,
'request_date_from': leave_start_datetime,
'request_date_to': leave_end_datetime,
})
holiday.with_user(SUPERUSER_ID).action_validate()
holiday.with_user(SUPERUSER_ID).action_approve()
self.assertEqual(len(holiday.timesheet_ids), 4, '4 timesheets should be generated for this time off.')
timesheets = self.env['account.analytic.line'].search([
@ -190,11 +182,11 @@ class TestTimesheetHolidays(TestCommonTimesheet):
# should not able to update timeoff timesheets
with self.assertRaises(UserError):
timesheets.with_user(self.empl_employee).write({'task_id': 4})
timesheets.with_user(self.user_employee).write({'task_id': 4})
# should not able to create timesheet in timeoff task
with self.assertRaises(UserError):
self.env['account.analytic.line'].with_user(self.empl_employee).create({
self.env['account.analytic.line'].with_user(self.user_employee).create({
'name': "my timesheet",
'project_id': self.internal_project.id,
'task_id': self.internal_task_leaves.id,
@ -205,23 +197,188 @@ class TestTimesheetHolidays(TestCommonTimesheet):
self.assertEqual(len(timesheets.filtered('holiday_id')), 4, "4 timesheet should be linked to employee's timeoff")
self.assertEqual(len(timesheets.filtered('global_leave_id')), 1, '1 timesheet should be linked to global leave')
@freeze_time('2018-02-01 08:00:00')
def test_timesheet_when_archiving_employee(self):
number_of_days = (self.leave_end_datetime - self.leave_start_datetime).days
holiday = self.Requests.with_user(self.user_employee).create({
'name': 'Leave 1',
def test_delete_timesheet_after_new_holiday_covers_whole_timeoff(self):
""" User should be able to delete a timesheet created after a new public holiday is added,
covering the *whole* period of a existing time off.
Test Case:
=========
1) Create a Time off, approve and validate it.
2) Create a new Public Holiday, covering the whole time off created in step 1.
3) Delete the new timesheet associated with the public holiday.
"""
leave_start_datetime = datetime(2022, 1, 31, 7, 0, 0, 0) # Monday
leave_end_datetime = datetime(2022, 1, 31, 18, 0, 0, 0)
# (1) Create a timeoff and validate it
time_off = self.Requests.with_user(self.user_employee).create({
'name': 'Test Time off please',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'date_from': self.leave_start_datetime,
'date_to': self.leave_end_datetime,
'number_of_days': number_of_days,
'request_date_from': leave_start_datetime,
'request_date_to': leave_end_datetime,
})
holiday.with_user(SUPERUSER_ID).action_validate()
time_off.with_user(SUPERUSER_ID).action_approve()
wizard = self.env['hr.departure.wizard'].create({
'employee_id': self.empl_employee.id,
'departure_date': datetime(2018, 2, 1, 12),
'archive_allocation': False,
# (2) Create a public holiday
self.env['resource.calendar.leaves'].create({
'name': 'New Public Holiday',
'calendar_id': self.employee_working_calendar.id,
'date_from': datetime(2022, 1, 31, 5, 0, 0, 0), # Covers the whole time off
'date_to': datetime(2022, 1, 31, 23, 0, 0, 0),
})
wizard.action_register_departure()
self.assertEqual(len(holiday.timesheet_ids), 0, 'Timesheets related to the archived employee should have been deleted')
# The timeoff should have been force_cancelled and its associated timesheet unlinked.
self.assertFalse(time_off.timesheet_ids, '0 timesheet should remain for this time off.')
# (3) Delete the timesheet
timesheets = self.env['account.analytic.line'].search([
('date', '>=', leave_start_datetime),
('date', '<=', leave_end_datetime),
('employee_id', '=', self.empl_employee.id),
])
# timesheet should be unlinked to the timeoff, and be able to delete it
timesheets.with_user(SUPERUSER_ID).unlink()
self.assertFalse(timesheets.exists(), 'Timesheet should be deleted')
def test_timeoff_task_creation_with_holiday_leave(self):
""" Test the search method on is_timeoff_task"""
company = self.env['res.company'].create({"name": "new company"})
self.empl_employee.write({
"company_id": company.id,
})
task_count = self.env['project.task'].search_count([('is_timeoff_task', '!=', False)])
timesheet_count = self.env['account.analytic.line'].search_count([('holiday_id', '!=', False)])
leave = self.Requests.with_user(SUPERUSER_ID).create({
'name': 'Test Leave',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'request_date_from': datetime(2024, 6, 24),
'request_date_to': datetime(2024, 6, 24),
})
leave.with_user(SUPERUSER_ID).action_approve()
new_task_count = self.env['project.task'].search_count([('is_timeoff_task', '!=', False)])
self.assertEqual(task_count + 1, new_task_count)
new_timesheet_count = self.env['account.analytic.line'].search_count([('holiday_id', '!=', False)])
self.assertEqual(timesheet_count + 1, new_timesheet_count)
def test_timesheet_timeoff_flexible_employee(self):
flex_40h_calendar = self.env['resource.calendar'].create({
'name': 'Flexible 40h/week',
'hours_per_day': 8.0,
'hours_per_week': 40.0,
'full_time_required_hours': 40.0,
'flexible_hours': True,
})
self.empl_employee.resource_calendar_id = flex_40h_calendar
time_off = self.Requests.with_user(self.user_employee).create({
'name': 'Test Time off please',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'request_date_from': self.leave_start_datetime,
'request_date_to': self.leave_end_datetime,
})
time_off.with_user(SUPERUSER_ID)._action_validate()
timesheet = self.env['account.analytic.line'].search([
('date', '>=', self.leave_start_datetime),
('date', '<=', self.leave_end_datetime),
('employee_id', '=', self.empl_employee.id),
])
self.assertEqual(len(timesheet), 3, "Three timesheets should be created for each leave day")
self.assertEqual(sum(timesheet.mapped('unit_amount')), 24, "The duration of the timesheet for flexible employee leave "
"should be number of days * hours per day")
def test_multi_create_timesheets_from_calendar(self):
"""
Simulate creating timesheets using the multi-create feature in the calendar view
"""
self.env['resource.calendar.leaves'].create({
'name': 'Public holiday',
'date_from': datetime(2025, 5, 27, 0, 0),
'date_to': datetime(2025, 5, 28, 23, 59),
})
leave_type = self.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'requires_allocation': False,
})
self.env['hr.leave'].sudo().create([
{
'holiday_status_id': leave_type.id,
'employee_id': self.empl_employee.id,
'request_date_from': datetime(2025, 5, 26, 8, 0),
'request_date_to': datetime(2025, 5, 26, 17, 0),
}, {
'holiday_status_id': leave_type.id,
'employee_id': self.empl_employee2.id,
'request_date_from': datetime(2025, 5, 29, 8, 0),
'request_date_to': datetime(2025, 5, 29, 17, 0),
},
])._action_validate()
# At this point:
# - empl_employee is on time off the 26th
# - both empl_employee and empl_employee2 are on public time off the 27th and 28th
# - empl_employee2 is on time off the 29th
timesheets = self.env['account.analytic.line'].with_context(timesheet_calendar=True).create([{
'project_id': self.project_customer.id,
'unit_amount': 1,
'date': f'2025-05-{day}',
'employee_id': employee.id,
} for day in ('26', '27', '28', '29') for employee in (self.empl_employee, self.empl_employee2)])
self.assertEqual(len(timesheets), 2, "Two leaves should have been created: one for each employee")
self.assertEqual(timesheets[0].employee_id, self.empl_employee2)
self.assertEqual(fields.Date.to_string(timesheets[0].date), '2025-05-26')
self.assertEqual(timesheets[1].employee_id, self.empl_employee)
self.assertEqual(fields.Date.to_string(timesheets[1].date), '2025-05-29')
def test_one_day_timesheet_timeoff_flexible_employee(self):
flex_40h_calendar = self.env['resource.calendar'].create({
'name': 'Flexible 10h/week',
'hours_per_day': 10,
'hours_per_week': 10,
'full_time_required_hours': 10,
'flexible_hours': True,
})
self.empl_employee.resource_calendar_id = flex_40h_calendar
time_off = self.Requests.with_user(self.user_employee).create({
'name': 'Test 1 day Time off',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'request_date_from': datetime(2025, 7, 12), # Random saturday
'request_date_to': datetime(2025, 7, 12),
})
time_off.with_user(SUPERUSER_ID)._action_validate()
timesheet = self.env['account.analytic.line'].search([
('date', '>=', datetime(2025, 7, 12)),
('date', '<=', datetime(2025, 7, 12)),
('employee_id', '=', self.empl_employee.id),
])
self.assertEqual(len(timesheet), 1, "One timesheet should be created")
self.assertEqual(sum(timesheet.mapped('unit_amount')), 10, "The duration of the timesheet for flexible employee leave "
"should be 10 hours")
def test_timeoff_validation_fully_flexible_employee(self):
self.empl_employee.resource_calendar_id = False
time_off = self.Requests.with_user(self.user_employee).create({
'name': 'Test Fully Flexible Employee Validation',
'employee_id': self.empl_employee.id,
'holiday_status_id': self.hr_leave_type_with_ts.id,
'request_date_from': datetime(2025, 8, 12),
'request_date_to': datetime(2025, 8, 12)
})
time_off.with_user(SUPERUSER_ID)._action_validate()
self.assertEqual(time_off.state, 'validate', "The time off for a fully flexible employee should be validated")