diff options
| author | Fabien Pinckaers <fp@openerp.com> | 2017-03-27 11:12:09 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2017-04-03 10:38:03 +0200 |
| commit | e25dd35a251006f2d3c2548a1292e9dae029851d (patch) | |
| tree | 24df5e78c56786fcadf3212115568cf2087f689e | |
| parent | 855e7eb043b720c8f343f2ec795bbe99584287a0 (diff) | |
[ADD] hr_org_chart: organizational chart on employee forms
This commit introduces a new widget specific to employees. It is used
to display an org chart centered on the current displayed employee.
Two levels of managers as well as subordinates are displayed.
It is contained in its own module and its installation is available from
hr settings.
Thanks to Stefano Rigano <sri@odoo.com> (@stefanorigano) for design and UX
implementation.
| -rw-r--r-- | addons/hr/models/hr_config_settings.py | 1 | ||||
| -rw-r--r-- | addons/hr/views/hr_config_settings_views.xml | 13 | ||||
| -rw-r--r-- | addons/hr/views/hr_views.xml | 2 | ||||
| -rw-r--r-- | addons/hr_org_chart/__init__.py | 5 | ||||
| -rw-r--r-- | addons/hr_org_chart/__manifest__.py | 24 | ||||
| -rw-r--r-- | addons/hr_org_chart/controllers/__init__.py | 2 | ||||
| -rw-r--r-- | addons/hr_org_chart/controllers/hr_org_chart.py | 53 | ||||
| -rw-r--r-- | addons/hr_org_chart/models/__init__.py | 4 | ||||
| -rw-r--r-- | addons/hr_org_chart/models/hr_employee.py | 18 | ||||
| -rw-r--r-- | addons/hr_org_chart/static/src/js/hr_org_chart.js | 140 | ||||
| -rw-r--r-- | addons/hr_org_chart/static/src/less/hr_org_chart.less | 286 | ||||
| -rw-r--r-- | addons/hr_org_chart/static/src/xml/hr_org_chart.xml | 160 | ||||
| -rw-r--r-- | addons/hr_org_chart/views/hr_templates.xml | 9 | ||||
| -rw-r--r-- | addons/hr_org_chart/views/hr_views.xml | 16 |
14 files changed, 733 insertions, 0 deletions
diff --git a/addons/hr/models/hr_config_settings.py b/addons/hr/models/hr_config_settings.py index bcf74552b82..6a04c29368c 100644 --- a/addons/hr/models/hr_config_settings.py +++ b/addons/hr/models/hr_config_settings.py @@ -13,3 +13,4 @@ class HrConfigurator(models.TransientModel): resource_calendar_id = fields.Many2one( 'resource.calendar', 'Company Working Hours', related='company_id.resource_calendar_id') + module_hr_org_chart = fields.Boolean(string="Show Organizational Chart") diff --git a/addons/hr/views/hr_config_settings_views.xml b/addons/hr/views/hr_config_settings_views.xml index e3071728307..a165dfcc39d 100644 --- a/addons/hr/views/hr_config_settings_views.xml +++ b/addons/hr/views/hr_config_settings_views.xml @@ -32,7 +32,20 @@ </div> </div> </div> + + <div class="col-xs-12 col-md-6 o_setting_box" title="Show organizational chart on employee form"> + <div class="o_setting_left_pane"> + <field name="module_hr_org_chart"/> + </div> + <div class="o_setting_right_pane"> + <label for="module_hr_org_chart"/> + <div class="text-muted"> + Show organizational chart on employee form. + </div> + </div> + </div> </div> + </sheet> </form> </field> diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index 0c17f47af64..4196942fcdf 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -44,6 +44,7 @@ options='{"terminology": "archive"}'/> </button> </div> + <div id="o_employee_container"><div id="o_employee_main"> <field name="image" widget='image' class="oe_avatar" options='{"preview_image":"image_medium"}'/> <div class="oe_title"> <label for="name" class="oe_edit_only"/> @@ -105,6 +106,7 @@ </group> </page> </notebook> + </div></div> </sheet> <div class="oe_chatter"> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> diff --git a/addons/hr_org_chart/__init__.py b/addons/hr_org_chart/__init__.py new file mode 100644 index 00000000000..ac0011716f6 --- /dev/null +++ b/addons/hr_org_chart/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import controllers +import models diff --git a/addons/hr_org_chart/__manifest__.py b/addons/hr_org_chart/__manifest__.py new file mode 100644 index 00000000000..c5f842b0961 --- /dev/null +++ b/addons/hr_org_chart/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'HR Org Chart', + 'category': 'Hidden', + 'version': '1.0', + 'description': + """ +Org Chart Widget for HR +======================= + +This module extend the employee form with a organizational chart. +(N+1, N+2, direct subordinates) + """, + 'depends': ['hr'], + 'data': [ + 'views/hr_templates.xml', + 'views/hr_views.xml' + ], + 'qweb': [ + 'static/src/xml/hr_org_chart.xml', + ] +} diff --git a/addons/hr_org_chart/controllers/__init__.py b/addons/hr_org_chart/controllers/__init__.py new file mode 100644 index 00000000000..1b81ea481c7 --- /dev/null +++ b/addons/hr_org_chart/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -* +import hr_org_chart diff --git a/addons/hr_org_chart/controllers/hr_org_chart.py b/addons/hr_org_chart/controllers/hr_org_chart.py new file mode 100644 index 00000000000..a088bb86663 --- /dev/null +++ b/addons/hr_org_chart/controllers/hr_org_chart.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.exceptions import AccessError +from odoo.http import request + + +class HrOrgChartController(http.Controller): + _managers_level = 2 # FP request + + def _prepare_employee_data(self, employee): + return dict( + id=employee.id, + name=employee.name, + link='/mail/view?model=hr.employee&res_id=%s' % employee.id, + job_id=employee.job_id.id, + job_name=employee.job_id.name or '', + direct_sub_count=len(employee.child_ids), + indirect_sub_count=employee.child_all_count, + ) + + @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: + return {} + else: + employee = Employee.browse(employee_id) + + # compute employee data for org chart + ancestors, current = request.env['hr.employee'], employee + while current.parent_id: + 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_more=len(ancestors) > self._managers_level, + children=[self._prepare_employee_data(child) for child in employee.child_ids], + ) + values['managers'].reverse() + return values diff --git a/addons/hr_org_chart/models/__init__.py b/addons/hr_org_chart/models/__init__.py new file mode 100644 index 00000000000..aeb53f4e93b --- /dev/null +++ b/addons/hr_org_chart/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hr_employee diff --git a/addons/hr_org_chart/models/hr_employee.py b/addons/hr_org_chart/models/hr_employee.py new file mode 100644 index 00000000000..ff56b1b1bb3 --- /dev/null +++ b/addons/hr_org_chart/models/hr_employee.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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) + + @api.depends('child_ids.child_all_count') + def _compute_child_all_count(self): + for employee in self: + employee.child_all_count = len(employee.child_ids) + sum(child.child_all_count for child in employee.child_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 new file mode 100644 index 00000000000..60b6f94bc8b --- /dev/null +++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js @@ -0,0 +1,140 @@ +odoo.define('web.OrgChart', function (require) { +"use strict"; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var form_common = require('web.form_common'); + +var QWeb = core.qweb; +var _t = core._t; + +var FieldOrgChart = form_common.AbstractField.extend({ + + events: { + "click .o_employee_redirect": "on_employee_redirect", + "click .o_employee_sub_redirect": "on_employee_sub_redirect", + }, + + init: function () { + this._super.apply(this, arguments); + }, + + start: function () { + this.reinit(); + return this._super(); + }, + + reinit: function () { + this.emp_data = { + managers: [], + children: [], + }; + }, + + set_value: function (_value) { + this.reinit(); + this._super(_value); + }, + + render_value: function () { + if (! this.view.datarecord.id) { + return this.$el.html(QWeb.render("hr_org_chart", {widget: this})); + } + + var self = this; + this.get_org_chart_data(this.view.datarecord.id).then(function () { + self.$el.html(QWeb.render("hr_org_chart", {widget: self})); + }).then(function () { + self.$el.find('[data-toggle="popover"]').each(function () { + $(this).popover({ + html: true, + title: function() { + var $title = $(QWeb.render('hr_orgchart_emp_popover_title', {employee: { + name: $(this).data('emp-name'), + id: $(this).data('emp-id'), + }})); + $title.on('click', '.o_employee_redirect', function(event) { + self.on_employee_redirect(event); + }); + return $title; + }, + container: 'body', + placement: 'left', + trigger: 'focus', + content: function() { + var $content = $(QWeb.render('hr_orgchart_emp_popover_content', {employee: { + id: $(this).data('emp-id'), + name: $(this).data('emp-name'), + direct_sub_count: parseInt($(this).data('emp-dir-subs')), + indirect_sub_count: parseInt($(this).data('emp-ind-subs')), + }})); + $content.on('click', '.o_employee_sub_redirect', function(event) { + self.on_employee_sub_redirect(event); + }); + return $content; + }, + template: $(QWeb.render('hr_orgchart_emp_popover', {})), + }); + }); + }); + }, + + get_org_chart_data: function (employee_id) { + var self = this; + return ajax.jsonRpc('/hr/get_org_chart', 'call', { + employee_id: employee_id, + }).then(function (data) { + self.emp_data = data; + }); + }, + + on_employee_redirect: function (event) { + event.preventDefault(); + var employee_id = parseInt($(event.currentTarget).data('employee-id')); + return this.do_action({ + type: 'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + views: [[false, 'form']], + target: 'current', + res_model: 'hr.employee', + res_id: employee_id, + }); + }, + + on_employee_sub_redirect: function (event) { + event.preventDefault(); + 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); + } + if (employee_id) { + return this.do_action({ + name: name, + type: 'ir.actions.act_window', + view_type: 'tree', + view_mode: 'kanban,tree,form', + views: [[false, 'kanban'], [false, 'tree'], [false, 'form']], + target: 'current', + res_model: 'hr.employee', + domain: domain, + }); + } + }, + +}); + +core.form_widget_registry.add('hr_org_chart', FieldOrgChart); + + +return FieldOrgChart; +}); diff --git a/addons/hr_org_chart/static/src/less/hr_org_chart.less b/addons/hr_org_chart/static/src/less/hr_org_chart.less new file mode 100644 index 00000000000..65a902bb3b6 --- /dev/null +++ b/addons/hr_org_chart/static/src/less/hr_org_chart.less @@ -0,0 +1,286 @@ +@o-org-chart-bg: white; +@o-org-chart-border-color: @odoo-brand-secondary; + +@o-org-chart-entry-v-gap: 6px; +@o-org-chart-entry-pic-size: 46px; +@o-org-chart-entry-line-w: 1px; +@o-org-chart-entry-border-color: darken(@o-org-chart-bg, 25%); + + +// MIXINS +.o-org-chart-line() { + content: ''; + background-color: @o-org-chart-bg; + border: 0px solid @o-org-chart-entry-border-color; +} + +// MOBILE LAYOUT CUSTOMIZATIONS +@media screen and (max-width: @screen-xs-max) { + #o_employee_right { + .o_org_chart_title { + font-size: 20px; + padding: 5px 0; + border-bottom: 1px solid @o-org-chart-border-color; + } + } +} + +// SMALL DESKTOP LAYOUT +@media screen and (min-width: @screen-sm-min) { + #o_employee_container { + .o-flex-display(); + width: 100%; + } + #o_employee_main { + .o-flex(1, 1, 60%); + } + #o_employee_right { + .o-flex(0, 1, 35%); + margin-left: 2%; + padding-left: 2%; + border-left: 1px solid @o-org-chart-border-color; + + .o_org_chart_title { + color: @gray-light + } + } +} + +// MEDIUM DESKTOP LAYOUT +@media screen and (min-width: @screen-md-min){ + #o_employee_right { + .o-flex(0, 1, 33%); + } +} + +// LARGE DESKTOP LAYOUT +@media screen and (min-width: @screen-lg-min){ + #o_employee_right { + .o-flex(0, 1, 30%); + } +} + +#o_employee_right { + @tmp-gap-base: @o-org-chart-entry-pic-size*0.7; + + // ORGANIGRAM LINES + .o_form_field, .o_org_chart_group_up, .o_org_chart_group_down { + position: relative; + } + + .o_org_chart_group_up { + &:before { + .o-org-chart-line(); + border-left-width: @o-org-chart-entry-line-w; + height: calc(100% ~'+' @o-org-chart-entry-pic-size*0.5) ; + .o-position-absolute( + @top: @o-org-chart-entry-pic-size*0.1 + 5px, + @left: @o-org-chart-entry-pic-size*0.5 - @o-org-chart-entry-line-w*0.5 + ); + } + .o_org_chart_entry:last-of-type { + &:before { + .o-org-chart-line(); + border-width: 0 0 @o-org-chart-entry-line-w @o-org-chart-entry-line-w; + .size((@o-org-chart-entry-pic-size*0.5) - (@o-org-chart-entry-v-gap*2), @o-org-chart-entry-pic-size*0.5 + @o-org-chart-entry-v-gap*2 ); + .o-position-absolute( + @left: @o-org-chart-entry-pic-size*0.5 - @o-org-chart-entry-line-w*0.5, + @top: 100% + ); + } + } + } + + .o_org_chart_group_up + .o_org_chart_entry_self { + margin-left: @tmp-gap-base; + + & + .o_org_chart_group_down { + margin-left: @tmp-gap-base*2; + } + } + + .o_org_chart_group_down { + margin-left: @tmp-gap-base; + + &:before { + .o-org-chart-line(); + border-left-width: @o-org-chart-entry-line-w; + height: 100%; + .o-position-absolute( + @top: @o-org-chart-entry-v-gap*-1, + @left: @tmp-gap-base*-0.5 + @o-org-chart-entry-pic-size*0.1 + @o-org-chart-entry-line-w*0.5 + ); + } + + .o_org_chart_entry { + &:before { + .o-org-chart-line(); + border-top-width: @o-org-chart-entry-line-w; + .size(@tmp-gap-base, 0); + .o-position-absolute( + @left: @tmp-gap-base*-0.5 + @o-org-chart-entry-pic-size*0.1 + @o-org-chart-entry-line-w*0.5, + @top: @o-org-chart-entry-pic-size*0.5 + ); + } + + &:last-of-type { + &:before { + height: 50%; + } + + } + + &.o_org_chart_more { + margin-top: @o-org-chart-entry-v-gap; + + &:before { + top: 15px; + } + } + } + } + + // ORGANIGRAM DESIGN + .o_org_chart_entry { + margin-bottom: @o-org-chart-entry-v-gap; + overflow: visible; + margin-top: 0; + + &, .media-left, .media-body { + position: relative; + } + + .media-body { + vertical-align: middle; + + .badge { + float: right; + cursor: pointer; + margin-right: 5px; + color: @gray-light; + background: @o-org-chart-bg; + border: 1px solid @gray-light; + &:hover { + color: @odoo-brand-optional; + border-color: @odoo-brand-optional; + } + &:focus { + outline: none; + } + } + + strong { + display: block; + line-height: 1.2; + font-size: 11px; + color: lighten(@gray-light, 15%); + } + } + + .media-object { + .square(@o-org-chart-entry-pic-size*0.8); + margin: @o-org-chart-entry-pic-size*0.1; + box-shadow: 0 0 0 @o-org-chart-entry-line-w darken(@o-org-chart-bg, 20%); + background-size: cover; + background-position: center center; + + &.well { + height: 20px; + box-shadow: none; + border-color: transparent; + padding: 0; + position: relative; + color: @text-color; + + .o_org_chart_show_more { + line-height: 13px; + } + + &:hover { + border-color: @o-org-chart-entry-border-color; + color:@odoo-brand-optional; + } + } + } + + &.o_org_chart_entry_manager, &.o_org_chart_entry_sub { + .media-left { + padding-right: 0; + } + .media-body > a { + padding-left: 10px; + max-width: calc(100% ~'-' 40px); + display: block; + + .media-heading { + color: lighten(@gray-light, 5%); + font-size: 13px; + } + } + + &:hover { + .media-object { + box-shadow: 0 0 0 @o-org-chart-entry-line-w*2 fade(@odoo-brand-optional, 60%); + } + .media-body > a { + .media-heading { + color: @odoo-brand-optional; + } + strong { + color: lighten(@gray-light, 5%); + } + } + } + } + + &.o_org_chart_entry_self { + &:not(:first-child) { + margin-top: @o-org-chart-entry-v-gap*1.5; + } + + strong { + color: @text-muted; + } + + .media-object { + .square(@o-org-chart-entry-pic-size); + margin: 0; + border: @o-org-chart-entry-line-w*2 solid @odoo-brand-optional; + box-shadow: inset 0 0 0 @o-org-chart-entry-line-w*2 white; + } + + .media-body { + opacity: 1; + } + } + } +} + +// POP OVER +.o_org_chart_popup.popover { + max-width: 400px; + + .popover-title { + height: 47px; + line-height: 33px; + padding-right: 50px; + + > a { + .o-position-absolute(@right: 14px); + } + + span { + .size(30px, 30px); + margin-right: 10px; + border-radius: 100%; + background-position: center; + background-size: cover; + float: left; + box-shadow: 0 1px 1px; + } + } + .table { + margin-bottom: 0; + } +} + 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 new file mode 100644 index 00000000000..661a1edc9d6 --- /dev/null +++ b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + +<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'"/> + + <div class="media-left"> + <!-- NOTE: Since by the default on not squared images odoo add white borders, + use bg-images to get a clean and centred images --> + <a t-if="! is_self" + class="media-object img-circle o_employee_redirect" + t-att-style="'background-image:url(\'/web/image/hr.employee/' + employee.id + '/image/\')'" + t-att-alt="employee.name" + t-att-data-employee-id="employee.id" + t-att-href="employee.link"/> + <div t-if="is_self" + class="media-object img-circle" + t-att-style="'background-image:url(\'/web/image/hr.employee/' + employee.id + '/image/\')'"/> + </div> + + <div class="media-body"> + <span + t-if="employee.indirect_sub_count > 0" + class="badge" + tabindex="0" + data-trigger="focus" + t-att-data-emp-name="employee.name" + t-att-data-emp-id="employee.id" + t-att-data-emp-dir-subs="employee.direct_sub_count" + t-att-data-emp-ind-subs="employee.indirect_sub_count" + data-toggle="popover"> + <t t-esc="employee.indirect_sub_count"/> + </span> + + <t t-if="!is_self"> + <a t-att-href="employee.link" class="o_employee_redirect" t-att-data-employee-id="employee.id"> + <h5 class="media-heading"><b><t t-esc="employee.name"/></b></h5> + <strong><t t-esc="employee.job_name"/></strong> + </a> + </t> + <t t-if="is_self"> + <h5 class="media-heading"><b><t t-esc="employee.name"/></b></h5> + <strong><t t-esc="employee.job_name"/></strong> + </t> + </div> + </div> +</t> + +<t t-name="hr_org_chart"> + <!-- NOTE: Desidered behaviour: + The maximun number of people is always 7 (including 'self'). Managers have priority over suburdinates + Eg. 1 Manager + 1 self = show just 5 subordinates (if availables) + Eg. 0 Manager + 1 self = show 6 subordinates (if available) + + --> + <t t-set="emp_count" t-value="0"/> + + <div t-if='widget.emp_data.managers.length > 0' class="o_org_chart_group_up"> + <t t-if='widget.emp_data.managers_more'> + <div class="o_org_chart_entry o_org_chart_more media"> + <div class="media-left"> + <a class="well media-object text-center o_employee_redirect" + t-att-href="widget.emp_data.managers[0].link" + t-att-data-employee-id="widget.emp_data.managers[0].id"> + <i t-attf-class="fa fa-angle-double-up"/> + </a> + </div> + </div> + </t> + + <t t-foreach="widget.emp_data.managers" t-as="employee"> + <t t-set="emp_count" t-value="emp_count + 1"/> + <t t-call="hr_org_chart_employee"> + <t t-set="employee_type" t-value="'manager'"/> + </t> + </t> + </div> + + <t t-if="widget.emp_data.children.length || widget.emp_data.managers.length" t-call="hr_org_chart_employee"> + <t t-set="employee_type" t-value="'self'"/> + <t t-set="employee" t-value="widget.emp_data.self"/> + </t> + + <t t-if="!widget.emp_data.children.length && !widget.emp_data.managers.length"> + <div class="alert alert-info"> + <p><b>No hierarchy position.</b></p> + <p>This employee has no manager or subordinate.</p> + <p>In order get an organigram, assign a manager to the related employees.</p> + </div> + </t> + + <div t-if="widget.emp_data.children.length" class="o_org_chart_group_down"> + <t t-foreach="widget.emp_data.children" t-as="employee"> + <t t-set="emp_count" t-value="emp_count + 1"/> + <t t-if="emp_count < 8"> + <t t-call="hr_org_chart_employee"> + <t t-set="employee_type" t-value="'sub'"/> + </t> + </t> + </t> + + <t t-if="(widget.emp_data.children.length + widget.emp_data.managers.length) > 7"> + <div class="o_org_chart_entry o_org_chart_more media"> + <div class="media-left"> + <a href="#" + t-att-data-employee-id="widget.emp_data.self.id" + class="well o_org_chart_show_more media-object text-center o_employee_sub_redirect">…</a> + </div> + </div> + </t> + </div> +</t> + +<t t-name="hr_orgchart_emp_popover"> + <div class="popover o_org_chart_popup" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div> +</t> + +<t t-name="hr_orgchart_emp_popover_content"> + <table class="table table-condensed"> + <thead> + <td class="text-right"><t t-esc="employee.direct_sub_count"/></td> + <td> + <a href="#" class="o_employee_sub_redirect" data-type='direct' + t-att-data-employee-name="employee.name" t-att-data-employee-id="employee.id"> + <b>Direct subordinates</b></a> + </td> + </thead> + <tbody> + <tr> + <td class="text-right"> + <t t-esc="employee.indirect_sub_count - employee.direct_sub_count"/> + </td> + <td> + <a href="#" class="o_employee_sub_redirect" data-type='indirect' + t-att-data-employee-name="employee.name" t-att-data-employee-id="employee.id"> + Indirect subordinates</a> + </td> + </tr> + <tr> + <td class="text-right"><t t-esc="employee.indirect_sub_count"/></td> + <td> + <a href="#" class="o_employee_sub_redirect" data-type='total' + t-att-data-employee-name="employee.name" t-att-data-employee-id="employee.id"> + Total</a> + </td> + </tr> + </tbody> + </table> +</t> + +<t t-name="hr_orgchart_emp_popover_title"> + <div> + <span t-att-style='"background-image:url(\"/web/image/hr.employee/" + employee.id + "/image/\")"'/> + <a href="#" class="pull-right o_employee_redirect" t-att-data-employee-id="employee.id"><i class="fa fa-external-link"></i></a> + <b><t t-esc="employee.name"/></b> + </div> +</t> + +</templates> diff --git a/addons/hr_org_chart/views/hr_templates.xml b/addons/hr_org_chart/views/hr_templates.xml new file mode 100644 index 00000000000..ae1e3665cfc --- /dev/null +++ b/addons/hr_org_chart/views/hr_templates.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_backend" name="hr_org_chart_assets_backend" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <link rel="stylesheet" type="text/less" href="/hr_org_chart/static/src/less/hr_org_chart.less"/> + <script type="text/javascript" src="/hr_org_chart/static/src/js/hr_org_chart.js"></script> + </xpath> + </template> +</odoo> diff --git a/addons/hr_org_chart/views/hr_views.xml b/addons/hr_org_chart/views/hr_views.xml new file mode 100644 index 00000000000..3aa5f5351d5 --- /dev/null +++ b/addons/hr_org_chart/views/hr_views.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="hr_employee_view_form_inherit_org_chart" model="ir.ui.view"> + <field name="name">hr.employee.view.form.inherit.org_chart</field> + <field name="model">hr.employee</field> + <field name="inherit_id" ref="hr.view_employee_form"/> + <field name="arch" type="xml"> + <div id="o_employee_main" position="after"> + <div id="o_employee_right"> + <h4 class="o_org_chart_title mb16 mt0">Organization Chart</h4> + <field name="child_ids" widget="hr_org_chart"/> + </div> + </div> + </field> + </record> +</odoo> |
