From 48254cc8a153e061b64c399abb90cf4d4440bd65 Mon Sep 17 00:00:00 2001 From: lul-odoo Date: Tue, 16 Oct 2018 07:49:42 +0000 Subject: [PATCH] [IMP] hr_org_chart: Allow recursion in hierarchy Employees can appear multiple times in the org chart Purpose ======= Allow recursive hierarchy (an employee can be its own manager; e.g. the CEO is manager of everyone but is also member of a department managed by the CTO who is himself managed by the CEO). This means an employee might appear in multiple place in the organisational chart. The button to see more of the hierarchy in the organisation chart opens all employees under the N+2 of the current employee being displayed but does not open the view of the N+2. Specification ============= Add 'subordinates_ids' computed field on 'hr.employee' to get all subordinates (direct and indirect). closes odoo/odoo#27900 --- addons/hr/models/hr.py | 8 +-- .../hr_org_chart/controllers/hr_org_chart.py | 60 ++++++++++++---- addons/hr_org_chart/models/hr_employee.py | 35 +++++++-- .../static/src/js/hr_org_chart.js | 72 ++++++++++++------- .../static/src/xml/hr_org_chart.xml | 9 ++- 5 files changed, 126 insertions(+), 58 deletions(-) diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index e6e7d2e47d5..949b6472b1e 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -189,7 +189,7 @@ class Employee(models.Model): job_id = fields.Many2one('hr.job', 'Job Position') department_id = fields.Many2one('hr.department', 'Department') parent_id = fields.Many2one('hr.employee', 'Manager') - child_ids = fields.One2many('hr.employee', 'parent_id', string='Subordinates') + child_ids = fields.One2many('hr.employee', 'parent_id', string='Direct subordinates') coach_id = fields.Many2one('hr.employee', 'Coach') category_ids = fields.Many2many( 'hr.employee.category', 'employee_category_rel', @@ -199,12 +199,6 @@ class Employee(models.Model): notes = fields.Text('Notes') color = fields.Integer('Color Index', default=0) - @api.constrains('parent_id') - def _check_parent_id(self): - for employee in self: - if not employee._check_recursion(): - raise ValidationError(_('You cannot create a recursive hierarchy.')) - @api.onchange('job_id') def _onchange_job_id(self): if self.job_id: diff --git a/addons/hr_org_chart/controllers/hr_org_chart.py b/addons/hr_org_chart/controllers/hr_org_chart.py index 5729997636f..20f1c30047a 100644 --- a/addons/hr_org_chart/controllers/hr_org_chart.py +++ b/addons/hr_org_chart/controllers/hr_org_chart.py @@ -7,7 +7,23 @@ from odoo.http import request class HrOrgChartController(http.Controller): - _managers_level = 2 # FP request + _managers_level = 5 # FP request + + def _check_employee(self, employee_id): + if not employee_id: # to check + return None + employee_id = int(employee_id) + + Employee = request.env['hr.employee'] + # check and raise + if not Employee.check_access_rights('read', raise_exception=False): + return None + try: + Employee.browse(employee_id).check_access_rule('read') + except AccessError: + return None + else: + return Employee.browse(employee_id) def _prepare_employee_data(self, employee): job = employee.sudo().job_id @@ -23,32 +39,46 @@ class HrOrgChartController(http.Controller): @http.route('/hr/get_org_chart', type='json', auth='user') def get_org_chart(self, employee_id): - if not employee_id: # to check - return {} - employee_id = int(employee_id) - Employee = request.env['hr.employee'] - # check and raise - if not Employee.check_access_rights('read', raise_exception=False): + employee = self._check_employee(employee_id) + if not employee: # to check return {} - try: - Employee.browse(employee_id).check_access_rule('read') - except AccessError: - return {} - else: - employee = Employee.browse(employee_id) # compute employee data for org chart ancestors, current = request.env['hr.employee'], employee - while current.parent_id: + while current.parent_id and len(ancestors) < self._managers_level+1: ancestors += current.parent_id current = current.parent_id values = dict( self=self._prepare_employee_data(employee), - managers=[self._prepare_employee_data(ancestor) for idx, ancestor in enumerate(ancestors) if idx < self._managers_level], + managers=[ + self._prepare_employee_data(ancestor) + for idx, ancestor in enumerate(ancestors) + if idx < self._managers_level + ], managers_more=len(ancestors) > self._managers_level, children=[self._prepare_employee_data(child) for child in employee.child_ids], ) values['managers'].reverse() return values + + @http.route('/hr/get_subordinates', type='json', auth='user') + def get_subordinates(self, employee_id, subordinates_type=None): + """ + Get employee subordinates. + Possible values for 'subordinates_type': + - 'indirect' + - 'direct' + """ + + employee = self._check_employee(employee_id) + if not employee: # to check + return {} + + if subordinates_type == 'direct': + return employee.child_ids.ids + elif subordinates_type == 'indirect': + return (employee.subordinate_ids - employee.child_ids).ids + else: + return employee.subordinate_ids.ids diff --git a/addons/hr_org_chart/models/hr_employee.py b/addons/hr_org_chart/models/hr_employee.py index ff56b1b1bb3..15fc50645d2 100644 --- a/addons/hr_org_chart/models/hr_employee.py +++ b/addons/hr_org_chart/models/hr_employee.py @@ -3,16 +3,41 @@ from odoo import api, fields, models - class Employee(models.Model): _name = "hr.employee" _inherit = "hr.employee" child_all_count = fields.Integer( 'Indirect Surbordinates Count', - compute='_compute_child_all_count', store=False) + compute='_compute_subordinates', store=False) - @api.depends('child_ids.child_all_count') - def _compute_child_all_count(self): + subordinate_ids = fields.One2many('hr.employee', string='Subordinates', compute='_compute_subordinates', help="Direct and indirect subordinates", groups='base.group_user') + + + def _get_subordinates(self, parents=None): + """ + Helper function to compute subordinates_ids. + Get all subordinates (direct and indirect) of an employee. + An employee can be a manager of his own manager (recursive hierarchy; e.g. the CEO is manager of everyone but is also + member of the RD department, managed by the CTO itself managed by the CEO). + In that case, the manager in not counted as a subordinate if it's in the 'parents' set. + """ + + if not parents: + parents = self.env['hr.employee'] + + indirect_subordinates = self.env['hr.employee'] + parents |= self + direct_subordinates = self.child_ids - parents + for child in direct_subordinates: + child_subordinate = child._get_subordinates(parents=parents) + child.subordinate_ids = child_subordinate + indirect_subordinates |= child_subordinate + return indirect_subordinates | direct_subordinates + + + @api.depends('child_ids', 'child_ids.child_all_count') + def _compute_subordinates(self): for employee in self: - employee.child_all_count = len(employee.child_ids) + sum(child.child_all_count for child in employee.child_ids) + employee.subordinate_ids = employee._get_subordinates() + employee.child_all_count = len(employee.subordinate_ids) diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js index b889375cac4..70bf295d4f5 100644 --- a/addons/hr_org_chart/static/src/js/hr_org_chart.js +++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js @@ -14,20 +14,21 @@ var FieldOrgChart = AbstractField.extend({ events: { "click .o_employee_redirect": "_onEmployeeRedirect", "click .o_employee_sub_redirect": "_onEmployeeSubRedirect", + "click .o_employee_more_managers": "_onEmployeeMoreManager" }, /** * @constructor * @override */ - init: function () { + init: function (parent, options) { this._super.apply(this, arguments); this.dm = new concurrency.DropMisordered(); + this.employee; }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - /** * Get the chart data through a rpc call. * @@ -40,12 +41,28 @@ var FieldOrgChart = AbstractField.extend({ return this.dm.add(this._rpc({ route: '/hr/get_org_chart', params: { - employee_id: employee_id, + employee_id: employee_id }, })).then(function (data) { self.orgData = data; }); }, + /** + * Get subordonates of an employee through a rpc call. + * + * @private + * @param {integer} employee_id + * @returns {Deferred} + */ + _getSubordinatesData: function (employee_id, type) { + return this.dm.add(this._rpc({ + route: '/hr/get_subordinates', + params: { + employee_id: employee_id, + subordinates_type: type + }, + })) + }, /** * @override * @private @@ -57,9 +74,14 @@ var FieldOrgChart = AbstractField.extend({ children: [], })); } + else if (!this.employee) { + this.employee = this.recordData.id + } var self = this; - return this._getOrgData(this.recordData.id).then(function () { + return this._getOrgData(this.employee).then(function () { + + self.orgData['view_employee_id'] = self.recordData.id; self.$el.html(QWeb.render("hr_org_chart", self.orgData)); self.$('[data-toggle="popover"]').each(function () { $(this).popover({ @@ -101,6 +123,11 @@ var FieldOrgChart = AbstractField.extend({ // Handlers //-------------------------------------------------------------------------- + _onEmployeeMoreManager: function(event) { + event.preventDefault(); + this.employee = parseInt($(event.currentTarget).data('employee-id')); + this._render() + }, /** * Redirect to the employee form view. * @@ -133,29 +160,22 @@ var FieldOrgChart = AbstractField.extend({ var employee_id = parseInt($(event.currentTarget).data('employee-id')); var employee_name = $(event.currentTarget).data('employee-name'); var type = $(event.currentTarget).data('type') || 'direct'; - var domain = [['parent_id', '=', employee_id]]; - var name = _.str.sprintf(_t("Direct Subordinates of %s"), employee_name); - if (type === 'total') { - domain = ['&', ['parent_id', 'child_of', employee_id], ['id', '!=', employee_id]]; - name = _.str.sprintf(_t("Subordinates of %s"), employee_name); - } else if (type === 'indirect') { - domain = ['&', '&', - ['parent_id', 'child_of', employee_id], - ['parent_id', '!=', employee_id], - ['id', '!=', employee_id] - ]; - name = _.str.sprintf(_t("Indirect Subordinates of %s"), employee_name); - } + var self = this if (employee_id) { - return this.do_action({ - name: name, - type: 'ir.actions.act_window', - view_mode: 'kanban,list,form', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], - target: 'current', - res_model: 'hr.employee', - domain: domain, - }); + this._getSubordinatesData(employee_id, type).then(function(data) { + var domain = [['id', 'in', data]]; + + return self.do_action({ + name: employee_name, + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: 'hr.employee', + domain: domain, + }); + }) + } }, }); diff --git a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml index e4e7f9fa231..fa5db5f32a8 100644 --- a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml +++ b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml @@ -3,7 +3,7 @@
- +