# Part of Odoo. See LICENSE file for full copyright and licensing details. from freezegun import freeze_time from datetime import date, datetime from dateutil.relativedelta import relativedelta from markupsafe import Markup from odoo.tests import Form from odoo.tests.common import TransactionCase from odoo import Command class TestHrAppraisal(TransactionCase): """ Test used to check that when doing appraisal creation.""" @classmethod def setUpClass(cls): super(TestHrAppraisal, cls).setUpClass() cls.HrEmployee = cls.env['hr.employee'] cls.HrAppraisal = cls.env['hr.appraisal'] cls.main_company = cls.env.ref('base.main_company') cls.dep_rd = cls.env['hr.department'].create({'name': 'RD Test'}) cls.manager_user = cls.env['res.users'].create({ 'name': 'Manager User', 'login': 'manager_user', 'password': 'manager_user', 'email': 'demo@demo.com', 'partner_id': cls.env['res.partner'].create({'name': 'Manager Partner'}).id, }) cls.manager = cls.env['hr.employee'].create({ 'name': 'Manager Test', 'department_id': cls.dep_rd.id, 'user_id': cls.manager_user.id, }) cls.job = cls.env['hr.job'].create({'name': 'Developer Test', 'department_id': cls.dep_rd.id}) cls.colleague = cls.env['hr.employee'].create({'name': 'Colleague Test', 'department_id': cls.dep_rd.id}) group = cls.env.ref('hr_appraisal.group_hr_appraisal_user').id cls.user = cls.env['res.users'].create({ 'name': 'Michael Hawkins', 'login': 'test', 'group_ids': [(6, 0, [group])], 'notification_type': 'email', }) cls.env.company.appraisal_plan = True cls.env['ir.config_parameter'].sudo().set_param("hr_appraisal.appraisal_create_in_advance_days", 8) cls.duration_after_recruitment = 6 cls.duration_first_appraisal = 9 cls.duration_next_appraisal = 12 cls.env.company.write({ 'duration_after_recruitment': cls.duration_after_recruitment, 'duration_first_appraisal': cls.duration_first_appraisal, 'duration_next_appraisal': cls.duration_next_appraisal, }) cls.appraisal_rating = cls.env['hr.appraisal.note'].create({'name': 'Exceeds expectations'}) cls.employee_feedback = Markup("Employee Feedback") cls.manager_feedback = Markup("Manager Feedback") with freeze_time(date.today() + relativedelta(months=-6)): cls.hr_employee = cls.HrEmployee.create(dict( name="Michael Hawkins", user_id=cls.user.id, department_id=cls.dep_rd.id, parent_id=cls.manager.id, job_id=cls.job.id, work_phone="+3281813700", work_email='michael@odoo.com', )) cls.hr_employee.write({'work_location_id': [(0, 0, {'name': "Grand-Rosière"})]}) def test_hr_appraisal(self): with freeze_time(date.today() + relativedelta(months=6)): # I run the scheduler self.env['res.company']._run_employee_appraisal_plans() # cronjob # I check whether new appraisal is created for above employee or not appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertTrue(appraisals, "Appraisal not created") # I start the appraisal process by click on "Start Appraisal" button. appraisals.action_confirm() # I check that state is "Appraisal Sent". self.assertEqual(appraisals.state, '2_pending', "appraisal should be 'Appraisal Sent' state") # A final rating is needed before closing the appraisal appraisals.assessment_note = self.appraisal_rating # I close this Apprisal appraisals.action_done() # I check that state of Appraisal is done. self.assertEqual(appraisals.state, '3_done', "Appraisal should be in done state") def test_01_appraisal_next_appraisal_date(self): """ An employee has just started working. Check that next_appraisal_date is set properly. Also, When there is ongoing appraisal for an employee, it means that there is no appraisal plan yet. Thus, next_appraisal_date should be empty. """ with freeze_time(date.today() - relativedelta(months=6)): months = self.hr_employee.company_id.duration_after_recruitment upcoming_appraisal_date = date.today() + relativedelta(months=months) self.assertEqual(self.hr_employee.next_appraisal_date, upcoming_appraisal_date, 'next_appraisal_date is not set properly for an employee that has just started') # create appraisal manually self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() + relativedelta(months=1), 'state': '1_new' }) self.assertEqual(self.hr_employee.next_appraisal_date, False, 'There is an ongoing appraisal for an employee, next_appraisal_date should be empty.') def test_appraisal_next_appraisal_date_uppcoming_appraisal(self): """ Check that next_appraisal_date is correct and that indeed, appraisal plan generates appraisal at that time. """ with freeze_time(date.today() - relativedelta(months=6)): self.hr_employee.last_ongoing_appraisal_date = date.today() months = self.hr_employee.company_id.duration_after_recruitment upcoming_appraisal_date = date.today() + relativedelta(months=months) self.assertEqual(self.hr_employee.next_appraisal_date, upcoming_appraisal_date, 'next_appraisal_date is not set properly') with freeze_time(self.hr_employee.next_appraisal_date): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertTrue(appraisals, "Appraisal not created by appraisal plan at next_appraisal_date") def test_08_check_new_employee_no_appraisal(self): """ Employee has started working recenlty less than duration_after_recruitment ago, check that appraisal is not set """ with freeze_time(date.today() - relativedelta(months=3)): self.hr_employee.last_ongoing_appraisal_date = date.today() self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertFalse(appraisals, "Appraisal created") def test_09_check_appraisal_after_recruitment(self): """ Employee has started working recently Time for a first appraisal after some time (duration_after_recruitment) has evolved since recruitment """ with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertTrue(appraisals, "Appraisal not created") def test_10_check_no_appraisal_since_recruitment_appraisal(self): """ After employees first recruitment appraisal some time has evolved, but not enough for the first real appraisal. Check that appraisal is not created """ with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment)): self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today(), 'state': '3_done', }) self.hr_employee.last_ongoing_appraisal_date = date.today() with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment + self.duration_first_appraisal - 2)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id), ('state', '=', '1_new')]) self.assertFalse(appraisals, "Appraisal created") def test_11_check_first_appraisal_since_recruitment_appraisal(self): """ Employee started while ago, has already had first recruitment appraisal and now it is time for a first real appraisal """ with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment)): # In order to make the second appraisal, cron checks that # there is alraedy one done appraisal for the employee self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today(), 'state': '3_done', }) self.hr_employee.last_ongoing_appraisal_date = date.today() with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment + self.duration_first_appraisal)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertTrue(appraisals, "Appraisal not created") def test_12_check_no_appraisal_after_first_appraisal(self): """ Employee has already had first recruitment appraisal and first real appraisal, but its not time yet for recurring appraisal. Check that appraisal is not set """ with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment + self.duration_first_appraisal)): # In order to make recurring appraisal, cron checks that # there are alraedy two done appraisals for the employee self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() - relativedelta(months=self.duration_first_appraisal), 'state': '3_done', }) self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today(), 'state': '3_done', }) self.hr_employee.last_ongoing_appraisal_date = date.today() with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment + self.duration_first_appraisal + self.duration_next_appraisal - 2)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id), ('state', '=', '1_new')]) self.assertFalse(appraisals, "Appraisal created") def test_12_check_recurring_appraisal(self): """ check that recurring appraisal is created """ with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment + self.duration_first_appraisal)): # In order to make recurring appraisal, cron checks that # there are alraedy two done appraisals for the employee self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() - relativedelta(months=self.duration_first_appraisal), 'state': '3_done', }) self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today(), 'state': '3_done', }) self.hr_employee.last_ongoing_appraisal_date = date.today() with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment + self.duration_first_appraisal + self.duration_next_appraisal)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertTrue(appraisals, "Appraisal not created") def test_load_scenario(self): self.env['hr.appraisal']._load_demo_data() def test_create_appraisal_without_hr_right(self): user_without_hr_right = self.env['res.users'].create({ 'name': 'Test without hr right', 'login': 'test_without_hr_right', 'group_ids': [(6, 0, [self.env.ref('base.group_user').id])], 'notification_type': 'email', }) user_without_hr_right.action_create_employee() appraisal_form = Form(self.env['hr.appraisal'].with_user(user_without_hr_right).with_context({'uid': user_without_hr_right.id})) appraisal_form.save() def test_create_appraisal_campaign_without_hr_right(self): user_without_hr_right = self.env['res.users'].create({ 'name': 'Test without hr right', 'login': 'test_without_hr_right', 'group_ids': [(6, 0, [self.env.ref('base.group_user').id])], 'notification_type': 'email', }) user_without_hr_right.action_create_employee() employees = self.env['hr.employee'].create([ { 'name': 'Emp1', 'parent_id': user_without_hr_right.employee_ids[0].id, }, { 'name': 'Emp2', 'parent_id': user_without_hr_right.employee_ids[0].id, } ]) appraisal_template = self.env['hr.appraisal.template'].create({'description': 'Test appraisal template'}) appraisal_campaign_form = Form(self.env['hr.appraisal.campaign.wizard'].with_user(user_without_hr_right).with_context( {'uid': user_without_hr_right.id} )) appraisal_campaign_form.employee_ids = employees appraisal_campaign_form.appraisal_template_id = appraisal_template appraisal_campaign = appraisal_campaign_form.save() appraisal_campaign.action_generate_appraisals() def _set_appraisal_data(self, appraisal): appraisal.employee_feedback = self.employee_feedback appraisal.manager_feedback = self.manager_feedback appraisal.assessment_note = self.appraisal_rating def test_reopen_appraisal(self): appraisal = self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() + relativedelta(months=1), 'state': '2_pending', }) self._set_appraisal_data(appraisal) appraisal.action_done() appraisal.action_reopen() self.assertEqual(appraisal.state, '2_pending', "A reopened appraisal should be in the pending state") self.assertEqual(appraisal.employee_feedback, self.employee_feedback, "Employee feedback should stay the same after the appraisal is reopened") self.assertEqual(appraisal.manager_feedback, self.manager_feedback, "Manager feedback should stay the same after the appraisal is reopened") self.assertEqual(appraisal.assessment_note, self.appraisal_rating, "Appraisal rating shouldn't change when an appraisal is reopened") def _get_appraisal_count(self, user): return self.env['hr.appraisal'].with_user(user).search_count([ ('employee_id', '=', self.hr_employee.id), ]) def test_appraisal_with_employee_officer(self): """ This test checks that an admin can see appraisals of all employees, while an employee officer can only see appraisals where they are the appraiser. """ admin_user = self.env.ref('base.user_admin') officer_group = self.env.ref('hr.group_hr_manager') officer_user = self.env['res.users'].create({ 'name': 'Employee Officer', 'login': 'employee_officer', 'group_ids': [(6, 0, [officer_group.id])], 'notification_type': 'email', }) officer_user.action_create_employee() # Create appraisal A with admin as appraiser appraisal_a = self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'manager_ids': [(6, 0, [admin_user.employee_id.id])], 'date_close': date.today() + relativedelta(months=1), 'state': '1_new', }) self.assertEqual(self._get_appraisal_count(admin_user), 1) self.assertEqual(self._get_appraisal_count(officer_user), 0) # opening employee appraisals should not fail self.hr_employee.with_user(officer_user).action_open_employee_appraisals() # Create appraisal B with officer as appraiser self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'manager_ids': [(6, 0, [officer_user.employee_id.id])], 'date_close': date.today() + relativedelta(months=1), 'state': '1_new', }) self.assertEqual(self._get_appraisal_count(admin_user), 2) self.assertEqual(self._get_appraisal_count(officer_user), 1) # Delete appraisal A appraisal_a.sudo().unlink() self.assertEqual(self._get_appraisal_count(admin_user), 1) self.assertEqual(self._get_appraisal_count(officer_user), 1) def test_appraisal_departement_from_employee(self): self.assertEqual(self.hr_employee.department_id, self.dep_rd) with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertEqual(appraisals.department_id, self.dep_rd) def test_appraisal_template_computation(self): # Delete all templates to prevent them from interacting with the test for template in self.env['hr.appraisal.template'].search([]): template.unlink() test_template = self.env['hr.appraisal.template'].create({'description': 'Test appraisal template'}) test_template.company_id = self.env.company self.hr_employee.department_id = False with freeze_time(self.hr_employee.date_version + relativedelta(months=self.duration_after_recruitment)): self.env['res.company']._run_employee_appraisal_plans() appraisals = self.HrAppraisal.search([('employee_id', '=', self.hr_employee.id)]) self.assertEqual(appraisals.appraisal_template_id, test_template) def test_appraisal_template_department_priority(self): # A template restricted to a department must take precedence over a # global template that was already auto-selected because no employee # (hence no department) was set on the form yet. self.env['hr.appraisal.template'].search([]).unlink() global_template = self.env['hr.appraisal.template'].create({ 'description': 'Global Template', 'company_id': False, }) department_template = self.env['hr.appraisal.template'].create({ 'description': 'Department Template', 'company_id': self.env.company.id, 'department_ids': [Command.set(self.dep_rd.ids)], }) appraisal_form = Form(self.HrAppraisal) # No employee yet: the global template is the only candidate. self.assertEqual(appraisal_form.appraisal_template_id, global_template) # Setting an employee of the department must switch to its template. appraisal_form.employee_id = self.colleague self.assertEqual(appraisal_form.department_id, self.dep_rd) self.assertEqual(appraisal_form.appraisal_template_id, department_template) def test_appraisal_template_keeps_department_choice(self): # When a department has several templates, a template the user chose # for that department must be preserved when the template is recomputed # (e.g. the employee or department is reselected), instead of being # reset to the first template of the department. self.env['hr.appraisal.template'].search([]).unlink() Template = self.env['hr.appraisal.template'] first_template = Template.create({ 'description': 'RD First', 'department_ids': [Command.set(self.dep_rd.ids)], }) second_template = Template.create({ 'description': 'RD Second', 'department_ids': [Command.set(self.dep_rd.ids)], }) appraisal = self.HrAppraisal.create({'employee_id': self.colleague.id}) self.assertEqual(appraisal.appraisal_template_id, first_template) # The user picks the other template of the same department. appraisal.appraisal_template_id = second_template # A recompute must not reset it to the department's first template. appraisal._compute_appraisal_template() self.assertEqual(appraisal.appraisal_template_id, second_template) def test_check_next_appraisal_date_is_unset_for_archived_employee(self): """ Check that when an employee is archived, its next_appraisal_date is unset. """ self.hr_employee.next_appraisal_date = date.today() + relativedelta(months=6) self.hr_employee.action_archive() self.assertFalse(self.hr_employee.next_appraisal_date, 'next_appraisal_date should be empty for archived employee') def test_check_appraisals_and_goals_after_employees_departure(self): """ In case of end of collaboration with an employee: - all their future appraisals should be removed - the employee should be removed from manager_ids in appraisals - all their personal goals should be removed """ future_appraisal_new = self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() + relativedelta(months=2), 'manager_ids': [(6, 0, [self.manager.id])], 'state': '1_new' }) future_appraisal_pending = self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() + relativedelta(months=1), 'manager_ids': [(6, 0, [self.manager.id])], 'state': '2_pending' }) completed_appraisal = self.HrAppraisal.create({ 'employee_id': self.hr_employee.id, 'date_close': date.today() + relativedelta(months=-1), 'manager_ids': [(6, 0, [self.manager.id])], 'state': '3_done' }) goal_test = self.env['hr.appraisal.goal'].create({ 'name': 'Goal Test', 'employee_ids': [(6, 0, [self.hr_employee.id])], 'manager_ids': [(6, 0, [self.manager.id])], }) self.assertTrue(future_appraisal_new, "Appraisal in state 'new' has not been created") self.assertTrue(future_appraisal_pending, "Appraisal in state 'pending' has not been created") self.assertTrue(completed_appraisal, "Appraisal in state 'done' has not been created") self.assertTrue(goal_test, "Goal has not been created") # appraiser leaves the company self.manager.create_date = date.today() + relativedelta(years=-1) self.env['hr.departure.wizard'].create({ 'employee_ids': [Command.set([self.manager.id])], 'departure_date': date.today(), }).action_register_departure() self.assertFalse(future_appraisal_new.manager_ids, "Manager wasn't removed from appraisal after departure") self.assertFalse(future_appraisal_pending.manager_ids, "Manager wasn't removed from appraisal after departure") # employee leaves the company self.env['hr.departure.wizard'].create({ 'employee_ids': [Command.set([self.hr_employee.id])], 'departure_date': date.today(), }).action_register_departure() self.assertFalse(future_appraisal_new.exists(), "Appraisal in state 'new' was not removed after employee's departure") self.assertFalse(future_appraisal_pending.exists(), "Appraisal in state 'pending' was not removed after employee's departure") self.assertTrue(completed_appraisal.exists(), "Appraisal in state 'done' was removed") self.assertFalse(goal_test.exists(), "Personal goal was not removed after employee's departure") def test_request_late_appraisal(self): """Tests that requesting an appraisal after next_appraisal_date properly creates the appraisal""" old_appraisal_date = self.hr_employee.next_appraisal_date with freeze_time(self.hr_employee.next_appraisal_date + relativedelta(days=7)): with Form.from_action(self.env, self.hr_employee.action_send_appraisal_request()) as form: form.employee_id = self.hr_employee appraisal = form.save() self.assertTrue(appraisal.exists()) self.assertEqual(self.hr_employee.next_appraisal_date, date.today()) with freeze_time(old_appraisal_date - relativedelta(days=7)): self.hr_employee.next_appraisal_date = old_appraisal_date with freeze_time(self.hr_employee.next_appraisal_date + relativedelta(days=7)): with Form.from_action(self.env, self.manager.action_send_appraisal_request()) as form: form.employee_id = self.manager appraisal = form.save() self.assertTrue(appraisal.exists()) with Form(appraisal) as form: form.employee_id = self.hr_employee self.assertEqual(self.hr_employee.next_appraisal_date, date.today())