From 2af0a86db89f08ce817e4327ba27d295845a9150 Mon Sep 17 00:00:00 2001 From: Giacomello Nathan Date: Mon, 24 Aug 2020 10:07:03 +0200 Subject: [PATCH 1/3] [IMP] Web: Add field to calendar & resize Year view - Resize Year view - Add "is_hatched" field - Scroll to current month in Year view - Hide the sidebar if no filter is present The goal with Year view is to display it in a single page without scroll bar. Before this commit, a scrollbar was displayed on the right of the screen. This results in some data wasn't displayed on the screen. Now, the year view is scaled to the browser. That means that all information are now on the screen (without scrollbar). "is_hatched" is used to hatch date under specific conditions The goal was that it was not possible to display some information differently depending on a variable. Now, if a variable named "is_hatched" is set to true and passed to the calendar view, the specific date is hatched. Task id #2318645 --- addons/web/static/src/js/libs/fullcalendar.js | 9 ++++ addons/web/static/src/scss/web_calendar.scss | 49 +++++++++++++------ addons/web/static/src/xml/web_calendar.xml | 2 +- .../web/static/tests/views/calendar_tests.js | 21 +++++--- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/addons/web/static/src/js/libs/fullcalendar.js b/addons/web/static/src/js/libs/fullcalendar.js index 7f9714e80e6..16040936ea0 100644 --- a/addons/web/static/src/js/libs/fullcalendar.js +++ b/addons/web/static/src/js/libs/fullcalendar.js @@ -64,6 +64,8 @@ odoo.define('/web/static/src/js/libs/fullcalendar.js', function () { this.el.classList.add('fc-readonly-year-view'); } this.months = []; + const currentMonthNumber = (new Date()).getMonth(); + let currentMonthEl = undefined; for (let monthNumber = 0; monthNumber < 12; monthNumber++) { const monthDate = new Date(this.currentDate.getFullYear(), monthNumber); const monthShortName = moment(monthDate).format('MMM').toLowerCase(); @@ -76,6 +78,13 @@ odoo.define('/web/static/src/js/libs/fullcalendar.js', function () { const calendar = this._createMonthCalendar(el, monthDate); this.months.push({ el, calendar }); calendar.render(); + if (monthNumber === currentMonthNumber) { + currentMonthEl = el; + } + } + + if (currentMonthEl !== undefined) { + currentMonthEl.scrollIntoView(); } } /** diff --git a/addons/web/static/src/scss/web_calendar.scss b/addons/web/static/src/scss/web_calendar.scss index 9c429491cd7..007e66d2bcd 100644 --- a/addons/web/static/src/scss/web_calendar.scss +++ b/addons/web/static/src/scss/web_calendar.scss @@ -40,7 +40,7 @@ $o-cw-filter-avatar-size: 20px; .fc-bg { background-color: mix(theme-color('primary'), white); // Used for placeholder events only (on creation) @include size(101%); // Compensate border - opacity: 0.9; + opacity: 0.7; transition: opacity 0.2s; } @@ -303,9 +303,6 @@ $o-cw-filter-avatar-size: 20px; } &.o_cw_nobg { - .fc-bg { - visibility: hidden; - } &.o_cw_custom_hover, &.o_cw_custom_highlight, &:hover { .fc-bg { @@ -377,8 +374,8 @@ $o-cw-filter-avatar-size: 20px; margin: auto; > .fc-toolbar.fc-header-toolbar { - margin-top: 15px; - margin-bottom: 5px; + margin-top: 10px; + margin-bottom: 4px; cursor: default; h2 { @@ -397,7 +394,7 @@ $o-cw-filter-avatar-size: 20px; } .fc-week.fc-row { - min-height: unset; + min-height: 2rem; } .fc-disabled-day { @@ -406,7 +403,7 @@ $o-cw-filter-avatar-size: 20px; .fc-day-top { text-align: center; - padding: 5px; + padding: 0.4vh; cursor: pointer; > .fc-day-number { @@ -415,7 +412,7 @@ $o-cw-filter-avatar-size: 20px; line-height: unset; margin: auto; padding: 0.1rem 0; - font-size: 1.2rem; + font-size: calc(11px + 0.2vh); } &.fc-today > .fc-day-number { @@ -718,15 +715,28 @@ $o-cw-filter-avatar-size: 20px; $color: nth($o-colors-complete, $i); .o_calendar_view .fc-view { + .o_calendar_hatched_#{$i - 1} { + background: repeating-linear-gradient( + 45deg, + $color, + $color 10px, + rgba($color, 0.4) 10px, + rgba($color, 0.4) 20px + ) !important; + + .fc-bg { + background: rgba(white, 0.5); + } + } .fc-bgevent.o_calendar_color_#{$i - 1} { border-color: $color; background-color: $color; - opacity: 0.2; + opacity: 0.5; } .fc-event.o_calendar_color_#{$i - 1} { border-color: $color; - color: darken($color, 35%); - opacity: 0.8; + color: darken($color, 45%); + opacity: 0.75; &.o_event_hightlight { opacity: 1; @@ -736,8 +746,11 @@ $o-cw-filter-avatar-size: 20px; } } - .fc-bg { - background: mix($color, white); + &:not(.o_calendar_hatched) { + background: $color; + .fc-bg { + background: mix($color, white); + } } &.o_cw_custom_hover, &.o_cw_custom_highlight { @@ -745,7 +758,13 @@ $o-cw-filter-avatar-size: 20px; color: color-yiq($color); .fc-bg { - background: $color; + background: repeating-linear-gradient( + 45deg, + $color, + $color 10px, + rgba($color, 0.5) 10px, + rgba($color, 0.5) 20px + ); } } } diff --git a/addons/web/static/src/xml/web_calendar.xml b/addons/web/static/src/xml/web_calendar.xml index c54329e663b..f77e1198217 100644 --- a/addons/web/static/src/xml/web_calendar.xml +++ b/addons/web/static/src/xml/web_calendar.xml @@ -12,7 +12,7 @@ -
+
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 6ecb0af0fe0..ab65524177b 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -59,15 +59,16 @@ QUnit.module('Views', { type: {string: "type", type: "integer"}, event_type_id: {string: "Event_Type", type: "many2one", relation: 'event_type'}, color: {string: "Color", type: "integer", related: 'event_type_id.color'}, + is_hatched: {string: "Hatched", type: "boolean"} }, records: [ - {id: 1, user_id: session.uid, partner_id: 1, name: "event 1", start: "2016-12-11 00:00:00", stop: "2016-12-11 00:00:00", allday: false, partner_ids: [1,2,3], type: 1}, - {id: 2, user_id: session.uid, partner_id: 1, name: "event 2", start: "2016-12-12 10:55:05", stop: "2016-12-12 14:55:05", allday: false, partner_ids: [1,2], type: 3}, - {id: 3, user_id: 4, partner_id: 4, name: "event 3", start: "2016-12-12 15:55:05", stop: "2016-12-12 16:55:05", allday: false, partner_ids: [1], type: 2}, - {id: 4, user_id: session.uid, partner_id: 1, name: "event 4", start: "2016-12-14 15:55:05", stop: "2016-12-14 18:55:05", allday: true, partner_ids: [1], type: 2}, - {id: 5, user_id: 4, partner_id: 4, name: "event 5", start: "2016-12-13 15:55:05", stop: "2016-12-20 18:55:05", allday: false, partner_ids: [2,3], type: 2}, - {id: 6, user_id: session.uid, partner_id: 1, name: "event 6", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [3], type: 3}, - {id: 7, user_id: session.uid, partner_id: 1, name: "event 7", start: "2016-11-14 08:00:00", stop: "2016-11-16 17:00:00", allday: false, partner_ids: [2], type: 1}, + {id: 1, user_id: session.uid, partner_id: 1, name: "event 1", start: "2016-12-11 00:00:00", stop: "2016-12-11 00:00:00", allday: false, partner_ids: [1,2,3], type: 1, is_hatched: false}, + {id: 2, user_id: session.uid, partner_id: 1, name: "event 2", start: "2016-12-12 10:55:05", stop: "2016-12-12 14:55:05", allday: false, partner_ids: [1,2], type: 3, is_hatched: false}, + {id: 3, user_id: 4, partner_id: 4, name: "event 3", start: "2016-12-12 15:55:05", stop: "2016-12-12 16:55:05", allday: false, partner_ids: [1], type: 2, is_hatched: true}, + {id: 4, user_id: session.uid, partner_id: 1, name: "event 4", start: "2016-12-14 15:55:05", stop: "2016-12-14 18:55:05", allday: true, partner_ids: [1], type: 2, is_hatched: false}, + {id: 5, user_id: 4, partner_id: 4, name: "event 5", start: "2016-12-13 15:55:05", stop: "2016-12-20 18:55:05", allday: false, partner_ids: [2,3], type: 2, is_hatched: true}, + {id: 6, user_id: session.uid, partner_id: 1, name: "event 6", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [3], type: 3, is_hatched: true}, + {id: 7, user_id: session.uid, partner_id: 1, name: "event 7", start: "2016-11-14 08:00:00", stop: "2016-11-16 17:00:00", allday: false, partner_ids: [2], type: 1, is_hatched: false}, ], check_access_rights: function () { return Promise.resolve(true); @@ -3681,7 +3682,7 @@ QUnit.module('Views', { }); QUnit.test('correctly display year view', async function (assert) { - assert.expect(27); + assert.expect(28); const calendar = await createCalendarView({ View: CalendarView, @@ -3700,6 +3701,7 @@ QUnit.module('Views', { > + `, archs: archs, viewOptions: { @@ -3716,6 +3718,9 @@ QUnit.module('Views', { assert.containsN(calendar.el, '.fc-bgevent', 7, 'There should be 6 events displayed but there is 1 split on 2 weeks'); + assert.containsN(calendar.el, '.o_calendar_hatched', 3, + 'There should be 3 events that are hatched') + async function clickDate(date) { const el = calendar.el.querySelector(`.fc-day-top[data-date="${date}"]`); el.scrollIntoView(); // scroll to it as the calendar could be too small From 6cfec48ba4d918b67015c8581b6c17536454c318 Mon Sep 17 00:00:00 2001 From: Giacomello Nathan Date: Fri, 28 Aug 2020 15:19:20 +0200 Subject: [PATCH 2/3] [FIX] Web: Cannot collapse calendar sidebar A calendar filter's id is the concatenation of "o_cw_filter_collapse_" and the title of the filter. When title has space (like "Time Off Type"), the id become "o_cw_filter_collapse_Time Off Type", which is invalid. So, we can't collapse the filter. With this fix, it replace space by nothing so if there is space in title (like "Time Off Type"), the id is "o_cw_filter_collapse_TimeOffType". Task id: 2318645 --- addons/web/static/src/xml/web_calendar.xml | 5 +++-- addons/web/static/tests/views/calendar_tests.js | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/xml/web_calendar.xml b/addons/web/static/src/xml/web_calendar.xml index f77e1198217..05c71c7312b 100644 --- a/addons/web/static/src/xml/web_calendar.xml +++ b/addons/web/static/src/xml/web_calendar.xml @@ -52,14 +52,15 @@ +
-
+
-
+
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index ab65524177b..308ac98fde8 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -57,7 +57,7 @@ QUnit.module('Views', { allday: {string: "allday", type: "boolean"}, partner_ids: {string: "attendees", type: "one2many", relation: 'partner', default: [[6, 0, [1]]]}, type: {string: "type", type: "integer"}, - event_type_id: {string: "Event_Type", type: "many2one", relation: 'event_type'}, + event_type_id: {string: "Event Type", type: "many2one", relation: 'event_type'}, color: {string: "Color", type: "integer", related: 'event_type_id.color'}, is_hatched: {string: "Hatched", type: "boolean"} }, @@ -2190,7 +2190,7 @@ QUnit.module('Views', { }); QUnit.test('Add filters and specific color', async function (assert) { - assert.expect(5); + assert.expect(6); this.data.event.records.push( {id: 8, user_id: 4, partner_id: 1, name: "event 8", start: "2016-12-11 09:00:00", stop: "2016-12-11 10:00:00", allday: false, partner_ids: [1,2,3], event_type_id: 3, color: 4}, @@ -2219,8 +2219,9 @@ QUnit.module('Views', { assert.containsN(calendar, '.o_calendar_filter', 2, "should display 2 filters"); - var $typeFilter = calendar.$('.o_calendar_filter:has(h5:contains(Event_Type))'); + var $typeFilter = calendar.$('.o_calendar_filter:has(h5:contains(Event Type))'); assert.ok($typeFilter.length, "should display 'Event Type' filter"); + assert.containsOnce($typeFilter, '#o_cw_filter_collapse_EventType', "Id should be equals to o_cw_filter_collapse_EventType for 'Event Type'"); assert.containsN($typeFilter, '.o_calendar_filter_item', 3, "should display 3 filter items for 'Event Type'"); assert.containsOnce($typeFilter, '.o_calendar_filter_item[data-value=3].o_cw_filter_color_4', "Filter for event type 3 must have the color 4"); From ec25487cfbf388ebd1b10d3db60657e39bbaa809 Mon Sep 17 00:00:00 2001 From: Giacomello Nathan Date: Thu, 10 Sep 2020 16:29:51 +0200 Subject: [PATCH 3/3] [IMP] Time Off: Year Calendar ajustments A new calendar view has been added, the "year" view. Some modifications should be done on this view, more specialy in "Time Off" application. Problems were the fact that it was hard to see if a time off has been approved or not. Another was the fact that week-ends & public holidays were not grey. This commit change year calendar in Time Off module. More specially, this commit: - Change default calendar in personal dashboard to "year" - Gray week-ends & public holiday (based on the worker's calendar) - Hatch non-approved time off - Removed a calendar view from My Time Off Requests - Fix a "cancel" button that doesn't work - Add a new view that is displayed when there is no filter to show on the filter view for calendars Task id #2318645 Task id #2317525 --- addons/hr_holidays/models/hr_leave.py | 11 ++++++-- .../hr_holidays/models/hr_leave_allocation.py | 4 +-- .../report/hr_leave_report_calendar.py | 5 +++- .../report/hr_leave_report_calendar.xml | 1 + .../static/src/js/time_off_calendar.js | 26 ++++++++++++++++--- .../hr_holidays/static/src/scss/time_off.scss | 6 ++++- .../static/src/xml/time_off_calendar.xml | 14 ++++++++++ addons/hr_holidays/views/hr_leave_views.xml | 8 +++--- 8 files changed, 62 insertions(+), 13 deletions(-) diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 1b82c77662d..bcd52facd13 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -256,6 +256,8 @@ class HolidaysRequest(models.Model): request_unit_half = fields.Boolean('Half Day', compute='_compute_request_unit_half', store=True, readonly=False) request_unit_hours = fields.Boolean('Custom Hours', compute='_compute_request_unit_hours', store=True, readonly=False) request_unit_custom = fields.Boolean('Days-long custom hours', compute='_compute_request_unit_custom', store=True, readonly=False) + # view + is_hatched = fields.Boolean('Hatched', compute='_compute_is_hatched') _sql_constraints = [ ('type_value', @@ -282,7 +284,7 @@ class HolidaysRequest(models.Model): is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user') for leave in self: - if is_officer or leave.user_id == self.env.user or leave.manager_id == self.env.user: + if is_officer or leave.user_id == self.env.user or leave.employee_id.leave_manager_id == self.env.user: leave.name = leave.sudo().private_name else: leave.name = '*****' @@ -291,7 +293,7 @@ class HolidaysRequest(models.Model): is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user') for leave in self: - if is_officer or leave.user_id == self.env.user or leave.manager_id == self.env.user: + if is_officer or leave.user_id == self.env.user or leave.employee_id.leave_manager_id == self.env.user: leave.sudo().private_name = leave.name def _search_description(self, operator, value): @@ -548,6 +550,11 @@ class HolidaysRequest(models.Model): else: holiday.can_approve = True + @api.depends('state') + def _compute_is_hatched(self): + for holiday in self: + holiday.is_hatched = holiday.state not in ['refuse', 'validate'] + @api.constrains('date_from', 'date_to', 'employee_id') def _check_date(self): for holiday in self.filtered('employee_id'): diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index f92c72b15af..c4896756a01 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -229,7 +229,7 @@ class HolidaysAllocation(models.Model): is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user') for allocation in self: - if is_officer or allocation.employee_id.user_id == self.env.user or allocation.manager_id == self.env.user: + if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user: allocation.name = allocation.sudo().private_name else: allocation.name = '*****' @@ -237,7 +237,7 @@ class HolidaysAllocation(models.Model): def _inverse_description(self): is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user') for allocation in self: - if is_officer or allocation.employee_id.user_id == self.env.user or allocation.manager_id == self.env.user: + if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user: allocation.sudo().private_name = allocation.name def _search_description(self, operator, value): diff --git a/addons/hr_holidays/report/hr_leave_report_calendar.py b/addons/hr_holidays/report/hr_leave_report_calendar.py index 0a5b45db9c6..1dd3cd1288a 100644 --- a/addons/hr_holidays/report/hr_leave_report_calendar.py +++ b/addons/hr_holidays/report/hr_leave_report_calendar.py @@ -28,6 +28,8 @@ class LeaveReportCalendar(models.Model): ('validate', 'Approved') ], readonly=True) + is_hatched = fields.Boolean('Hatched', readonly=True) + def init(self): tools.drop_view_if_exists(self._cr, 'hr_leave_report_calendar') self._cr.execute("""CREATE OR REPLACE VIEW hr_leave_report_calendar AS @@ -42,7 +44,8 @@ class LeaveReportCalendar(models.Model): CASE WHEN hl.holiday_type = 'employee' THEN rr.tz ELSE %s - END AS tz + END AS tz, + state != 'validate' as is_hatched FROM hr_leave hl LEFT JOIN hr_employee em ON em.id = hl.employee_id diff --git a/addons/hr_holidays/report/hr_leave_report_calendar.xml b/addons/hr_holidays/report/hr_leave_report_calendar.xml index c7f8a78e477..a62f10faa1e 100644 --- a/addons/hr_holidays/report/hr_leave_report_calendar.xml +++ b/addons/hr_holidays/report/hr_leave_report_calendar.xml @@ -19,6 +19,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 ef9f262d861..3f863de98bf 100644 --- a/addons/hr_holidays/static/src/js/time_off_calendar.js +++ b/addons/hr_holidays/static/src/js/time_off_calendar.js @@ -138,6 +138,20 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { var self = this; return this._super.apply(this, arguments).then(function () { self.$el.parent().find('.o_calendar_mini').hide(); + + // Check if there is a filter to display on the sidebar + // If there is no filter, hide the sidebar + const noFilters = !Object.values(self.state.filters).some(f => f.filters.length); + + // Remove the no data sidebar + self.$sidebar.find('#o_calendar_filter_no_data').remove(); + if (noFilters) { + // Show a special sidebar + self.$sidebar.html(QWeb.render('hr_holidays.calendar.sidebar.nofilter', { + title: 'Time Off Type', + description: '(no data)' + })); + } }); }, }); @@ -154,10 +168,14 @@ odoo.define('hr_holidays.dashboard.view_custo', function(require) { }).then(function (result) { self.$el.parent().find('.o_calendar_mini').hide(); self.$el.parent().find('.o_timeoff_container').remove(); - var elem = QWeb.render('hr_holidays.dashboard_calendar_header', { - timeoffs: result, - }); - self.$el.before(elem); + + // Do not display header if there is no element to display + if (result.length > 0) { + var elem = QWeb.render('hr_holidays.dashboard_calendar_header', { + timeoffs: result, + }); + self.$el.before(elem); + } }); }, }); diff --git a/addons/hr_holidays/static/src/scss/time_off.scss b/addons/hr_holidays/static/src/scss/time_off.scss index f37b89caf03..ffd80c80722 100644 --- a/addons/hr_holidays/static/src/scss/time_off.scss +++ b/addons/hr_holidays/static/src/scss/time_off.scss @@ -1,10 +1,14 @@ .o_timeoff_calendar .o_content { .o_timeoff_container { height: 6rem; + + + .o_calendar_container { + height: calc(100% - 6rem); + } } .o_calendar_container { - height: calc(100% - 6rem); + height: 100%; } @include media-breakpoint-down(sm) { diff --git a/addons/hr_holidays/static/src/xml/time_off_calendar.xml b/addons/hr_holidays/static/src/xml/time_off_calendar.xml index b041222d7fc..97397fc8d5b 100644 --- a/addons/hr_holidays/static/src/xml/time_off_calendar.xml +++ b/addons/hr_holidays/static/src/xml/time_off_calendar.xml @@ -75,4 +75,18 @@
+ + +
+
+
+
+

+ + +

+
+
+
+
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index be46d191e47..1ee485953d5 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -353,10 +353,11 @@ hr.leave.view.dashboard hr.leave - + + @@ -432,10 +433,11 @@ hr.leave.view.calendar hr.leave - + + @@ -605,7 +607,7 @@ My Time Off Requests ir.actions.act_window hr.leave - tree,form,calendar,kanban,activity + tree,form,kanban,activity {} [('user_id', '=', uid)]