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
+
+
+
+
+
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)"
);
});