summaryrefslogtreecommitdiff
path: root/addons/hr_org_chart/static
diff options
context:
space:
mode:
authorFabien Pinckaers <fp@openerp.com>2017-03-27 11:12:09 +0200
committerThibault Delavallée <tde@odoo.com>2017-04-03 10:38:03 +0200
commite25dd35a251006f2d3c2548a1292e9dae029851d (patch)
tree24df5e78c56786fcadf3212115568cf2087f689e /addons/hr_org_chart/static
parent855e7eb043b720c8f343f2ec795bbe99584287a0 (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.
Diffstat (limited to 'addons/hr_org_chart/static')
-rw-r--r--addons/hr_org_chart/static/src/js/hr_org_chart.js140
-rw-r--r--addons/hr_org_chart/static/src/less/hr_org_chart.less286
-rw-r--r--addons/hr_org_chart/static/src/xml/hr_org_chart.xml160
3 files changed, 586 insertions, 0 deletions
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 &gt; 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 &gt; 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 &amp;&amp; !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 &lt; 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) &gt; 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">&#8230;</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>