diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index 4d35fb33fd6..ac5ebc03c17 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -474,8 +474,8 @@ Public Time Off - - + + diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index 4bc7630c685..03be816aab1 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -264,6 +264,14 @@ class HrEmployee(models.Model): }, } + def _get_contextual_employee(self): + if self.env.context.get('employee_id'): + return self.browse(self.env.context['employee_id']) + return self.env.user.employee_id + + def _is_leave_user(self): + return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user') + def get_stress_days(self, start_date, end_date): all_days = {} @@ -277,22 +285,82 @@ class HrEmployee(models.Model): return all_days + @api.model + def get_special_days_data(self, date_start, date_end): + return { + 'stressDays': self.get_stress_days_data(date_start, date_end), + 'bankHolidays': self.get_public_holidays_data(date_start, date_end), + } + + @api.model + def get_public_holidays_data(self, date_start, date_end): + self = self._get_contextual_employee() + public_holidays = self._get_public_holidays(date_start, date_end).sorted('date_from') + return list(map(lambda bh: { + 'id': -bh.id, + 'colorIndex': 0, + 'end': datetime.datetime.combine(bh.date_to, datetime.datetime.max.time()).isoformat(), + 'endType': "datetime", + 'isAllDay': True, + 'start': datetime.datetime.combine(bh.date_from, datetime.datetime.min.time()).isoformat(), + 'startType': "datetime", + 'title': bh.name, + }, public_holidays)) + + def _get_public_holidays(self, date_start, date_end): + domain = [ + ('resource_id', '=', False), + ('company_id', 'in', (self.company_id.id, False)), + ('date_from', '<=', date_end), + ('date_to', '>=', date_start), + ] + + # a user with hr_holidays permissions will be able to see all public holidays from his calendar + if not self._is_leave_user(): + domain += [ + '|', + ('calendar_id', '=', False), + ('calendar_id', '=', self.resource_calendar_id.id), + ] + + return self.env['resource.calendar.leaves'].search(domain) + + @api.model + def get_stress_days_data(self, date_start, date_end): + self = self._get_contextual_employee() + stress_days = self._get_stress_days(date_start, date_end).sorted('start_date') + return list(map(lambda sd: { + 'id': -sd.id, + 'colorIndex': sd.color, + 'end': datetime.datetime.combine(sd.end_date, datetime.datetime.max.time()).isoformat(), + 'endType': "datetime", + 'isAllDay': True, + 'start': datetime.datetime.combine(sd.start_date, datetime.datetime.min.time()).isoformat(), + 'startType': "datetime", + 'title': sd.name, + }, stress_days)) + def _get_stress_days(self, start_date, end_date): - stress_days = self.env['hr.leave.stress.day'].search([ + domain = [ ('start_date', '<=', end_date), ('end_date', '>=', start_date), - '|', - ('resource_calendar_id', '=', False), - ('resource_calendar_id', 'in', (self.resource_calendar_id | self.env.company.resource_calendar_id).ids), ('company_id', 'in', self.env.companies.ids), - ]) + ] # a user with hr_holidays permissions will be able to see all stress days from his calendar - is_leave_user = self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user') + if not self._is_leave_user(): + domain += [ + '|', + ('resource_calendar_id', '=', False), + ('resource_calendar_id', '=', self.resource_calendar_id.id), + ] + if self.department_id: + domain += [ + '|', + ('department_ids', '=', False), + ('department_ids', 'parent_of', self.department_id.id), + ] + else: + domain += [('department_ids', '=', False)] - if not is_leave_user and stress_days.department_ids: - stress_days = stress_days.filtered(lambda sd:\ - not sd.department_ids\ - or self.department_id and not set(self.department_id.ids).isdisjoint(sd.department_ids.get_children_department_ids().ids)) - - return stress_days + return self.env['hr.leave.stress.day'].search(domain) diff --git a/addons/hr_holidays/static/src/views/calendar/calendar_controller.js b/addons/hr_holidays/static/src/views/calendar/calendar_controller.js index 2ab051f7354..2baefb26bcf 100644 --- a/addons/hr_holidays/static/src/views/calendar/calendar_controller.js +++ b/addons/hr_holidays/static/src/views/calendar/calendar_controller.js @@ -26,6 +26,13 @@ export class TimeOffCalendarController extends CalendarController { return this.model.employeeId; } + get filterPanelProps() { + return { + ...super.filterPanelProps, + employee_id: this.employeeId, + }; + } + newTimeOffRequest() { const context = {}; if (this.employeeId) { diff --git a/addons/hr_holidays/static/src/views/calendar/calendar_renderer.scss b/addons/hr_holidays/static/src/views/calendar/calendar_renderer.scss index b6c7e39898b..3fb3dd55048 100644 --- a/addons/hr_holidays/static/src/views/calendar/calendar_renderer.scss +++ b/addons/hr_holidays/static/src/views/calendar/calendar_renderer.scss @@ -2,11 +2,13 @@ .o_calendar_renderer { height: unset; - .o_calendar_widget { - @for $size from 1 through length($o-colors) { - .hr_stress_day_#{$size - 1}:not(.fc-disabled-day) { - background: nth($o-colors, $size) !important; - opacity: 0.4; + @for $size from 1 through length($o-colors) { + .o_calendar_widget { + .hr_stress_day_top_#{$size - 1}:not(.fc-disabled-day) { + .fc-day-number { + color: nth($o-colors, $size) !important; + font-weight: 600; + } } } } diff --git a/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.js b/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.js index 60a9f242b36..66f27f2efe8 100644 --- a/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.js +++ b/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.js @@ -2,23 +2,55 @@ import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel"; import { TimeOffCardMobile } from "../../../dashboard/time_off_card"; +import { getFormattedDateSpan } from '@web/views/calendar/utils'; import { useService } from "@web/core/utils/hooks"; +import { serializeDate } from "@web/core/l10n/dates"; -const { useState, onWillStart } = owl; +const { useState, onWillStart, onWillUpdateProps } = owl; export class TimeOffCalendarFilterPanel extends CalendarFilterPanel { setup() { super.setup(); this.orm = useService('orm'); + this.getFormattedDateSpan = getFormattedDateSpan; this.leaveState = useState({ holidays: [], + stressDays: [], + bankHolidays: [], }); onWillStart(async () => { await this.loadFilterData(); + await this.updateSpecialDays(); }); + onWillUpdateProps(this.updateSpecialDays); + } + + async updateSpecialDays() { + const context = { + 'employee_id': this.props.employee_id, + } + const specialDays = await this.orm.call( + 'hr.employee', 'get_special_days_data', [ + serializeDate(this.props.model.rangeStart, "datetime"), + serializeDate(this.props.model.rangeEnd, "datetime"), + ], + { + 'context': context, + }, + ); + specialDays['bankHolidays'].forEach(bankHoliday => { + bankHoliday.start = luxon.DateTime.fromISO(bankHoliday.start) + bankHoliday.end = luxon.DateTime.fromISO(bankHoliday.end) + }); + specialDays['stressDays'].forEach(stressDay => { + stressDay.start = luxon.DateTime.fromISO(stressDay.start) + stressDay.end = luxon.DateTime.fromISO(stressDay.end) + }); + this.leaveState.bankHolidays = specialDays['bankHolidays']; + this.leaveState.stressDays = specialDays['stressDays']; } async loadFilterData() { diff --git a/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.scss b/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.scss index 64678abaf28..b54714048b8 100644 --- a/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.scss +++ b/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.scss @@ -1,15 +1,45 @@ -.o_calendar_filter img { - width:30px; - - &.o_calendar_filter_plain { - content:var(--calendarFilter-icon--plain); - } +.o_calendar_filter { + span { + vertical-align: middle; + } - &.o_calendar_filter_hatched { - content:var(--calendarFilter-icon--hatched); - } - - &.o_calendar_filter_line { - content:var(--calendarFilter-icon--line); - } + & img { + width:30px; + + &.o_calendar_filter_plain { + content:var(--calendarFilter-icon--plain); + } + + &.o_calendar_filter_hatched { + content:var(--calendarFilter-icon--hatched); + } + + &.o_calendar_filter_line { + content:var(--calendarFilter-icon--line); + } + } + + .o_timeoff_legend { + display: inline-block; + width: 24px; + height: 30px; + margin: 0 3px; + padding: 3px 0; + text-align: center; + + &_bankholiday { + background-color: $gray-200; + } + + &_stressday { + font-weight: 600; + } + } + + @for $size from 1 through length($o-colors) { + .hr_stress_day_#{$size - 1}:not(.fc-disabled-day) { + color: nth($o-colors, $size) !important; + } + } } + diff --git a/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.xml b/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.xml index b6af3492bdc..5b61787c1c0 100644 --- a/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.xml +++ b/addons/hr_holidays/static/src/views/calendar/filter_panel/calendar_filter_panel.xml @@ -10,6 +10,31 @@ Validated To Approve Refused + 13 Public Holiday + 13 Stress Day + + +
+
Stress Days
+
    +
  • + + : +
  • +
+
+ +
+
Public Holidays
+
    +
  • + + : +
  • +
diff --git a/addons/hr_holidays/static/src/views/calendar/year/calendar_year_popover.js b/addons/hr_holidays/static/src/views/calendar/year/calendar_year_popover.js new file mode 100644 index 00000000000..0349a89a944 --- /dev/null +++ b/addons/hr_holidays/static/src/views/calendar/year/calendar_year_popover.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { Dialog } from "@web/core/dialog/dialog"; + +import { CalendarYearPopover } from "@web/views/calendar/calendar_year/calendar_year_popover"; + +export class TimeOffCalendarYearPopover extends CalendarYearPopover {} +TimeOffCalendarYearPopover.components = { Dialog }; +TimeOffCalendarYearPopover.template = "web.CalendarYearPopover"; +TimeOffCalendarYearPopover.subTemplates = { + ...CalendarYearPopover.subTemplates, + body: "hr_holidays.StressDayCalendarYearPopover.body", +}; diff --git a/addons/hr_holidays/static/src/views/calendar/year/calendar_year_popover.xml b/addons/hr_holidays/static/src/views/calendar/year/calendar_year_popover.xml new file mode 100644 index 00000000000..eb47650baec --- /dev/null +++ b/addons/hr_holidays/static/src/views/calendar/year/calendar_year_popover.xml @@ -0,0 +1,19 @@ + + + + +
+ + +

+ + +

+
+ + + + +
+ + diff --git a/addons/hr_holidays/static/src/views/calendar/year/calendar_year_renderer.js b/addons/hr_holidays/static/src/views/calendar/year/calendar_year_renderer.js index 65403f2d7a8..b30211269aa 100644 --- a/addons/hr_holidays/static/src/views/calendar/year/calendar_year_renderer.js +++ b/addons/hr_holidays/static/src/views/calendar/year/calendar_year_renderer.js @@ -2,14 +2,20 @@ import { CalendarYearRenderer } from '@web/views/calendar/calendar_year/calendar_year_renderer'; +import { useService } from "@web/core/utils/hooks"; import { useStressDays } from '../../hooks'; +import { useCalendarPopover } from '@web/views/calendar/hooks'; +import { TimeOffCalendarYearPopover } from './calendar_year_popover'; const { useEffect } = owl; export class TimeOffCalendarYearRenderer extends CalendarYearRenderer { setup() { super.setup(); + this.orm = useService("orm"); this.stressDays = useStressDays(this.props); + this.stressDaysList = []; + this.stressDayPopover = useCalendarPopover(TimeOffCalendarYearPopover); useEffect((el) => { for (const week of el) { @@ -33,8 +39,33 @@ export class TimeOffCalendarYearRenderer extends CalendarYearRenderer { }); } + /** @override **/ + async onDateClick(info) { + const is_stress_day = [...info.dayEl.classList].some(elClass => elClass.startsWith('hr_stress_day_')) + this.stressDayPopover.close(); + if (is_stress_day && !this.env.isSmall) { + this.popover.close(); + const date = luxon.DateTime.fromISO(info.dateStr); + const target = info.dayEl; + const stress_days_data = await this.orm.call("hr.employee", "get_stress_days_data", [date, date]); + stress_days_data.forEach(stress_day_data => { + stress_day_data['start'] = luxon.DateTime.fromISO(stress_day_data['start']) + stress_day_data['end'] = luxon.DateTime.fromISO(stress_day_data['end']) + }); + const records = Object.values(this.props.model.records).filter((r) => + luxon.Interval.fromDateTimes(r.start.startOf("day"), r.end.endOf("day")).contains(date) + ); + const props = this.getPopoverProps(date, records) + props['records'] = stress_days_data.concat(props['records']) + this.stressDayPopover.open(target, props, "o_cw_popover"); + } + else { + super.onDateClick(info); + } + } + onDayRender(info) { super.onDayRender(info); - this.stressDays(info); + this.stressDaysList = this.stressDays(info); } } diff --git a/addons/hr_holidays/static/src/views/hooks.js b/addons/hr_holidays/static/src/views/hooks.js index 563f2312bdb..1bb18204b54 100644 --- a/addons/hr_holidays/static/src/views/hooks.js +++ b/addons/hr_holidays/static/src/views/hooks.js @@ -9,8 +9,16 @@ export function useStressDays(props) { const date = luxon.DateTime.fromJSDate(info.date).toISODate(); const stressDay = props.model.stressDays[date]; if (stressDay) { - info.el.classList.add('hr_stress_day_' + stressDay); + const dayNumberElTop = info.view.el.querySelector(`.fc-day-top[data-date="${info.el.dataset.date }"]`) + const dayNumberEl = info.view.el.querySelector(`.fc-day[data-date="${info.el.dataset.date }"]`) + if (dayNumberElTop) { + dayNumberElTop.classList.add(`hr_stress_day_top_${stressDay}`); + } + if (dayNumberEl) { + dayNumberEl.classList.add(`hr_stress_day_${stressDay}`); + } } + return props.model.stressDays; } } diff --git a/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.js b/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.js index 530a63cb57c..e50adc111e0 100644 --- a/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.js +++ b/addons/web/static/src/views/calendar/calendar_common/calendar_common_popover.js @@ -5,6 +5,7 @@ import { is24HourFormat } from "@web/core/l10n/dates"; import { Field } from "@web/views/fields/field"; import { Record } from "@web/views/record"; import { evalDomain } from "@web/views/utils"; +import { getFormattedDateSpan } from '@web/views/calendar/utils'; import { Component } from "@odoo/owl"; @@ -58,7 +59,7 @@ export class CalendarCommonPopover extends Component { } if (!this.props.model.isDateHidden) { - this.date = this.getFormattedDate(start, end, record.isAllDay); + this.date = getFormattedDateSpan(start, end); if (record.isAllDay) { if (isSameDay) { @@ -70,17 +71,6 @@ export class CalendarCommonPopover extends Component { } } } - getFormattedDate(start, end, isAllDay) { - const isSameDay = start.hasSame(end, "day"); - if (!isSameDay && start.hasSame(end, "month")) { - // Simplify date-range if an event occurs into the same month (eg. "August, 4-5 2019") - return start.toFormat("LLLL d") + "-" + end.toFormat("d, y"); - } else { - return isSameDay - ? start.toFormat("DDDD") - : start.toFormat("DDD") + " - " + end.toFormat("DDD"); - } - } onEditEvent() { this.props.editRecord(this.props.record); diff --git a/addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.js b/addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.js index d48a16cd978..4b1ea746bf7 100644 --- a/addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.js +++ b/addons/web/static/src/views/calendar/calendar_year/calendar_year_popover.js @@ -3,6 +3,7 @@ import { Dialog } from "@web/core/dialog/dialog"; import { formatDate } from "@web/core/l10n/dates"; import { getColor } from "../colors"; +import { getFormattedDateSpan } from '@web/views/calendar/utils'; import { Component } from "@odoo/owl"; @@ -30,7 +31,7 @@ export class CalendarYearPopover extends Component { modifiedRecord.startHour = !record.isAllDay && duration < 1 ? start.toFormat("HH:mm") : ""; - const formattedDate = this.getFormattedDate(start, end); + const formattedDate = getFormattedDateSpan(start, end); if (!(formattedDate in recordGroups)) { recordGroups[formattedDate] = { title: formattedDate, @@ -71,17 +72,6 @@ export class CalendarYearPopover extends Component { return a.start.toMillis() - b.start.toMillis(); }); } - getFormattedDate(start, end) { - const isSameDay = start.hasSame(end, "days"); - if (!isSameDay && start.hasSame(end, "month")) { - // Simplify date-range if an event occurs into the same month (eg. "August 4-5, 2019") - return start.toFormat("LLLL d") + "-" + end.toFormat("d, y"); - } else { - return isSameDay - ? start.toFormat("DDD") - : start.toFormat("DDD") + " - " + end.toFormat("DDD"); - } - } onCreateButtonClick() { this.props.createRecord({ diff --git a/addons/web/static/src/views/calendar/utils.js b/addons/web/static/src/views/calendar/utils.js new file mode 100644 index 00000000000..7378ddf104b --- /dev/null +++ b/addons/web/static/src/views/calendar/utils.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +export function getFormattedDateSpan(start, end) { + const isSameDay = start.hasSame(end, "days"); + + if (!isSameDay && start.hasSame(end, "month")) { + // Simplify date-range if an event occurs into the same month (eg. "August 4-5, 2019") + return start.toFormat("LLLL d") + "-" + end.toFormat("d, y"); + } else { + return isSameDay + ? start.toFormat("DDD") + : start.toFormat("DDD") + " - " + end.toFormat("DDD"); + } +} diff --git a/addons/web/static/tests/views/calendar/calendar_common_popover_tests.js b/addons/web/static/tests/views/calendar/calendar_common_popover_tests.js index 93353d6317f..b515173a2de 100644 --- a/addons/web/static/tests/views/calendar/calendar_common_popover_tests.js +++ b/addons/web/static/tests/views/calendar/calendar_common_popover_tests.js @@ -62,7 +62,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => { }); const dateTimeGroup = target.querySelector(`.list-group`); const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim(); - assert.strictEqual(dateTimeLabels, "Friday, July 16, 2021 (All day)"); + assert.strictEqual(dateTimeLabels, "July 16, 2021 (All day)"); }); QUnit.test("date duration: is all day and two days duration", async (assert) => { @@ -154,7 +154,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => { const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim(); assert.strictEqual( dateTimeLabels, - "Friday, July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)" + "July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)" ); }); @@ -166,7 +166,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => { }); const dateTimeGroup = target.querySelector(`.list-group`); const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim(); - assert.strictEqual(dateTimeLabels, "Friday, July 16, 2021"); + assert.strictEqual(dateTimeLabels, "July 16, 2021"); }); QUnit.test("isTimeHidden is false", async (assert) => { @@ -179,7 +179,7 @@ QUnit.module("CalendarView - CommonPopover", ({ beforeEach }) => { const dateTimeLabels = dateTimeGroup.textContent.replace(/\s+/g, " ").trim(); assert.strictEqual( dateTimeLabels, - "Friday, July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)" + "July 16, 2021 08:00 - 11:15 (3 hours, 15 minutes)" ); }); diff --git a/addons/web/static/tests/views/calendar/calendar_view_tests.js b/addons/web/static/tests/views/calendar/calendar_view_tests.js index 00b15cf3b12..9b965ab1c52 100644 --- a/addons/web/static/tests/views/calendar/calendar_view_tests.js +++ b/addons/web/static/tests/views/calendar/calendar_view_tests.js @@ -1314,8 +1314,8 @@ QUnit.module("Views", ({ beforeEach }) => { ); assert.strictEqual( target.querySelector(".o_cw_popover .list-group-item b.text-capitalize").textContent, - "Wednesday, December 14, 2016", - "should display date 'Wednesday, December 14, 2016'" + "December 14, 2016", + "should display date 'December 14, 2016'" ); assert.containsN( target, @@ -3495,7 +3495,7 @@ QUnit.module("Views", ({ beforeEach }) => { await clickEvent(target, 1); assert.strictEqual( target.querySelector(".o_cw_popover .list-group-item").textContent, - "Wednesday, December 14, 2016 (All day)" + "December 14, 2016 (All day)" ); });