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"/> - + + + + + hr.leave.stress.day + +
+ + + + + + + + + + + + +
+
+ + + hr.leave.stress.day + + + + + + + + + + + + + + hr.leave.stress.day + + + + + + + + + + + + + + Stress Days + hr.leave.stress.day + tree,form + + {'search_default_filter_date': True} + +
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 2d60c3c0562..7aea31ae1e2 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -210,6 +210,7 @@ +