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 + + + + + + + +