diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py
index 6a0dae5d5a2..f870d61892a 100644
--- a/addons/hr_holidays/models/hr.py
+++ b/addons/hr_holidays/models/hr.py
@@ -9,6 +9,7 @@ from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round
+# YTI TODO: Split into 2 files
class Department(models.Model):
_inherit = 'hr.department'
diff --git a/addons/hr_holidays/static/src/js/leave_stats_widget.js b/addons/hr_holidays/static/src/js/leave_stats_widget.js
new file mode 100644
index 00000000000..d224fe6ddca
--- /dev/null
+++ b/addons/hr_holidays/static/src/js/leave_stats_widget.js
@@ -0,0 +1,152 @@
+odoo.define('hr_holidays.LeaveStatsWidget', function (require) {
+ "use strict";
+
+ var time = require('web.time');
+ var Widget = require('web.Widget');
+ var widget_registry = require('web.widget_registry');
+
+ var LeaveStatsWidget = Widget.extend({
+ template: 'hr_holidays.leave_stats',
+
+ /**
+ * @override
+ * @param {Widget|null} parent
+ * @param {Object} params
+ */
+ init: function (parent, params) {
+ this._setState(params);
+ this._super(parent);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override to fetch data before rendering.
+ */
+ willStart: function () {
+ return Promise.all([this._super(), this._fetchLeaveTypesData(), this._fetchDepartmentLeaves()]);
+ },
+
+ /**
+ * Fetch new data if needed (according to updated fields) and re-render the widget.
+ * Called by the basic renderer when the view changes.
+ * @param {Object} state
+ * @returns {Promise}
+ */
+ updateState: function (state) {
+ var self = this;
+ var to_await = [];
+ var updatedFields = this._setState(state);
+
+ if (_.intersection(updatedFields, ['employee', 'date']).length) {
+ to_await.push(this._fetchLeaveTypesData());
+ }
+ if (_.intersection(updatedFields, ['department', 'date']).length) {
+ to_await.push(this._fetchDepartmentLeaves());
+ }
+ return Promise.all(to_await).then(function () {
+ self.renderElement();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Update the state
+ * @param {Object} state
+ * @returns {String[]} list of updated fields
+ */
+ _setState(state) {
+ var updatedFields = [];
+ if (state.data.employee_id.res_id !== (this.employee && this.employee.res_id)) {
+ updatedFields.push('employee');
+ this.employee = state.data.employee_id;
+ }
+ if (state.data.department_id.res_id !== (this.department && this.department.res_id)) {
+ updatedFields.push('department');
+ this.department = state.data.department_id;
+ }
+ if (state.data.date_from !== this.date) {
+ updatedFields.push('date');
+ this.date = state.data.date_from;
+ }
+ return updatedFields;
+ },
+
+ /**
+ * Fetch leaves taken by members of ``this.department`` in the
+ * month of ``this.date``.
+ * Three fields are fetched for each leave, namely: employee_id, date_from
+ * and date_to.
+ * The resulting data is assigned to ``this.departmentLeaves``
+ * @private
+ * @returns {Promise}
+ */
+ _fetchDepartmentLeaves: function () {
+ if (!this.date || !this.department) {
+ this.departmentLeaves = null;
+ return Promise.resolve();
+ }
+ var self = this;
+ var month_date_from = this.date.clone().startOf('month');
+ var month_date_to = this.date.clone().endOf('month');
+ return this._rpc({
+ model: 'hr.leave',
+ method: 'search_read',
+ args: [
+ [['department_id', '=', this.department.res_id],
+ ['state', '=', 'validate'],
+ ['holiday_type', '=', 'employee'],
+ ['date_from', '<=', month_date_to],
+ ['date_to', '>=', month_date_from]],
+ ['employee_id', 'date_from', 'date_to'],
+ ],
+ }).then(function (data) {
+ var dateFormat = time.getLangDateFormat();
+ self.departmentLeaves = data.map(function (leave) {
+ // Format datetimes to date (in the user's format)
+ return _.extend(leave, {
+ date_from: moment(leave.date_from).format(dateFormat),
+ date_to: moment(leave.date_to).format(dateFormat),
+ });
+ });
+ });
+ },
+
+ /**
+ * Fetch the number of leaves, grouped by leave type, taken by ``this.employee``
+ * in the year of ``this.date``.
+ * The resulting data is assigned to ``this.leavesPerType``
+ * @private
+ * @returns {Promise}
+ */
+ _fetchLeaveTypesData: function () {
+ if (!this.date || !this.employee) {
+ this.leavesPerType = null;
+ return Promise.resolve();
+ }
+ var self = this;
+ var year_date_from = this.date.clone().startOf('year');
+ var year_date_to = this.date.clone().endOf('year');
+ return this._rpc({
+ model: 'hr.leave',
+ method: 'read_group',
+ kwargs: {
+ domain: [['employee_id', '=', this.employee.res_id], ['state', '=', 'validate'], ['date_from', '<=', year_date_to], ['date_to', '>=', year_date_from]],
+ fields: ['holiday_status_id', 'number_of_days:sum'],
+ groupby: ['holiday_status_id'],
+ },
+ }).then(function (data) {
+ self.leavesPerType = data;
+ });
+ }
+ });
+
+ widget_registry.add('hr_leave_stats', LeaveStatsWidget);
+
+ return LeaveStatsWidget;
+});
diff --git a/addons/hr_holidays/static/src/xml/leave_stats_templates.xml b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml
new file mode 100644
index 00000000000..594ff0e6b61
--- /dev/null
+++ b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+ |
+ Time Off taken by in
+ |
+
+
+
+
+
+ | None |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+ In for Department
+
+ |
+
+
+
+
+
+ | None |
+
+
+
+
+ |
+ - |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_holidays/static/tests/test_leave_stats_widget.js b/addons/hr_holidays/static/tests/test_leave_stats_widget.js
new file mode 100644
index 00000000000..157459dc61d
--- /dev/null
+++ b/addons/hr_holidays/static/tests/test_leave_stats_widget.js
@@ -0,0 +1,156 @@
+odoo.define('hr_holidays.leave_stats_widget_tests', function (require) {
+ "use strict";
+
+ var FormView = require("web.FormView");
+ var testUtils = require('web.test_utils');
+
+ var createView = testUtils.createView;
+
+ QUnit.module('leave_stats_widget', {
+ beforeEach: function () {
+ this.data = {
+ department: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ },
+ records: [{id:11, name: "R&D"}],
+ },
+ employee: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ department_id: { string: "Department", type: "many2one", relation: 'department' },
+ },
+ records: [{
+ id: 100,
+ name: "Richard",
+ department_id: 11,
+ },{
+ id: 200,
+ name: "Jesus",
+ department_id: 11,
+ }],
+ },
+ 'hr.leave.type': {
+ fields: {
+ name: { string: "Name", type: "char" }
+ },
+ records: [{
+ id: 55,
+ name: "Legal Leave",
+ }]
+ },
+ 'hr.leave': {
+ fields: {
+ employee_id: { string: "Employee", type: "many2one", relation: 'employee' },
+ department_id: { string: "Department", type: "many2one", relation: 'department' },
+ date_from: { string: "From", type: "datetime" },
+ date_to: { string: "To", type: "datetime" },
+ holiday_status_id: { string: "Leave type", type: "many2one", relation: 'hr.leave.type' },
+ state: { string: "State", type: "char" },
+ holiday_type: { string: "Holiday Type", type: "char" },
+ number_of_days: { string: "State", type: "integer" },
+ },
+ records: [{
+ id: 12,
+ employee_id: 100,
+ department_id: 11,
+ date_from: "2016-10-20 09:00:00",
+ date_to: "2016-10-25 18:00:00",
+ holiday_status_id: 55,
+ state: 'validate',
+ number_of_days: 5,
+ holiday_type: 'employee',
+ },{
+ id: 13,
+ employee_id: 100,
+ department_id: 11,
+ date_from: "2016-10-2 09:00:00",
+ date_to: "2016-10-2 18:00:00",
+ holiday_status_id: 55,
+ state: 'validate',
+ number_of_days: 1,
+ holiday_type: 'employee',
+ },{
+ id: 14,
+ employee_id: 200,
+ department_id: 11,
+ date_from: "2016-10-15 09:00:00",
+ date_to: "2016-10-20 18:00:00",
+ holiday_status_id: 55,
+ state: 'validate',
+ number_of_days: 8,
+ holiday_type: 'employee',
+ }]
+ }
+ };
+ }
+ }, function () {
+ QUnit.test('leave stats renders correctly', async function (assert) {
+ assert.expect(5);
+ var self = this;
+ var form = await createView({
+ View: FormView,
+ model: 'hr.leave',
+ data: this.data,
+ arch: '
',
+ res_id: 12,
+ mockRPC: function (route, args) {
+ if (args.model === 'hr.leave' && args.method === 'search') {
+ return Promise.resolve(self.data['hr.leave'].records.map(function (record) { return record.id; }));
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ var $leaveTypeBody = form.$('.o_leave_stats table:first > tbody');
+ var $leavesDepartmentBody = form.$('.o_leave_stats table:nth-child(2) > tbody');
+ var $leavesDepartmentHeader = form.$('.o_leave_stats table:nth-child(2) > thead');
+
+ assert.strictEqual($leaveTypeBody.find('td:contains(Legal Leave)').length, 1, "it should have leave type");
+ assert.strictEqual($leaveTypeBody.find('td:contains(6)').length, 1, "it should have 6 days");
+
+ assert.strictEqual($leavesDepartmentBody.find('td:contains(Richard)').length, 2, "it should have 2 leaves for Richard");
+ assert.strictEqual($leavesDepartmentBody.find('td:contains(Jesus)').length, 1, "it should have 1 leaves for Jesus");
+ assert.strictEqual($leavesDepartmentHeader.find('td:contains(R&D)').length, 1, "it should have R&D title");
+ form.destroy();
+ });
+ QUnit.test('leave stats reload when employee/department changes', async function (assert) {
+ assert.expect(2);
+ var form = await createView({
+ View: FormView,
+ model: 'hr.leave',
+ mode: 'edit',
+ data: this.data,
+ arch: '',
+ mockRPC: function (route, args) {
+ if (args.model === 'hr.leave' && args.method === 'search_read') {
+ assert.ok(_.some(args.args[0], ['department_id', '=', 11]), "It should load department's leaves data");
+ }
+ if (args.model === 'hr.leave' && args.method === 'read_group') {
+ assert.ok(_.some(args.kwargs.domain, ['employee_id', '=', 200]), "It should load employee's leaves data");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ // Set date => shouldn't load data yet (no employee nor department defined)
+ await testUtils.fields.editSelect($('input[name="date_from"]'), '2016-10-12 09:00:00');
+ // Set employee => should load employee's date
+ await testUtils.fields.many2one.clickOpenDropdown("employee_id");
+ await testUtils.fields.many2one.clickItem("employee_id", "Jesus");
+ // Set department => should load department's data
+ await testUtils.fields.many2one.clickOpenDropdown("department_id");
+ await testUtils.fields.many2one.clickItem("department_id", "R&D");
+
+ form.destroy();
+ });
+ });
+});
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index 93381f54285..a87889e25a9 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -362,6 +362,29 @@
+
+ hr.leave.view.form.manager.approve
+ hr.leave
+
+ primary
+ 16
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
hr.holidays.view.tree
hr.leave