From 6367ff2b33872f71fcc5f1b1003392a913b16beb Mon Sep 17 00:00:00 2001 From: Philippe Wauthy Date: Mon, 20 Sep 2021 13:26:31 +0000 Subject: [PATCH] [FIX] hr_holidays: Improve general UX (Back2basics) General fixes for usability - Time off type configuration visual : https://tinyurl.com/yj2z6okp - When you create an allocation, the end date should have a place holder "no limit". If there is no end date recorded, that value should be displayed on loading. - In Overview, remove filter "people i manage" - In Approvals / time off : change filters : https://i.imgur.com/uDNjZv8.png - In Approvals / approvals : change filters like in point 5 (remove people I manage replace with my team) - In Reporting / By Employee : filters : https://tinyurl.com/yg3u3rkf - In Reporting / By Type : filters : https://tinyurl.com/yhbpgz5m - Remove Allocations smartbutton from Employee App - When you click on TimeOff Smartbutton from Employee App, you should be redirect to this screen : https://tinyurl.com/ygwxzx5b - When an allocation is created, the validity is added to the name of the allocation like "Allocation X (validity Date --> Date) and hide the validity field in "My allocations". - Users will have the information of the validity from the name of the allocation task-2643692 closes odoo/odoo#77376 X-original-commit: 7f92c830d3607167bd76bc43d992fbf720a3ef29 Signed-off-by: Yannick Tivisse (yti) --- addons/hr_holidays/__manifest__.py | 1 + addons/hr_holidays/models/hr_employee.py | 18 +- .../hr_holidays/models/hr_leave_allocation.py | 10 + addons/hr_holidays/models/hr_leave_type.py | 3 +- .../report/hr_leave_report_calendar.xml | 5 +- .../hr_holidays/report/hr_leave_reports.xml | 3 +- .../static/src/js/time_off_calendar.js | 71 +++-- .../src/js/time_off_calendar_employee.js | 266 ++++++++++++++++++ .../views/hr_leave_allocation_views.xml | 66 +++-- .../hr_holidays/views/hr_leave_type_views.xml | 13 +- addons/hr_holidays/views/hr_leave_views.xml | 22 +- addons/hr_holidays/views/hr_views.xml | 25 +- .../views/hr_holidays_views.xml | 5 +- 13 files changed, 430 insertions(+), 78 deletions(-) create mode 100644 addons/hr_holidays/static/src/js/time_off_calendar_employee.js diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index b2911b88e2b..76baca06f2d 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -66,6 +66,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p ], 'web.assets_backend': [ 'hr_holidays/static/src/js/time_off_calendar.js', + 'hr_holidays/static/src/js/time_off_calendar_employee.js', 'hr_holidays/static/src/js/radio_image.js', 'hr_holidays/static/src/js/leave_stats_widget.js', 'hr_holidays/static/src/models/*/*.js', diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index 1b344387c39..f14706120ba 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -4,7 +4,7 @@ import datetime from dateutil.relativedelta import relativedelta -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.tools.float_utils import float_round @@ -246,3 +246,19 @@ class HrEmployee(models.Model): def _get_user_m2o_to_empty_on_archived_employees(self): return super()._get_user_m2o_to_empty_on_archived_employees() + ['leave_manager_id'] + + def action_time_off_dashboard(self): + domain = [] + if self.env.context.get('active_ids'): + domain = [('employee_id', 'in', self.env.context.get('active_ids', []))] + + return { + 'name': _('Time Off Dashboard'), + 'type': 'ir.actions.act_window', + 'res_model': 'hr.leave', + 'views': [[self.env.ref('hr_holidays.hr_leave_employee_view_dashboard').id, 'calendar']], + 'domain': domain, + 'context': { + 'employee_id': self.ids, + }, + } diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index c98cb9205f9..71019014c5d 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -41,6 +41,7 @@ class HolidaysAllocation(models.Model): return [('employee_requests', '=', 'yes')] name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False) + name_validity = fields.Char('Description with validity', compute='_compute_description_validity') active = fields.Boolean(default=True) private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user') state = fields.Selection([ @@ -181,6 +182,15 @@ class HolidaysAllocation(models.Model): allocations = self.sudo().search(domain) return [('id', 'in', allocations.ids)] + @api.depends('name', 'date_from', 'date_to') + def _compute_description_validity(self): + for allocation in self: + if allocation.date_to: + name_validity = _("%s (from %s to %s)", allocation.name, allocation.date_from.strftime("%b %d %Y"), allocation.date_to.strftime("%b %d %Y")) + else: + name_validity = _("%s (from %s to No Limit)", allocation.name, allocation.date_from.strftime("%b %d %Y")) + allocation.name_validity = name_validity + @api.depends('employee_id', 'holiday_status_id', 'taken_leave_ids.number_of_days', 'taken_leave_ids.state') def _compute_leaves(self): for allocation in self: diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index d93f54ebb30..241c29316ff 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -327,7 +327,8 @@ class HolidaysType(models.Model): employee_id = self._get_contextual_employee_id() if employee_id: - data_days = self.get_employees_days([employee_id])[employee_id] + data_days = (self.get_employees_days(employee_id)[employee_id[0]] if isinstance(employee_id, list) else + self.get_employees_days([employee_id])[employee_id]) for holiday_status in self: result = data_days.get(holiday_status.id, {}) diff --git a/addons/hr_holidays/report/hr_leave_report_calendar.xml b/addons/hr_holidays/report/hr_leave_report_calendar.xml index 341761d99ce..4af565e6a7f 100644 --- a/addons/hr_holidays/report/hr_leave_report_calendar.xml +++ b/addons/hr_holidays/report/hr_leave_report_calendar.xml @@ -37,10 +37,9 @@ - - + - + diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml index f6b9b822bc2..b3ef2d4296d 100644 --- a/addons/hr_holidays/report/hr_leave_reports.xml +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -13,8 +13,7 @@ - - + diff --git a/addons/hr_holidays/static/src/js/time_off_calendar.js b/addons/hr_holidays/static/src/js/time_off_calendar.js index 120a8fa3167..03db8ffc202 100644 --- a/addons/hr_holidays/static/src/js/time_off_calendar.js +++ b/addons/hr_holidays/static/src/js/time_off_calendar.js @@ -8,6 +8,7 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { var CalendarRenderer = require("web.CalendarRenderer"); var CalendarModel = require("web.CalendarModel"); var CalendarView = require("web.CalendarView"); + var dialogs = require('web.view_dialogs'); var viewRegistry = require('web.view_registry'); var _t = core._t; @@ -82,12 +83,32 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { * @private */ _onNewTimeOff: function () { - var self = this; - //use formViewId for this view? - this.do_action('hr_holidays.hr_leave_action_my_request', { - on_close: function () { - self.reload(); - } + let self = this; + + let domain = [ + ['name', '=', 'hr.leave.view.form.dashboard.new.time.off'], + ['model', '=', 'hr.leave'] + ]; + + self._rpc({ + model: 'ir.ui.view', + method: 'search', + args: [domain], + }).then(function(ids) { + self.timeOffDialog = new dialogs.FormViewDialog(self, { + res_model: "hr.leave", + view_id: ids, + context: { + 'default_date_from': moment().format('YYYY-MM-DD'), + 'default_date_to': moment().add(1, 'days').format('YYYY-MM-DD'), + }, + title: _t("New time off"), + disable_multiple_selection: true, + on_saved: function() { + self.reload(); + }, + }); + self.timeOffDialog.open(); }); }, @@ -97,18 +118,32 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { * @private */ _onNewAllocation: function () { - var self = this; - this.do_action({ - type: 'ir.actions.act_window', - res_model: 'hr.leave.allocation', - name: 'New Allocation Request', - views: [[false,'form']], - context: {'form_view_ref': 'hr_holidays.hr_leave_allocation_view_form_dashboard'}, - target: 'new', - }, { - on_close: function () { - self.reload(); - } + let self = this; + + let domain = [ + ['name', '=', 'hr.leave.view.form.dashboard'], + ['model', '=', 'hr.leave.allocation'] + ]; + + self._rpc({ + model: 'ir.ui.view', + method: 'search', + args: [domain], + }).then(function(ids) { + self.allocationDialog = new dialogs.FormViewDialog(self, { + res_model: "hr.leave.allocation", + view_id: ids, + context: { + 'default_employee_ids': self.context.employee_id, + 'default_state': 'confirm', + }, + title: _t("New Allocation"), + disable_multiple_selection: true, + on_saved: function() { + self.reload(); + }, + }); + self.allocationDialog.open(); }); }, diff --git a/addons/hr_holidays/static/src/js/time_off_calendar_employee.js b/addons/hr_holidays/static/src/js/time_off_calendar_employee.js new file mode 100644 index 00000000000..3aa71c6c367 --- /dev/null +++ b/addons/hr_holidays/static/src/js/time_off_calendar_employee.js @@ -0,0 +1,266 @@ +odoo.define('hr_holidays.employee.dashboard.views', function(require) { + 'use strict'; + + var core = require('web.core'); + const config = require('web.config'); + var CalendarController = require("web.CalendarController"); + var CalendarRenderer = require("web.CalendarRenderer"); + var CalendarPopover = require('web.CalendarPopover'); + var CalendarView = require("web.CalendarView"); + var dialogs = require('web.view_dialogs'); + var viewRegistry = require('web.view_registry'); + + var _t = core._t; + var QWeb = core.qweb; + + // TODO in master: Split the timeoff calendar components into several files to reuse or extend them here + + var TimeOffCalendarPopover = CalendarPopover.extend({ + template: 'hr_holidays.calendar.popover', + + init: function (parent, eventInfo) { + this._super.apply(this, arguments); + const state = this.event.extendedProps.record.state; + this.canDelete = state && ['validate', 'refuse'].indexOf(state) === -1; + this.canEdit = state !== undefined; + this.displayFields = []; + + if (this.modelName === "hr.leave.report.calendar") { + const duration = this.event.extendedProps.record.display_name.split(':').slice(-1); + this.display_name = _.str.sprintf(_t("Time Off : %s"), duration); + } else { + this.display_name = this.event.extendedProps.record.display_name; + } + }, + }); + + var TimeOffPopoverRenderer = CalendarRenderer.extend({ + template: "TimeOff.CalendarView.extend", + /** + * We're overriding this to display the weeknumbers on the year view + * + * @override + * @private + */ + _getFullCalendarOptions: function () { + const oldOptions = this._super(...arguments); + // Parameters + oldOptions.views.dayGridYear.weekNumbers = true; + oldOptions.views.dayGridYear.weekNumbersWithinDays = false; + return oldOptions; + }, + + config: _.extend({}, CalendarRenderer.prototype.config, { + CalendarPopover: TimeOffCalendarPopover, + }), + + _getPopoverParams: function (eventData) { + let params = this._super.apply(this, arguments); + let calendarIcon; + let state = eventData.extendedProps.record.state; + + if (state === 'validate') { + calendarIcon = 'fa-calendar-check-o'; + } else if (state === 'refuse') { + calendarIcon = 'fa-calendar-times-o'; + } else if(state) { + calendarIcon = 'fa-calendar-o'; + } + + params['title'] = eventData.extendedProps.record.display_name.split(':').slice(0, -1).join(':'); + params['template'] = QWeb.render('hr_holidays.calendar.popover.placeholder', {color: this.getColor(eventData.color_index), calendarIcon: calendarIcon}); + return params; + }, + + _render: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$el.parent().find('.o_calendar_mini').hide(); + }); + }, + /** + * @override + * @private + */ + _renderCalendar: function() { + this._super.apply(this, arguments); + let weekNumbers = this.$el.find('.fc-week-number'); + weekNumbers.each( function() { + let weekRow = this.parentNode; + // By default, each month has 6 weeks displayed, hide the week number if there is no days for the week + if(!weekRow.children[1].classList.length && !weekRow.children[weekRow.children.length-1].classList.length) { + this.innerHTML = ''; + } + }); + }, + }); + + var TimeOffCalendarRenderer = TimeOffPopoverRenderer.extend({ + _render: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + return self._rpc({ + model: 'hr.leave.type', + method: 'get_days_all_request', + context: self.state.context, + }); + }).then(function (result) { + self.$el.parent().find('.o_calendar_mini').hide(); + self.$el.parent().find('.o_timeoff_container').remove(); + + // Do not display header if there is no element to display + if (result.length > 0) { + if (config.device.isMobile) { + result.forEach((data) => { + const elem = QWeb.render('hr_holidays.dashboard_calendar_header_mobile', { + timeoff: data, + }); + self.$el.find('.o_calendar_filter_item[data-value=' + data[4] + '] .o_cw_filter_title').append(elem); + }); + } else { + const elem = QWeb.render('hr_holidays.dashboard_calendar_header', { + timeoffs: result, + }); + self.$el.before(elem); + } + } + }); + }, + }); + + var TimeOffCalendarEmployeeController = CalendarController.extend({ + + events: _.extend({}, CalendarController.prototype.events, { + 'click .btn-time-off': '_onNewTimeOff', + 'click .btn-allocation': '_onNewAllocation', + }), + + /** + * @override + */ + start: function () { + this.$el.addClass('o_timeoff_calendar'); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Render the buttons and add new button about + * time off and allocations request + * + * @override + */ + + renderButtons: function ($node) { + this._super.apply(this, arguments); + + $(QWeb.render('hr_holidays.dashboard.calendar.button', { + time_off: _t('New Time Off'), + request: _t('New Allocation Request'), + })).appendTo(this.$buttons); + + if ($node) { + this.$buttons.appendTo($node); + } else { + this.$('.o_calendar_buttons').replaceWith(this.$buttons); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Action: create a new time off request + * + * @private + */ + _onNewTimeOff: function () { + let self = this; + + let domain = [ + ['name', '=', 'hr.leave.view.form.dashboard.new.time.off'] + ]; + + self._rpc({ + model: 'ir.ui.view', + method: 'search', + args: [domain], + }).then(function(ids) { + self.timeOffDialog = new dialogs.FormViewDialog(self, { + res_model: "hr.leave", + view_id: ids, + context: { + 'default_employee_id': self.context.employee_id[0], + 'default_date_from': moment().format('YYYY-MM-DD'), + 'default_date_to': moment().add(1, 'days').format('YYYY-MM-DD'), + }, + title: _t("New time off"), + disable_multiple_selection: true, + on_saved: function() { + self.reload(); + }, + }); + self.timeOffDialog.open(); + }); + }, + + /** + * Action: create a new allocation request + * + * @private + */ + _onNewAllocation: function () { + let self = this; + + let domain = [ + ['name', '=', 'hr.leave.allocation.view.form.manager.dashboard'] + ]; + + self._rpc({ + model: 'ir.ui.view', + method: 'search', + args: [domain], + }).then(function(ids) { + self.allocationDialog = new dialogs.FormViewDialog(self, { + res_model: "hr.leave.allocation", + view_id: ids, + context: { + 'default_employee_ids': self.context.employee_id, + 'default_state': 'confirm', + }, + title: _t("New Allocation"), + disable_multiple_selection: true, + on_saved: function() { + self.reload(); + }, + }); + self.allocationDialog.open(); + }); + }, + + _onOpenCreate: function () { + this.context['default_employee_id'] = this.context.employee_id[0]; + this._super(...arguments); + }, + + /** + * @override + */ + _setEventTitle: function () { + return _t('Time Off Request'); + }, + }); + + var TimeOffCalendarEmployeeView = CalendarView.extend({ + config: _.extend({}, CalendarView.prototype.config, { + Controller: TimeOffCalendarEmployeeController, + Renderer: TimeOffCalendarRenderer, + }), + }); + + viewRegistry.add('time_off_employee_calendar', TimeOffCalendarEmployeeView); +}); diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index 058d62fe4bd..22d36f03f4b 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -17,8 +17,8 @@ - - + + @@ -90,8 +90,9 @@ -
-

+
+

+

@@ -103,14 +104,15 @@
-
- - + +
@@ -157,6 +159,11 @@
- -
diff --git a/addons/project_timesheet_holidays/views/hr_holidays_views.xml b/addons/project_timesheet_holidays/views/hr_holidays_views.xml index 1ccfcfad294..2c25ad5c8b7 100644 --- a/addons/project_timesheet_holidays/views/hr_holidays_views.xml +++ b/addons/project_timesheet_holidays/views/hr_holidays_views.xml @@ -6,8 +6,9 @@ hr.leave.type - - + + +

Timesheets