[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
This commit is contained in:
Pierre-Yves Dufays
2023-10-12 16:12:09 +00:00
committed by Thibault Delavallée
parent 4d2ac63de6
commit 7053e37df1
27 changed files with 604 additions and 535 deletions
+2 -2
View File
@@ -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',
+19 -12
View File
@@ -15,41 +15,48 @@
<field name="image_1920" eval="obj(ref('base.partner_admin')).image_1920" model="res.partner"/>
</record>
<record id="onboarding_plan" model='hr.plan'>
<record id="onboarding_plan" model="mail.activity.plan">
<field name="name">Onboarding</field>
<field name="res_model">hr.employee</field>
</record>
<record id="onboarding_setup_it_materials" model="hr.plan.activity.type">
<record id="onboarding_setup_it_materials" model="mail.activity.plan.template">
<field name="sequence">10</field>
<field name="summary">Setup IT Materials</field>
<field name="responsible">manager</field>
<field name="responsible_type">manager</field>
<field name="plan_id" ref="onboarding_plan"/>
</record>
<record id="onboarding_plan_training" model="hr.plan.activity.type">
<record id="onboarding_plan_training" model="mail.activity.plan.template">
<field name="sequence">20</field>
<field name="summary">Plan Training</field>
<field name="responsible">manager</field>
<field name="responsible_type">manager</field>
<field name="plan_id" ref="onboarding_plan"/>
</record>
<record id="onboarding_training" model="hr.plan.activity.type">
<record id="onboarding_training" model="mail.activity.plan.template">
<field name="sequence">30</field>
<field name="summary">Training</field>
<field name="responsible">employee</field>
<field name="responsible_type">employee</field>
<field name="plan_id" ref="onboarding_plan"/>
</record>
<record id="offboarding_plan" model='hr.plan'>
<record id="offboarding_plan" model="mail.activity.plan">
<field name="name">Offboarding</field>
<field name="res_model">hr.employee</field>
</record>
<record id="offboarding_setup_compute_out_delais" model="hr.plan.activity.type">
<record id="offboarding_setup_compute_out_delais" model="mail.activity.plan.template">
<field name="sequence">10</field>
<field name="summary">Compute Out Delais</field>
<field name="responsible">manager</field>
<field name="responsible_type">manager</field>
<field name="plan_id" ref="offboarding_plan"/>
</record>
<record id="offboarding_take_back_hr_materials" model="hr.plan.activity.type">
<record id="offboarding_take_back_hr_materials" model="mail.activity.plan.template">
<field name="sequence">20</field>
<field name="summary">Take Back HR Materials</field>
<field name="responsible">manager</field>
<field name="responsible_type">manager</field>
<field name="plan_id" ref="offboarding_plan"/>
</record>
+2 -3
View File
@@ -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
+3 -3
View File
@@ -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
@@ -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()
+31 -20
View File
@@ -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
+29 -48
View File
@@ -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,
}
+17 -24
View File
@@ -49,30 +49,6 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_plan_company_rule" model="ir.rule">
<field name="name">Plan multi-company rule</field>
<field name="model_id" ref="model_hr_plan"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_plan_activity_type_company_rule" model="ir.rule">
<field name="name">Plan Activity Type multi-company rule</field>
<field name="model_id" ref="model_hr_plan_activity_type"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_plan_employee_activity_company_rule" model="ir.rule">
<field name="name">Plan Employee Activity: multi-company rule</field>
<field name="model_id" ref="model_hr_plan_employee_activity"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_plan_employee_activity_own_user" model="ir.rule">
<field name="name">Plan Employee Activity: own employee or subordinates</field>
<field name="model_id" ref="model_hr_plan_employee_activity"/>
<field name="domain_force">['|', ('user_id', '=', user.id), ('activity_ids.user_id', '=', user.id)]</field>
</record>
<record id="ir_rule_res_partner_bank_internal_users" model="ir.rule">
<field name="name">HR: Prevent non HR officers from accessing employee bank accounts</field>
<field name="model_id" ref="base.model_res_partner_bank"/>
@@ -92,5 +68,22 @@
<field name="model_id" ref="model_hr_contract_type"/>
<field name="domain_force">['|', ('country_id', '=', False), ('country_id', 'in', user.env.companies.country_id.ids)]</field>
</record>
<record id="mail_plan_rule_group_hr_manager" model="ir.rule">
<field name="name">Manager can edit employee plan</field>
<field name="groups" eval="[(4, ref('group_hr_manager'))]"/>
<field name="model_id" ref="mail.model_mail_activity_plan"/>
<field name="domain_force">[('res_model', '=', 'hr.employee')]</field>
<field name="perm_read" eval="False"/>
</record>
<record id="mail_plan_templates_rule_group_hr_manager" model="ir.rule">
<field name="name">Manager can edit employee plan template</field>
<field name="groups" eval="[(4, ref('group_hr_manager'))]"/>
<field name="model_id" ref="mail.model_mail_activity_plan_template"/>
<field name="domain_force">[('plan_id.res_model', '=', 'hr.employee')]</field>
<field name="perm_read" eval="False"/>
</record>
</data>
</odoo>
+2 -6
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 access_hr_department_user hr.department.user model_hr_department group_hr_user 1 1 1 1
9 access_hr_department_employee hr.department.employee model_hr_department base.group_user 1 0 0 0
10 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
11 access_hr_departure_wizard access.hr.departure.wizard model_hr_departure_wizard hr.group_hr_user 1 1 1 0
12 access_hr_work_location_user access_hr_work_location_user model_hr_work_location base.group_user 1 0 0 0
13 access_hr_work_location_manager access_hr_work_location_manager model_hr_work_location group_hr_manager 1 1 1 1
14 access_hr_departure_reason access_hr_departure_reason_user model_hr_departure_reason group_hr_user 1 1 1 1
15 access_hr_contract_type_manager hr.contract.type.manager model_hr_contract_type hr.group_hr_user 1 1 1 1
16 access_hr_plan_employee_activity access_mail_activity_plan_hr_manager hr.plan.employee.activity mail.activity.plan.hr.manager model_hr_plan_employee_activity mail.model_mail_activity_plan base.group_user hr.group_hr_manager 1 0 1 0 1 0 1
17 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
+1
View File
@@ -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
+240
View File
@@ -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,
'<ul><li>To-Do - Manager: Plan training</li><li>To-Do - Coach: Training</li>'
'<li>To-Do - Employee: Send feedback to the manager</li></ul>')
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('<li>')
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('<li>')
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
+1 -6
View File
@@ -59,9 +59,7 @@
<field name="last_activity_time" invisible="1"/>
<field name="last_activity" invisible="1"/>
<field name="work_contact_id" invisible="1"/>
<header>
<button name="%(plan_wizard_action)d" string="Launch Plan" type="action" groups="hr.group_hr_user" invisible="not active"/>
</header>
<header><!-- For extension --></header>
<sheet>
<div name="button_box" class="oe_button_box">
</div>
@@ -252,9 +250,6 @@
<field name="model">hr.employee</field>
<field name="arch" type="xml">
<tree string="Employees" expand="context.get('expand', False)" multi_edit="1" sample="1" js_class="hr_employee_list">
<header>
<button name="%(plan_wizard_action)d" type="action" string="Launch Plan" context="{'action_plan': True}"/>
</header>
<field name="name" readonly="1"/>
<field name="work_phone" class="o_force_ltr" readonly="1" optional="show"/>
<field name="work_email"/>
+3 -3
View File
@@ -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"/>
<menuitem
id="menu_config_plan_plan"
name="On/Offboarding Plans"
action="hr_plan_action"
action="mail_activity_plan_action"
parent="menu_config_plan"
groups="group_hr_user"
groups="group_hr_manager"
sequence="100"/>
</data>
+44 -183
View File
@@ -1,109 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="hr_plan_view_search" model="ir.ui.view">
<field name="name">hr.plan.view.search</field>
<field name="model">hr.plan</field>
<record id="mail_activity_plan_template_view_form" model="ir.ui.view">
<field name="name">mail.activity.plan.template.view.form.inherit.hr</field>
<field name="model">mail.activity.plan.template</field>
<field name="inherit_id" ref="mail.mail_activity_plan_template_view_form"/>
<field name="arch" type="xml">
<search string="Plan">
<field name="name"/>
<field name="department_id"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
<xpath expr="//field[@name='responsible_type']" position="replace">
<field name="responsible_type"
invisible="res_model != 'hr.employee'"/>
<field name="responsible_type" widget="filterable_selection"
options="{'whitelisted_values': ['on_demand', 'other']}"
invisible="res_model == 'hr.employee'"/>
</xpath>
</field>
</record>
<record id="hr_plan_view_tree" model="ir.ui.view">
<field name="name">hr.plan.view.tree</field>
<field name="model">hr.plan</field>
<record id="mail_activity_plan_view_form" model="ir.ui.view">
<field name="name">mail.activity.plan.view.form.inherit.hr</field>
<field name="model">mail.activity.plan</field>
<field name="inherit_id" ref="mail.mail_activity_plan_view_form"/>
<field name="arch" type="xml">
<tree string="Planning">
<field name="name"/>
<field name="steps_count"/>
<xpath expr="//group[@name='group_plan_fields']" position="inside">
<field name="department_id" invisible="res_model != 'hr.employee'"/>
</xpath>
<xpath expr="//field[@name='res_model']" position="attributes">
<attribute name="invisible">res_model == 'hr.employee'</attribute>
</xpath>
</field>
</record>
<record id="mail_activity_plan_view_tree" model="ir.ui.view">
<field name="name">mail.activity.plan.view.tree.inherit.hr</field>
<field name="model">mail.activity.plan</field>
<field name="inherit_id" ref="mail.mail_activity_plan_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='steps_count']" position="after">
<field name="department_id" optional="show"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="company_id" column_invisible="True"/>
</tree>
</xpath>
</field>
</record>
<record id="hr_plan_view_form" model="ir.ui.view">
<field name="name">hr.plan.view.form</field>
<field name="model">hr.plan</field>
<field name="arch" type="xml">
<form string="Planning">
<field name="company_id" invisible="1"/>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<label for="name" string="Plan Name"/>
<h1>
<field name="name" placeholder="e.g. Onboarding"/>
</h1>
</div>
<group>
<field name="department_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" invisible="1"/>
</group>
<notebook>
<page string="Activities To Generate">
<field name="plan_activity_type_ids" nolabel="1">
<tree>
<field name="company_id" column_invisible="True"/>
<field name="activity_type_id" context="{'form_view_ref': 'hr.mail_activity_type_form_inherit'}"/>
<field name="summary"/>
<field name="responsible"/>
<field name="responsible_id" readonly="responsible != 'other'" optional="hide"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="hr_plan_activity_type_view_tree" model="ir.ui.view">
<field name="name">hr.plan.activity.type.view.tree</field>
<field name="model">hr.plan.activity.type</field>
<field name="arch" type="xml">
<tree string="Activities">
<field name="activity_type_id" context="{'form_view_ref': 'hr.mail_activity_type_form_inherit'}"/>
<field name="summary"/>
<field name="responsible"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="hr_plan_activity_type_view_form" model="ir.ui.view">
<field name="name">hr.plan.activity.type.view.form</field>
<field name="model">hr.plan.activity.type</field>
<field name="arch" type="xml">
<form string="Activity">
<field name="company_id" invisible="1"/>
<sheet>
<group>
<field name="activity_type_id" context="{'form_view_ref': 'hr.mail_activity_type_form_inherit'}"/>
<field name="summary"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="responsible"/>
<field name="responsible_id" invisible="responsible != 'other'"/>
<field name="note"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_plan_action" model="ir.actions.act_window">
<field name="name">Plans</field>
<field name="res_model">hr.plan</field>
<record id="mail_activity_plan_action" model="ir.actions.act_window">
<field name="name">On/Offboarding Plans</field>
<field name="res_model">mail.activity.plan</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="hr_plan_view_search"/>
<field name="search_view_id" ref="mail.mail_activity_plan_view_search"/>
<field name="context">{'default_res_model': 'hr.employee'}</field>
<field name="domain">[('res_model', '=', 'hr.employee')]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new plan
@@ -111,96 +55,13 @@
</field>
</record>
<record id="hr_plan_activity_type_action" model="ir.actions.act_window">
<field name="name">Plan Activities</field>
<field name="res_model">hr.plan.activity.type</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new plan activity
</p>
</field>
</record>
<record id="hr_plan_employee_activity_kanban" model="ir.ui.view">
<field name="model">hr.plan.employee.activity</field>
<field name="arch" type="xml">
<kanban>
<templates>
<t t-name="kanban-box">
<div class="d-flex flex-column">
<div class="d-flex flex-row">
<div class="flex-grow-1">
<h3 class="h3 mb-0"><field name="employee_id"/></h3>
<span class="fst-italic"><field name="department_id"/></span>
</div>
<div style="max-width: 64px; max-height: 64px;">
<field name="image_128" widget="image" class="oe_avatar"/>
</div>
</div>
<field name="summary"/>
<div class="mt-auto d-flex flex-row-reverse">
<span>
Next Activity: <field name="activity_ids" widget="kanban_activity"/>
</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="hr_plan_employee_activity_list" model="ir.ui.view">
<field name="model">hr.plan.employee.activity</field>
<field name="arch" type="xml">
<tree>
<field name="employee_id" widget="many2one_avatar_employee"/>
<field name="department_id"/>
<field name="activity_ids" widget="list_activity"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="hr_plan_employee_activity_form" model="ir.ui.view">
<field name="model">hr.plan.employee.activity</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="employee_id" widget="many2one_avatar_employee"/>
<field name="department_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="activity_ids"/>
</div>
</form>
</field>
</record>
<record id="mail_activity_type_form_inherit" model="ir.ui.view">
<field name="name">mail.activity.type.form.inherit</field>
<field name="model">mail.activity.type</field>
<field name="inherit_id" ref="mail.mail_activity_type_view_form"/>
<field name="mode">primary</field>
<field name="priority">50</field>
<field name="arch" type="xml">
<xpath expr="//label[@for='delay_count']" position="attributes">
<attribute name="invisible">chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id</attribute> </xpath>
<field name="delay_count" position="attributes">
<attribute name="invisible">chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id</attribute>
</field>
<field name="delay_from" position="attributes">
<attribute name="invisible">chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id</attribute>
</field>
<field name="delay_unit" position="attributes">
<attribute name="invisible">chaining_type == 'suggest' and not suggested_next_type_ids or chaining_type == 'trigger' and not triggered_next_type_id</attribute>
</field>
</field>
<!-- This id is referenced in chatter message as a link to launch the plan -->
<record id="plan_wizard_action" model="ir.actions.act_window">
<field name="name">Launch Plan</field>
<field name="res_model">mail.activity.schedule</field>
<field name="view_mode">form</field>
<field name="context">{'plan_mode': True}</field>
<field name="target">new</field>
</record>
</data>
</odoo>
+1 -1
View File
@@ -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
+22 -126
View File
@@ -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: <br><ul>', wizard.plan_id.name)
for warning in warnings:
warning_display += '<li>%s</li>' % warning
warning_display += '</ul>'
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('<ul>')
for activity in activities:
body += Markup('<li>%s</li>') % activity
body += Markup('</ul>')
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
@@ -1,37 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="plan_wizard" model="ir.ui.view">
<field name="name">plan wizard</field>
<field name="model">hr.plan.wizard</field>
<record id="mail_activity_schedule_view_form" model="ir.ui.view">
<field name="name">mail.activity.schedule.view.form.inherit.hr</field>
<field name="model">mail.activity.schedule</field>
<field name="inherit_id" ref="mail.mail_activity_schedule_view_form"/>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="department_id" invisible="not department_id"/>
<field name="plan_id" widget="radio"/>
<field name="due_date"/>
<field name="employee_ids" invisible="1"/>
<field name="company_id" invisible="1"/>
</group>
<div role="alert" class="alert alert-danger mb8" invisible="not warning">
<field name="warning"/>
</div>
</sheet>
<footer>
<button name="action_launch" string="Launch Plan" type="object" class="oe_highlight" invisible="warning" groups="hr.group_hr_user" data-hotkey="q"/>
<button name="action_launch" string="Launch Plan" type="object" class="oe_highlight disabled" invisible="not warning" groups="hr.group_hr_user" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
<xpath expr="//field[@name='plan_available_ids']" position="after">
<field name="department_id" invisible="1"/>
</xpath>
</field>
</record>
<record id="plan_wizard_action" model="ir.actions.act_window">
<field name="name">Launch Plan</field>
<field name="res_model">hr.plan.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</odoo>
+1
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -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
@@ -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()
+2 -2
View File
@@ -2,9 +2,9 @@
<odoo>
<data noupdate="1">
<record id="offboarding_fleet" model="hr.plan.activity.type">
<record id="offboarding_fleet" model="mail.activity.plan.template">
<field name="summary">Take Back Fleet</field>
<field name="responsible">fleet_manager</field>
<field name="responsible_type">fleet_manager</field>
<field name="plan_id" ref="hr.offboarding_plan"/>
</record>
+2 -2
View File
@@ -21,9 +21,9 @@
<value model="fleet.vehicle.assignation.log" eval="obj().search([('driver_employee_id', '=', False), ('driver_id', '!=', False)]).ids"/>
</function>
<record id="onboarding_fleet_training" model="hr.plan.activity.type">
<record id="onboarding_fleet_training" model="mail.activity.plan.template">
<field name="summary">Fleet Training</field>
<field name="responsible">fleet_manager</field>
<field name="responsible_type">fleet_manager</field>
<field name="plan_id" ref="hr.onboarding_plan"/>
</record>
+1 -1
View File
@@ -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
@@ -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)
+4
View File
@@ -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
@@ -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,
'<ul><li>To-Do - Fleet Manager: Car return</li></ul>')
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('<li>')
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('<li>')
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()