diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py
index e49f561e880..c380798da01 100644
--- a/addons/hr_holidays/__manifest__.py
+++ b/addons/hr_holidays/__manifest__.py
@@ -39,6 +39,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
'views/hr_leave_type_views.xml',
'views/hr_leave_allocation_views.xml',
'views/hr_leave_accrual_views.xml',
+ 'views/hr_leave_stress_day_views.xml',
'views/mail_activity_views.xml',
'wizard/hr_holidays_cancel_leave_views.xml',
diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py
index 1ab720a3edb..abd7f9a3688 100644
--- a/addons/hr_holidays/models/__init__.py
+++ b/addons/hr_holidays/models/__init__.py
@@ -9,6 +9,7 @@ from . import hr_leave_allocation
from . import hr_leave_type
from . import hr_leave_accrual_plan_level
from . import hr_leave_accrual_plan
+from . import hr_leave_stress_day
from . import mail_message_subtype
from . import res_partner
from . import res_users
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index bb30a34e52f..94bae63a9f2 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -278,6 +278,7 @@ class HolidaysRequest(models.Model):
# view
is_hatched = fields.Boolean('Hatched', compute='_compute_is_hatched')
is_striked = fields.Boolean('Striked', compute='_compute_is_hatched')
+ has_stress_day = fields.Boolean(compute='_compute_has_stress_day')
_sql_constraints = [
('type_value',
@@ -520,6 +521,34 @@ class HolidaysRequest(models.Model):
else:
holiday.department_id = False
+ @api.depends('date_from', 'date_to', 'holiday_status_id')
+ def _compute_has_stress_day(self):
+ date_from, date_to = min(self.mapped('date_from')), max(self.mapped('date_to'))
+ resource_calendar_id = self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
+ if date_from and date_to:
+ stress_days = self.env['hr.leave.stress.day'].search([
+ ('start_date', '<=', date_to.date()),
+ ('end_date', '>=', date_from.date()),
+ '|',
+ ('resource_calendar_id', '=', False),
+ ('resource_calendar_id', 'in', resource_calendar_id.ids),
+ ])
+
+ for leave in self:
+ domain = [
+ ('start_date', '<=', leave.date_to.date()),
+ ('end_date', '>=', leave.date_from.date()),
+ '|',
+ ('resource_calendar_id', '=', False),
+ ('resource_calendar_id', '=', (leave.employee_id.resource_calendar_id or self.env.company.resource_calendar_id).id)
+ ]
+
+ if leave.holiday_status_id.company_id:
+ domain += [('company_id', '=', leave.holiday_status_id.company_id.id)]
+ leave.has_stress_day = leave.date_from and leave.date_to and stress_days.filtered_domain(domain)
+ else:
+ self.has_stress_day = False
+
@api.depends('date_from', 'date_to', 'employee_id')
def _compute_number_of_days(self):
for holiday in self:
@@ -900,6 +929,12 @@ class HolidaysRequest(models.Model):
date=vstop
))
+ @api.constrains('date_from', 'date_to')
+ def _check_stress_day(self):
+ is_leave_user = self.user_has_groups('hr_holidays.group_hr_holidays_user')
+ if not is_leave_user and any(leave.has_stress_day for leave in self):
+ raise ValidationError(_('You are not allowed to request a time off on a Stress Day.'))
+
def _check_double_validation_rules(self, employees, state):
if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
return
diff --git a/addons/hr_holidays/models/hr_leave_stress_day.py b/addons/hr_holidays/models/hr_leave_stress_day.py
new file mode 100644
index 00000000000..62bd56bf30e
--- /dev/null
+++ b/addons/hr_holidays/models/hr_leave_stress_day.py
@@ -0,0 +1,42 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from dateutil.relativedelta import relativedelta
+from random import randint
+
+from odoo import api, fields, models
+
+
+class StressDay(models.Model):
+ _name = 'hr.leave.stress.day'
+ _description = 'Stress Day'
+ _order = 'start_date desc, end_date desc'
+
+ name = fields.Char(required=True)
+ company_id = fields.Many2one('res.company', default=lambda self: self.env.company, required=True)
+ start_date = fields.Date(required=True)
+ end_date = fields.Date(required=True)
+ color = fields.Integer(default=lambda dummy: randint(1, 11))
+ resource_calendar_id = fields.Many2one('resource.calendar', 'Working Hours',
+ default=lambda self: self.env.company.resource_calendar_id.id, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
+
+ _sql_constraints = [
+ ('date_from_after_day_to', 'CHECK(start_date <= end_date)', 'The start date must be anterior than the end date.')
+ ]
+
+ @api.model
+ def get_stress_days(self, start_date, end_date, resource_calendar_id=None):
+ resource_calendar_id = resource_calendar_id or self.env.user.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
+ all_days = {}
+ stress_days = self.env['hr.leave.stress.day'].search([
+ ('start_date', '>=', start_date),
+ ('end_date', '<=', end_date),
+ '|',
+ ('resource_calendar_id', '=', False),
+ ('resource_calendar_id', '=', resource_calendar_id.id),
+ ])
+
+ for stress_day in stress_days:
+ num_days = (stress_day.end_date - stress_day.start_date).days
+ for d in range(num_days + 1):
+ all_days[str(stress_day.start_date + relativedelta(days=d))] = stress_day.color
+
+ return all_days
diff --git a/addons/hr_holidays/security/hr_holidays_security.xml b/addons/hr_holidays/security/hr_holidays_security.xml
index f3f00790762..fe372159ae6 100644
--- a/addons/hr_holidays/security/hr_holidays_security.xml
+++ b/addons/hr_holidays/security/hr_holidays_security.xml
@@ -244,6 +244,12 @@
['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
+
+ Stress Day: multi company rule
+
+ ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]
+
+
Time Off Report Calendar: multi company global rule
diff --git a/addons/hr_holidays/security/ir.model.access.csv b/addons/hr_holidays/security/ir.model.access.csv
index 38a3ffc0580..f6979d41243 100644
--- a/addons/hr_holidays/security/ir.model.access.csv
+++ b/addons/hr_holidays/security/ir.model.access.csv
@@ -22,3 +22,5 @@ access_hr_leave_accrual_level_user,hr.leave.accrual.level.user,model_hr_leave_ac
access_hr_leave_accrual_plan_manager,hr.leave.accrual.plan.manager,model_hr_leave_accrual_plan,hr_holidays.group_hr_holidays_manager,1,1,1,1
access_hr_leave_accrual_level_manager,hr.leave.accrual.level.manager,model_hr_leave_accrual_level,hr_holidays.group_hr_holidays_manager,1,1,1,1
access_hr_holidays_cancel_leave,access_hr_holidays_cancel_leave,hr_holidays.model_hr_holidays_cancel_leave,base.group_user,1,1,1,1
+access_hr_leave_stress_day_user,access_hr_holidays_stress_day_user,hr_holidays.model_hr_leave_stress_day,base.group_user,1,0,0,0
+access_hr_leave_stress_day_manager,access_hr_holidays_stress_day_manager,hr_holidays.model_hr_leave_stress_day,hr_holidays.group_hr_holidays_manager,1,1,1,1
diff --git a/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_controller.js b/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_controller.js
index 0ece094d944..c382f88ba04 100644
--- a/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_controller.js
+++ b/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_controller.js
@@ -104,6 +104,23 @@ export const TimeOffCalendarController = CalendarController.extend({
}
},
+ _update() {
+ return this._super(...arguments).then(() => {
+ this._rpc({
+ model: 'hr.leave.stress.day',
+ method: 'get_stress_days',
+ args: [this.model.data.start_date, this.model.data.end_date],
+ context: this.context,
+ }).then((stressDays) => {
+ this.$el.find('td.fc-day').toArray().forEach((td) => {
+ if (stressDays[td.dataset.date]) {
+ td.classList.add('hr_stress_day_' + stressDays[td.dataset.date]);
+ }
+ });
+ });
+ });
+ },
+
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
diff --git a/addons/hr_holidays/static/src/scss/time_off.scss b/addons/hr_holidays/static/src/scss/time_off.scss
index 00ce77d84c8..f8f139538ab 100644
--- a/addons/hr_holidays/static/src/scss/time_off.scss
+++ b/addons/hr_holidays/static/src/scss/time_off.scss
@@ -44,4 +44,23 @@
font-size: 0.85rem;
}
}
+
+ @for $size from 1 through length($o-colors) {
+ .hr_stress_day_#{$size - 1} {
+ background-color: lighten(nth($o-colors, $size), 25%) !important;
+ }
+ }
+}
+
+.o_timeoff_dashboard_popover {
+ hr {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ }
+
+ ul {
+ list-style: none;
+ padding-left: 0;
+ margin: 0px;
+ }
}
diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py
index c3160c237d7..c33839e85ec 100644
--- a/addons/hr_holidays/tests/__init__.py
+++ b/addons/hr_holidays/tests/__init__.py
@@ -12,3 +12,4 @@ from . import test_leave_requests
from . import test_out_of_office
from . import test_company_leave
from . import test_res_partner
+from . import test_stress_days
diff --git a/addons/hr_holidays/tests/test_stress_days.py b/addons/hr_holidays/tests/test_stress_days.py
new file mode 100644
index 00000000000..4408d83d9b4
--- /dev/null
+++ b/addons/hr_holidays/tests/test_stress_days.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime
+from freezegun import freeze_time
+
+from odoo import tests
+from odoo.tests import new_test_user
+from odoo.tests.common import Form, TransactionCase
+from odoo.exceptions import ValidationError
+
+
+@tests.tagged('access_rights', 'post_install', '-at_install')
+class TestHrLeaveStressDays(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.default_calendar = cls.env['resource.calendar'].create({
+ 'name': 'moon calendar',
+ })
+
+ cls.company = cls.env['res.company'].create({
+ 'name': 'super company',
+ 'resource_calendar_id': cls.default_calendar.id,
+ })
+
+ cls.employee_user = new_test_user(cls.env, login='user', groups='base.group_user', company_ids=[(6, 0, cls.company.ids)], company_id=cls.company.id)
+ cls.manager_user = new_test_user(cls.env, login='manager', groups='base.group_user,hr_holidays.group_hr_holidays_manager', company_ids=[(6, 0, cls.company.ids)], company_id=cls.company.id)
+
+ cls.employee_emp = cls.env['hr.employee'].create({
+ 'name': 'Toto Employee',
+ 'company_id': cls.company.id,
+ 'user_id': cls.employee_user.id,
+ 'resource_calendar_id': cls.default_calendar.id,
+ })
+ cls.manager_emp = cls.env['hr.employee'].create({
+ 'name': 'Toto Mananger',
+ 'company_id': cls.company.id,
+ 'user_id': cls.manager_user.id,
+ })
+
+ cls.leave_type = cls.env['hr.leave.type'].create({
+ 'name': 'Unlimited',
+ 'leave_validation_type': 'hr',
+ 'requires_allocation': 'no',
+ 'company_id': cls.company.id,
+ })
+
+ cls.stress_day = cls.env['hr.leave.stress.day'].create({
+ 'name': 'Super Event',
+ 'company_id': cls.company.id,
+ 'start_date': datetime(2021, 11, 2),
+ 'end_date': datetime(2021, 11, 2),
+ 'color': 1,
+ 'resource_calendar_id': cls.default_calendar.id,
+ })
+ cls.stress_week = cls.env['hr.leave.stress.day'].create({
+ 'name': 'Super Event End Of Week',
+ 'company_id': cls.company.id,
+ 'start_date': datetime(2021, 11, 8),
+ 'end_date': datetime(2021, 11, 12),
+ 'color': 2,
+ 'resource_calendar_id': cls.default_calendar.id,
+ })
+
+ @freeze_time('2021-10-15')
+ def test_request_stress_days(self):
+ # An employee can request time off outside stress days
+ self.env['hr.leave'].with_user(self.employee_user.id).create({
+ 'name': 'coucou',
+ 'holiday_status_id': self.leave_type.id,
+ 'employee_id': self.employee_emp.id,
+ 'date_from': datetime(2021, 11, 3),
+ 'date_to': datetime(2021, 11, 3),
+ 'number_of_days': 1,
+ })
+
+ # Taking a time off during a Stress Day is not allowed for a simple employee...
+ with self.assertRaises(ValidationError):
+ self.env['hr.leave'].with_user(self.employee_user.id).create({
+ 'name': 'coucou',
+ 'holiday_status_id': self.leave_type.id,
+ 'employee_id': self.employee_emp.id,
+ 'date_from': datetime(2021, 11, 3),
+ 'date_to': datetime(2021, 11, 17),
+ 'number_of_days': 1,
+ })
+
+ # ... but is allowed for a Time Off Officer
+ self.env['hr.leave'].with_user(self.manager_user.id).create({
+ 'name': 'coucou',
+ 'holiday_status_id': self.leave_type.id,
+ 'employee_id': self.employee_emp.id,
+ 'date_from': datetime(2021, 11, 2),
+ 'date_to': datetime(2021, 11, 2),
+ 'number_of_days': 1,
+ })
+
+ @freeze_time('2021-10-15')
+ def test_get_stress_days(self):
+ stress_days = self.env['hr.leave.stress.day'].with_user(self.employee_user.id).get_stress_days('2021-11-01', '2021-11-30')
+
+ # Stress Days spanning multiple days should be split in single days
+ expected_data = {'2021-11-02': 1, '2021-11-08': 2, '2021-11-09': 2, '2021-11-10': 2, '2021-11-11': 2, '2021-11-12': 2}
+
+ self.assertEqual(len(stress_days), len(expected_data))
+ for day, color in expected_data.items():
+ self.assertTrue(day in stress_days)
+ self.assertEqual(color, stress_days[day])
+
+ with self.assertRaises(ValidationError), Form(self.env['hr.leave'].with_user(self.employee_user.id).with_context(default_employee_id=self.employee_emp.id)) as leave_form:
+ leave_form.holiday_status_id = self.leave_type
+ leave_form.date_from = datetime(2021, 11, 1)
+ leave_form.date_to = datetime(2021, 11, 1)
+ self.assertFalse(leave_form.has_stress_day)
+
+ leave_form.date_to = datetime(2021, 11, 5)
+ self.assertTrue(leave_form.has_stress_day)
diff --git a/addons/hr_holidays/views/hr_holidays_views.xml b/addons/hr_holidays/views/hr_holidays_views.xml
index 959bfdc473a..9e41cd2d5f0 100644
--- a/addons/hr_holidays/views/hr_holidays_views.xml
+++ b/addons/hr_holidays/views/hr_holidays_views.xml
@@ -105,7 +105,7 @@
groups="hr_holidays.group_hr_holidays_manager"
sequence="2"/>
-
+
+