Skip to content

Commit

Permalink
[IMP] hr_org_chart: Allow recursion in hierarchy
Browse files Browse the repository at this point in the history
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 #27900
  • Loading branch information
LucasLefevre authored and tivisse committed Dec 6, 2018
1 parent 2460972 commit 48254cc
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 58 deletions.
8 changes: 1 addition & 7 deletions addons/hr/models/hr.py
Expand Up @@ -189,7 +189,7 @@ def _default_image(self):
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',
Expand All @@ -199,12 +199,6 @@ def _default_image(self):
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:
Expand Down
60 changes: 45 additions & 15 deletions addons/hr_org_chart/controllers/hr_org_chart.py
Expand Up @@ -7,7 +7,23 @@


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
Expand All @@ -23,32 +39,46 @@ def _prepare_employee_data(self, employee):

@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):
return {}
try:
Employee.browse(employee_id).check_access_rule('read')
except AccessError:
employee = self._check_employee(employee_id)
if not employee: # to check
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
35 changes: 30 additions & 5 deletions addons/hr_org_chart/models/hr_employee.py
Expand Up @@ -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)

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_all_count')
def _compute_child_all_count(self):
@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)
72 changes: 46 additions & 26 deletions addons/hr_org_chart/static/src/js/hr_org_chart.js
Expand Up @@ -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.
*
Expand All @@ -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
Expand All @@ -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({
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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,
});
})

}
},
});
Expand Down
9 changes: 4 additions & 5 deletions addons/hr_org_chart/static/src/xml/hr_org_chart.xml
Expand Up @@ -3,7 +3,7 @@

<t t-name="hr_org_chart_employee">
<div t-attf-class="o_org_chart_entry o_org_chart_entry_#{employee_type} media">
<t t-set="is_self" t-value="employee_type == 'self'"/>
<t t-set="is_self" t-value="employee.id == view_employee_id"/>

<div class="o_media_left">
<!-- NOTE: Since by the default on not squared images odoo add white borders,
Expand Down Expand Up @@ -60,8 +60,7 @@
<t t-if='managers_more'>
<div class="o_org_chart_entry o_org_chart_more media">
<div class="o_media_left">
<a class="text-center o_employee_redirect"
t-att-href="managers[0].link"
<a class="text-center o_employee_more_managers"
t-att-data-employee-id="managers[0].id">
<i t-attf-class="fa fa-angle-double-up" role="img" aria-label="More managers" title="More managers"/>
</a>
Expand Down Expand Up @@ -93,14 +92,14 @@
<div t-if="children.length" class="o_org_chart_group_down">
<t t-foreach="children" t-as="employee">
<t t-set="emp_count" t-value="emp_count + 1"/>
<t t-if="emp_count &lt; 8">
<t t-if="emp_count &lt; 20">
<t t-call="hr_org_chart_employee">
<t t-set="employee_type" t-value="'sub'"/>
</t>
</t>
</t>

<t t-if="(children.length + managers.length) &gt; 7">
<t t-if="(children.length + managers.length) &gt; 19">
<div class="o_org_chart_entry o_org_chart_more media">
<div class="o_media_left">
<a href="#"
Expand Down

0 comments on commit 48254cc

Please sign in to comment.