[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:
committed by
Yannick Tivisse
parent
290ddb3686
commit
3ffe2cdb97
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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')]}"/>
|
||||
|
||||
Reference in New Issue
Block a user