[IMP] hr,hr_payroll: plan automation on triggers

Automate activites linked to plan on certain triggers.
To automate onboarding and offboarding processes, we can now define
plans that will activate upon triggers, such as employee creation,
departure (archive), contract start or contract end.
Manual plans are still possible.

Upon activation of a plan, a message will be added to the employee's
chatter with the name of the plan.

More options have been added related to when to schedule the plan's
activities.

The blocking mechanism has been removed (the plan would not launch if an
activity could not be started, due to lack of information), failed
activities will now be logged into the employee's chatter.

The list and kanban employee views have also been updated to include
info such as first contract date and activity information.

Task ID: 2489095

closes odoo/odoo#68451

Related: odoo/enterprise#17343
Related: odoo/upgrade#2314
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
William Braeckman
2021-06-03 12:09:32 +00:00
committed by Yannick Tivisse
parent 2cc481d1a6
commit 864595901e
10 changed files with 247 additions and 25 deletions
+4
View File
@@ -53,6 +53,8 @@
<record id="onboarding_plan" model='hr.plan'>
<field name="name">Onboarding</field>
<field name="plan_type">onboarding</field>
<field name="trigger">manual</field>
<field name="plan_activity_type_ids" eval="[(6, 0, [
ref('hr.onboarding_setup_it_materials'),
ref('hr.onboarding_plan_training'),
@@ -62,6 +64,8 @@
<record id="offboarding_plan" model='hr.plan'>
<field name="name">Offboarding</field>
<field name="plan_type">offboarding</field>
<field name="trigger">manual</field>
<field name="plan_activity_type_ids" eval="[(6, 0, [
ref('hr.offboarding_setup_compute_out_delais'),
ref('hr.offboarding_take_back_hr_materials'),
+81 -9
View File
@@ -6,10 +6,11 @@ from random import choice
from string import digits
from werkzeug.urls import url_encode
from dateutil.relativedelta import relativedelta
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.osv.query import Query
from odoo.exceptions import ValidationError, AccessError
from odoo.exceptions import ValidationError, AccessError, UserError
from odoo.osv import expression
from odoo.tools.misc import format_date
@@ -281,17 +282,20 @@ class HrEmployeePrivate(models.Model):
vals.update(self._sync_user(user, bool(vals.get('image_1920'))))
vals['name'] = vals.get('name', user.name)
employee = super(HrEmployeePrivate, self).create(vals)
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=_('<b>Congratulations!</b> May I recommend you to setup an <a href="%s">onboarding plan?</a>') % (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=_('<b>Congratulations!</b> May I recommend you to setup an <a href="%s">onboarding plan?</a>') % (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='<br/>'.join(errors),
)
# ---------------------------------------------------------
# Messaging
# ---------------------------------------------------------
+89 -1
View File
@@ -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'
+12
View File
@@ -50,5 +50,17 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_plan_comp_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_comp_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>
</data>
</odoo>
+5
View File
@@ -23,4 +23,9 @@
z-index: 100;
}
.hr_activity_container {
margin-bottom: 2px;
margin-right: -3px;
}
}
+6
View File
@@ -221,6 +221,9 @@
<field name="name" readonly="1"/>
<field name="work_phone" class="o_force_ltr" readonly="1"/>
<field name="work_email"/>
<field name="activity_ids" widget="list_activity"/>
<field name="activity_user_id" optional="hide" string="Activity by" widget="many2one_avatar_user"/>
<field name="activity_date_deadline" widget="remaining_days" options="{'allow_order': '1'}"/>
<field name="company_id" groups="base.group_multi_company" readonly="1"/>
<field name="department_id"/>
<field name="job_id"/>
@@ -303,6 +306,9 @@
<div class="oe_kanban_bottom_left"/>
<div class="oe_kanban_bottom_right">
<a title="Chat" icon="fa-comments" href="#" class="ml8 o_employee_chat_btn" attrs="{'invisible': [('user_id','=', False)]}" role="button"><i class="fa fa-comments"/></a>
<div class="hr_activity_container">
<field name="activity_ids" widget="kanban_activity"/>
</div>
</div>
</div>
</div>
+27 -1
View File
@@ -20,6 +20,7 @@
<field name="arch" type="xml">
<tree string="Planning">
<field name="name"/>
<field name="plan_type"/>
</tree>
</field>
</record>
@@ -37,12 +38,23 @@
<field name="name" placeholder="e.g. Onboarding"/>
</h1>
</div>
<field name="active" invisible="1"/>
<group>
<group>
<field name="plan_type" widget="radio"/>
<field name="trigger" invisible="1"/>
<field name="trigger_onboarding" string="Activation Trigger" attrs="{'invisible': [('plan_type', '!=', 'onboarding')]}"/>
<field name="trigger_offboarding" string="Activation Trigger" attrs="{'invisible': [('plan_type', '!=', 'offboarding')]}"/>
<field name="trigger_other" string="Activation Trigger" attrs="{'invisible': [('plan_type', '!=', 'other')], 'readonly': True}"/>
</group>
</group>
<group string="Activities">
<field name="active" invisible="1"/>
<field name="plan_activity_type_ids" nolabel="1">
<tree editable="bottom">
<field name="activity_type_id"/>
<field name="summary"/>
<field name="deadline_type"/>
<field name="deadline_days" attrs="{'readonly': [('deadline_type', '!=', 'trigger_offset')]}"/>
<field name="responsible"/>
<field name="responsible_id" attrs="{'readonly': [('responsible', '!=', 'other')]}"/>
</tree>
@@ -60,6 +72,8 @@
<tree string="Activities">
<field name="activity_type_id"/>
<field name="summary"/>
<field name="deadline_type"/>
<field name="deadline_days" attrs="{'invisible': [('deadline_type', '!=', 'trigger_offset')]}"/>
<field name="responsible"/>
</tree>
</field>
@@ -74,6 +88,8 @@
<group>
<field name="activity_type_id"/>
<field name="summary"/>
<field name="deadline_type"/>
<field name="deadline_days" attrs="{'invisible': [('deadline_type', '!=', 'trigger_offset')]}"/>
<field name="responsible"/>
<field name="responsible_id" attrs="{'invisible': [('responsible', '!=', 'other')]}"/>
<field name="note"/>
@@ -88,12 +104,22 @@
<field name="res_model">hr.plan</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="hr_plan_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new plan
</p>
</field>
</record>
<record id="hr_plan_activity_type_action" model="ir.actions.act_window">
<field name="name">Planning Types</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 planning activity
</p>
</field>
</record>
</data>
+9 -14
View File
@@ -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',
+1
View File
@@ -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',
],
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_employee_tree" model="ir.ui.view">
<field name="name">hr.employee.tree</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_tree"></field>
<field name="arch" type="xml">
<xpath expr="//field[@name='work_email']" position="after">
<field name="first_contract_date" optional="hide"/>
</xpath>
</field>
</record>
</odoo>