From 7053e37df1d69d3cf7901593e0b00120739dd2b4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Mon, 14 Aug 2023 10:58:13 +0200 Subject: [PATCH] [IMP] hr(_contract/_fleet): convert hr.plan to mail.activity.plan We convert the custom plan implementation to use the generic one. As the generic one can define plans for multiple model. We use the check dedicated_to_res_model == 'hr.employee' to activate the specific feature for hr.employee. Indeed, that field contains the model name when the plan is applicable only for one model. We also remove the 'launch plan' button as we can now launch a plan directly from the activities button in the chatter. Technical notes: For the activity schedule wizard, we add the support for active_ids and active_model as default values for res_ids and res_model because it is used as link in the chatter to launch the wizard and we want to avoid a big migration by keeping the link identical (and there are probably no simple solution to keep the same behavior). HR CONTRACT Before the first contract date of the first selected element was chosen to determine the planned due date if all first contract date were different otherwise the minimum was chosen. So if the selection included 2 different date among 3, the minimum was chosen but the first if the 3 were different. With this change, the minimum is always used to determine the default planned due date. Task-3390865 Part-of: odoo/odoo#137969 --- addons/hr/__manifest__.py | 4 +- addons/hr/data/hr_data.xml | 31 ++- addons/hr/models/__init__.py | 5 +- addons/hr/models/hr_department.py | 6 +- addons/hr/models/hr_plan_employee_activity.py | 32 --- addons/hr/models/mail_activity_plan.py | 51 ++-- .../hr/models/mail_activity_plan_template.py | 77 +++--- addons/hr/security/hr_security.xml | 41 ++- addons/hr/security/ir.model.access.csv | 8 +- addons/hr/tests/__init__.py | 1 + addons/hr/tests/test_mail_activity_plan.py | 240 ++++++++++++++++++ addons/hr/views/hr_employee_views.xml | 7 +- addons/hr/views/hr_views.xml | 6 +- addons/hr/views/mail_activity_plan_views.xml | 227 ++++------------- addons/hr/wizard/__init__.py | 2 +- addons/hr/wizard/mail_activity_schedule.py | 148 ++--------- .../wizard/mail_activity_schedule_views.xml | 36 +-- addons/hr_contract/tests/__init__.py | 1 + .../tests/test_mail_activity_plan.py | 58 +++++ addons/hr_contract/wizard/__init__.py | 2 +- .../wizard/mail_activity_schedule.py | 35 ++- addons/hr_fleet/data/hr_fleet_data.xml | 4 +- addons/hr_fleet/data/hr_fleet_demo.xml | 4 +- addons/hr_fleet/models/__init__.py | 2 +- .../models/mail_activity_plan_template.py | 32 ++- addons/hr_fleet/tests/__init__.py | 4 + .../hr_fleet/tests/test_mail_activity_plan.py | 75 ++++++ 27 files changed, 604 insertions(+), 535 deletions(-) delete mode 100644 addons/hr/models/hr_plan_employee_activity.py create mode 100644 addons/hr/tests/test_mail_activity_plan.py create mode 100644 addons/hr_contract/tests/test_mail_activity_plan.py create mode 100644 addons/hr_fleet/tests/__init__.py create mode 100644 addons/hr_fleet/tests/test_mail_activity_plan.py diff --git a/addons/hr/__manifest__.py b/addons/hr/__manifest__.py index e107ca096f6..2c59d18266f 100644 --- a/addons/hr/__manifest__.py +++ b/addons/hr/__manifest__.py @@ -21,12 +21,12 @@ 'data': [ 'security/hr_security.xml', 'security/ir.model.access.csv', - 'wizard/hr_plan_wizard_views.xml', 'wizard/hr_departure_wizard_views.xml', + 'wizard/mail_activity_schedule_views.xml', + 'views/mail_activity_plan_views.xml', 'views/hr_departure_reason_views.xml', 'views/hr_contract_type_views.xml', 'views/hr_job_views.xml', - 'views/hr_plan_views.xml', 'views/hr_employee_category_views.xml', 'views/hr_employee_public_views.xml', 'report/hr_employee_badge.xml', diff --git a/addons/hr/data/hr_data.xml b/addons/hr/data/hr_data.xml index e05352d383e..cdbf5747750 100644 --- a/addons/hr/data/hr_data.xml +++ b/addons/hr/data/hr_data.xml @@ -15,41 +15,48 @@ - + Onboarding + hr.employee - + + 10 Setup IT Materials - manager + manager - + + 20 Plan Training - manager + manager - + + 30 Training - employee + employee - + Offboarding + hr.employee - + + 10 Compute Out Delais - manager + manager - + + 20 Take Back HR Materials - manager + manager diff --git a/addons/hr/models/__init__.py b/addons/hr/models/__init__.py index 3fcdcd3af8a..c970d0c603b 100644 --- a/addons/hr/models/__init__.py +++ b/addons/hr/models/__init__.py @@ -9,10 +9,9 @@ from . import hr_employee_public from . import hr_department from . import hr_departure_reason from . import hr_job -from . import hr_plan -from . import hr_plan_activity_type -from . import hr_plan_employee_activity from . import hr_work_location +from . import mail_activity_plan +from . import mail_activity_plan_template from . import mail_alias from . import discuss_channel from . import models diff --git a/addons/hr/models/hr_department.py b/addons/hr/models/hr_department.py index 2ed7d22cede..3c8e3e7f215 100644 --- a/addons/hr/models/hr_department.py +++ b/addons/hr/models/hr_department.py @@ -23,7 +23,7 @@ class Department(models.Model): member_ids = fields.One2many('hr.employee', 'department_id', string='Members', readonly=True) total_employee = fields.Integer(compute='_compute_total_employee', string='Total Employee') jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs') - plan_ids = fields.One2many('hr.plan', 'department_id') + plan_ids = fields.One2many('mail.activity.plan', 'department_id') plans_count = fields.Integer(compute='_compute_plan_count') note = fields.Text('Note') color = fields.Integer('Color Index') @@ -63,7 +63,7 @@ class Department(models.Model): department.total_employee = result.get(department.id, 0) def _compute_plan_count(self): - plans_data = self.env['hr.plan']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['__count']) + plans_data = self.env['mail.activity.plan']._read_group([('department_id', 'in', self.ids)], ['department_id'], ['__count']) plans_count = {department.id: count for department, count in plans_data} for department in self: department.plans_count = plans_count.get(department.id, 0) @@ -128,7 +128,7 @@ class Department(models.Model): return res def action_plan_from_department(self): - action = self.env['ir.actions.actions']._for_xml_id('hr.hr_plan_action') + action = self.env['ir.actions.actions']._for_xml_id('hr.mail_activity_plan_action') action['context'] = {'default_department_id': self.id, 'search_default_department_id': self.id} return action diff --git a/addons/hr/models/hr_plan_employee_activity.py b/addons/hr/models/hr_plan_employee_activity.py deleted file mode 100644 index ba4b1599809..00000000000 --- a/addons/hr/models/hr_plan_employee_activity.py +++ /dev/null @@ -1,32 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, fields, models - - -class HrPlanEmployeeActivity(models.Model): - _name = 'hr.plan.employee.activity' - _description = 'Plan' - _inherit = ['mail.thread', 'mail.activity.mixin'] - _mail_post_access = 'read' - _rec_name = 'employee_id' - - employee_id = fields.Many2one('hr.employee', required=True, ondelete='cascade') - department_id = fields.Many2one(related='employee_id.department_id') - image_128 = fields.Image(related='employee_id.image_128') - company_id = fields.Many2one(related='employee_id.company_id') - user_id = fields.Many2one(related='employee_id.user_id') - summary = fields.Html(compute='_compute_summary') - - @api.depends('activity_ids.summary') - def _compute_summary(self): - for plan in self: - plan.summary = self.env['ir.ui.view']._render_template('hr.hr_employee_plan_activity_summary', { - 'activity_ids': plan.activity_ids, - }) - - @api.autovacuum - def _gc_employee_plan_activity(self): - no_activities = self.env['hr.plan.employee.activity'].search( - [('activity_ids', '=', False)] - ) - no_activities.unlink() diff --git a/addons/hr/models/mail_activity_plan.py b/addons/hr/models/mail_activity_plan.py index 1f234226ed9..01d4626c03e 100644 --- a/addons/hr/models/mail_activity_plan.py +++ b/addons/hr/models/mail_activity_plan.py @@ -1,27 +1,38 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError -class HrPlan(models.Model): - _name = 'hr.plan' - _description = 'plan' +class MailActivityPlan(models.Model): + _inherit = 'mail.activity.plan' - name = fields.Char('Name', required=True) - company_id = fields.Many2one( - 'res.company', default=lambda self: self.env.company) - department_id = fields.Many2one('hr.department', check_company=True) - plan_activity_type_ids = fields.One2many( - 'hr.plan.activity.type', 'plan_id', - string='Activities', - check_company=True) - active = fields.Boolean(default=True) - steps_count = fields.Integer(compute='_compute_steps_count') + department_id = fields.Many2one( + 'hr.department', check_company=True, + compute='_compute_department_id', ondelete='cascade', readonly=False, store=True) - @api.depends('plan_activity_type_ids') - def _compute_steps_count(self): - activity_type_data = self.env['hr.plan.activity.type']._read_group([('plan_id', 'in', self.ids)], ['plan_id'], ['__count']) - steps_count = {plan.id: count for plan, count in activity_type_data} - for plan in self: - plan.steps_count = steps_count.get(plan.id, 0) + @api.constrains('res_model') + def _check_compatibility_with_model(self): + """ Check that when the model is updated to a model different from employee, + there are no remaining specific values to employee. """ + plan_tocheck = self.filtered(lambda plan: plan.res_model != 'hr.employee') + failing_plans = plan_tocheck.filtered('department_id') + if failing_plans: + raise UserError( + _('Plan %(plan_names)s cannot use a department as it is used only for employee plans.', + plan_names=', '.join(failing_plans.mapped('name'))) + ) + failing_templates = plan_tocheck.template_ids.filtered( + lambda tpl: tpl.responsible_type in {'coach', 'manager', 'employee'} + ) + if failing_templates: + raise UserError( + _('Plan activities %(template_names)s cannot use coach, manager or employee responsible as it is used only for employee plans.', + template_names=', '.join(failing_templates.mapped('activity_type_id.name'))) + ) + + @api.onchange('res_model') + def _compute_department_id(self): + for plan in self.filtered(lambda plan: plan.res_model != 'hr.employee'): + plan.department_id = False diff --git a/addons/hr/models/mail_activity_plan_template.py b/addons/hr/models/mail_activity_plan_template.py index 05b04fb542f..a16794da845 100644 --- a/addons/hr/models/mail_activity_plan_template.py +++ b/addons/hr/models/mail_activity_plan_template.py @@ -2,67 +2,48 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.exceptions import UserError +from odoo.exceptions import ValidationError -class HrPlanActivityType(models.Model): - _name = 'hr.plan.activity.type' - _description = 'Plan activity type' - _rec_name = 'summary' - _check_company_auto = True +class MailActivityPLanTemplate(models.Model): + _inherit = 'mail.activity.plan.template' - company_id = fields.Many2one('res.company', default=lambda self: self.env.company) - activity_type_id = fields.Many2one( - 'mail.activity.type', 'Activity Type', - default=lambda self: self.env.ref('mail.mail_activity_data_todo'), - domain=lambda self: ['|', ('res_model', '=', False), ('res_model', '=', 'hr.employee')], - ondelete='restrict' - ) - summary = fields.Char('Summary', compute="_compute_default_summary", store=True, readonly=False) - responsible = fields.Selection([ + responsible_type = fields.Selection(selection_add=[ ('coach', 'Coach'), ('manager', 'Manager'), ('employee', 'Employee'), - ('other', 'Other')], default='employee', string='Responsible', required=True) - responsible_id = fields.Many2one( - 'res.users', - 'Other Responsible', - check_company=True, - help='Specific responsible of activity if not linked to the employee.') - plan_id = fields.Many2one('hr.plan') - note = fields.Html('Note') + ], ondelete={'coach': 'cascade', 'manager': 'cascade', 'employee': 'cascade'}) - @api.depends('activity_type_id') - def _compute_default_summary(self): - for plan_type in self: - if plan_type.activity_type_id and plan_type.activity_type_id.summary: - plan_type.summary = plan_type.activity_type_id.summary - else: - plan_type.summary = False + @api.constrains('plan_id', 'responsible_type') + def _check_responsible_hr(self): + """ Ensure that hr types are used only on employee model """ + for template in self.filtered(lambda tpl: tpl.plan_id.res_model != 'hr.employee'): + if template.responsible_type in {'coach', 'manager', 'employee'}: + raise ValidationError(_('Those responsible types are limited to Employee plans.')) - def get_responsible_id(self, employee): - warning = False - if self.responsible == 'coach': + def _determine_responsible(self, on_demand_responsible, employee): + if self.plan_id.res_model != 'hr.employee' or self.responsible_type not in {'coach', 'manager', 'employee'}: + return super()._determine_responsible(on_demand_responsible, employee) + error = False + responsible = False + if self.responsible_type == 'coach': if not employee.coach_id: - warning = _('Coach of employee %s is not set.', employee.name) + error = _('Coach of employee %s is not set.', employee.name) responsible = employee.coach_id.user_id if employee.coach_id and not responsible: - warning = _("The user of %s's coach is not set.", employee.name) - elif self.responsible == 'manager': + error = _("The user of %s's coach is not set.", employee.name) + elif self.responsible_type == 'manager': if not employee.parent_id: - warning = _('Manager of employee %s is not set.', employee.name) + error = _('Manager of employee %s is not set.', employee.name) responsible = employee.parent_id.user_id if employee.parent_id and not responsible: - warning = _("The manager of %s should be linked to a user.", employee.name) - elif self.responsible == 'employee': + error = _("The manager of %s should be linked to a user.", employee.name) + elif self.responsible_type == 'employee': responsible = employee.user_id if not responsible: - warning = _('The employee %s should be linked to a user.', employee.name) - elif self.responsible == 'other': - responsible = self.responsible_id - if not responsible: - warning = _('No specific user given on activity %s.', self.activity_type_id.name) - return { - 'responsible': responsible, - 'warning': warning, - } + error = _('The employee %s should be linked to a user.', employee.name) + if error or responsible: + return { + 'responsible': responsible, + 'error': error, + } diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml index ddf2ef03351..27e5917f9cb 100644 --- a/addons/hr/security/hr_security.xml +++ b/addons/hr/security/hr_security.xml @@ -49,30 +49,6 @@ ['|',('company_id','=',False),('company_id', 'in', company_ids)] - - Plan multi-company rule - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] - - - - Plan Activity Type multi-company rule - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] - - - - Plan Employee Activity: multi-company rule - - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] - - - - Plan Employee Activity: own employee or subordinates - - ['|', ('user_id', '=', user.id), ('activity_ids.user_id', '=', user.id)] - - HR: Prevent non HR officers from accessing employee bank accounts @@ -92,5 +68,22 @@ ['|', ('country_id', '=', False), ('country_id', 'in', user.env.companies.country_id.ids)] + + + Manager can edit employee plan + + + [('res_model', '=', 'hr.employee')] + + + + + Manager can edit employee plan template + + + [('plan_id.res_model', '=', 'hr.employee')] + + + diff --git a/addons/hr/security/ir.model.access.csv b/addons/hr/security/ir.model.access.csv index 9affc1c6bea..d2ef0822d2f 100644 --- a/addons/hr/security/ir.model.access.csv +++ b/addons/hr/security/ir.model.access.csv @@ -8,14 +8,10 @@ access_hr_employee_resource_user,resource.resource.user,resource.model_resource_ access_hr_department_user,hr.department.user,model_hr_department,group_hr_user,1,1,1,1 access_hr_department_employee,hr.department.employee,model_hr_department,base.group_user,1,0,0,0 access_hr_job_user,hr.job user,model_hr_job,group_hr_user,1,1,1,1 -access_hr_plan_activity_type_employee,access_hr_plan_activity_type,model_hr_plan_activity_type,base.group_user,1,0,0,0 -access_hr_plan_employee,access_hr_plan_employee,model_hr_plan,base.group_user,1,0,0,0 -access_hr_plan_activity_type_hr_user,access_hr_plan_activity_type,model_hr_plan_activity_type,group_hr_user,1,1,1,1 -access_hr_plan_hr_user,access_hr_plan_hr_user,model_hr_plan,group_hr_user,1,1,1,1 -access_hr_plan_wizard,access.hr.plan.wizard,model_hr_plan_wizard,hr.group_hr_user,1,1,1,0 access_hr_departure_wizard,access.hr.departure.wizard,model_hr_departure_wizard,hr.group_hr_user,1,1,1,0 access_hr_work_location_user,access_hr_work_location_user,model_hr_work_location,base.group_user,1,0,0,0 access_hr_work_location_manager,access_hr_work_location_manager,model_hr_work_location,group_hr_manager,1,1,1,1 access_hr_departure_reason,access_hr_departure_reason_user,model_hr_departure_reason,group_hr_user,1,1,1,1 access_hr_contract_type_manager,hr.contract.type.manager,model_hr_contract_type,hr.group_hr_user,1,1,1,1 -access_hr_plan_employee_activity,hr.plan.employee.activity,model_hr_plan_employee_activity,base.group_user,1,0,0,0 +access_mail_activity_plan_hr_manager,mail.activity.plan.hr.manager,mail.model_mail_activity_plan,hr.group_hr_manager,1,1,1,1 +access_mail_activity_plan_template_hr_manager,mail.activity.plan.template.hr.manager,mail.model_mail_activity_plan_template,hr.group_hr_manager,1,1,1,1 diff --git a/addons/hr/tests/__init__.py b/addons/hr/tests/__init__.py index 7cefadd5133..c7df1893ad3 100644 --- a/addons/hr/tests/__init__.py +++ b/addons/hr/tests/__init__.py @@ -4,6 +4,7 @@ from . import test_hr_employee from . import test_channel from . import test_self_user_access +from . import test_mail_activity_plan from . import test_multi_company from . import test_resource from . import test_ui diff --git a/addons/hr/tests/test_mail_activity_plan.py b/addons/hr/tests/test_mail_activity_plan.py new file mode 100644 index 00000000000..0c392cfadb8 --- /dev/null +++ b/addons/hr/tests/test_mail_activity_plan.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import Command +from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.mail.tests.test_mail_activity import ActivityScheduleCase +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged, users + + +class ActivityScheduleHRCase(ActivityScheduleCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.plan_onboarding = cls.env['mail.activity.plan'].create({ + 'name': 'Test Onboarding', + 'res_model': 'hr.employee', + 'template_ids': [ + Command.create({ + 'activity_type_id': cls.activity_type_todo.id, + 'responsible_id': cls.user_admin.id, + 'responsible_type': 'other', + 'sequence': 10, + 'summary': 'Plan training', + }), Command.create({ + 'activity_type_id': cls.activity_type_todo.id, + 'responsible_id': False, + 'responsible_type': 'coach', + 'sequence': 20, + 'summary': 'Training', + }), + ] + }) + cls.plan_party = cls.env['mail.activity.plan'].create({ + 'name': 'Test Party Plan', + 'res_model': 'res.partner', + 'template_ids': [ + Command.create({ + 'activity_type_id': cls.activity_type_todo.id, + 'responsible_id': cls.user_admin.id, + 'responsible_type': 'on_demand', + 'sequence': 10, + 'summary': 'Party', + }), + ] + }) + + cls.user_manager = mail_new_test_user( + cls.env, + email='test.manager@test.mycompany.com', + groups='base.group_user,hr.group_hr_manager', + login='test_manager', + name='Test Manager', + ) + cls.user_coach = mail_new_test_user( + cls.env, + email='test.coach@test.mycompany.com', + groups='base.group_user,hr.group_hr_manager', + login='test_coach', + name='Test Coach', + ) + cls.user_employee_1 = mail_new_test_user( + cls.env, + email='test.employee1@test.mycompany.com', + groups='base.group_user,hr.group_hr_manager', + login='test_employee1', + name='Test Employee 1', + ) + cls.user_employee_2 = mail_new_test_user( + cls.env, + email='test.employee2@test.mycompany.com', + groups='base.group_user,hr.group_hr_manager', + login='test_employee2', + name='Test Employee 2', + ) + cls.user_employee_dep_b = mail_new_test_user( + cls.env, + email='test.employeedepb@test.mycompany.com', + groups='base.group_user,hr.group_hr_manager', + login='test_employee_dep_b', + name='Test Employee DepB', + ) + cls.users = cls.user_manager + cls.user_coach + cls.user_employee_1 + cls.user_employee_2 + cls.user_employee_dep_b + + cls.user_internal_basic = mail_new_test_user( + cls.env, + email='non.employee@test.mycompany.com', + groups='base.group_user', + login='non_employee', + name='Non Employee', + ) + + cls.employees = cls.env['hr.employee'].create([ + { + 'name': user.name, + 'user_id': user.id, + 'work_email': user.email, + } for user in cls.users + ]) + cls.employee_manager, cls.employee_coach, cls.employee_1, cls.employee_2, cls.employee_dep_b = cls.employees + cls.department_a = cls.env['hr.department'].create({ + 'name': 'Test Department A', + 'member_ids': [Command.link(employee.id) for employee in cls.employees - cls.employee_dep_b], + }) + cls.department_b = cls.env['hr.department'].create({ + 'name': 'Test Department B', + 'member_ids': [Command.link(cls.employee_dep_b.id)], + }) + cls.employee_1.coach_id = cls.employee_coach + cls.employee_1.parent_id = cls.employee_manager + cls.employee_2.coach_id = cls.employee_coach + cls.employee_2.parent_id = cls.employee_manager + cls.employee_coach.parent_id = cls.employee_manager + cls.employee_dep_b.coach_id = cls.employee_coach + + +@tagged('mail_activity', 'mail_activity_plan') +class TestActivitySchedule(ActivityScheduleHRCase): + + @users('admin') + def test_department(self): + """ Check that the allowed plan are filtered according to the department. """ + no_plan = self.env['mail.activity.plan'] + plan_department_a, plan_department_b = self.env['mail.activity.plan'].create([ + { + 'department_id': department.id, + 'name': f'plan {department.name}', + 'res_model': 'hr.employee', + 'template_ids': [(0, 0, {'activity_type_id': self.activity_type_todo.id})], + } for department in self.department_a + self.department_b + ]) + for employees, expected_department, authorized_plans, non_authorized_plans in ( + (self.employee_1 + self.employee_dep_b, False, self.plan_onboarding, no_plan), + (self.employee_1 + self.employee_2, self.department_a, self.plan_onboarding + plan_department_a, plan_department_b), + (self.employee_1, self.department_a, self.plan_onboarding + plan_department_a, plan_department_b), + (self.employee_dep_b, self.department_b, self.plan_onboarding + plan_department_b, plan_department_a), + ): + with self._instantiate_activity_schedule_wizard(employees) as form: + if expected_department: + self.assertEqual(form.department_id, expected_department) + else: + self.assertFalse(form.department_id) + for plan in non_authorized_plans: + self.assertNotIn(plan, form.plan_available_ids) + for plan in authorized_plans: + with self._instantiate_activity_schedule_wizard(employees) as form: + form.plan_id = plan + + def test_res_model_compatibility(self): + """ Check that we cannot change the plan model to a model different + of employee if hr plan specific features are used. """ + with self.assertRaises( + UserError, + msg="Coach, manager or employee can only be chosen as template responsible with employee plan."): + self.plan_onboarding.res_model = 'res.partner' + self.plan_onboarding.template_ids[1].responsible_type = 'on_demand' + self.plan_onboarding.res_model = 'res.partner' + self.plan_onboarding.res_model = 'hr.employee' + self.plan_onboarding.template_ids[1].responsible_type = 'manager' + with self.assertRaises( + UserError, + msg="Coach, manager or employee can only be chosen as template responsible with employee plan."): + self.plan_onboarding.res_model = 'res.partner' + self.plan_onboarding.template_ids[1].responsible_type = 'employee' + with self.assertRaises( + UserError, + msg="Coach, manager or employee can only be chosen as template responsible with employee plan."): + self.plan_onboarding.res_model = 'res.partner' + self.plan_onboarding.template_ids[1].responsible_type = 'on_demand' + self.plan_onboarding.res_model = 'res.partner' + self.plan_onboarding.res_model = 'hr.employee' + self.plan_onboarding.department_id = self.department_a + with self.assertRaises( + UserError, + msg="Department can only be set with employee plan."): + self.plan_onboarding.res_model = 'res.partner' + + def test_responsible(self): + """ Check that the responsible is correctly configured. """ + self.plan_onboarding.template_ids[0].write({ + 'responsible_type': 'manager', + 'responsible_id': False, + }) + self.plan_onboarding.write({ + 'template_ids': [(0, 0, { + 'activity_type_id': self.activity_type_todo.id, + 'summary': 'Send feedback to the manager', + 'responsible_type': 'employee', + 'sequence': 30, + })], + }) + for employees in (self.employee_1, self.employee_1 + self.employee_2): + # Happy case + form = self._instantiate_activity_schedule_wizard(employees) + form.plan_id = self.plan_onboarding + self.assertEqual(form.plan_assignation_summary, + '
  • To-Do - Manager: Plan training
  • To-Do - Coach: Training
  • ' + '
  • To-Do - Employee: Send feedback to the manager
') + self.assertFalse(form.has_error) + wizard = form.save() + wizard.action_schedule_plan() + for employee in employees: + activities = self.get_last_activities(employee, 3) + self.assertEqual(len(activities), 3) + self.assertEqual(activities[0].user_id, self.user_manager) + self.assertEqual(activities[1].user_id, self.user_coach) + self.assertEqual(activities[2].user_id, employee.user_id) + + # Cases with errors + self.employee_1.parent_id = False + self.employee_1.coach_id = False + form = self._instantiate_activity_schedule_wizard(employees) + form.plan_id = self.plan_onboarding + self.assertTrue(form.has_error) + n_error = form.error.count('
  • ') + self.assertEqual(n_error, 2) + self.assertIn(f'Manager of employee {self.employee_1.name} is not set.', form.error) + self.assertIn(f'Coach of employee {self.employee_1.name} is not set.', form.error) + with self.assertRaises(ValidationError): + form.save() + self.employee_1.parent_id = self.employee_manager + self.employee_1.coach_id = self.employee_coach + self.employee_coach.user_id = False + self.employee_manager.user_id = False + form = self._instantiate_activity_schedule_wizard(employees) + form.plan_id = self.plan_onboarding + self.assertTrue(form.has_error) + n_error = form.error.count('
  • ') + self.assertEqual(n_error, 2 * len(employees)) + self.assertIn(f"The user of {self.employee_1.name}'s coach is not set.", form.error) + self.assertIn(f'The manager of {self.employee_1.name} should be linked to a user.', form.error) + if len(employees) > 1: + self.assertIn(f"The user of {self.employee_2.name}'s coach is not set.", form.error) + self.assertIn(f'The manager of {self.employee_2.name} should be linked to a user.', form.error) + with self.assertRaises(ValidationError): + form.save() + self.employee_coach.user_id = self.user_coach + self.employee_manager.user_id = self.user_manager diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml index 10d5ae7bcfa..0196033953b 100644 --- a/addons/hr/views/hr_employee_views.xml +++ b/addons/hr/views/hr_employee_views.xml @@ -59,9 +59,7 @@ -
    -
    +
    @@ -252,9 +250,6 @@ hr.employee -
    -
    diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index 6360e83792c..410cf177e9e 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -130,15 +130,15 @@ id="menu_config_plan" name="Activity Planning" parent="menu_human_resources_configuration" - groups="group_hr_user" + groups="group_hr_manager" sequence="100"/> diff --git a/addons/hr/views/mail_activity_plan_views.xml b/addons/hr/views/mail_activity_plan_views.xml index 1cb822ab2a9..af578ab4252 100644 --- a/addons/hr/views/mail_activity_plan_views.xml +++ b/addons/hr/views/mail_activity_plan_views.xml @@ -1,109 +1,53 @@ - - - hr.plan.view.search - hr.plan + + mail.activity.plan.template.view.form.inherit.hr + mail.activity.plan.template + - - - - - - + + + + - - hr.plan.view.tree - hr.plan + + mail.activity.plan.view.form.inherit.hr + mail.activity.plan + - - - + + + + + res_model == 'hr.employee' + + + + + + mail.activity.plan.view.tree.inherit.hr + mail.activity.plan + + + - - -
    +
    - - hr.plan.view.form - hr.plan - -
    - - - -
    -
    - - - - - - - - - - - - - - - - - - -
    - -
    -
    - - - hr.plan.activity.type.view.tree - hr.plan.activity.type - - - - - - - - - - - - hr.plan.activity.type.view.form - hr.plan.activity.type - -
    - - - - - - - - - - - - -
    -
    - - - Plans - hr.plan + + On/Offboarding Plans + mail.activity.plan tree,form - + + {'default_res_model': 'hr.employee'} + [('res_model', '=', 'hr.employee')]

    Add a new plan @@ -111,96 +55,13 @@ - - Plan Activities - hr.plan.activity.type - tree,form - -

    - Add a new plan activity -

    -
    -
    - - - hr.plan.employee.activity - - - - -
    -
    -
    -

    - -
    -
    - -
    -
    - -
    - - Next Activity: - -
    -
    -
    -
    -
    -
    -
    - - - hr.plan.employee.activity - - - - - - - - - - - - hr.plan.employee.activity - -
    - - - - - - - -
    - -
    -
    -
    -
    - - - mail.activity.type.form.inherit - mail.activity.type - - primary - 50 - - - chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id - - - chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id - - - chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id - - - chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id - - + + + Launch Plan + mail.activity.schedule + form + {'plan_mode': True} + new diff --git a/addons/hr/wizard/__init__.py b/addons/hr/wizard/__init__.py index 09e602381ea..b3454814216 100644 --- a/addons/hr/wizard/__init__.py +++ b/addons/hr/wizard/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import hr_plan_wizard from . import hr_departure_wizard +from . import mail_activity_schedule diff --git a/addons/hr/wizard/mail_activity_schedule.py b/addons/hr/wizard/mail_activity_schedule.py index b51e4390ab9..fde323115a4 100644 --- a/addons/hr/wizard/mail_activity_schedule.py +++ b/addons/hr/wizard/mail_activity_schedule.py @@ -1,137 +1,33 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from markupsafe import Markup - -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError +from odoo import api, fields, models +from odoo.osv import expression -class HrPlanWizard(models.TransientModel): - _name = 'hr.plan.wizard' - _description = 'Plan Wizard' +class MailActivitySchedule(models.TransientModel): + _inherit = 'mail.activity.schedule' - def _default_plan_id(self): - # We know that all employees belong to the same company - employees = self.env['hr.employee'].browse(self.env.context.get('active_ids') if self.env.context.get('active_ids') else []) - if not employees: - return None - if len(employees.department_id) > 1: - return self.env['hr.plan'].search([ - ('company_id', '=', employees[0].company_id.id), - ('department_id', '=', False) - ], limit=1) - else: - return self.env['hr.plan'].search([ - ('company_id', '=', employees[0].company_id.id), - '|', - ('department_id', '=', employees[0].department_id.id), - ('department_id', '=', False) - ], limit=1) - - plan_id = fields.Many2one('hr.plan', default=lambda self: self._default_plan_id(), - check_company=True, - domain="['|', ('department_id', '=', department_id), ('department_id', '=', False)]") department_id = fields.Many2one('hr.department', compute='_compute_department_id') - employee_ids = fields.Many2many( - 'hr.employee', 'hr_employee_hr_plan_wizard_rel', 'employee_id', 'plan_wizard_id', string='Employee', required=True, - default=lambda self: self.env.context.get('active_ids', []), - ) - company_id = fields.Many2one('res.company', 'Company', compute='_compute_company_id', required=True) - warning = fields.Html(compute='_compute_warning') - due_date = fields.Date(string="Due Date") - @api.depends('employee_ids') + @api.depends('department_id') + def _compute_plan_available_ids(self): + todo = self.filtered(lambda s: s.res_model == 'hr.employee') + for scheduler in todo: + base_domain = self._get_plan_available_base_domain() + if not scheduler.department_id: + final_domain = expression.AND([base_domain, [('department_id', '=', False)]]) + else: + final_domain = expression.AND([base_domain, ['|', ('department_id', '=', False), ('department_id', '=', self.department_id.id)]]) + scheduler.plan_available_ids = self.env['mail.activity.plan'].search(final_domain) + super(MailActivitySchedule, self - todo)._compute_plan_available_ids() + + @api.depends('res_model_id', 'res_ids') def _compute_department_id(self): for wizard in self: - all_departments = wizard.employee_ids.department_id - wizard.department_id = False if len(all_departments) > 1 else all_departments - - @api.constrains('employee_ids') - def _check_employee_companies(self): - for wizard in self: - if len(wizard.employee_ids.mapped('company_id')) > 1: - raise ValidationError(_('The employees should belong to the same company.')) - - @api.depends('employee_ids') - def _compute_company_id(self): - for wizard in self: - wizard.company_id = wizard.employee_ids and wizard.employee_ids[0].company_id or self.env.company - - def _get_warnings(self): - self.ensure_one() - warnings = set() - for employee in self.employee_ids: - for activity_type in self.plan_id.plan_activity_type_ids: - warning = activity_type.get_responsible_id(employee)['warning'] - if warning: - warnings.add(warning) - return warnings - - @api.depends('employee_ids', 'plan_id') - def _compute_warning(self): - for wizard in self: - warnings = wizard._get_warnings() - - if warnings: - warning_display = _('The plan %s cannot be launched:
      ', wizard.plan_id.name) - for warning in warnings: - warning_display += '
    • %s
    • ' % warning - warning_display += '
    ' + if wizard.res_model == 'hr.employee': + applied_on = wizard._get_applied_on_records() + all_departments = applied_on.department_id + wizard.department_id = False if len(all_departments) > 1 else all_departments else: - warning_display = False - wizard.warning = warning_display - - def _get_activities_to_schedule(self): - return self.plan_id.plan_activity_type_ids - - def action_launch(self): - self.ensure_one() - for employee in self.employee_ids: - body = _('The plan %s has been started', self.plan_id.name) - activities = set() - for activity_type in self._get_activities_to_schedule(): - responsible = activity_type.get_responsible_id(employee)['responsible'] - record = employee - if not self.env['hr.employee'].with_user(responsible).check_access_rights('read', raise_exception=False): - record = self.env['hr.plan.employee.activity'].sudo().search([('employee_id', '=', employee.id)], limit=1) - if not record: - record = self.env['hr.plan.employee.activity'].sudo().create({ - 'employee_id': employee.id, - }) - date_deadline = self.env['mail.activity']._calculate_date_deadline(activity_type.activity_type_id) if not self.due_date else self.due_date - record.activity_schedule( - activity_type_id=activity_type.activity_type_id.id, - summary=activity_type.summary, - note=activity_type.note, - user_id=responsible.id, - date_deadline=date_deadline - ) - activity = _('%(activity)s, assigned to %(name)s, due on the %(deadline)s', activity=activity_type.summary, name=responsible.name, deadline=date_deadline) - activities.add(activity) - - if activities: - body += Markup('
      ') - for activity in activities: - body += Markup('
    • %s
    • ') % activity - body += Markup('
    ') - employee.message_post(body=body) - - if len(self.employee_ids) == 1: - return { - 'type': 'ir.actions.act_window', - 'res_model': 'hr.employee', - 'res_id': self.employee_ids.id, - 'name': self.employee_ids.display_name, - 'view_mode': 'form', - 'views': [(False, "form")], - } - - return { - 'type': 'ir.actions.act_window', - 'res_model': 'hr.employee', - 'name': _('Launch Plans'), - 'view_mode': 'tree,form', - 'target': 'current', - 'domain': [('id', 'in', self.employee_ids.ids)], - } + wizard.department_id = False diff --git a/addons/hr/wizard/mail_activity_schedule_views.xml b/addons/hr/wizard/mail_activity_schedule_views.xml index e199ebc4756..145d6cf4f54 100644 --- a/addons/hr/wizard/mail_activity_schedule_views.xml +++ b/addons/hr/wizard/mail_activity_schedule_views.xml @@ -1,37 +1,15 @@ - - plan wizard - hr.plan.wizard + + mail.activity.schedule.view.form.inherit.hr + mail.activity.schedule + -
    - - - - - - - - - - -
    -
    -
    + + +
    - - - Launch Plan - hr.plan.wizard - form - new -
    diff --git a/addons/hr_contract/tests/__init__.py b/addons/hr_contract/tests/__init__.py index 353b82263a3..7cd66c1dd6b 100644 --- a/addons/hr_contract/tests/__init__.py +++ b/addons/hr_contract/tests/__init__.py @@ -5,4 +5,5 @@ from . import test_access from . import test_auto_status from . import test_contract from . import test_calendar_sync +from . import test_mail_activity_plan from . import test_resource diff --git a/addons/hr_contract/tests/test_mail_activity_plan.py b/addons/hr_contract/tests/test_mail_activity_plan.py new file mode 100644 index 00000000000..4556a0ef361 --- /dev/null +++ b/addons/hr_contract/tests/test_mail_activity_plan.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from freezegun import freeze_time + +from odoo import fields +from odoo.addons.hr.tests.test_mail_activity_plan import ActivityScheduleHRCase +from odoo.tests import tagged, users + + +@tagged('mail_activity', 'mail_activity_plan') +class TestActivitySchedule(ActivityScheduleHRCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.employee_3 = cls.employee_coach + cls.employee_4 = cls.employee_manager + cls.employee_4.coach_id = cls.employee_coach + for employee, date_start in ((cls.employee_1, '2023-08-01'), + (cls.employee_2, '2023-09-01'), + (cls.employee_3, '2023-12-01'), + (cls.employee_4, '2024-01-01')): + employee.contract_ids = cls.env['hr.contract'].create({ + 'employee_id': employee.id, + 'date_end': fields.Date.from_string('2025-12-31'), + 'date_start': fields.Date.from_string(date_start), + 'name': 'Contract', + 'state': 'draft', + 'wage': 1, + }) + + @freeze_time('2023-08-31') + @users('admin') + def test_default_due_date(self): + for employees, plan_date_deadline in ( + (self.employee_1, '2023-09-30'), + (self.employee_2, '2023-09-30'), + (self.employee_3, '2023-12-01'), + (self.employee_4, '2024-01-01'), + (self.employee_1 + self.employee_2 + self.employee_3, '2023-09-30'), + (self.employee_2 + self.employee_3, '2023-09-30'), + (self.employee_1 + self.employee_3, '2023-09-30'), + (self.employee_3 + self.employee_4, '2023-12-01'), + (self.employee_4 + self.employee_3, '2023-12-01'), + ): + with self._instantiate_activity_schedule_wizard(employees) as form: + form.plan_id = self.plan_onboarding + self.assertEqual(form.plan_date_deadline, fields.Date.from_string(plan_date_deadline)) + + # not applicable on other models + customers = self.env['res.partner'].create([ + {'name': 'Customer1',}, + {'name': 'Customer2',}, + ]) + with self._instantiate_activity_schedule_wizard(customers) as form: + form.plan_id = self.plan_party + self.assertFalse(form.plan_date_deadline) diff --git a/addons/hr_contract/wizard/__init__.py b/addons/hr_contract/wizard/__init__.py index 78a353a93f0..b3454814216 100644 --- a/addons/hr_contract/wizard/__init__.py +++ b/addons/hr_contract/wizard/__init__.py @@ -2,4 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import hr_departure_wizard -from . import hr_plan_wizard +from . import mail_activity_schedule diff --git a/addons/hr_contract/wizard/mail_activity_schedule.py b/addons/hr_contract/wizard/mail_activity_schedule.py index da51faf357a..bf11ff686be 100644 --- a/addons/hr_contract/wizard/mail_activity_schedule.py +++ b/addons/hr_contract/wizard/mail_activity_schedule.py @@ -1,28 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import fields, models from dateutil.relativedelta import relativedelta -class HrPlanWizard(models.TransientModel): - _inherit = 'hr.plan.wizard' +class MailActivitySchedule(models.TransientModel): + _inherit = 'mail.activity.schedule' - @api.model - def default_get(self, field_list=None): - result = super(HrPlanWizard, self).default_get(field_list) - selected_employees = self.env['hr.employee'].browse(self.env.context['active_ids']) - start_dates = selected_employees.filtered('first_contract_date').mapped('first_contract_date') - if start_dates: - today = fields.Date.today() - planned_due_date = start_dates[0] if len(set(start_dates)) == len(selected_employees) else min(start_dates) - - if planned_due_date < today: - result['due_date'] = today + relativedelta(days=+30) - else: - if (planned_due_date-today).days < 30: - result['due_date'] = today + relativedelta(days=+30) + def _compute_plan_date_deadline(self): + todo = self.filtered(lambda s: s.res_model == 'hr.employee') + for scheduler in todo: + selected_employees = scheduler._get_applied_on_records() + start_dates = selected_employees.filtered('first_contract_date').mapped('first_contract_date') + if start_dates: + today = fields.Date.today() + planned_due_date = min(start_dates) + if planned_due_date < today or (planned_due_date - today).days < 30: + scheduler.plan_date_deadline = today + relativedelta(days=+30) else: - result['due_date'] = planned_due_date - - return result + scheduler.plan_date_deadline = planned_due_date + super(MailActivitySchedule, self - todo)._compute_plan_date_deadline() diff --git a/addons/hr_fleet/data/hr_fleet_data.xml b/addons/hr_fleet/data/hr_fleet_data.xml index 5dd61ac9b0e..bf402238abe 100644 --- a/addons/hr_fleet/data/hr_fleet_data.xml +++ b/addons/hr_fleet/data/hr_fleet_data.xml @@ -2,9 +2,9 @@ - + Take Back Fleet - fleet_manager + fleet_manager diff --git a/addons/hr_fleet/data/hr_fleet_demo.xml b/addons/hr_fleet/data/hr_fleet_demo.xml index 85516aff431..d63afb1f7ba 100644 --- a/addons/hr_fleet/data/hr_fleet_demo.xml +++ b/addons/hr_fleet/data/hr_fleet_demo.xml @@ -21,9 +21,9 @@ - + Fleet Training - fleet_manager + fleet_manager diff --git a/addons/hr_fleet/models/__init__.py b/addons/hr_fleet/models/__init__.py index 93d6b757f88..0c6a5a31f08 100644 --- a/addons/hr_fleet/models/__init__.py +++ b/addons/hr_fleet/models/__init__.py @@ -8,4 +8,4 @@ from . import fleet_vehicle from . import fleet_vehicle_log_contract from . import fleet_vehicle_log_services from . import fleet_vehicle_odometer -from . import hr_plan_activity_type +from . import mail_activity_plan_template diff --git a/addons/hr_fleet/models/mail_activity_plan_template.py b/addons/hr_fleet/models/mail_activity_plan_template.py index 9f528c9f6b9..02f757ceb9d 100644 --- a/addons/hr_fleet/models/mail_activity_plan_template.py +++ b/addons/hr_fleet/models/mail_activity_plan_template.py @@ -1,25 +1,35 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models, _ +from odoo import api, fields, models, _ +from odoo import exceptions -class HrPlanActivityType(models.Model): - _inherit = 'hr.plan.activity.type' +class MailActivityPlanTemplate(models.Model): + _inherit = 'mail.activity.plan.template' - responsible = fields.Selection(selection_add=[('fleet_manager', "Fleet Manager"), ('employee',)], ondelete={'fleet_manager': 'set default'}) + responsible_type = fields.Selection( + selection_add=[('fleet_manager', "Fleet Manager")], + ondelete={'fleet_manager': 'set default'}) - def get_responsible_id(self, employee): - if self.responsible == 'fleet_manager': + @api.constrains('plan_id', 'responsible_type') + def _check_responsible_hr_fleet(self): + """ Ensure that hr types are used only on employee model """ + for template in self.filtered(lambda tpl: tpl.plan_id.res_model != 'hr.employee'): + if template.responsible_type == 'fleet_manager': + raise exceptions.ValidationError(_('Fleet manager responsible is limited to Employee plans.')) + + def _determine_responsible(self, on_demand_responsible, employee): + if self.responsible_type == 'fleet_manager' and self.plan_id.res_model == 'hr.employee': employee_id = self.env['hr.employee'].browse(employee._origin.id) vehicle = employee_id.car_ids[:1] - warning = False + error = False if not vehicle: - warning = _('Employee %s is not linked to a vehicle.', employee_id.name) + error = _('Employee %s is not linked to a vehicle.', employee_id.name) if vehicle and not vehicle.manager_id: - warning = _("Employee's vehicle %s is not linked to a fleet manager.", employee_id.name) + error = _("Employee's vehicle %s is not linked to a fleet manager.", employee_id.name) return { 'responsible': vehicle.manager_id, - 'warning': warning, + 'error': error, } - return super().get_responsible_id(employee) + return super()._determine_responsible(on_demand_responsible, employee) diff --git a/addons/hr_fleet/tests/__init__.py b/addons/hr_fleet/tests/__init__.py new file mode 100644 index 00000000000..29cb3694159 --- /dev/null +++ b/addons/hr_fleet/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_mail_activity_plan diff --git a/addons/hr_fleet/tests/test_mail_activity_plan.py b/addons/hr_fleet/tests/test_mail_activity_plan.py new file mode 100644 index 00000000000..ddf4ae92864 --- /dev/null +++ b/addons/hr_fleet/tests/test_mail_activity_plan.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import Command +from odoo.addons.hr.tests.test_mail_activity_plan import ActivityScheduleHRCase +from odoo.exceptions import ValidationError +from odoo.tests import tagged, users + + +@tagged('mail_activity', 'mail_activity_plan') +class TestActivitySchedule(ActivityScheduleHRCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.plan_fleet = cls.env['mail.activity.plan'].create({ + 'name': 'Car return plan', + 'res_model': 'hr.employee', + 'template_ids': [Command.create({ + 'activity_type_id': cls.activity_type_todo.id, + 'responsible_type': 'fleet_manager', + 'summary': 'Car return', + })] + }) + cls.brand = cls.env["fleet.vehicle.model.brand"].create({"name": "Audi"}) + for employee in (cls.employee_1, cls.employee_2): + car = cls.env["fleet.vehicle"].create({ + "driver_id": employee.user_id.partner_id.id, + "model_id": cls.env["fleet.vehicle.model"].create({ + "brand_id": cls.brand.id, + "name": "A3", + }).id, + "manager_id": cls.user_manager.id, + "plan_to_change_car": False, + }) + employee.car_ids = car + + @users('admin') + def test_responsible(self): + """ Check that the responsible is correctly configured. """ + employees = (self.employee_1 + self.employee_2).with_env(self.env) + + # Happy case + form = self._instantiate_activity_schedule_wizard(employees) + form.plan_id = self.plan_fleet + self.assertEqual(form.plan_assignation_summary, + '
    • To-Do - Fleet Manager: Car return
    ') + self.assertFalse(form.has_error) + wizard = form.save() + wizard.action_schedule_plan() + for employee in employees: + activities = self.get_last_activities(employee, 1) + self.assertEqual(len(activities), 1) + self.assertEqual(activities[0].user_id, self.user_manager) + + # Cases with errors + self.employee_1.car_ids[0].manager_id = False + form = self._instantiate_activity_schedule_wizard(employees) + form.plan_id = self.plan_fleet + self.assertTrue(form.has_error) + n_error = form.error.count('
  • ') + self.assertEqual(n_error, 1) + self.assertIn(f"Employee's vehicle {self.employee_1.name} is not linked to a fleet manager.", form.error) + with self.assertRaises(ValidationError): + form.save() + + self.employee_1.car_ids = self.env["fleet.vehicle"] + form = self._instantiate_activity_schedule_wizard(employees) + form.plan_id = self.plan_fleet + self.assertTrue(form.has_error) + n_error = form.error.count('
  • ') + self.assertEqual(n_error, 1) + self.assertIn(f"Employee {self.employee_1.name} is not linked to a vehicle.", form.error) + with self.assertRaises(ValidationError): + form.save()