diff --git a/addons/hr/data/hr_data.xml b/addons/hr/data/hr_data.xml
index 94d72d10471..fd8c08d9f01 100644
--- a/addons/hr/data/hr_data.xml
+++ b/addons/hr/data/hr_data.xml
@@ -53,6 +53,8 @@
Onboarding
+ onboarding
+ manual
Offboarding
+ offboarding
+ manual
onboarding plan?') % (url))
if employee.department_id:
self.env['mail.channel'].sudo().search([
('subscription_department_ids', 'in', employee.department_id.id)
])._subscribe_users_automatically()
+ # Launch onboarding plans
+ if not employee._launch_plans_from_trigger(trigger='employee_creation'):
+ # Keep the recommend message if no plans are launched
+ url = '/web#%s' % url_encode({
+ 'action': 'hr.plan_wizard_action',
+ 'active_id': employee.id,
+ 'active_model': 'hr.employee',
+ 'menu_id': self.env.ref('hr.menu_hr_root').id,
+ })
+ employee._message_log(body=_('Congratulations! May I recommend you to setup an onboarding plan?') % (url))
return employee
def write(self, vals):
@@ -336,9 +340,9 @@ class HrEmployeePrivate(models.Model):
archived_addresses = unarchived_employees.mapped('address_home_id').filtered(lambda addr: not addr.active)
archived_addresses.toggle_active()
- # Empty links to this employees (example: manager, coach, time off responsible, ...)
archived_employees = self.filtered(lambda e: not e.active)
if archived_employees:
+ # Empty links to this employees (example: manager, coach, time off responsible, ...)
employee_fields_to_empty = self._get_employee_m2o_to_empty_on_archived_employees()
user_fields_to_empty = self._get_user_m2o_to_empty_on_archived_employees()
employee_domain = [[(field, 'in', archived_employees.ids)] for field in employee_fields_to_empty]
@@ -352,6 +356,9 @@ class HrEmployeePrivate(models.Model):
if employee[field] in archived_employees.user_id:
employee[field] = False
+ # Launch automatic offboarding plans
+ archived_employees._launch_plans_from_trigger(trigger='employee_archive')
+
if len(self) == 1 and not self.active:
return {
'type': 'ir.actions.act_window',
@@ -410,6 +417,71 @@ class HrEmployeePrivate(models.Model):
self = self.with_user(real_user)
return self
+ def _launch_plans_from_trigger(self, trigger):
+ '''
+ Launches all plans for given trigger
+
+ Returns False if no plans are launched, True otherwise
+ '''
+ plan_ids = self.env['hr.plan'].search([('trigger', '=', trigger)])
+ if not plan_ids or not self:
+ return False
+ #Group plans and employees by company id
+ plans_per_company = defaultdict(lambda: self.env['hr.plan'])
+ for plan_id in plan_ids:
+ plans_per_company[plan_id.company_id.id] |= plan_id
+ employees_per_company = defaultdict(lambda: self.env['hr.employee'])
+ for employee_id in self:
+ employees_per_company[employee_id.company_id.id] |= employee_id
+ #Launch the plans
+ for company_id in employees_per_company:
+ employees_per_company[company_id]._launch_plan(plans_per_company[company_id])
+ return True
+
+ def _launch_plan(self, plan_ids):
+ '''
+ Launch all given plans
+ '''
+ for employee_id in self:
+ for plan_id in plan_ids:
+ employee_id._message_log(
+ body=_('Plan %s has been launched.', plan_id.name),
+ )
+ errors = []
+ for activity_type in plan_id.plan_activity_type_ids:
+ responsible = False
+ try:
+ responsible = activity_type.get_responsible_id(employee_id)
+ except UserError as error:
+ errors.append(_(
+ 'Warning ! The step "%(name)s: %(summary)s" assigned to %(responsible)s '
+ 'could not be started because: "%(error)s"',
+ name=activity_type.activity_type_id.name,
+ summary=activity_type.summary,
+ responsible=activity_type.responsible,
+ error=str(error)
+ ))
+ continue
+
+ if self.env['hr.employee'].with_user(responsible).check_access_rights('read', raise_exception=False):
+ if activity_type.deadline_type == 'default':
+ date_deadline = self.env['mail.activity']._calculate_date_deadline(activity_type.activity_type_id)
+ elif activity_type.deadline_type == 'plan_active':
+ date_deadline = fields.Date.context_today(self)
+ elif activity_type.deadline_type == 'trigger_offset':
+ date_deadline = fields.Date.add(fields.Date.context_today(self), days=activity_type.deadline_days)
+
+ employee_id.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,
+ )
+ employee_id._message_log(
+ body='
'.join(errors),
+ )
+
# ---------------------------------------------------------
# Messaging
# ---------------------------------------------------------
diff --git a/addons/hr/models/hr_plan.py b/addons/hr/models/hr_plan.py
index 47b4ecc9ef5..1fe92b60cb2 100644
--- a/addons/hr/models/hr_plan.py
+++ b/addons/hr/models/hr_plan.py
@@ -10,6 +10,13 @@ class HrPlanActivityType(models.Model):
_description = 'Plan activity type'
_rec_name = 'summary'
+ _sql_constraints = [
+ (
+ 'check_deadline_days', 'CHECK (COALESCE(deadline_days) >= 0)',
+ 'Days deadline must be positive.'
+ ),
+ ]
+
activity_type_id = fields.Many2one(
'mail.activity.type', 'Activity Type',
default=lambda self: self.env.ref('mail.mail_activity_data_todo'),
@@ -22,8 +29,24 @@ class HrPlanActivityType(models.Model):
('manager', 'Manager'),
('employee', 'Employee'),
('other', 'Other')], default='employee', string='Responsible', required=True)
- responsible_id = fields.Many2one('res.users', 'Responsible Person', help='Specific responsible of activity if not linked to the employee.')
+ responsible_id = fields.Many2one('res.users', 'Name', help='Specific responsible of activity if not linked to the employee.')
note = fields.Html('Note')
+ deadline_type = fields.Selection(
+ [
+ ('default', 'Default value'),
+ ('plan_active', "At plan's activation"),
+ ('trigger_offset', 'Days after activation trigger'),
+ ],
+ string='Activity Deadline',
+ default='default',
+ required=True,
+ )
+ deadline_days = fields.Integer(string='Days Deadline')
+ company_id = fields.Many2one(
+ 'res.company',
+ string='Company',
+ default=lambda self: self.env.company,
+ )
@api.depends('activity_type_id')
def _compute_default_summary(self):
@@ -62,3 +85,68 @@ class HrPlan(models.Model):
name = fields.Char('Name', required=True)
plan_activity_type_ids = fields.Many2many('hr.plan.activity.type', string='Activities')
active = fields.Boolean(default=True)
+ plan_type = fields.Selection(
+ [
+ ('onboarding', 'Onboarding'),
+ ('offboarding', 'Offboarding'),
+ ('other', 'Other'),
+ ], string='Type', default='onboarding', required=True,
+ )
+ trigger_onboarding = fields.Selection(
+ [
+ ('manual', 'Manual'),
+ ('employee_creation', 'Employee Creation'),
+ ], compute='_compute_triggers', inverse='_inverse_triggers',
+ required=True, readonly=False,
+ )
+ trigger_offboarding = fields.Selection(
+ [
+ ('manual', 'Manual'),
+ ('employee_archive', 'Archived Employee'),
+ ], compute='_compute_triggers', inverse='_inverse_triggers',
+ required=True, readonly=False,
+ )
+ trigger_other = fields.Selection(
+ [
+ ('manual', 'Manual'),
+ ], compute='_compute_triggers', inverse='_inverse_triggers',
+ required=True, readonly=False,
+ )
+ trigger = fields.Char(default='manual', compute='_compute_trigger', store=True)
+ company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
+
+ @api.depends('trigger')
+ def _compute_triggers(self):
+ trigger_types = {'trigger_onboarding', 'trigger_offboarding', 'trigger_other'}
+ type_to_trigger = {
+ 'onboarding': 'trigger_onboarding',
+ 'offboarding': 'trigger_offboarding',
+ 'other': 'trigger_other',
+ }
+ for record in self:
+ #trigger for active
+ record[type_to_trigger[record.plan_type]] = record.trigger or 'manual'
+ #'manual' for all others
+ for disabled_trigger in trigger_types - {type_to_trigger[record.plan_type]}:
+ record[disabled_trigger] = 'manual'
+
+ def _inverse_triggers(self):
+ type_to_trigger = {
+ 'onboarding': 'trigger_onboarding',
+ 'offboarding': 'trigger_offboarding',
+ 'other': 'trigger_other',
+ }
+ for record in self:
+ # or 'manual' required is for trigger_other since it can not be changed it's always False here
+ record.trigger = record[type_to_trigger[record.plan_type]] or 'manual'
+
+ @api.depends('plan_type')
+ def _compute_trigger(self):
+ # In case only plan_type changes
+ type_to_trigger = {
+ 'onboarding': 'trigger_onboarding',
+ 'offboarding': 'trigger_offboarding',
+ 'other': 'trigger_other',
+ }
+ for record in self:
+ record.trigger = record[type_to_trigger[record.plan_type]] or 'manual'
diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml
index 5a8d4f01aac..d5484fad8d9 100644
--- a/addons/hr/security/hr_security.xml
+++ b/addons/hr/security/hr_security.xml
@@ -50,5 +50,17 @@
['|',('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)]
+
+
diff --git a/addons/hr/static/src/scss/hr.scss b/addons/hr/static/src/scss/hr.scss
index bfadf389a0d..b7a4e07778f 100644
--- a/addons/hr/static/src/scss/hr.scss
+++ b/addons/hr/static/src/scss/hr.scss
@@ -23,4 +23,9 @@
z-index: 100;
}
+ .hr_activity_container {
+ margin-bottom: 2px;
+ margin-right: -3px;
+ }
+
}
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml
index 2e782dc9b56..ef8afdb525f 100644
--- a/addons/hr/views/hr_employee_views.xml
+++ b/addons/hr/views/hr_employee_views.xml
@@ -221,6 +221,9 @@
+
+
+
@@ -303,6 +306,9 @@
diff --git a/addons/hr/views/hr_plan_views.xml b/addons/hr/views/hr_plan_views.xml
index 6390b762236..8b093214a30 100644
--- a/addons/hr/views/hr_plan_views.xml
+++ b/addons/hr/views/hr_plan_views.xml
@@ -20,6 +20,7 @@
+
@@ -37,12 +38,23 @@
+
+
+
+
+
+
+
+
+
+
-
+
+
@@ -60,6 +72,8 @@
+
+
@@ -74,6 +88,8 @@
+
+
@@ -88,12 +104,22 @@
hr.plan
tree,form
+
+
+ Add a new plan
+
+
Planning Types
hr.plan.activity.type
tree,form
+
+
+ Add a new planning activity
+
+
diff --git a/addons/hr/wizard/hr_plan_wizard.py b/addons/hr/wizard/hr_plan_wizard.py
index 69e1ee280b9..907ba7374f0 100644
--- a/addons/hr/wizard/hr_plan_wizard.py
+++ b/addons/hr/wizard/hr_plan_wizard.py
@@ -1,32 +1,27 @@
# -*- 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
class HrPlanWizard(models.TransientModel):
_name = 'hr.plan.wizard'
_description = 'Plan Wizard'
- plan_id = fields.Many2one('hr.plan', default=lambda self: self.env['hr.plan'].search([], limit=1))
+ plan_id = fields.Many2one(
+ 'hr.plan',
+ default=lambda self: self.env['hr.plan'].search([('trigger', '=', 'manual')], limit=1),
+ domain="[('trigger', '=', 'manual')]",
+ required=True,
+ )
employee_id = fields.Many2one(
'hr.employee', string='Employee', required=True,
default=lambda self: self.env.context.get('active_id', None),
)
def action_launch(self):
- for activity_type in self.plan_id.plan_activity_type_ids:
- responsible = activity_type.get_responsible_id(self.employee_id)
-
- if self.env['hr.employee'].with_user(responsible).check_access_rights('read', raise_exception=False):
- date_deadline = self.env['mail.activity']._calculate_date_deadline(activity_type.activity_type_id)
- self.employee_id.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
- )
+ self.ensure_one()
+ self.employee_id._launch_plan(self.plan_id)
return {
'type': 'ir.actions.act_window',
diff --git a/addons/hr_contract/__manifest__.py b/addons/hr_contract/__manifest__.py
index fb47f3ec376..a8959342304 100644
--- a/addons/hr_contract/__manifest__.py
+++ b/addons/hr_contract/__manifest__.py
@@ -25,6 +25,7 @@ You can assign several contracts per employee.
'data/hr_contract_data.xml',
'report/hr_contract_history_report_views.xml',
'views/hr_contract_views.xml',
+ 'views/hr_employee_views.xml',
'views/resource_calendar_views.xml',
'wizard/hr_departure_wizard_views.xml',
],
diff --git a/addons/hr_contract/views/hr_employee_views.xml b/addons/hr_contract/views/hr_employee_views.xml
new file mode 100644
index 00000000000..0c03d94bbfb
--- /dev/null
+++ b/addons/hr_contract/views/hr_employee_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ hr.employee.tree
+ hr.employee
+
+
+
+
+
+
+
+