diff options
| author | Lucas Lefèvre <lul@odoo.com> | 2019-05-02 15:35:09 +0200 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-05-17 11:22:10 +0200 |
| commit | 2b9aec24155e2ecc6164ad44a0b3d2dde14ea7f4 (patch) | |
| tree | 2d1d0c41884a16b37ee9f4ca48769ca1d21b4352 | |
| parent | 09e515eecc5610c8af0c3989b5b023ad435ca437 (diff) | |
[IMP] hr_holidays: Add leave stats widget
When approving a leave, to make an informed decision, it might
be needed to check leaves already taken by the employee or leaves
of its collaborators. This commit adds a widget displaying some stats
in a leave form view. Stats displayed are:
- the number of days taken
by the employee this year, grouped by leave type
- Leaves taken by employees in the same department in the month
of the leave.
This widget is only displayed in the context of the action leading
to leaves to approve.
| -rw-r--r-- | addons/hr_holidays/models/hr.py | 1 | ||||
| -rw-r--r-- | addons/hr_holidays/static/src/js/leave_stats_widget.js | 152 | ||||
| -rw-r--r-- | addons/hr_holidays/static/src/xml/leave_stats_templates.xml | 64 | ||||
| -rw-r--r-- | addons/hr_holidays/static/tests/test_leave_stats_widget.js | 156 | ||||
| -rw-r--r-- | addons/hr_holidays/views/hr_leave_views.xml | 23 |
5 files changed, 396 insertions, 0 deletions
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 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates id="template" xml:space="preserve"> + <t t-name="hr_holidays.leave_per_type"> + <table class="o_group o_inner_group table-striped"> + <thead> + <tr> + <td colspan="2"> + <div class="o_horizontal_separator">Time Off taken by <t t-esc="widget.employee.data.display_name"/> in <t t-esc="widget.date.format('YYYY')"/></div> + </td> + </tr> + </thead> + <tbody> + <t t-if="widget.leavesPerType.length === 0"> + <tr> + <td>None</td> + </tr> + </t> + <t t-foreach="widget.leavesPerType" t-as="leave_type"> + <tr> + <td><t t-esc="leave_type.holiday_status_id[1]"/></td> + <td class="w-50"><t t-esc="leave_type.number_of_days"/></td> + </tr> + </t> + </tbody> + </table> + </t> + + <t t-name="hr_holidays.department_leave"> + <table class="o_group o_inner_group table-striped"> + <thead> + <tr> + <td colspan="2"> + <div class="o_horizontal_separator">In <t t-esc="widget.date.format('MMMM')"/> for <t t-esc="widget.department.data.display_name"/> Department + </div> + </td> + </tr> + </thead> + <tbody> + <t t-if="widget.departmentLeaves.length === 0"> + <tr> + <td>None</td> + </tr> + </t> + <t t-foreach="widget.departmentLeaves" t-as="leave"> + <tr t-attf-class="{{leave.employee_id[0] === widget.employee.res_id ? 'font-weight-bold' : ''}}"> + <td><t t-esc="leave.employee_id[1]"/></td> + <td class="w-50"><t t-esc="leave.date_from"/> - <t t-esc="leave.date_to"/></td> + </tr> + </t> + </tbody> + </table> + </t> + + <div t-name="hr_holidays.leave_stats" class="o_leave_stats"> + <t t-if="widget.leavesPerType"> + <t t-call="hr_holidays.leave_per_type"/> + </t> + <t t-if="widget.departmentLeaves"> + <t t-call="hr_holidays.department_leave"/> + </t> + </div> + +</templates> 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: '<form string="Leave">' + + '<field name="employee_id"/>' + + '<field name="department_id"/>' + + '<field name="date_from"/>' + + '<widget name="hr_leave_stats"/>' + + '</form>', + 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: '<form string="Leave">' + + '<field name="employee_id"/>' + + '<field name="department_id"/>' + + '<field name="date_from"/>' + + '<widget name="hr_leave_stats"/>' + + '</form>', + 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 @@ </field> </record> + <record id="hr_leave_view_form_manager_approve" model="ir.ui.view"> + <field name="name">hr.leave.view.form.manager.approve</field> + <field name="model">hr.leave</field> + <field name="inherit_id" ref="hr_leave_view_form"/> + <field name="mode">primary</field> + <field name="priority">16</field> + <field name="arch" type="xml"> + <xpath expr="//field[@name='holiday_status_id']" position="before"> + <field name="employee_id"/> + </xpath> + <xpath expr="//group[@name='col_right']" position="replace"> + <field name="department_id" invisible="1"/> + <group> + <widget name="hr_leave_stats"/> + </group> + <div groups="hr_holidays.group_hr_holidays_manager"> + <separator string="Comment by Manager"/> + <field name="report_note" placeholder="e.g. Report to the next month..."/> + </div> + </xpath> + </field> + </record> + <record id="hr_leave_view_tree" model="ir.ui.view"> <field name="name">hr.holidays.view.tree</field> <field name="model">hr.leave</field> |
