[IMP] hr_holidays: add Stress Days

Add "Stress Days", days where employees are *not* allowed to request
a time off. Time Off officers are still allowed to request holidays on
those dates.

closes odoo/odoo#79280

Taskid: 2679873
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Kevin Baptiste
2022-02-16 13:04:44 +00:00
committed by Yannick Tivisse
parent 290ddb3686
commit 3ffe2cdb97
13 changed files with 319 additions and 2 deletions
+1
View File
@@ -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',
+1
View File
@@ -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
+35
View File
@@ -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
@@ -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
@@ -244,6 +244,12 @@
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_leave_stress_day_rule_multi_company" model="ir.rule">
<field name="name">Stress Day: multi company rule</field>
<field name="model_id" ref="model_hr_leave_stress_day"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="hr_leave_report_calendar_rule_multi_company" model="ir.rule">
<field name="name">Time Off Report Calendar: multi company global rule</field>
<field name="model_id" ref="model_hr_leave_report_calendar"/>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
22 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
23 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
24 access_hr_holidays_cancel_leave access_hr_holidays_cancel_leave hr_holidays.model_hr_holidays_cancel_leave base.group_user 1 1 1 1
25 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
26 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
@@ -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
//--------------------------------------------------------------------------
@@ -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;
}
}
+1
View File
@@ -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
@@ -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)
@@ -105,7 +105,7 @@
groups="hr_holidays.group_hr_holidays_manager"
sequence="2"/>
<menuitem
<menuitem
id="hr_holidays_public_time_off_menu_configuration"
action="open_view_public_holiday"
name="Public Holidays"
@@ -113,6 +113,14 @@
groups="hr_holidays.group_hr_holidays_manager"
sequence="3"/>
<menuitem
id="hr_holidays_stress_day_menu_configuration"
action="hr_leave_stress_day_action"
name="Stress Days"
parent="menu_hr_holidays_configuration"
groups="hr_holidays.group_hr_holidays_manager"
sequence="4"/>
<menuitem id="hr_holidays_menu_config_activity_type"
action="mail_activity_type_action_config_hr_holidays"
parent="menu_hr_holidays_configuration"
@@ -0,0 +1,63 @@
<?xml version='1.0' encoding='UTF-8' ?>
<odoo>
<record id="hr_leave_stress_day_view_form" model="ir.ui.view">
<field name="model">hr.leave.stress.day</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<label for="start_date" string="Dates"/>
<div class="o_row">
<field name="start_date" nolabel="1" class="oe_inline" widget="daterange" options="{'related_end_date': 'end_date'}"/>
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="To"/>
<field name="end_date" nolabel="1" class="oe_inline" widget="daterange" options="{'related_start_date': 'start_date'}"/>
</div>
</group>
<group>
<field name="color" widget="color_picker"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="hr_leave_stress_day_view_list" model="ir.ui.view">
<field name="model">hr.leave.stress.day</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="name"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="color" widget="color_picker" width="1"/>
<field name="resource_calendar_id" optional="hide"/>
</tree>
</field>
</record>
<record id="hr_leave_stress_day_view_search" model="ir.ui.view">
<field name="model">hr.leave.stress.day</field>
<field name="arch" type="xml">
<search string="Stress Days">
<field name="name"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="company_id" groups="base.group_multi_company"/>
<separator />
<filter name="filter_date" date="start_date" default_period="this_year" string="Period"/>
</search>
</field>
</record>
<record id="hr_leave_stress_day_action" model="ir.actions.act_window">
<field name="name">Stress Days</field>
<field name="res_model">hr.leave.stress.day</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="hr_leave_stress_day_view_search"/>
<field name="context">{'search_default_filter_date': True}</field>
</record>
</odoo>
+4 -1
View File
@@ -210,6 +210,7 @@
<field name="can_approve" invisible="1"/>
<field name="can_cancel" invisible="1"/>
<field name="holiday_allocation_id" invisible="1" force_save="1"/>
<field name="has_stress_day" invisible="1"/>
<header>
<button string="Confirm" name="action_confirm" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('active', '=', False)]}"/>
<button string="Approve" name="action_approve" type="object" class="oe_highlight" attrs="{'invisible': ['|', '|', ('active', '=', False), ('can_approve', '=', False), ('state', '!=', 'confirm')]}"/>
@@ -315,8 +316,10 @@
'required': [('request_unit_hours', '=', True)],
'invisible': [('request_unit_hours', '=', False)]}"/>
</div>
<div class="o_row mb-2 alert alert-warning text-justify" role="alert" attrs="{'invisible': [('has_stress_day', '=', False)]}">
Your time off request covers one (or more) stress days. Taking time off during stress days is not authorized.
</div>
</div>
<!-- When the user is leave manager, he should always see `number_of_days` to allow
him to edit the value. `number_of_hours_display` is only an informative field -->
<label for="number_of_days" string="Duration" attrs="{'invisible': [('request_unit_half', '=', True), ('leave_type_request_unit', '!=', 'hour')]}"/>