diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_holidays/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr_holidays/tests')
| -rw-r--r-- | addons/hr_holidays/tests/__init__.py | 13 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/common.py | 56 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_access_rights.py | 801 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_accrual_allocations.py | 251 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_allocation_access_rights.py | 243 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_automatic_leave_dates.py | 293 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_change_department.py | 67 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_company_leave.py | 323 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_holidays_flow.py | 288 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_hr_leave_type.py | 43 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_leave_requests.py | 440 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_out_of_office.py | 100 |
12 files changed, 2918 insertions, 0 deletions
diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py new file mode 100644 index 00000000..1fb97c40 --- /dev/null +++ b/addons/hr_holidays/tests/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_access_rights +from . import test_automatic_leave_dates +from . import test_allocation_access_rights +from . import test_holidays_flow +from . import test_hr_leave_type +from . import test_accrual_allocations +from . import test_change_department +from . import test_leave_requests +from . import test_out_of_office +from . import test_company_leave diff --git a/addons/hr_holidays/tests/common.py b/addons/hr_holidays/tests/common.py new file mode 100644 index 00000000..83e0df4d --- /dev/null +++ b/addons/hr_holidays/tests/common.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.tests import common + + +class TestHrHolidaysCommon(common.TransactionCase): + + def setUp(self): + super(TestHrHolidaysCommon, self).setUp() + self.env.user.tz = 'Europe/Brussels' + + # Test users to use through the various tests + self.user_hruser = mail_new_test_user(self.env, login='armande', groups='base.group_user,hr_holidays.group_hr_holidays_user') + self.user_hruser_id = self.user_hruser.id + + self.user_hrmanager = mail_new_test_user(self.env, login='bastien', groups='base.group_user,hr_holidays.group_hr_holidays_manager') + self.user_hrmanager_id = self.user_hrmanager.id + + self.user_employee = mail_new_test_user(self.env, login='david', groups='base.group_user') + self.user_employee_id = self.user_employee.id + + # Hr Data + Department = self.env['hr.department'].with_context(tracking_disable=True) + + self.hr_dept = Department.create({ + 'name': 'Human Resources', + }) + self.rd_dept = Department.create({ + 'name': 'Research and devlopment', + }) + + self.employee_emp = self.env['hr.employee'].create({ + 'name': 'David Employee', + 'user_id': self.user_employee_id, + 'department_id': self.rd_dept.id, + }) + self.employee_emp_id = self.employee_emp.id + + self.employee_hruser = self.env['hr.employee'].create({ + 'name': 'Armande HrUser', + 'user_id': self.user_hruser_id, + 'department_id': self.rd_dept.id, + }) + self.employee_hruser_id = self.employee_hruser.id + + self.employee_hrmanager = self.env['hr.employee'].create({ + 'name': 'Bastien HrManager', + 'user_id': self.user_hrmanager_id, + 'department_id': self.hr_dept.id, + 'parent_id': self.employee_hruser_id, + }) + self.employee_hrmanager_id = self.employee_hrmanager.id + + self.rd_dept.write({'manager_id': self.employee_hruser_id}) diff --git a/addons/hr_holidays/tests/test_access_rights.py b/addons/hr_holidays/tests/test_access_rights.py new file mode 100644 index 00000000..45aafba8 --- /dev/null +++ b/addons/hr_holidays/tests/test_access_rights.py @@ -0,0 +1,801 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo import tests +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon +from odoo.exceptions import AccessError, UserError, ValidationError +from odoo.tools import mute_logger + + +@tests.tagged('access_rights', 'post_install', '-at_install') +class TestHrHolidaysAccessRightsCommon(TestHrHolidaysCommon): + def setUp(self): + super(TestHrHolidaysAccessRightsCommon, self).setUp() + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Unlimited', + 'leave_validation_type': 'hr', + 'allocation_type': 'no', + 'validity_start': False, + }) + self.rd_dept.manager_id = False + self.hr_dept.manager_id = False + self.employee_emp.parent_id = False + self.employee_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Test', + 'holiday_status_id': self.leave_type.id, + 'department_id': self.employee_emp.department_id.id, + 'employee_id': self.employee_emp.id, + 'date_from': datetime.now(), + 'date_to': datetime.now() + relativedelta(days=1), + 'number_of_days': 1, + }) + + self.lt_no_validation = self.env['hr.leave.type'].create({ + 'name': 'Validation = no_validation', + 'leave_validation_type': 'hr', + 'allocation_type': 'no', + 'validity_start': False, + }) + + self.lt_validation_hr = self.env['hr.leave.type'].create({ + 'name': 'Validation = HR', + 'leave_validation_type': 'hr', + 'allocation_type': 'no', + 'validity_start': False, + }) + + self.lt_validation_manager = self.env['hr.leave.type'].create({ + 'name': 'Validation = manager', + 'leave_validation_type': 'hr', + 'allocation_type': 'no', + 'validity_start': False, + }) + + self.lt_validation_both = self.env['hr.leave.type'].create({ + 'name': 'Validation = both', + 'leave_validation_type': 'hr', + 'allocation_type': 'no', + 'validity_start': False, + }) + + self.draft_status = [ + self.lt_validation_hr, + self.lt_validation_manager, + self.lt_validation_both + ] + + self.confirm_status = [ + self.lt_no_validation, + self.lt_validation_hr, + self.lt_validation_manager, + self.lt_validation_both + ] + + def request_leave(self, user_id, date_from, number_of_days, values=None): + values = dict(values or {}, **{ + 'date_from': date_from, + 'date_to': date_from + relativedelta(days=number_of_days), + 'number_of_days': number_of_days, + }) + return self.env['hr.leave'].with_user(user_id).create(values) + + +@tests.tagged('access_rights', 'access_rights_states') +class TestAcessRightsStates(TestHrHolidaysAccessRightsCommon): + # ****************************************************** + # Action draft + # ****************************************************** + + def test_draft_status(self): + """ + We should only be able to draft a leave that is + in confirm or refuse state + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Ranoi', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.action_draft() + + values = { + 'name': 'Ranoi', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=20 + i), 1, values) + # the state has to be set to draft in a write because it is initialized to confirm if it has validation + leave.write({'state': 'draft'}) + with self.assertRaises(UserError): + leave.action_draft() + + def test_base_user_draft_his_leave(self): + """ + Should be able to draft his own leave + whatever the holiday_status_id + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_employee.id).action_draft() + + def test_base_user_draft_other_employee_leave(self): + """ + Should not be able to draft the leave of someone else + whatever the holiday_status_id + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + with self.assertRaises(UserError): + leave.with_user(self.user_employee.id).action_draft() + + def test_base_user_draft_other_employee_leave_and_is_leave_manager_id(self): + """ + Should not be able to draft the leave of someone else + even when being the leave manager id for this person + whatever the holiday_status_id + """ + self.employee_hruser.write({'leave_manager_id': self.user_employee.id}) + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + with self.assertRaises(UserError): + leave.with_user(self.user_employee.id).action_draft() + + def test_base_user_draft_self_and_is_leave_manager_id(self): + """ + Should be able to draft his own leave + even when being leave manager id + whatever the holiday_status_id + """ + self.employee_emp.write({'leave_manager_id': self.user_employee.id}) + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_employee.id).action_draft() + + def test_base_user_draft_refused_leave(self): + """ + Should not be able to draft a refused leave + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.action_refuse() + with self.assertRaises(UserError): + leave.with_user(self.user_employee.id).action_draft() + + def test_base_user_draft_current_leave(self): + """ + Should not be able to draft a passed leave + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=-20 + i), 1, values) + with self.assertRaises(UserError): + leave.with_user(self.user_employee.id).action_draft() + + def test_holiday_user_draft_his_leave(self): + """ + Should be able to draft his own leave + whatever the holiday_status_id + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_hruser.id).action_draft() + + def test_holiday_user_draft_other_employee_leave(self): + """ + Should not be able to draft other employee leave + whatever the holiday_status_id + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + with self.assertRaises(UserError): + leave.with_user(self.user_hruser.id).action_draft() + + def test_holiday_user_draft_other_employee_leave_and_is_leave_manager_id(self): + """ + Should not be able to draft other employee leave + even if he is the leave manager id + whatever the holiday_status_id + """ + self.employee_emp.write({'leave_manager_id': self.user_hruser.id}) + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + with self.assertRaises(UserError): + leave.with_user(self.user_hruser.id).action_draft() + + def test_holiday_user_draft_self_and_is_manager_id(self): + """ + Should be able to draft his own leave + even if he is leave manager id + whatever the holiday_status_id + """ + self.employee_hruser.write({'leave_manager_id': self.user_hruser.id}) + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_hruser.id).action_draft() + + def test_holiday_user_draft_refused_leave(self): + """ + Should not be able to draft a refused leave + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.action_refuse() + with self.assertRaises(UserError): + leave.with_user(self.user_hruser.id).action_draft() + + def test_holiday_user_draft_current_leave(self): + """ + Should not be able to draft a passed leave + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=-20 + i), 1, values) + with self.assertRaises(UserError): + leave.with_user(self.user_hruser.id).action_draft() + + def test_holiday_manager_draft_his_leave(self): + """ + The holiday manager should be able to do everything + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hrmanager.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_hrmanager.id).action_draft() + + def test_holiday_manager_draft_other_employee_leave(self): + """ + The holiday manager should be able to do everything + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_hrmanager.id).action_draft() + + def test_holiday_manager_draft_other_employee_leave_and_is_leave_manager_id(self): + """ + The holiday manager should be able to do everything + """ + self.employee_hruser.write({'leave_manager_id': self.user_hrmanager.id}) + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_hrmanager.id).action_draft() + + def test_holiday_manager_draft_self_and_is_manager_id(self): + """ + The holiday manager should be able to do everything + """ + self.employee_hrmanager.write({'leave_manager_id': self.user_hrmanager.id}) + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hrmanager.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.with_user(self.user_hrmanager.id).action_draft() + + def test_holiday_manager_draft_refused_leave(self): + """ + The holiday manager should be able to do everything + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=5 + i), 1, values) + leave.action_refuse() + leave.with_user(self.user_hrmanager.id).action_draft() + + def test_holiday_manager_draft_current_leave(self): + """ + The holiday manager should be able to do everything + """ + for i, status in enumerate(self.draft_status): + values = { + 'name': 'Random Leave', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': status.id, + } + leave = self.request_leave(1, datetime.today() + relativedelta(days=-20 + i), 1, values) + leave.with_user(self.user_hrmanager.id).action_draft() + +@tests.tagged('access_rights', 'access_rights_create') +class TestAccessRightsCreate(TestHrHolidaysAccessRightsCommon): + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_base_user_create_self(self): + """ A simple user can create a leave for himself """ + values = { + 'name': 'Hol10', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.leave_type.id, + } + self.request_leave(self.user_employee_id, datetime.today() + relativedelta(days=5), 1, values) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_base_user_create_other(self): + """ A simple user cannot create a leave for someone else """ + values = { + 'name': 'Hol10', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.leave_type.id, + } + with self.assertRaises(AccessError): + self.request_leave(self.user_employee_id, datetime.today() + relativedelta(days=5), 1, values) + + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_base_user_create_batch(self): + """ A simple user cannot create a leave in bacth mode (by company, by department, by tag)""" + values = { + 'name': 'Hol10', + 'holiday_status_id': self.leave_type.id, + 'holiday_type': 'company', + 'mode_company_id': 1, + } + with self.assertRaises(AccessError): + self.request_leave(self.user_employee_id, datetime.today() + relativedelta(days=5), 1, values) + + # hr_holidays.group_hr_holidays_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_holidays_user_create_self(self): + """ A holidays user can create a leave for himself """ + values = { + 'name': 'Hol10', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.leave_type.id, + } + self.request_leave(self.user_hruser_id, datetime.today() + relativedelta(days=5), 1, values) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_holidays_user_create_other(self): + """ A holidays user can create a leave for someone else """ + values = { + 'name': 'Hol10', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.leave_type.id, + } + self.request_leave(self.user_hruser_id, datetime.today() + relativedelta(days=5), 1, values) + + # hr_holidays.group_hr_holidays_manager + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_holidays_manager_create_self(self): + """ A holidays manager can create a leave for himself """ + values = { + 'name': 'Hol10', + 'employee_id': self.employee_hrmanager_id, + 'holiday_status_id': self.leave_type.id, + } + self.request_leave(self.user_hrmanager_id, datetime.today() + relativedelta(days=5), 1, values) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_holidays_manager_create_other(self): + """ A holidays manager can create a leave for someone else """ + values = { + 'name': 'Hol10', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.leave_type.id, + } + self.request_leave(self.user_hrmanager_id, datetime.today() + relativedelta(days=5), 1, values) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_holidays_manager_create_batch(self): + """ A holidays manager can create a leave in bacth mode (by company, by department, by tag)""" + values = { + 'name': 'Hol10', + 'holiday_status_id': self.leave_type.id, + 'holiday_type': 'company', + 'mode_company_id': 1, + } + self.request_leave(self.user_hrmanager_id, datetime.today() + relativedelta(days=5), 1, values) + + +@tests.tagged('access_rights', 'access_rights_read') +class TestAccessRightsRead(TestHrHolidaysAccessRightsCommon): + # base.group_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_read_by_user_other(self): + """ Users should not be able to read other people requests """ + other_leave = self.env['hr.leave'].with_user(self.user_hruser).create({ + 'name': 'Test', + 'holiday_status_id': self.leave_type.id, + 'department_id': self.employee_hruser.department_id.id, + 'employee_id': self.employee_hruser.id, + 'date_from': datetime.now(), + 'date_to': datetime.now() + relativedelta(days=1), + 'number_of_days': 1, + }) + with self.assertRaises(AccessError), self.cr.savepoint(): + res = other_leave.with_user(self.user_employee_id).read(['number_of_days', 'state', 'name']) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_read_by_user_other_browse(self): + """ Users should not be able to browse other people requests """ + other_leave = self.env['hr.leave'].with_user(self.user_hruser).create({ + 'name': 'Test', + 'holiday_status_id': self.leave_type.id, + 'department_id': self.employee_hruser.department_id.id, + 'employee_id': self.employee_hruser.id, + 'date_from': datetime.now(), + 'date_to': datetime.now() + relativedelta(days=1), + 'number_of_days': 1, + }) + with self.assertRaises(AccessError), self.cr.savepoint(): + other_leave.invalidate_cache(['name']) + name = other_leave.with_user(self.user_employee_id).name + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_read_by_user_own(self): + """ Users should be able to read name field of own requests """ + res = self.employee_leave.read(['name', 'number_of_days', 'state']) + self.assertEqual(res[0]['name'], 'Test') + + +@tests.tagged('access_rights', 'access_rights_write') +class TestAccessRightsWrite(TestHrHolidaysAccessRightsCommon): + # base.group_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_update_by_user(self): + """ User may update its leave """ + self.employee_leave.with_user(self.user_employee_id).write({'name': 'Crocodile Dundee is my man'}) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_update_by_user_other(self): + """ User cannot update other people leaves """ + other_leave = self.env['hr.leave'].with_user(self.user_hruser).create({ + 'name': 'Test', + 'holiday_status_id': self.leave_type.id, + 'department_id': self.employee_hruser.department_id.id, + 'employee_id': self.employee_hruser.id, + 'date_from': datetime.now(), + 'date_to': datetime.now() + relativedelta(days=1), + 'number_of_days': 1, + }) + with self.assertRaises(AccessError): + other_leave.with_user(self.user_employee_id).write({'name': 'Crocodile Dundee is my man'}) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_creation_for_other_user(self): + """ Employee cannot creates a leave request for another employee """ + HolidaysEmployeeGroup = self.env['hr.leave'].with_user(self.user_employee_id) + with self.assertRaises(AccessError): + HolidaysEmployeeGroup.create({ + 'name': 'Hol10', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.leave_type.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': datetime.today(), + 'number_of_days': 1, + }) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_messaging_by_user(self): + """ User may communicate on its own leaves, even if validated """ + self.employee_leave.with_user(self.user_employee_id).message_post( + body='I haz messaging', + subtype_xmlid='mail.mt_comment', + message_type='comment' + ) + + self.employee_leave.with_user(self.user_hrmanager_id).action_approve() + + self.employee_leave.with_user(self.user_employee_id).message_post( + body='I still haz messaging', + subtype_xmlid='mail.mt_comment', + message_type='comment' + ) + + # ---------------------------------------- + # Validation: one validation, HR + # ---------------------------------------- + + # base.group_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_hr_to_validate_by_user(self): + """ User may not validate any leaves in HR mode """ + with self.assertRaises(UserError): + self.employee_leave.with_user(self.user_employee_id).action_approve() + + with self.assertRaises(UserError): + self.employee_leave.with_user(self.user_employee_id).write({'state': 'validate'}) + + # hr_holidays.group_hr_holidays_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_hr_to_validate_by_holiday_user(self): + """ Manager can validate leaves in HR mode """ + self.assertEqual(self.employee_leave.state, 'confirm') + self.employee_leave.with_user(self.user_hrmanager_id).action_approve() + self.assertEqual(self.employee_leave.state, 'validate') + + # hr_holidays.group_hr_holidays_manager + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_hr_to_validate_by_manager(self): + """ Manager validate its own leaves """ + manager_leave = self.env['hr.leave'].with_user(self.user_hrmanager_id).create({ + 'name': 'Hol manager', + 'holiday_status_id': self.leave_type.id, + 'employee_id': self.employee_hrmanager_id, + 'date_from': (datetime.today() + relativedelta(days=15)), + 'date_to': (datetime.today() + relativedelta(days=16)), + 'number_of_days': 1, + }) + self.assertEqual(manager_leave.state, 'confirm') + manager_leave.action_approve() + self.assertEqual(manager_leave.state, 'validate') + + # ---------------------------------------- + # Validation: one validation, manager + # ---------------------------------------- + + # base.group_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_manager_to_validate_by_user(self): + """ A simple user can validate in manager mode if he is leave_manager_id """ + self.leave_type.write({'leave_validation_type': 'manager'}) + values = { + 'name': 'Hol HrUser', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.leave_type.id, + 'state': 'confirm', + } + hr_leave = self.request_leave(self.user_hruser_id, datetime.now() + relativedelta(days=2), 1, values) + with self.assertRaises(AccessError): + hr_leave.with_user(self.user_employee_id).action_approve() + self.employee_hruser.write({'leave_manager_id': self.user_employee_id}) + hr_leave.with_user(self.user_employee_id).action_approve() + + # hr_holidays.group_hr_holidays_user + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_manager_to_validate_by_holiday_user(self): + """ A holiday user can validate in manager mode """ + self.leave_type.write({'leave_validation_type': 'manager'}) + values = { + 'name': 'Hol HrUser', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.leave_type.id, + 'state': 'confirm', + } + hr_leave = self.request_leave(self.user_hruser_id, datetime.now() + relativedelta(days=2), 1, values) + hr_leave.with_user(self.user_hruser_id).action_approve() + + # ---------------------------------------- + # Validation: double + # ---------------------------------------- + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_double_validate(self): + self.leave_type.write({'leave_validation_type': 'both'}) + values = { + 'name': 'double HrManager', + 'employee_id': self.employee_hrmanager_id, + 'holiday_status_id': self.leave_type.id, + 'state': 'confirm', + } + self.employee_hrmanager.leave_manager_id = self.env['res.users'].browse(1) + hr_leave = self.request_leave(self.user_hruser_id, datetime.now() + relativedelta(days=6), 1, values) + + with self.assertRaises(AccessError): + hr_leave.with_user(self.user_employee_id).action_approve() + + self.employee_hrmanager.leave_manager_id = self.user_hruser + hr_leave.with_user(self.user_hruser_id).action_approve() + + with self.assertRaises(AccessError): + hr_leave.with_user(self.user_employee_id).action_validate() + hr_leave.with_user(self.user_hruser_id).action_validate() + + # hr_holidays.group_hr_holidays_manager + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_double_validate_holiday_manager(self): + self.leave_type.write({'leave_validation_type': 'both'}) + values = { + 'name': 'double HrManager', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.leave_type.id, + 'state': 'confirm', + } + hr_leave = self.request_leave(self.user_hrmanager_id, datetime.now() + relativedelta(days=4), 1, values).with_user(self.user_hrmanager_id) + hr_leave.action_approve() + hr_leave.action_validate() + + # ---------------------------------------- + # State = Refuse + # ---------------------------------------- + + # base.group_user + + # hr_holidays.group_hr_holidays_user + + # TODO Can refuse + + # hr_holidays.group_hr_holidays_manager + + # TODO Can refuse + + # ---------------------------------------- + # State = Cancel + # ---------------------------------------- + + # base.group_user + + # TODO Can Cancel if start_date in the future + + # hr_holidays.group_hr_holidays_user + + # TODO Can Cancel if not in validate + + # hr_holidays.group_hr_holidays_manager + + # TODO Can always cancel with great powers comes great responbilities + + +class TestMultiCompany(TestHrHolidaysCommon): + + def setUp(self): + super(TestMultiCompany, self).setUp() + self.new_company = self.env['res.company'].create({ + 'name': 'Crocodile Dundee Company', + }) + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Unlimited - Company New', + 'company_id': self.new_company.id, + 'leave_validation_type': 'hr', + 'allocation_type': 'no', + }) + self.rd_dept.manager_id = False + self.hr_dept.manager_id = False + + self.employee_leave = self.env['hr.leave'].create({ + 'name': 'Test', + 'holiday_status_id': self.leave_type.id, + 'department_id': self.employee_emp.department_id.id, + 'employee_id': self.employee_emp.id, + 'date_from': datetime.now(), + 'date_to': datetime.now() + relativedelta(days=1), + 'number_of_days': 1, + }) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_access_other_company_user(self): + employee_leave = self.employee_leave.with_user(self.user_employee) + employee_leave.invalidate_cache(['name']) + with self.assertRaises(AccessError): + employee_leave.name + + with self.assertRaises(AccessError): + employee_leave.action_approve() + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_access_other_company_officer(self): + employee_leave_hruser = self.employee_leave.with_user(self.user_hruser) + employee_leave_hruser.invalidate_cache(['name']) + with self.assertRaises(AccessError): + employee_leave_hruser.name + + with self.assertRaises(AccessError): + employee_leave_hruser.action_approve() + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_access_other_company_manager(self): + employee_leave_hrmanager = self.employee_leave.with_user(self.user_hrmanager) + employee_leave_hrmanager.invalidate_cache(['name']) + with self.assertRaises(AccessError): + employee_leave_hrmanager.name + + with self.assertRaises(AccessError): + employee_leave_hrmanager.action_approve() + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_access_no_company_user(self): + self.leave_type.write({'company_id': False}) + employee_leave = self.employee_leave.with_user(self.user_employee) + + employee_leave.name + with self.assertRaises(UserError): + employee_leave.action_approve() + self.assertEqual(employee_leave.state, 'confirm') + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_access_no_company_officer(self): + self.leave_type.write({'company_id': False}) + employee_leave_hruser = self.employee_leave.with_user(self.user_hruser) + + employee_leave_hruser.name + employee_leave_hruser.action_approve() + self.assertEqual(employee_leave_hruser.state, 'validate') + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_access_no_company_manager(self): + self.leave_type.write({'company_id': False}) + employee_leave_hrmanager = self.employee_leave.with_user(self.user_hrmanager) + + employee_leave_hrmanager.name + employee_leave_hrmanager.action_approve() + self.assertEqual(employee_leave_hrmanager.state, 'validate') diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py new file mode 100644 index 00000000..b46da059 --- /dev/null +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime, time +from dateutil.relativedelta import relativedelta + +from odoo import fields + +from odoo.tools import mute_logger + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon + + +class TestAccrualAllocations(TestHrHolidaysCommon): + def setUp(self): + super(TestAccrualAllocations, self).setUp() + + # Make sure we have the rights to create, validate and delete the leaves, leave types and allocations + LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) + + self.accrual_type = LeaveType.create({ + 'name': 'accrual', + 'allocation_type': 'fixed', + 'validity_start': False, + }) + + self.unpaid_type = LeaveType.create({ + 'name': 'unpaid', + 'allocation_type': 'no', + 'unpaid': True, + 'validity_start': False, + }) + + self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00') + self.set_employee_create_date(self.employee_hruser_id, '2010-02-03 00:00:00') + + def set_employee_create_date(self, id, newdate): + """ This method is a hack in order to be able to define/redefine the create_date + of the employees. + This is done in SQL because ORM does not allow to write onto the create_date field. + """ + self.env.cr.execute(""" + UPDATE + hr_employee + SET create_date = '%s' + WHERE id = %s + """ % (newdate, id)) + + def test_accrual_base_no_leaves(self): + """ Test if we can allocate some leaves accrually to an employee """ + alloc = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 1, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'weeks', + }) + alloc.action_approve() + alloc._update_accrual() + + self.assertEqual(alloc.number_of_days, 1, 'Employee should have been allocated one leave day') + + def test_accrual_base_leaves(self): + """ Test if the accrual allocation take the unpaid leaves into account when allocating leaves """ + alloc = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual allocation for employee with leaves', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 1, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'weeks', + }) + + alloc.action_approve() + + employee = self.env['hr.employee'].browse(self.employee_hruser_id) + # Getting the previous work date + df = employee.resource_calendar_id.plan_days(-2, fields.Datetime.now()).date() + + leave = self.env['hr.leave'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Leave for hruser', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.unpaid_type.id, + 'date_from': datetime.combine(df, time(0, 0, 0)), + 'date_to': datetime.combine(df + relativedelta(days=1), time(0, 0, 0)), + 'number_of_days': 1, + }) + + leave.action_approve() + + alloc._update_accrual() + + self.assertEqual(alloc.number_of_days, .8, 'As employee took some unpaid leaves last week, he should be allocated only .8 days') + + def test_accrual_many(self): + """ + Test different configuration of accrual allocations + """ + Allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) + + alloc_0 = Allocation.create({ + 'name': '1 day per 2 weeks', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 1, + 'interval_number': 2, + 'unit_per_interval': 'days', + 'interval_unit': 'weeks', + }) + + alloc_1 = Allocation.create({ + 'name': '4 hours per week', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 4, + 'interval_number': 1, + 'unit_per_interval': 'hours', + 'interval_unit': 'weeks', + }) + + alloc_2 = Allocation.create({ + 'name': '2 day per 1 month', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 2, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'months', + }) + + alloc_3 = Allocation.create({ + 'name': '20 days per year', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 20, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'years', + }) + + (alloc_0 | alloc_1 | alloc_2 | alloc_3).action_approve() + + Allocation._update_accrual() + + self.assertEqual(alloc_0.number_of_days, 1) + self.assertEqual(alloc_1.number_of_days, .5) + self.assertEqual(alloc_2.number_of_days, 2) + self.assertEqual(alloc_3.number_of_days, 20) + + def test_accrual_new_employee(self): + """ + Test if accrual allocation takes into account the creation date + of an employee + """ + Allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) + + self.set_employee_create_date(self.employee_emp_id, fields.Datetime.to_string(fields.Datetime.now())) + + alloc_0 = Allocation.create({ + 'name': 'one shot one kill', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 1, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'weeks', + }) + + alloc_0.action_approve() + + Allocation._update_accrual() + + self.assertEqual(alloc_0.number_of_days, 0, 'Employee is new he should not get any accrual leaves') + + def test_accrual_multi(self): + """ Test if the cron does not allocate leaves every time it's called but only when necessary """ + alloc = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': '2 days per week', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'number_of_days': 0, + 'number_per_interval': 1, + 'interval_number': 2, + 'unit_per_interval': 'days', + 'interval_unit': 'weeks', + }) + alloc.action_approve() + alloc._update_accrual() + alloc._update_accrual() + + self.assertEqual(alloc.number_of_days, 1, 'Cron only allocates 1 days every two weeks') + + def test_accrual_validation(self): + """ + Test if cron does not allocate past it's validity date + """ + Allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) + + alloc_0 = Allocation.create({ + 'name': '20 days per year', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'number_of_days': 0, + 'number_per_interval': 20, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'years', + 'date_to': fields.Datetime.from_string('2015-02-03 00:00:00'), + }) + + alloc_0.action_approve() + + Allocation._update_accrual() + + self.assertEqual(alloc_0.number_of_days, 0, 'Cron validity passed, should not allocate any leave') + + def test_accrual_balance_limit(self): + """ Test if accrual allocation does not allocate more than the balance limit""" + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'accrual 5 max', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.accrual_type.id, + 'allocation_type': 'accrual', + 'accrual_limit': 5, + 'number_of_days': 0, + 'number_per_interval': 6, + 'interval_number': 1, + 'unit_per_interval': 'days', + 'interval_unit': 'weeks', + }) + allocation.action_approve() + allocation._update_accrual() + + self.assertEqual(allocation.number_of_days, 5, 'Should have allocated only 5 days as balance limit is 5') diff --git a/addons/hr_holidays/tests/test_allocation_access_rights.py b/addons/hr_holidays/tests/test_allocation_access_rights.py new file mode 100644 index 00000000..2d4c973e --- /dev/null +++ b/addons/hr_holidays/tests/test_allocation_access_rights.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tests +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon +from odoo.exceptions import AccessError, UserError + + +@tests.tagged('access_rights', 'post_install', '-at_install') +class TestAllocationRights(TestHrHolidaysCommon): + + def setUp(self): + super().setUp() + self.rd_dept.manager_id = False + self.hr_dept.manager_id = False + self.employee_emp.parent_id = False + self.employee_emp.leave_manager_id = False + + self.lt_validation_hr = self.env['hr.leave.type'].create({ + 'name': 'Validation = HR', + 'allocation_validation_type': 'hr', + 'allocation_type': 'fixed_allocation', + 'validity_start': False, + }) + + self.lt_validation_manager = self.env['hr.leave.type'].create({ + 'name': 'Validation = manager', + 'allocation_validation_type': 'manager', + 'allocation_type': 'fixed_allocation', + 'validity_start': False, + }) + + self.lt_validation_both = self.env['hr.leave.type'].create({ + 'name': 'Validation = both', + 'allocation_validation_type': 'both', + 'allocation_type': 'fixed_allocation', + 'validity_start': False, + }) + + def request_allocation(self, user, values={}): + values = dict(values, **{ + 'name': 'Allocation', + 'number_of_days': 1, + }) + return self.env['hr.leave.allocation'].with_user(user).create(values) + + +class TestAccessRightsSimpleUser(TestAllocationRights): + + def test_simple_user_request_allocation(self): + """ A simple user can request an allocation but not approve it """ + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_employee.id, values) + with self.assertRaises(UserError): + allocation.action_approve() + + def test_simple_user_request_fixed_allocation(self): + """ A simple user cannot request an allocation if set by HR """ + self.lt_validation_hr.allocation_type = 'fixed' + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + with self.assertRaises(AccessError): + self.request_allocation(self.user_employee.id, values) + + def test_simple_user_reset_to_draft(self): + """ A simple user can reset to draft only his own allocation """ + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_employee.id, values) + self.assertEqual(allocation.state, 'confirm') + allocation.action_draft() + self.assertEqual(allocation.state, 'draft', "It should be reset to draft state") + + +class TestAccessRightsEmployeeManager(TestAllocationRights): + + def setUp(self): + super().setUp() + self.managed_employee = self.env['hr.employee'].create({ + 'name': 'Jolly Jumper', + 'leave_manager_id': self.user_employee.id, + }) + + def test_manager_request_allocation_other(self): + """ A manager cannot request and approve an allocation for employees he doesn't manage """ + values = { + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': self.lt_validation_manager.id, + } + with self.assertRaises(AccessError): + self.request_allocation(self.user_employee.id, values) # user is not the employee's manager + + def test_manager_approve_request_allocation(self): + """ A manager can request and approve an allocation for managed employees """ + values = { + 'employee_id': self.managed_employee.id, + 'holiday_status_id': self.lt_validation_manager.id, + } + allocation = self.request_allocation(self.user_employee.id, values) + allocation.action_approve() + self.assertEqual(allocation.state, 'validate', "The allocation should be validated") + + def test_manager_refuse_request_allocation(self): + """ A manager can request and refuse an allocation for managed employees """ + values = { + 'employee_id': self.managed_employee.id, + 'holiday_status_id': self.lt_validation_manager.id, + } + allocation = self.request_allocation(self.user_employee.id, values) + allocation.action_refuse() + self.assertEqual(allocation.state, 'refuse', "The allocation should be validated") + + def test_manager_batch_allocation(self): + """ A manager cannot create batch allocation """ + values = { + 'holiday_status_id': self.lt_validation_manager.id, + 'holiday_type': 'company', + 'mode_company_id': self.user_employee.company_id.id, + } + with self.assertRaises(AccessError): + self.request_allocation(self.user_employee.id, values) + + def test_manager_approve_own(self): + """ A manager cannot approve his own allocation """ + values = { + 'employee_id': self.user_employee.employee_id.id, + 'holiday_status_id': self.lt_validation_manager.id, + } + allocation = self.request_allocation(self.user_employee.id, values) + with self.assertRaises(UserError): + allocation.action_approve() + + def test_manager_only_first_approval(self): + """ A manager can only do the first approval """ + values = { + 'employee_id': self.managed_employee.id, + 'holiday_status_id': self.lt_validation_both.id, + } + allocation = self.request_allocation(self.user_employee.id, values) + allocation.action_approve() + with self.assertRaises(UserError): + allocation.action_validate() + + +class TestAccessRightsHolidayUser(TestAllocationRights): + + def test_holiday_user_request_allocation(self): + """ A holiday user can request and approve an allocation for any employee """ + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_hruser.id, values) + allocation.action_approve() + self.assertEqual(allocation.state, 'validate', "It should have been validated") + + def test_holiday_user_request_fixed_allocation(self): + """ A holiday user can request and approve an allocation if set by HR """ + self.lt_validation_hr.allocation_type = 'fixed' + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_hruser.id, values) + allocation.action_approve() + self.assertEqual(allocation.state, 'validate', "It should have been validated") + + def test_holiday_user_both_second_approval(self): + """ A holiday user can only do the second approval when double validation """ + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_both.id, + } + allocation = self.request_allocation(self.user_hruser.id, values) + with self.assertRaises(UserError): + allocation.action_approve() + allocation.sudo().action_approve() # First approval by someone else + allocation.action_validate() + self.assertEqual(allocation.state, 'validate', "It should have been validated") + + def test_holiday_user_batch_allocation(self): + """ A holiday user cannot create a batch allocation """ + values = { + 'holiday_status_id': self.lt_validation_hr.id, + 'holiday_type': 'company', + 'mode_company_id': self.user_employee.company_id.id, + } + with self.assertRaises(AccessError): + self.request_allocation(self.user_hruser.id, values) + + def test_holiday_user_cannot_approve_own(self): + """ A holiday user cannot approve his own allocation """ + values = { + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_hruser.id, values) + with self.assertRaises(UserError): + allocation.action_approve() + + +class TestAccessRightsHolidayManager(TestAllocationRights): + + def test_holiday_manager_can_approve_own(self): + """ A holiday manager can approve his own allocation """ + values = { + 'employee_id': self.employee_hrmanager.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_hrmanager.id, values) + allocation.action_approve() + self.assertEqual(allocation.state, 'validate', "It should have been validated") + + def test_holiday_manager_both_validation(self): + """ A holiday manager can perform both validation """ + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_both.id, + } + allocation = self.request_allocation(self.user_hrmanager.id, values) + allocation.action_approve() + self.assertEqual(allocation.state, 'validate1', "It should have been validated one time") + allocation.action_validate() + self.assertEqual(allocation.state, 'validate', "It should have been completely validated") + + def test_holiday_manager_refuse_validated(self): + """ A holiday manager can refuse a validated allocation """ + values = { + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.lt_validation_hr.id, + } + allocation = self.request_allocation(self.user_hrmanager.id, values) + allocation.action_approve() + self.assertEqual(allocation.state, 'validate', "It should have been validated") + allocation.action_refuse() + self.assertEqual(allocation.state, 'refuse', "It should have been refused") diff --git a/addons/hr_holidays/tests/test_automatic_leave_dates.py b/addons/hr_holidays/tests/test_automatic_leave_dates.py new file mode 100644 index 00000000..d3774e09 --- /dev/null +++ b/addons/hr_holidays/tests/test_automatic_leave_dates.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +from datetime import date, datetime + +from odoo.tests.common import Form + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon +from odoo.exceptions import ValidationError + + +class TestAutomaticLeaveDates(TestHrHolidaysCommon): + def setUp(self): + super(TestAutomaticLeaveDates, self).setUp() + + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Automatic Test', + 'time_type': 'leave', + 'allocation_type': 'no', + 'validity_start': False, + }) + + def test_no_attendances(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'No Attendances', + 'attendance_ids': [(5, 0, 0)], + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0 Hours') + + def test_single_attendance_on_morning_and_afternoon(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'simple morning + afternoon', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + }), + (0, 0, { + 'name': 'monday afternoon', + 'hour_from': 13, + 'hour_to': 17, + 'day_period': 'afternoon', + 'dayofweek': '0', + })] + }) + + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_text, '4 Hours') + + leave_form.request_date_from_period = 'pm' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_text, '4 Hours') + + def test_multiple_attendance_on_morning(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'multi morning', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning 1', + 'hour_from': 8, + 'hour_to': 10, + 'day_period': 'morning', + 'dayofweek': '0', + }), + (0, 0, { + 'name': 'monday morning 2', + 'hour_from': 10.25, + 'hour_to': 12.25, + 'day_period': 'morning', + 'dayofweek': '0', + }), + (0, 0, { + 'name': 'monday afternoon', + 'hour_from': 13, + 'hour_to': 17, + 'day_period': 'afternoon', + 'dayofweek': '0', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_text, '4 Hours') + + leave_form.request_date_from_period = 'pm' + + self.assertEqual(leave_form.number_of_days_display, .5) + self.assertEqual(leave_form.number_of_hours_text, '4 Hours') + + def test_attendance_on_morning(self): + calendar = self.env['resource.calendar'].create({ + 'name': 'Morning only', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'Monday All day', + 'hour_from': 8, + 'hour_to': 16, + 'day_period': 'morning', + 'dayofweek': '0', + })], + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + # Ask for morning + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 0.5) + self.assertEqual(leave_form.number_of_hours_text, '8 Hours') + + # Ask for afternoon + leave_form.request_date_from_period = 'pm' + + self.assertEqual(leave_form.number_of_days_display, 0.5) + self.assertEqual(leave_form.number_of_hours_text, '8 Hours') + + def test_attendance_next_day(self): + self.env.user.tz = 'Europe/Brussels' + calendar = self.env['resource.calendar'].create({ + 'name': 'auto next day', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'tuesday morning', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '1', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + # does not work on mondays + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) + + def test_attendance_previous_day(self): + self.env.user.tz = 'Europe/Brussels' + calendar = self.env['resource.calendar'].create({ + 'name': 'auto next day', + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + # does not work on tuesdays + leave_form.request_date_from = date(2019, 9, 3) + leave_form.request_date_to = date(2019, 9, 3) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0)) + + def test_2weeks_calendar(self): + self.env.user.tz = 'Europe/Brussels' + calendar = self.env['resource.calendar'].create({ + 'name': 'auto next day', + 'two_weeks_calendar': True, + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning odd week', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + 'week_type': '0', + }), + (0, 0, { + 'name': 'monday morning even week', + 'hour_from': 10, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + 'week_type': '1', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + # even week, works 2 hours + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 0.5) + self.assertEqual(leave_form.number_of_hours_text, '2 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 8, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + # odd week, works 4 hours + leave_form.request_date_from = date(2019, 9, 9) + leave_form.request_date_to = date(2019, 9, 9) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 0.5) + self.assertEqual(leave_form.number_of_hours_text, '4 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 9, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 9, 10, 0, 0)) + + def test_2weeks_calendar_next_week(self): + self.env.user.tz = 'Europe/Brussels' + calendar = self.env['resource.calendar'].create({ + 'name': 'auto next day', + 'two_weeks_calendar': True, + 'attendance_ids': [(5, 0, 0), + (0, 0, { + 'name': 'monday morning odd week', + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + 'dayofweek': '0', + 'week_type': '0', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + # even week, does not work + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' + + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) diff --git a/addons/hr_holidays/tests/test_change_department.py b/addons/hr_holidays/tests/test_change_department.py new file mode 100644 index 00000000..83ebb158 --- /dev/null +++ b/addons/hr_holidays/tests/test_change_department.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon + + +class TestChangeDepartment(TestHrHolidaysCommon): + def test_employee_change_department_request_change_department(self): + self.HolidaysEmployeeGroup = self.env['hr.leave'].with_user(self.user_employee_id) + + HolidayStatusManagerGroup = self.env['hr.leave.type'].with_user(self.user_hrmanager_id) + self.holidays_status_1 = HolidayStatusManagerGroup.create({ + 'name': 'NotLimitedHR', + 'allocation_type': 'no', + 'validity_start': False, + }) + + def create_holiday(name, start, end): + return self.HolidaysEmployeeGroup.create({ + 'name': name, + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_status_1.id, + 'date_from': (datetime.today() + relativedelta(days=start)).strftime('%Y-%m-%d %H:%M'), + 'date_to': datetime.today() + relativedelta(days=end), + 'number_of_days': end-start, + }) + + # Non approved leave request change department + self.employee_emp.department_id = self.rd_dept + hol1_employee_group = create_holiday("hol1", 1, 2) + self.employee_emp.department_id = self.hr_dept + self.assertEqual(hol1_employee_group.department_id, self.hr_dept, 'hr_holidays: non approved leave request should change department if employee change department') + + # Approved passed leave request change department + self.employee_emp.department_id = self.hr_dept + hol2_employee_group = create_holiday("hol2", -4, -3) + hol2_user_group = hol2_employee_group.with_user(self.user_hruser_id) + hol2_user_group.action_approve() + self.employee_emp.department_id = self.rd_dept + self.assertEqual(hol2_employee_group.department_id, self.hr_dept, 'hr_holidays: approved passed leave request should stay in previous department if employee change department') + + # Approved futur leave request change department + self.employee_emp.department_id = self.hr_dept + hol22_employee_group = create_holiday("hol22", 3, 4) + hol22_user_group = hol22_employee_group.with_user(self.user_hruser_id) + hol22_user_group.action_approve() + self.employee_emp.department_id = self.rd_dept + self.assertEqual(hol22_employee_group.department_id, self.rd_dept, 'hr_holidays: approved futur leave request should change department if employee change department') + + # Refused passed leave request change department + self.employee_emp.department_id = self.rd_dept + hol3_employee_group = create_holiday("hol3", -6, -5) + hol3_user_group = hol3_employee_group.with_user(self.user_hruser_id) + hol3_user_group.action_refuse() + self.employee_emp.department_id = self.hr_dept # Change department + self.assertEqual(hol3_employee_group.department_id, self.rd_dept, 'hr_holidays: refused passed leave request should stay in previous department if employee change department') + + # Refused futur leave request change department + self.employee_emp.department_id = self.rd_dept + hol32_employee_group = create_holiday("hol32", 5, 6) + hol32_user_group = hol32_employee_group.with_user(self.user_hruser_id) + hol32_user_group.action_refuse() + self.employee_emp.department_id = self.hr_dept # Change department + self.assertEqual(hol32_employee_group.department_id, self.hr_dept, 'hr_holidays: refused futur leave request should change department if employee change department') diff --git a/addons/hr_holidays/tests/test_company_leave.py b/addons/hr_holidays/tests/test_company_leave.py new file mode 100644 index 00000000..941a5595 --- /dev/null +++ b/addons/hr_holidays/tests/test_company_leave.py @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date, datetime + +from odoo.tests import tagged +from odoo.tests.common import SavepointCase + + +@tagged('company_leave') +class TestCompanyLeave(SavepointCase): + """ Test leaves for a whole company, conflict resolutions """ + + @classmethod + def setUpClass(cls): + super(TestCompanyLeave, cls).setUpClass() + cls.company = cls.env['res.company'].create({'name': 'A company'}) + + cls.bank_holiday = cls.env['hr.leave.type'].create({ + 'name': 'Bank Holiday', + 'responsible_id': cls.env.user.id, + 'company_id': cls.company.id, + }) + + cls.paid_time_off = cls.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'request_unit': 'day', + 'leave_validation_type': 'both', + 'company_id': cls.company.id, + }) + + cls.employee = cls.env['hr.employee'].create({ + 'name': 'My Employee', + 'company_id': cls.company.id, + }) + + def test_leave_whole_company_01(self): + # TEST CASE 1: Leaves taken in days. Take a 3 days leave + # Add a company leave on the second day. + # Check that leave is split into 2. + + leave = self.env['hr.leave'].create({ + 'name': 'Hol11', + 'employee_id': self.employee.id, + 'holiday_status_id': self.paid_time_off.id, + 'request_date_from': date(2020, 1, 7), + 'date_from': date(2020, 1, 7), + 'request_date_to': date(2020, 1, 9), + 'date_to': date(2020, 1, 9), + 'number_of_days': 3, + }) + leave._compute_date_from_to() + + company_leave = self.env['hr.leave'].create({ + 'name': 'Bank Holiday', + 'holiday_type': 'company', + 'mode_company_id': self.company.id, + 'holiday_status_id': self.bank_holiday.id, + 'date_from': date(2020, 1, 8), + 'request_date_from': date(2020, 1, 8), + 'date_to': date(2020, 1, 8), + 'request_date_to': date(2020, 1, 8), + 'number_of_days': 1, + }) + company_leave._compute_date_from_to() + + company_leave.action_validate() + + all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id') + self.assertEqual(len(all_leaves), 4) + # Original Leave + self.assertEqual(leave.state, 'refuse') + # before leave + self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 7, 7, 0)) + self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 7, 16, 0)) + self.assertEqual(all_leaves[1].number_of_days, 1) + self.assertEqual(all_leaves[1].state, 'confirm') + # After leave + self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 9, 7, 0)) + self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 9, 16, 0)) + self.assertEqual(all_leaves[2].number_of_days, 1) + self.assertEqual(all_leaves[2].state, 'confirm') + # Company Leave + self.assertEqual(all_leaves[3].date_from, datetime(2020, 1, 8, 7, 0)) + self.assertEqual(all_leaves[3].date_to, datetime(2020, 1, 8, 16, 0)) + self.assertEqual(all_leaves[3].number_of_days, 1) + self.assertEqual(all_leaves[3].state, 'validate') + + + def test_leave_whole_company_02(self): + # TEST CASE 2: Leaves taken in half-days. Take a 3 days leave + # Add a company leave on the second day + # Check that leave is split into 2 + self.paid_time_off.request_unit = 'half_day' + + leave = self.env['hr.leave'].create({ + 'name': 'Hol11', + 'employee_id': self.employee.id, + 'holiday_status_id': self.paid_time_off.id, + 'request_date_from': date(2020, 1, 7), + 'date_from': date(2020, 1, 7), + 'request_date_to': date(2020, 1, 9), + 'date_to': date(2020, 1, 9), + 'number_of_days': 3, + }) + leave._compute_date_from_to() + + company_leave = self.env['hr.leave'].create({ + 'name': 'Bank Holiday', + 'holiday_type': 'company', + 'mode_company_id': self.company.id, + 'holiday_status_id': self.bank_holiday.id, + 'date_from': date(2020, 1, 8), + 'request_date_from': date(2020, 1, 8), + 'date_to': date(2020, 1, 8), + 'request_date_to': date(2020, 1, 8), + 'number_of_days': 1, + }) + company_leave._compute_date_from_to() + + company_leave.action_validate() + + all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id') + self.assertEqual(len(all_leaves), 4) + # Original Leave + self.assertEqual(leave.state, 'refuse') + # before leave + self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 7, 7, 0)) + self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 7, 16, 0)) + self.assertEqual(all_leaves[1].number_of_days, 1) + self.assertEqual(all_leaves[1].state, 'confirm') + # After leave + self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 9, 7, 0)) + self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 9, 16, 0)) + self.assertEqual(all_leaves[2].number_of_days, 1) + self.assertEqual(all_leaves[2].state, 'confirm') + # Company Leave + self.assertEqual(all_leaves[3].date_from, datetime(2020, 1, 8, 7, 0)) + self.assertEqual(all_leaves[3].date_to, datetime(2020, 1, 8, 16, 0)) + self.assertEqual(all_leaves[3].number_of_days, 1) + self.assertEqual(all_leaves[3].state, 'validate') + + def test_leave_whole_company_03(self): + # TEST CASE 3: Leaves taken in half-days. Take a 0.5 days leave + # Add a company leave on the same day + # Check that leave refused + self.paid_time_off.request_unit = 'half_day' + + leave = self.env['hr.leave'].create({ + 'name': 'Hol11', + 'employee_id': self.employee.id, + 'holiday_status_id': self.paid_time_off.id, + 'request_date_from': date(2020, 1, 7), + 'request_date_to': date(2020, 1, 7), + 'number_of_days': 0.5, + 'request_unit_half': True, + 'request_date_from_period': 'am', + + }) + leave._compute_date_from_to() + + company_leave = self.env['hr.leave'].create({ + 'name': 'Bank Holiday', + 'holiday_type': 'company', + 'mode_company_id': self.company.id, + 'holiday_status_id': self.bank_holiday.id, + 'date_from': date(2020, 1, 7), + 'request_date_from': date(2020, 1, 7), + 'date_to': date(2020, 1, 7), + 'request_date_to': date(2020, 1, 7), + 'number_of_days': 1, + }) + company_leave._compute_date_from_to() + + company_leave.action_validate() + + all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id') + self.assertEqual(len(all_leaves), 2) + # Original Leave + self.assertEqual(leave.state, 'refuse') + # Company Leave + self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 7, 7, 0)) + self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 7, 16, 0)) + self.assertEqual(all_leaves[1].number_of_days, 1) + self.assertEqual(all_leaves[1].state, 'validate') + + def test_leave_whole_company_04(self): + # TEST CASE 4: Leaves taken in days. Take a 1 days leave + # Add a company leave on the same day + # Check that leave is refused + self.paid_time_off.request_unit = 'day' + + leave = self.env['hr.leave'].create({ + 'name': 'Hol11', + 'employee_id': self.employee.id, + 'holiday_status_id': self.paid_time_off.id, + 'request_date_from': date(2020, 1, 9), + 'request_date_to': date(2020, 1, 9), + 'number_of_days': 1, + + }) + leave._compute_date_from_to() + + company_leave = self.env['hr.leave'].create({ + 'name': 'Bank Holiday', + 'holiday_type': 'company', + 'mode_company_id': self.company.id, + 'holiday_status_id': self.bank_holiday.id, + 'date_from': date(2020, 1, 9), + 'request_date_from': date(2020, 1, 9), + 'date_to': date(2020, 1, 9), + 'request_date_to': date(2020, 1, 9), + 'number_of_days': 1, + }) + company_leave._compute_date_from_to() + + company_leave.action_validate() + + all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id') + self.assertEqual(len(all_leaves), 2) + # Original Leave + self.assertEqual(leave.state, 'refuse') + # Company Leave + self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 9, 7, 0)) + self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 9, 16, 0)) + self.assertEqual(all_leaves[1].number_of_days, 1) + self.assertEqual(all_leaves[1].state, 'validate') + + def test_leave_whole_company_06(self): + # Test case 6: Leaves taken in days. But the employee + # only works on Monday, Wednesday and Friday + # Takes a time off for all the week (3 days), should be split + + self.employee.resource_calendar_id.write({'attendance_ids': [ + (5, 0, 0), + (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}) + ]}) + + leave = self.env['hr.leave'].create({ + 'name': 'Hol11', + 'employee_id': self.employee.id, + 'holiday_status_id': self.paid_time_off.id, + 'request_date_from': date(2020, 1, 6), + 'request_date_to': date(2020, 1, 10), + 'number_of_days': 3, + }) + leave._compute_date_from_to() + + company_leave = self.env['hr.leave'].create({ + 'name': 'Bank Holiday', + 'holiday_type': 'company', + 'mode_company_id': self.company.id, + 'holiday_status_id': self.bank_holiday.id, + 'date_from': date(2020, 1, 10), + 'request_date_from': date(2020, 1, 10), + 'date_to': date(2020, 1, 10), + 'request_date_to': date(2020, 1, 10), + 'number_of_days': 1, + }) + company_leave._compute_date_from_to() + company_leave.action_validate() + + all_leaves = self.env['hr.leave'].search([('employee_id', '=', self.employee.id)], order='id') + self.assertEqual(len(all_leaves), 3) + # Original Leave + self.assertEqual(leave.state, 'refuse') + # before leave + self.assertEqual(all_leaves[1].date_from, datetime(2020, 1, 6, 7, 0)) + self.assertEqual(all_leaves[1].date_to, datetime(2020, 1, 9, 16, 0)) + self.assertEqual(all_leaves[1].number_of_days, 2) + self.assertEqual(all_leaves[1].state, 'confirm') + # Company Leave + self.assertEqual(all_leaves[2].date_from, datetime(2020, 1, 10, 7, 0)) + self.assertEqual(all_leaves[2].date_to, datetime(2020, 1, 10, 16, 0)) + self.assertEqual(all_leaves[2].number_of_days, 1) + self.assertEqual(all_leaves[2].state, 'validate') + + def test_leave_whole_company_07(self): + # Test Case 7: Try to create a bank holidays for a lot of + # employees, and check the performances + # 100 employees - 15 already on holidays that day + + employees = self.env['hr.employee'].create([{ + 'name': 'Employee %s' % i, + 'company_id': self.company.id + } for i in range(100)]) + + leaves = self.env['hr.leave'].create([{ + 'name': 'Holiday - %s' % employee.name, + 'employee_id': employee.id, + 'holiday_status_id': self.paid_time_off.id, + 'request_date_from': date(2020, 3, 29), + 'request_date_to': date(2020, 4, 1), + 'number_of_days': 3, + } for employee in employees[0:15]]) + leaves._compute_date_from_to() + + company_leave = self.env['hr.leave'].create({ + 'name': 'Bank Holiday', + 'holiday_type': 'company', + 'mode_company_id': self.company.id, + 'holiday_status_id': self.bank_holiday.id, + 'date_from': date(2020, 4, 1), + 'request_date_from': date(2020, 4, 1), + 'date_to': date(2020, 4, 1), + 'request_date_to': date(2020, 4, 1), + 'number_of_days': 1, + }) + company_leave._compute_date_from_to() + + count = 732 + with self.assertQueryCount(__system__=count, admin=count): + # Original query count: 1987 + # Without tracking/activity context keys: 5154 + company_leave.action_validate() + + leaves = self.env['hr.leave'].search([('holiday_status_id', '=', self.bank_holiday.id)]) + self.assertEqual(len(leaves), 102) diff --git a/addons/hr_holidays/tests/test_holidays_flow.py b/addons/hr_holidays/tests/test_holidays_flow.py new file mode 100644 index 00000000..b63c4486 --- /dev/null +++ b/addons/hr_holidays/tests/test_holidays_flow.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time +from datetime import datetime +from dateutil.relativedelta import relativedelta +from psycopg2 import IntegrityError + +from odoo import fields +from odoo.exceptions import AccessError, ValidationError, UserError +from odoo.tools import mute_logger, test_reports + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon + + +class TestHolidaysFlow(TestHrHolidaysCommon): + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') + def test_00_leave_request_flow_unlimited(self): + """ Testing leave request flow: unlimited type of leave request """ + Requests = self.env['hr.leave'] + HolidaysStatus = self.env['hr.leave.type'] + + # HrManager creates some holiday statuses + HolidayStatusManagerGroup = HolidaysStatus.with_user(self.user_hrmanager_id) + HolidayStatusManagerGroup.create({ + 'name': 'WithMeetingType', + 'allocation_type': 'no', + }) + self.holidays_status_hr = HolidayStatusManagerGroup.create({ + 'name': 'NotLimitedHR', + 'allocation_type': 'no', + 'leave_validation_type': 'hr', + 'validity_start': False, + }) + self.holidays_status_manager = HolidayStatusManagerGroup.create({ + 'name': 'NotLimitedManager', + 'allocation_type': 'no', + 'leave_validation_type': 'manager', + 'validity_start': False, + }) + + HolidaysEmployeeGroup = Requests.with_user(self.user_employee_id) + + # Employee creates a leave request in a no-limit category hr manager only + hol1_employee_group = HolidaysEmployeeGroup.create({ + 'name': 'Hol11', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_status_hr.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': datetime.today(), + 'number_of_days': 1, + }) + hol1_user_group = hol1_employee_group.with_user(self.user_hruser_id) + hol1_manager_group = hol1_employee_group.with_user(self.user_hrmanager_id) + self.assertEqual(hol1_user_group.state, 'confirm', 'hr_holidays: newly created leave request should be in confirm state') + + # HrUser validates the employee leave request -> should work + hol1_user_group.action_approve() + self.assertEqual(hol1_manager_group.state, 'validate', 'hr_holidays: validated leave request should be in validate state') + + # Employee creates a leave request in a no-limit category department manager only + hol12_employee_group = HolidaysEmployeeGroup.create({ + 'name': 'Hol12', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_status_manager.id, + 'date_from': (datetime.today() + relativedelta(days=12)), + 'date_to': (datetime.today() + relativedelta(days=13)), + 'number_of_days': 1, + }) + hol12_user_group = hol12_employee_group.with_user(self.user_hruser_id) + hol12_manager_group = hol12_employee_group.with_user(self.user_hrmanager_id) + self.assertEqual(hol12_user_group.state, 'confirm', 'hr_holidays: newly created leave request should be in confirm state') + + # HrManager validate the employee leave request + hol12_manager_group.action_approve() + self.assertEqual(hol1_user_group.state, 'validate', 'hr_holidays: validates leave request should be in validate state') + + + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') + def test_01_leave_request_flow_limited(self): + """ Testing leave request flow: limited type of leave request """ + Requests = self.env['hr.leave'] + Allocations = self.env['hr.leave.allocation'] + HolidaysStatus = self.env['hr.leave.type'] + + holiday_status_paid_time_off = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'allocation_type': 'fixed', + 'leave_validation_type': 'both', + 'validity_start': time.strftime('%Y-%m-01'), + 'responsible_id': self.env.ref('base.user_admin').id, + }) + + self.env['hr.leave.allocation'].create([ + { + 'name': 'Paid Time off for David', + 'holiday_status_id': holiday_status_paid_time_off.id, + 'number_of_days': 20, + 'employee_id': self.employee_emp_id, + 'state': 'validate', + }, { + 'name': 'Paid Time off for David', + 'holiday_status_id': holiday_status_paid_time_off.id, + 'number_of_days': 20, + 'employee_id': self.ref('hr.employee_admin'), + 'state': 'validate', + } + ]) + + def _check_holidays_status(holiday_status, ml, lt, rl, vrl): + self.assertEqual(holiday_status.max_leaves, ml, + 'hr_holidays: wrong type days computation') + self.assertEqual(holiday_status.leaves_taken, lt, + 'hr_holidays: wrong type days computation') + self.assertEqual(holiday_status.remaining_leaves, rl, + 'hr_holidays: wrong type days computation') + self.assertEqual(holiday_status.virtual_remaining_leaves, vrl, + 'hr_holidays: wrong type days computation') + + # HrManager creates some holiday statuses + HolidayStatusManagerGroup = HolidaysStatus.with_user(self.user_hrmanager_id) + HolidayStatusManagerGroup.create({ + 'name': 'WithMeetingType', + 'allocation_type': 'no', + 'validity_start': False, + }) + + self.holidays_status_limited = HolidayStatusManagerGroup.create({ + 'name': 'Limited', + 'allocation_type': 'fixed', + 'allocation_validation_type': 'both', + 'leave_validation_type': 'both', + 'validity_start': False, + }) + HolidaysEmployeeGroup = Requests.with_user(self.user_employee_id) + + # HrUser allocates some leaves to the employee + aloc1_user_group = Allocations.with_user(self.user_hruser_id).create({ + 'name': 'Days for limited category', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_status_limited.id, + 'number_of_days': 2, + }) + # HrUser validates the first step + aloc1_user_group.action_approve() + + # HrManager validates the second step + aloc1_user_group.with_user(self.user_hrmanager_id).action_validate() + # Checks Employee has effectively some days left + hol_status_2_employee_group = self.holidays_status_limited.with_user(self.user_employee_id) + _check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0) + + # Employee creates a leave request in the limited category, now that he has some days left + hol2 = HolidaysEmployeeGroup.create({ + 'name': 'Hol22', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_status_limited.id, + 'date_from': (datetime.today() + relativedelta(days=2)).strftime('%Y-%m-%d %H:%M'), + 'date_to': (datetime.today() + relativedelta(days=3)), + 'number_of_days': 1, + }) + hol2_user_group = hol2.with_user(self.user_hruser_id) + # Check left days: - 1 virtual remaining day + hol_status_2_employee_group.invalidate_cache() + _check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 1.0) + + # HrManager validates the first step + hol2_user_group.with_user(self.user_hrmanager_id).action_approve() + self.assertEqual(hol2.state, 'validate1', + 'hr_holidays: first validation should lead to validate1 state') + + # HrManager validates the second step + hol2_user_group.with_user(self.user_hrmanager_id).action_validate() + self.assertEqual(hol2.state, 'validate', + 'hr_holidays: second validation should lead to validate state') + # Check left days: - 1 day taken + _check_holidays_status(hol_status_2_employee_group, 2.0, 1.0, 1.0, 1.0) + + # HrManager finds an error: he refuses the leave request + hol2.with_user(self.user_hrmanager_id).action_refuse() + self.assertEqual(hol2.state, 'refuse', + 'hr_holidays: refuse should lead to refuse state') + # Check left days: 2 days left again + + hol_status_2_employee_group.invalidate_cache(['max_leaves']) + _check_holidays_status(hol_status_2_employee_group, 2.0, 0.0, 2.0, 2.0) + + self.assertEqual(hol2.state, 'refuse', + 'hr_holidays: hr_user should not be able to reset a refused leave request') + + # HrManager resets the request + hol2_manager_group = hol2.with_user(self.user_hrmanager_id) + hol2_manager_group.action_draft() + self.assertEqual(hol2.state, 'draft', + 'hr_holidays: resetting should lead to draft state') + + employee_id = self.ref('hr.employee_admin') + # cl can be of maximum 20 days for employee_admin + hol3_status = holiday_status_paid_time_off.with_context(employee_id=employee_id) + # I assign the dates in the holiday request for 1 day + hol3 = Requests.create({ + 'name': 'Sick Time Off', + 'holiday_status_id': hol3_status.id, + 'date_from': datetime.today().strftime('%Y-%m-10 10:00:00'), + 'date_to': datetime.today().strftime('%Y-%m-11 19:00:00'), + 'employee_id': employee_id, + 'number_of_days': 1, + }) + # I find a small mistake on my leave request to I click on "Refuse" button to correct a mistake. + hol3.action_refuse() + self.assertEqual(hol3.state, 'refuse', 'hr_holidays: refuse should lead to refuse state') + # I again set to draft and then confirm. + hol3.action_draft() + self.assertEqual(hol3.state, 'draft', 'hr_holidays: resetting should lead to draft state') + hol3.action_confirm() + self.assertEqual(hol3.state, 'confirm', 'hr_holidays: confirming should lead to confirm state') + # I validate the holiday request by clicking on "To Approve" button. + hol3.action_approve() + hol3.action_validate() + self.assertEqual(hol3.state, 'validate', 'hr_holidays: validation should lead to validate state') + # Check left days for casual leave: 19 days left + _check_holidays_status(hol3_status, 20.0, 1.0, 19.0, 19.0) + + def test_10_leave_summary_reports(self): + # Print the HR Holidays(Summary Employee) Report through the wizard + ctx = { + 'model': 'hr.employee', + 'active_ids': [self.ref('hr.employee_admin')] + } + data_dict = { + 'date_from': datetime.today().strftime('%Y-%m-01'), + 'emp': [(6, 0, [self.ref('hr.employee_admin')])], + 'holiday_type': 'Approved' + } + self.env.company.external_report_layout_id = self.env.ref('web.external_layout_standard').id + test_reports.try_report_action(self.env.cr, self.env.uid, 'action_hr_holidays_summary_employee', wiz_data=data_dict, context=ctx, our_module='hr_holidays') + + def test_sql_constraint_dates(self): + # The goal is mainly to verify that a human friendly + # error message is triggered if the date_from is after + # date_to. Coming from a bug due to the new ORM 13.0 + + holiday_status_paid_time_off = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'allocation_type': 'fixed', + 'leave_validation_type': 'both', + 'validity_start': time.strftime('%Y-%m-01'), + 'responsible_id': self.env.ref('base.user_admin').id, + }) + + self.env['hr.leave.allocation'].create({ + 'name': 'Paid Time off for David', + 'holiday_status_id': holiday_status_paid_time_off.id, + 'number_of_days': 20, + 'employee_id': self.ref('hr.employee_admin'), + 'state': 'validate', + }) + + leave_vals = { + 'name': 'Sick Time Off', + 'holiday_status_id': holiday_status_paid_time_off.id, + 'date_from': datetime.today().strftime('%Y-%m-11 19:00:00'), + 'date_to': datetime.today().strftime('%Y-%m-10 10:00:00'), + 'employee_id': self.ref('hr.employee_admin'), + 'number_of_days': 1, + } + with mute_logger('odoo.sql_db'): + with self.assertRaises(IntegrityError): + with self.cr.savepoint(): + self.env['hr.leave'].create(leave_vals) + + leave_vals = { + 'name': 'Sick Time Off', + 'holiday_status_id': holiday_status_paid_time_off.id, + 'date_from': datetime.today().strftime('%Y-%m-10 10:00:00'), + 'date_to': datetime.today().strftime('%Y-%m-11 19:00:00'), + 'employee_id': self.ref('hr.employee_admin'), + 'number_of_days': 1, + } + leave = self.env['hr.leave'].create(leave_vals) + with mute_logger('odoo.sql_db'): + with self.assertRaises(IntegrityError): # No ValidationError + with self.cr.savepoint(): + leave.write({ + 'date_from': datetime.today().strftime('%Y-%m-11 19:00:00'), + 'date_to': datetime.today().strftime('%Y-%m-10 10:00:00'), + }) diff --git a/addons/hr_holidays/tests/test_hr_leave_type.py b/addons/hr_holidays/tests/test_hr_leave_type.py new file mode 100644 index 00000000..852603a2 --- /dev/null +++ b/addons/hr_holidays/tests/test_hr_leave_type.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo.exceptions import AccessError + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon + + +class TestHrLeaveType(TestHrHolidaysCommon): + + def test_time_type(self): + leave_type = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'time_type': 'leave', + 'allocation_type': 'no', + 'validity_start': False, + }) + + leave_1 = self.env['hr.leave'].create({ + 'name': 'Doctor Appointment', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': leave_type.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': datetime.today(), + 'number_of_days': 1, + }) + leave_1.action_approve() + + self.assertEqual( + self.env['resource.calendar.leaves'].search([('holiday_id', '=', leave_1.id)]).time_type, + 'leave' + ) + + def test_type_creation_right(self): + # HrUser creates some holiday statuses -> crash because only HrManagers should do this + with self.assertRaises(AccessError): + self.env['hr.leave.type'].with_user(self.user_hruser_id).create({ + 'name': 'UserCheats', + 'allocation_type': 'no', + }) diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py new file mode 100644 index 00000000..c0c4f3ea --- /dev/null +++ b/addons/hr_holidays/tests/test_leave_requests.py @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, date +from dateutil.relativedelta import relativedelta +from pytz import timezone, UTC + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tools import mute_logger +from odoo.tests.common import Form +from odoo.tests import tagged + +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon + +@tagged('leave_requests') +class TestLeaveRequests(TestHrHolidaysCommon): + + def _check_holidays_status(self, holiday_status, ml, lt, rl, vrl): + self.assertEqual(holiday_status.max_leaves, ml, + 'hr_holidays: wrong type days computation') + self.assertEqual(holiday_status.leaves_taken, lt, + 'hr_holidays: wrong type days computation') + self.assertEqual(holiday_status.remaining_leaves, rl, + 'hr_holidays: wrong type days computation') + self.assertEqual(holiday_status.virtual_remaining_leaves, vrl, + 'hr_holidays: wrong type days computation') + + def setUp(self): + super(TestLeaveRequests, self).setUp() + + # Make sure we have the rights to create, validate and delete the leaves, leave types and allocations + LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True) + + self.holidays_type_1 = LeaveType.create({ + 'name': 'NotLimitedHR', + 'allocation_type': 'no', + 'leave_validation_type': 'hr', + 'validity_start': False, + }) + self.holidays_type_2 = LeaveType.create({ + 'name': 'Limited', + 'allocation_type': 'fixed_allocation', + 'leave_validation_type': 'hr', + 'validity_start': False, + }) + self.holidays_type_3 = LeaveType.create({ + 'name': 'TimeNotLimited', + 'allocation_type': 'no', + 'leave_validation_type': 'manager', + 'validity_start': fields.Datetime.from_string('2017-01-01 00:00:00'), + 'validity_stop': fields.Datetime.from_string('2017-06-01 00:00:00'), + }) + + self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00') + self.set_employee_create_date(self.employee_hruser_id, '2010-02-03 00:00:00') + + def set_employee_create_date(self, id, newdate): + """ This method is a hack in order to be able to define/redefine the create_date + of the employees. + This is done in SQL because ORM does not allow to write onto the create_date field. + """ + self.env.cr.execute(""" + UPDATE + hr_employee + SET create_date = '%s' + WHERE id = %s + """ % (newdate, id)) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_overlapping_requests(self): + """ Employee cannot create a new leave request at the same time, avoid interlapping """ + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Hol11', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_1.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': datetime.today(), + 'number_of_days': 1, + }) + + with self.assertRaises(ValidationError): + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Hol21', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_1.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': datetime.today(), + 'number_of_days': 1, + }) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_limited_type_no_days(self): + """ Employee creates a leave request in a limited category but has not enough days left """ + + with self.assertRaises(ValidationError): + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Hol22', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_2.id, + 'date_from': (datetime.today() + relativedelta(days=1)).strftime('%Y-%m-%d %H:%M'), + 'date_to': (datetime.today() + relativedelta(days=2)), + 'number_of_days': 1, + }) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_limited_type_days_left(self): + """ Employee creates a leave request in a limited category and has enough days left """ + aloc1_user_group = self.env['hr.leave.allocation'].with_user(self.user_hruser_id).create({ + 'name': 'Days for limited category', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_2.id, + 'number_of_days': 2, + }) + aloc1_user_group.action_approve() + + holiday_status = self.holidays_type_2.with_user(self.user_employee_id) + self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 2.0) + + hol = self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Hol11', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_2.id, + 'date_from': (datetime.today() - relativedelta(days=2)), + 'date_to': datetime.today(), + 'number_of_days': 2, + }) + + holiday_status.invalidate_cache() + self._check_holidays_status(holiday_status, 2.0, 0.0, 2.0, 0.0) + + hol.with_user(self.user_hrmanager_id).action_approve() + + holiday_status.invalidate_cache(['max_leaves']) + self._check_holidays_status(holiday_status, 2.0, 2.0, 0.0, 0.0) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_accrual_validity_time_valid(self): + """ Employee ask leave during a valid validity time """ + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Valid time period', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_3.id, + 'date_from': fields.Datetime.from_string('2017-03-03 06:00:00'), + 'date_to': fields.Datetime.from_string('2017-03-11 19:00:00'), + 'number_of_days': 1, + }) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_accrual_validity_time_not_valid(self): + """ Employee ask leav during a not valid validity time """ + with self.assertRaises(ValidationError): + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Sick Time Off', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_3.id, + 'date_from': fields.Datetime.from_string('2017-07-03 06:00:00'), + 'date_to': fields.Datetime.from_string('2017-07-11 19:00:00'), + 'number_of_days': 1, + }) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_department_leave(self): + """ Create a department leave """ + self.employee_hrmanager.write({'department_id': self.hr_dept.id}) + self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)])) + leave_form = Form(self.env['hr.leave'].with_user(self.user_hrmanager), view='hr_holidays.hr_leave_view_form_manager') + leave_form.holiday_type = 'department' + leave_form.department_id = self.hr_dept + leave_form.holiday_status_id = self.holidays_type_1 + leave_form.request_date_from = date(2019, 5, 6) + leave_form.request_date_to = date(2019, 5, 6) + leave = leave_form.save() + leave.action_approve() + member_ids = self.hr_dept.member_ids.ids + self.assertEqual(self.env['hr.leave'].search_count([('employee_id', 'in', member_ids)]), len(member_ids), "Leave should be created for members of department") + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_allocation_request(self): + """ Create an allocation request """ + # employee should be set to current user + allocation_form = Form(self.env['hr.leave.allocation'].with_user(self.user_employee)) + allocation_form.name = 'New Allocation Request' + allocation_form.holiday_status_id = self.holidays_type_2 + allocation = allocation_form.save() + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_employee_is_absent(self): + """ Only the concerned employee should be considered absent """ + self.env['hr.leave'].with_user(self.user_employee_id).create({ + 'name': 'Hol11', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_1.id, + 'date_from': (fields.Datetime.now() - relativedelta(days=1)), + 'date_to': fields.Datetime.now() + relativedelta(days=1), + 'number_of_days': 2, + }) + (self.employee_emp | self.employee_hrmanager).mapped('is_absent') # compute in batch + self.assertTrue(self.employee_emp.is_absent, "He should be considered absent") + self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent") + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_timezone_employee_leave_request(self): + """ Create a leave request for an employee in another timezone """ + self.employee_emp.tz = 'NZ' # GMT+12 + leave = self.env['hr.leave'].new({ + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.holidays_type_1.id, + 'request_unit_hours': True, + 'request_date_from': date(2019, 5, 6), + 'request_date_to': date(2019, 5, 6), + 'request_hour_from': '8', # 8:00 AM in the employee's timezone + 'request_hour_to': '17', # 5:00 PM in the employee's timezone + }) + self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC") + self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC") + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_timezone_company_leave_request(self): + """ Create a leave request for a company in another timezone """ + company = self.env['res.company'].create({'name': "Hergé"}) + company.resource_calendar_id.tz = 'NZ' # GMT+12 + leave = self.env['hr.leave'].new({ + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.holidays_type_1.id, + 'request_unit_hours': True, + 'holiday_type': 'company', + 'mode_company_id': company.id, + 'request_date_from': date(2019, 5, 6), + 'request_date_to': date(2019, 5, 6), + 'request_hour_from': '8', # 8:00 AM in the company's timezone + 'request_hour_to': '17', # 5:00 PM in the company's timezone + }) + self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC") + self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC") + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_timezone_company_validated(self): + """ Create a leave request for a company in another timezone and validate it """ + self.env.user.tz = 'NZ' # GMT+12 + company = self.env['res.company'].create({'name': "Hergé"}) + employee = self.env['hr.employee'].create({'name': "Remi", 'company_id': company.id}) + leave_form = Form(self.env['hr.leave'], view='hr_holidays.hr_leave_view_form_manager') + leave_form.holiday_type = 'company' + leave_form.mode_company_id = company + leave_form.holiday_status_id = self.holidays_type_1 + leave_form.request_date_from = date(2019, 5, 6) + leave_form.request_date_to = date(2019, 5, 6) + leave = leave_form.save() + leave.state = 'confirm' + leave.action_validate() + employee_leave = self.env['hr.leave'].search([('employee_id', '=', employee.id)]) + self.assertEqual( + employee_leave.request_date_from, date(2019, 5, 6), + "Timezone should be kept between company and employee leave" + ) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_timezone_department_leave_request(self): + """ Create a leave request for a department in another timezone """ + company = self.env['res.company'].create({'name': "Hergé"}) + company.resource_calendar_id.tz = 'NZ' # GMT+12 + department = self.env['hr.department'].create({'name': "Museum", 'company_id': company.id}) + leave = self.env['hr.leave'].new({ + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.holidays_type_1.id, + 'request_unit_hours': True, + 'holiday_type': 'department', + 'department_id': department.id, + 'request_date_from': date(2019, 5, 6), + 'request_date_to': date(2019, 5, 6), + 'request_hour_from': '8', # 8:00 AM in the department's timezone + 'request_hour_to': '17', # 5:00 PM in the department's timezone + }) + self.assertEqual(leave.date_from, datetime(2019, 5, 5, 20, 0, 0), "It should have been localized before saving in UTC") + self.assertEqual(leave.date_to, datetime(2019, 5, 6, 5, 0, 0), "It should have been localized before saving in UTC") + + def test_number_of_hours_display(self): + # Test that the field number_of_hours_dispay doesn't change + # after time off validation, as it takes the attendances + # minus the resource leaves to compute that field. + calendar = self.env['resource.calendar'].create({ + 'name': 'Monday Morning Else Full Time 38h/week', + 'hours_per_day': 7.6, + 'attendance_ids': [ + (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}), + (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}), + (0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}), + (0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}), + (0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8.5, 'hour_to': 12.5, 'day_period': 'morning'}), + (0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17.5, 'day_period': 'afternoon'}) + ], + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + self.env.user.company_id.resource_calendar_id = calendar + leave_type = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'request_unit': 'hour', + 'leave_validation_type': 'both', + }) + allocation = self.env['hr.leave.allocation'].create({ + 'name': '20 days allocation', + 'holiday_status_id': leave_type.id, + 'number_of_days': 20, + 'employee_id': employee.id, + }) + allocation.action_approve() + + leave1 = self.env['hr.leave'].create({ + 'name': 'Holiday 1 week', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'), + 'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'), + 'number_of_days': 5, + }) + + self.assertEqual(leave1.number_of_hours_display, 38) + leave1.action_approve() + self.assertEqual(leave1.number_of_hours_display, 38) + leave1.action_validate() + self.assertEqual(leave1.number_of_hours_display, 38) + + leave2 = self.env['hr.leave'].create({ + 'name': 'Holiday 1 Day', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'date_from': fields.Datetime.from_string('2019-12-30 06:00:00'), + 'date_to': fields.Datetime.from_string('2019-12-30 13:00:00'), + 'number_of_days': 1, + }) + + self.assertEqual(leave2.number_of_hours_display, 4) + leave2.action_approve() + self.assertEqual(leave2.number_of_hours_display, 4) + leave2.action_validate() + self.assertEqual(leave2.number_of_hours_display, 4) + + def test_number_of_hours_display_global_leave(self): + # Check that the field number_of_hours_display + # takes the global leaves into account, even + # after validation + calendar = self.env['resource.calendar'].create({ + 'name': 'Classic 40h/week', + 'hours_per_day': 8.0, + 'attendance_ids': [ + (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}) + ], + 'global_leave_ids': [(0, 0, { + 'name': 'Christmas Leave', + 'date_from': fields.Datetime.from_string('2019-12-25 00:00:00'), + 'date_to': fields.Datetime.from_string('2019-12-26 23:59:59'), + 'resource_id': False, + 'time_type': 'leave', + })] + }) + employee = self.employee_emp + employee.resource_calendar_id = calendar + self.env.user.company_id.resource_calendar_id = calendar + leave_type = self.env['hr.leave.type'].create({ + 'name': 'Sick', + 'request_unit': 'hour', + 'leave_validation_type': 'both', + 'allocation_type': 'no', + }) + leave1 = self.env['hr.leave'].create({ + 'name': 'Sick 1 week during christmas snif', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'date_from': fields.Datetime.from_string('2019-12-23 06:00:00'), + 'date_to': fields.Datetime.from_string('2019-12-27 20:00:00'), + 'number_of_days': 5, + }) + self.assertEqual(leave1.number_of_hours_display, 24) + leave1.action_approve() + self.assertEqual(leave1.number_of_hours_display, 24) + leave1.action_validate() + self.assertEqual(leave1.number_of_hours_display, 24) + + def _test_leave_with_tz(self, tz, local_date_from, local_date_to, number_of_days): + self.user_employee.tz = tz + tz = timezone(tz) + + # Mimic what is done by the calendar widget when clicking on a day. It + # will take the local datetime from 7:00 to 19:00 and then convert it + # to UTC before sending it. Values here are for PST (UTC -8) and + # represent a leave on 2019/1/1 from 7:00 to 19:00 local time. + values = { + 'date_from': tz.localize(local_date_from).astimezone(UTC).replace(tzinfo=None), + 'date_to': tz.localize(local_date_to).astimezone(UTC).replace(tzinfo=None), # note that this can be the next day in UTC + } + values.update(self.env['hr.leave'].with_user(self.user_employee_id)._default_get_request_parameters(values)) + + # Dates should be local to the user + self.assertEqual(values['request_date_from'], local_date_from.date()) + self.assertEqual(values['request_date_to'], local_date_to.date()) + + values.update({ + 'name': 'Test', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_1.id, + }) + leave = self.env['hr.leave'].with_user(self.user_employee_id).new(values) + self.assertEqual(leave.number_of_days, number_of_days) + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_leave_defaults_with_timezones(self): + """ Make sure that leaves start with correct defaults for non-UTC timezones """ + timezones_to_test = ('UTC', 'Pacific/Midway', 'US/Pacific', 'Asia/Taipei', 'Pacific/Kiritimati') # UTC, UTC -11, UTC -8, UTC +8, UTC +14 + + # January 2020 + # Su Mo Tu We Th Fr Sa + # 1 2 3 4 + # 5 6 7 8 9 10 11 + # 12 13 14 15 16 17 18 + # 19 20 21 22 23 24 25 + # 26 27 28 29 30 31 + local_date_from = datetime(2019, 1, 1, 7, 0, 0) + local_date_to = datetime(2019, 1, 1, 19, 0, 0) + for tz in timezones_to_test: + self._test_leave_with_tz(tz, local_date_from, local_date_to, 1) + + # We, Th, Fr, Mo, Tu, We => 6 days + local_date_from = datetime(2019, 1, 1, 7, 0, 0) + local_date_to = datetime(2019, 1, 8, 19, 0, 0) + for tz in timezones_to_test: + self._test_leave_with_tz(tz, local_date_from, local_date_to, 6) diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py new file mode 100644 index 00000000..ad316c7a --- /dev/null +++ b/addons/hr_holidays/tests/test_out_of_office.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo.addons.base.tests.common import TransactionCaseWithUserDemo +from odoo.tests.common import tagged, users, warmup +from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon + + +@tagged('out_of_office') +class TestOutOfOffice(TestHrHolidaysCommon): + + def setUp(self): + super().setUp() + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Legal Leaves', + 'time_type': 'leave', + 'allocation_type': 'no', + 'validity_start': False, + }) + + def test_leave_ooo(self): + self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave') + self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave') + leave_date_end = (datetime.today() + relativedelta(days=3)) + leave = self.env['hr.leave'].create({ + 'name': 'Christmas', + 'employee_id': self.employee_hruser.id, + 'holiday_status_id': self.leave_type.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': leave_date_end, + 'number_of_days': 4, + }) + leave.action_approve() + self.assertEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should be out (leave_offline)') + self.assertEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should be out (leave_offline)') + + partner = self.employee_hruser.user_id.partner_id + partner2 = self.user_employee.partner_id + + channel = self.env['mail.channel'].with_user(self.user_employee).with_context({ + 'mail_create_nolog': True, + 'mail_create_nosubscribe': True, + 'mail_channel_noautofollow': True, + }).create({ + 'channel_partner_ids': [(4, partner.id), (4, partner2.id)], + 'public': 'private', + 'channel_type': 'chat', + 'email_send': False, + 'name': 'test' + }) + channel_info = channel.channel_info()[0] + self.assertFalse(channel_info['members'][0]['out_of_office_date_end'], "current user should not be out of office") + self.assertEqual(channel_info['members'][1]['out_of_office_date_end'], leave_date_end, "correspondent should be out of office") + + +@tagged('out_of_office') +class TestOutOfOfficePerformance(TestHrHolidaysCommon, TransactionCaseWithUserDemo): + + def setUp(self): + super(TestOutOfOfficePerformance, self).setUp() + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Legal Leaves', + 'time_type': 'leave', + 'allocation_type': 'no', + 'validity_start': False, + }) + self.leave_date_end = (datetime.today() + relativedelta(days=3)) + self.leave = self.env['hr.leave'].create({ + 'name': 'Christmas', + 'employee_id': self.employee_hruser_id, + 'holiday_status_id': self.leave_type.id, + 'date_from': (datetime.today() - relativedelta(days=1)), + 'date_to': (datetime.today() + relativedelta(days=3)), + 'number_of_days': 4, + }) + + self.hr_user = self.employee_hruser.user_id + self.hr_partner = self.employee_hruser.user_id.partner_id + self.employer_partner = self.user_employee.partner_id + + @users('__system__', 'demo') + @warmup + def test_leave_im_status_performance_partner_offline(self): + with self.assertQueryCount(__system__=2, demo=2): + self.assertEqual(self.employer_partner.im_status, 'offline') + + @users('__system__', 'demo') + @warmup + def test_leave_im_status_performance_user_leave_offline(self): + with self.assertQueryCount(__system__=2, demo=2): + self.assertEqual(self.hr_user.im_status, 'leave_offline') + + @users('__system__', 'demo') + @warmup + def test_leave_im_status_performance_partner_leave_offline(self): + with self.assertQueryCount(__system__=2, demo=2): + self.assertEqual(self.hr_partner.im_status, 'leave_offline') |
