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