[FW][FIX] hr_holidays: Make accrual plan prorated

closes odoo/odoo#77342

Forward-port-of: #77311
Related: odoo/enterprise#21229
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
William Braeckman
2021-09-29 08:33:03 +00:00
committed by Yannick Tivisse
parent 09da30fbcd
commit bd13371781
12 changed files with 329 additions and 190 deletions
@@ -51,6 +51,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -61,6 +62,7 @@
<field name="holiday_status_id" ref="holiday_status_comp"/>
<field name="number_of_days">7</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -75,6 +77,7 @@
<field name="number_of_days">7</field>
<field name="state">confirm</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
@@ -84,6 +87,7 @@
<field name="holiday_status_id" ref="holiday_status_comp"/>
<field name="number_of_days">12</field>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -98,6 +102,7 @@
<field eval="(datetime.now() + relativedelta(day=1, weekday=0)).strftime('%Y-%m-%d 04:00:00')" name="request_date_from"/>
<field eval="(datetime.now() + relativedelta(day=1, weekday=0) + relativedelta(weekday=2)).strftime('%Y-%m-%d 20:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
</record>
<record id="hr_holidays_sl" model="hr.leave">
@@ -108,6 +113,7 @@
<field eval="(datetime.now() + relativedelta(day=20, weekday=0)).strftime('%Y-%m-%d 04:00:00')" name="request_date_from"/>
<field eval="(datetime.now() + relativedelta(day=20, weekday=0) + relativedelta(weekday=2)).strftime('%Y-%m-%d 20:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_admin'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
@@ -138,6 +144,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -148,6 +155,7 @@
<field name="holiday_status_id" ref="hr_holiday_status_dv"/>
<field name="number_of_days">10</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -158,6 +166,7 @@
<field name="holiday_status_id" ref="holiday_status_unpaid"/>
<field name="number_of_days">12</field>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -172,6 +181,7 @@
<field eval="time.strftime('%Y-%m-04')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-10')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_cl_al')"/>
@@ -185,6 +195,7 @@
<field eval="(datetime.now()+relativedelta(months=1, day=17, weekday=0)).strftime('%Y-%m-%d 04:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1, day=17, weekday=0) + relativedelta(weekday=2)).strftime('%Y-%m-%d 20:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_al"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_al'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
@@ -198,6 +209,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -209,6 +221,7 @@
<field name="number_of_days">7</field>
<field name="state">confirm</field>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
@@ -222,6 +235,7 @@
<field eval="time.strftime('%Y-%m-18')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-24')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
</record>
<function model="hr.leave" name="action_validate">
<value eval="ref('hr_holidays.hr_holidays_cl_mit')"/>
@@ -235,6 +249,7 @@
<field eval="(datetime.now() + relativedelta(day=5, weekday=0)).strftime('%Y-%m-%d 04:00:00')" name="request_date_from"/>
<field eval="(datetime.now() + relativedelta(day=5, weekday=0) + relativedelta(weekday=2)).strftime('%Y-%m-%d 20:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_mit"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_mit'))]"/>
</record>
<!-- ++++++++++++++++++++++ Marc Demo ++++++++++++++++++++++ -->
@@ -244,6 +259,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -254,6 +270,7 @@
<field name="holiday_status_id" ref="holiday_status_unpaid"/>
<field name="number_of_days">7</field>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -271,6 +288,7 @@
<field eval="(datetime.now()+relativedelta(months=1, day=3, weekday=0)).strftime('%Y-%m-%d 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1, day=3, weekday=0) + relativedelta(weekday=2)).strftime('%Y-%m-%d 23:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
@@ -285,6 +303,7 @@
<field eval="(datetime.now() + relativedelta(day=1, weekday=0)).strftime('%Y-%m-%d 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now() + relativedelta(day=1, weekday=0) + relativedelta(days=2)).strftime('%Y-%m-%d 23:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_qdp"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_qdp'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
@@ -298,6 +317,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_fpi"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_fpi'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -308,6 +328,7 @@
<field name="holiday_status_id" ref="holiday_status_unpaid"/>
<field name="number_of_days">7</field>
<field name="employee_id" ref="hr.employee_fpi"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_fpi'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -320,6 +341,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">validate</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -330,6 +352,7 @@
<field name="holiday_status_id" ref="holiday_status_unpaid"/>
<field name="number_of_days">5</field>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -347,6 +370,7 @@
<field eval="time.strftime('%Y-%m-09')" name="request_date_from"/>
<field eval="time.strftime('%Y-%m-16')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
</record>
@@ -358,6 +382,7 @@
<field eval="(datetime.now() + relativedelta(day=25, weekday=0)).strftime('%Y-%m-%d 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now() + relativedelta(day=25, weekday=0) + relativedelta(weekday=2)).strftime('%Y-%m-%d 23:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_niv"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_niv'))]"/>
<field name="state">confirm</field>
</record>
<function model="hr.leave" name="action_validate">
@@ -371,6 +396,7 @@
<field name="holiday_status_id" ref="holiday_status_cl"/>
<field name="number_of_days">20</field>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="state">confirm</field>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
@@ -382,6 +408,7 @@
<field name="number_of_days">5</field>
<field name="state">confirm</field>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="date_from" eval="time.strftime('%Y-1-1')"/>
<field name="date_to" eval="time.strftime('%Y-12-31')"/>
</record>
@@ -395,6 +422,7 @@
<field eval="(datetime.now()+relativedelta(months=1, day=1, weekday=0)).strftime('%Y-%m-%d 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=1, day=1, weekday=0)).strftime('%Y-%m-%d 23:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="state">confirm</field>
</record>
@@ -406,6 +434,7 @@
<field eval="(datetime.now()+relativedelta(months=4, day=1, weekday=2)).strftime('%Y-%m-%d 01:00:00')" name="request_date_from"/>
<field eval="(datetime.now()+relativedelta(months=4, day=1, weekday=2)).strftime('%Y-%m-%d 23:00:00')" name="request_date_to"/>
<field name="employee_id" ref="hr.employee_jve"/>
<field name="employee_ids" eval="[(4, ref('hr.employee_jve'))]"/>
<field name="state">confirm</field>
</record>
+7 -3
View File
@@ -471,7 +471,9 @@ class HolidaysRequest(models.Model):
for holiday in self:
if holiday.holiday_type == 'employee':
if not holiday.employee_ids:
holiday.employee_ids = self.env.user.employee_id
# This handles the case where a request is made with only the employee_id
# but does not need to be recomputed on employee_id changes
holiday.employee_ids = holiday.employee_id or self.env.user.employee_id
holiday.mode_company_id = False
holiday.category_id = False
elif holiday.holiday_type == 'company':
@@ -487,7 +489,7 @@ class HolidaysRequest(models.Model):
holiday.employee_ids = False
holiday.mode_company_id = False
else:
holiday.employee_ids = self.env.context.get('default_employee_id') or self.env.user.employee_id
holiday.employee_ids = self.env.context.get('default_employee_id') or holiday.employee_id or self.env.user.employee_id
@api.depends('employee_id')
def _compute_from_employee_id(self):
@@ -781,7 +783,8 @@ class HolidaysRequest(models.Model):
@api.constrains('holiday_allocation_id')
def _check_allocation_id(self):
for leave in self:
if leave.holiday_status_id.requires_allocation == 'yes' and not leave.holiday_allocation_id:
if leave.holiday_type == 'employee' and not leave.multi_employee and\
leave.holiday_status_id.requires_allocation == 'yes' and not leave.holiday_allocation_id:
raise ValidationError(_(
'Could not find an allocation of type %(leave_type)s for the requested time period.',
leave_type=leave.holiday_status_id.display_name,
@@ -1024,6 +1027,7 @@ class HolidaysRequest(models.Model):
'number_of_days': work_days_data[employee.id]['days'],
'parent_id': self.id,
'employee_id': employee.id,
'employee_ids': employee,
'state': 'validate',
} for employee in employees if work_days_data[employee.id]['days']]
@@ -9,7 +9,9 @@ class AccrualPlan(models.Model):
_description = "Accrual Plan"
name = fields.Char('Name', required=True)
time_off_type_id = fields.Many2one('hr.leave.type', string="Time Off Type")
time_off_type_id = fields.Many2one('hr.leave.type', string="Time Off Type",
help="""Specify if this accrual plan can only be used with this Time Off Type.
Leave empty if this accrual plan can be used with any Time Off Type.""")
employees_count = fields.Integer("Employees", compute='_compute_employee_count')
level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id', copy=True)
allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id')
@@ -26,6 +28,19 @@ class AccrualPlan(models.Model):
for plan in self:
plan.show_transition_mode = len(plan.level_ids) > 1
level_count = fields.Integer('Levels', compute='_compute_level_count')
@api.depends('level_ids')
def _compute_level_count(self):
level_read_group = self.env['hr.leave.accrual.level'].read_group(
[('accrual_plan_id', 'in', self.ids)],
fields=['accrual_plan_id'],
groupby=['accrual_plan_id'],
)
mapped_count = {group['accrual_plan_id'][0]: group['accrual_plan_id_count'] for group in level_read_group}
for plan in self:
plan.level_count = mapped_count[plan.id]
@api.depends('allocation_ids')
def _compute_employee_count(self):
allocations_read_group = self.env['hr.leave.allocation'].read_group(
@@ -5,23 +5,25 @@ import datetime
import calendar
from dateutil.relativedelta import relativedelta
from num2words import num2words
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.tools.date_utils import get_timedelta
from odoo.tools.misc import get_lang
DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
# Used for displaying the days and reversing selection -> integer
DAY_SELECT_VALUES = [str(i) for i in range(1, 29)] + ['last']
def _get_date_check_month(year, month, day):
"""
Returns the day it would be if day is outside of the month's range
Example: 2021 feb 30 -> 2021 mar 2
"""
month_range = calendar.monthrange(year, month)
if day > month_range[1]:
return datetime.date(year, month, month_range[1]) + relativedelta(days=(day - month_range[1]))
return datetime.date(year, month, day)
def _get_selection_days(self):
lang = get_lang(self.env).code
return [
(DAY_SELECT_VALUES[i - 1],
num2words(i, lang=lang, to='ordinal_num') if i < 29 else _('last day'))
for i in range(1, 30)
]
class AccrualPlanLevel(models.Model):
_name = "hr.leave.accrual.level"
@@ -35,19 +37,20 @@ class AccrualPlanLevel(models.Model):
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True)
start_count = fields.Integer(
"Start after",
help="The accrual starts after a defined period from the employee start date. This field define the number of days, month or years after which accrual is used.", default="1")
help="The accrual starts after a defined period from the employee start date. This field defines the number of days, months or years after which accrual is used.", default="1")
start_type = fields.Selection(
[('day', 'day(s)'),
('month', 'month(s)'),
('year', 'year(s)')],
default='day', string=" ", required=True,
help="This field define the unit of time after which the accrual starts.")
is_based_on_worked_time = fields.Boolean("Based on worked time")
help="This field defines the unit of time after which the accrual starts.")
is_based_on_worked_time = fields.Boolean("Based on worked time",
help="Only accrue for the time worked by the employee. This is the time when the employee did not take time off.")
# Accrue of
added_value = fields.Float(
"Gain", required=True,
help="The number of days that will be incremented for every period")
"Rate", required=True,
help="The number of hours/days that will be incremented in the specified Time Off Type for every period")
added_value_type = fields.Selection(
[('days', 'Days'),
('hours', 'Hours')],
@@ -70,8 +73,14 @@ class AccrualPlanLevel(models.Model):
('sun', 'Sunday'),
], default='mon', required=True, string="Allocation on")
first_day = fields.Integer(default=1)
first_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_day_display')
second_day = fields.Integer(default=15)
second_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_day_display')
first_month_day = fields.Integer(default=1)
first_month_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_month_day_display')
first_month = fields.Selection([
('jan', 'January'),
('feb', 'February'),
@@ -81,6 +90,8 @@ class AccrualPlanLevel(models.Model):
('jun', 'June'),
], default="jan")
second_month_day = fields.Integer(default=1)
second_month_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_month_day_display')
second_month = fields.Selection([
('jul', 'July'),
('aug', 'August'),
@@ -104,16 +115,18 @@ class AccrualPlanLevel(models.Model):
('dec', 'December')
], default="jan")
yearly_day = fields.Integer(default=1)
yearly_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_yearly_day_display')
maximum_leave = fields.Float(
'Limit to', required=False, default=100,
help="Choose a maximum limit of days for this accrual. 0 means no limit.")
help="Choose a cap for this accrual. 0 means no cap.")
parent_id = fields.Many2one(
'hr.leave.accrual.level', string="Previous Level",
help="If this field is empty, this level is the first one.")
action_with_unused_accruals = fields.Selection(
[('postponed', 'Postponed to next year'),
[('postponed', 'Transferred to the next year'),
('lost', 'Lost')],
string="At the end of the year, unused accruals will be",
string="At the end of the calendar year, unused accruals will be",
default='postponed', required='True')
_sql_constraints = [
@@ -125,8 +138,8 @@ class AccrualPlanLevel(models.Model):
"(first_month_day > 0 AND first_month_day <= 31 AND second_month_day > 0 AND second_month_day <= 31 AND frequency = 'biyearly') or "
"(yearly_day > 0 AND yearly_day <= 31 AND frequency = 'yearly'))",
"The dates you've set up aren't correct. Please check them."),
('start_count_check', "CHECK( start_count >= 1 )", "You must start after more than 0 days."),
('added_value_greater_than_zero', 'CHECK(added_value > 0)', 'You must give the gain greater than 0 in accrual plan levels.')
('start_count_check', "CHECK( start_count >= 0 )", "You can not start an accrual in the past."),
('added_value_greater_than_zero', 'CHECK(added_value > 0)', 'You must give a rate greater than 0 in accrual plan levels.')
]
@api.depends('start_count', 'start_type')
@@ -153,90 +166,50 @@ class AccrualPlanLevel(models.Model):
else:
level.level = 1
def _get_accrual_values(self, allocation_create_date):
"""
This method returns all the accrual linked to their accrual_plan with the updated dynamic parameters depending
on the date.
:return: dict: {accrual_id, accrual_start, accrual_stop, nextcall, sufficient_seniority}
where accrual_start and accrual_stop are start and stop of the current period
"""
today = fields.Date.context_today(self, )
results = []
for accrual in self:
seniority = allocation_create_date + get_timedelta(accrual.start_count, accrual.start_type)
frequency = accrual.frequency
if frequency == 'daily':
accrual_start = max(today, seniority.date())
accrual_stop = accrual_start + relativedelta(days=1)
nextcall = accrual_stop
elif frequency == 'weekly':
min_accrual_date = max(today, seniority.date())
if min_accrual_date.isoweekday() == DAYS.index(accrual.week_day):
accrual_stop = min_accrual_date
else:
accrual_stop = accrual._get_next_weekday(min_accrual_date, accrual.week_day)
accrual_start = accrual_stop - relativedelta(days=7)
nextcall = accrual._get_next_weekday(min_accrual_date, accrual.week_day)
elif frequency == 'bimonthly':
if today.day <= accrual.first_day:
accrual_start = datetime.date(today.year, today.month, accrual.second_day) - relativedelta(months=1)
accrual_stop = datetime.date(today.year, today.month, accrual.first_day)
nextcall = datetime.date(today.year, today.month, accrual.second_day)
else:
if today.day <= accrual.second_day:
accrual_start = datetime.date(today.year, today.month, accrual.first_day)
accrual_stop = datetime.date(today.year, today.month, accrual.second_day)
nextcall = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1)
else:
accrual_start = datetime.date(today.year, today.month, accrual.second_day)
accrual_stop = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1)
nextcall = datetime.date(today.year, today.month, accrual.second_day) + relativedelta(months=1)
elif frequency == 'monthly':
if today.day <= accrual.first_day:
accrual_start = datetime.date(today.year, today.month, accrual.first_day) - relativedelta(months=1)
accrual_stop = datetime.date(today.year, today.month, accrual.first_day)
nextcall = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1)
else:
accrual_start = datetime.date(today.year, today.month, accrual.first_day)
accrual_stop = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=1)
nextcall = datetime.date(today.year, today.month, accrual.first_day) + relativedelta(months=2)
elif frequency == 'biyearly':
first_month = MONTHS.index(accrual.first_month) + 1
second_month = MONTHS.index(accrual.second_month) + 1
potential_first_accrual_date = datetime.date(today.year, first_month, accrual.first_month_day)
potential_second_accrual_date = datetime.date(today.year, second_month, accrual.second_month_day)
if today <= potential_first_accrual_date:
accrual_start = potential_second_accrual_date - relativedelta(years=1)
accrual_stop = potential_first_accrual_date
nextcall = potential_second_accrual_date
else:
if today <= potential_second_accrual_date:
accrual_start = potential_first_accrual_date
accrual_stop = potential_second_accrual_date
nextcall = potential_first_accrual_date + relativedelta(years=1)
else:
accrual_start = potential_second_accrual_date
accrual_stop = potential_first_accrual_date + relativedelta(years=1)
nextcall = potential_first_accrual_date + relativedelta(years=1)
elif frequency == 'yearly':
month = MONTHS.index(accrual.yearly_month) + 1
potential_accrual_date = datetime.date(today.year, month, accrual.yearly_day)
if today <= potential_accrual_date:
accrual_start = potential_accrual_date - relativedelta(years=1)
accrual_stop = potential_accrual_date
nextcall = potential_accrual_date + relativedelta(years=1)
else:
accrual_start = potential_accrual_date
accrual_stop = potential_accrual_date + relativedelta(years=1)
nextcall = accrual_stop
@api.depends('first_day', 'second_day', 'first_month_day', 'second_month_day', 'yearly_day')
def _compute_days_display(self):
days_select = _get_selection_days(self)
for level in self:
level.first_day_display = days_select[min(level.first_day - 1, 28)][0]
level.second_day_display = days_select[min(level.second_day - 1, 28)][0]
level.first_month_day_display = days_select[min(level.first_month_day - 1, 28)][0]
level.second_month_day_display = days_select[min(level.second_month_day - 1, 28)][0]
level.yearly_day_display = days_select[min(level.yearly_day - 1, 28)][0]
results.append({'accrual_level_id': accrual.id,
'start_after': accrual.start_count,
'accrual_start': datetime.datetime.combine(accrual_start, datetime.datetime.min.time()),
'accrual_stop': datetime.datetime.combine(accrual_stop, datetime.datetime.min.time()),
'nextcall': nextcall,
'sufficient_seniority': seniority.date() <= today})
return results
def _inverse_first_day_display(self):
for level in self:
if level.first_day_display == 'last':
level.first_day = 31
else:
level.first_day = DAY_SELECT_VALUES.index(level.first_day_display) + 1
def _inverse_second_day_display(self):
for level in self:
if level.second_day_display == 'last':
level.second_day = 31
else:
level.second_day = DAY_SELECT_VALUES.index(level.second_day_display) + 1
def _inverse_first_month_day_display(self):
for level in self:
if level.first_month_day_display == 'last':
level.first_month_day = 31
else:
level.first_month_day = DAY_SELECT_VALUES.index(level.first_month_day_display) + 1
def _inverse_second_month_day_display(self):
for level in self:
if level.second_month_day_display == 'last':
level.second_month_day = 31
else:
level.second_month_day = DAY_SELECT_VALUES.index(level.second_month_day_display) + 1
def _inverse_yearly_day_display(self):
for level in self:
if level.yearly_day_display == 'last':
level.yearly_day = 31
else:
level.yearly_day = DAY_SELECT_VALUES.index(level.yearly_day_display) + 1
def _get_next_date(self, last_call):
"""
@@ -246,47 +219,90 @@ class AccrualPlanLevel(models.Model):
if self.frequency == 'daily':
return last_call + relativedelta(days=1)
elif self.frequency == 'weekly':
return self._get_next_weekday(last_call, self.week_day)
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
weekday = daynames.index(self.week_day)
return last_call + relativedelta(days=1, weekday=weekday)
elif self.frequency == 'bimonthly':
if last_call.day < self.first_day:
return _get_date_check_month(last_call.year, last_call.month, self.first_day)
elif last_call.day < self.second_day:
return _get_date_check_month(last_call.year, last_call.month, self.second_day)
first_date = last_call + relativedelta(day=self.first_day)
second_date = last_call + relativedelta(day=self.second_day)
if last_call < first_date:
return first_date
elif last_call < second_date:
return second_date
else:
return _get_date_check_month(last_call.year, last_call.month, self.first_day) + relativedelta(months=1)
return last_call + relativedelta(months=1, day=self.first_day)
elif self.frequency == 'monthly':
if last_call.day < self.first_day:
return _get_date_check_month(last_call.year, last_call.month, self.first_day)
date = last_call + relativedelta(self.first_day)
if last_call < date:
return date
else:
return _get_date_check_month(last_call.year, last_call.month, self.first_day) + relativedelta(months=1)
return last_call + relativedelta(months=1, day=self.first_day)
elif self.frequency == 'biyearly':
first_month = MONTHS.index(self.first_month) + 1
second_month = MONTHS.index(self.second_month) + 1
if last_call < _get_date_check_month(last_call.year, first_month, self.first_month_day):
return _get_date_check_month(last_call.year, first_month, self.first_month_day)
elif last_call < _get_date_check_month(last_call.year, second_month, self.second_month_day):
return _get_date_check_month(last_call.year, second_month, self.second_month_day)
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
if last_call < first_date:
return first_date
elif last_call < second_date:
return second_date
else:
return _get_date_check_month(last_call.year, first_month, self.first_month_day) + relativedelta(years=1)
return last_call + relativedelta(years=1, month=first_month, day=self.first_month_day)
elif self.frequency == 'yearly':
month = MONTHS.index(self.yearly_month) + 1
if last_call < _get_date_check_month(last_call.year, month, self.yearly_day):
return _get_date_check_month(last_call.year, month, self.yearly_day)
date = last_call + relativedelta(month=month, day=self.yearly_day)
if last_call < date:
return date
else:
return _get_date_check_month(last_call.year, month, self.yearly_day) + relativedelta(years=1)
return last_call + relativedelta(years=1, month=month, day=self.yearly_day)
else:
return False
@api.model
def _get_next_weekday(self, day, weekday):
def _get_previous_date(self, last_call):
"""
:param day: a datetime object
:param weekday: Weekday as a decimal number, where 0 is Sunday and 6 is Saturday.
:return: datetime of the next weekday
Returns the date a potential previous call would have been at
For example if you have a monthly level giving 16/02 would return 01/02
Contrary to `_get_next_date` this function will return the 01/02 if that date is given
"""
daynames = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
weekday = daynames.index(weekday)
days_ahead = weekday - day.isoweekday()
if days_ahead <= 0:
days_ahead += 7
return day + relativedelta(days=days_ahead)
self.ensure_one()
if self.frequency == 'daily':
return last_call
elif self.frequency == 'weekly':
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
weekday = daynames.index(self.week_day)
return last_call + relativedelta(days=-6, weekday=weekday)
elif self.frequency == 'bimonthly':
second_date = last_call + relativedelta(day=self.second_day)
first_date = last_call + relativedelta(day=self.first_day)
if last_call >= second_date:
return second_date
elif last_call >= first_date:
return first_date
else:
return last_call + relativedelta(months=-1, day=self.second_day)
elif self.frequency == 'monthly':
date = last_call + relativedelta(day=self.first_day)
if last_call >= date:
return date
else:
return last_call + relativedelta(months=-1, day=self.first_day)
elif self.frequency == 'biyearly':
first_month = MONTHS.index(self.first_month) + 1
second_month = MONTHS.index(self.second_month) + 1
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
if last_call >= second_date:
return second_date
elif last_call >= first_date:
return first_date
else:
return last_call + relativedelta(years=-1, month=second_month, day=self.second_month_day)
elif self.frequency == 'yearly':
month = MONTHS.index(self.yearly_month) + 1
year_date = last_call + relativedelta(month=month, day=self.yearly_day)
if last_call >= year_date:
return year_date
else:
return last_call + relativedelta(years=-1, month=month, day=self.yearly_day)
else:
return False
@@ -282,13 +282,11 @@ class HolidaysAllocation(models.Model):
for allocation in self:
allocation.manager_id = allocation.employee_id and allocation.employee_id.parent_id
@api.depends('employee_id', 'accrual_plan_id')
@api.depends('accrual_plan_id')
def _compute_holiday_status_id(self):
default_holiday_status_id = None
for holiday in self:
if holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id:
holiday.holiday_status_id = False
elif not holiday.holiday_status_id:
if not holiday.holiday_status_id:
if holiday.accrual_plan_id:
holiday.holiday_status_id = holiday.accrual_plan_id.time_off_type_id
else:
@@ -321,18 +319,19 @@ class HolidaysAllocation(models.Model):
# to override in payroll
today = fields.Date.today()
for allocation in self:
current_level = allocation._get_current_accrual_plan_level_id(today)
current_level = allocation._get_current_accrual_plan_level_id(today)[0]
if current_level and current_level.action_with_unused_accruals == 'lost':
# Allocations are lost but number_of_days should not be lower than leaves_taken
allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': today, 'nextcall': False})
def _get_current_accrual_plan_level_id(self, date, level_ids=False):
"""
Returns the accrual_plan_level for the given date of the record's accrual plan
Returns a pair (accrual_plan_level, idx) where accrual_plan_level is the level for the given date
and idx is the index for the plan in the ordered set of levels
"""
self.ensure_one()
if not self.accrual_plan_id.level_ids:
return False
return (False, False)
# Sort by sequence which should be equivalent to the level
if not level_ids:
level_ids = self.accrual_plan_id.level_ids.sorted('sequence')
@@ -345,28 +344,28 @@ class HolidaysAllocation(models.Model):
# If transition_mode is set to `immediately` or we are currently on the first level
# the current_level is simply the first level in the list.
if current_level_idx <= 0 or self.accrual_plan_id.transition_mode == "immediately":
return current_level
return (current_level, current_level_idx)
# In this case we have to verify that the 'previous level' is not the current one due to `end_of_accrual`
level_start_date = self.date_from + get_timedelta(current_level.start_count, current_level.start_type)
previous_level = level_ids[current_level_idx - 1]
# If the next date from the current level's start date is before the last call of the previous level
# return the previous level
if current_level._get_next_date(level_start_date) < previous_level._get_next_date(level_start_date):
return previous_level
return current_level
return (previous_level, current_level_idx - 1)
return (current_level, current_level_idx)
def _process_accrual_plan_level(self, level, start_date, end_date):
def _process_accrual_plan_level(self, level, start_period, start_date, end_period, end_date):
"""
Returns the added days for that level
"""
self.ensure_one()
if level.is_based_on_worked_time:
start_dt = datetime.combine(start_date, datetime.min.time())
end_dt = datetime.combine(end_date, datetime.min.time())
end_dt = datetime.combine(end_date, datetime.max.time())
worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
[self.employee_id.id]['days']
[self.employee_id.id]['hours']
left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt,
domain=[('time_type', '=', 'leave')])[self.employee_id.id]['days']
domain=[('time_type', '=', 'leave')])[self.employee_id.id]['hours']
work_entry_prorata = worked / (left + worked) if worked else 0
added_value = work_entry_prorata * level.added_value
else:
@@ -374,7 +373,12 @@ class HolidaysAllocation(models.Model):
# Convert time in hours to time in days in case the level is encoded in hours
if level.added_value_type == 'hours':
added_value = added_value / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
return added_value
period_prorata = 1
if start_period != start_date or end_period != end_date:
period_days = (end_period - start_period)
call_days = (end_date - start_date)
period_prorata = min(1, call_days / period_days) if period_days else 1
return added_value * period_prorata
def _process_accrual_plans(self):
"""
@@ -384,7 +388,7 @@ class HolidaysAllocation(models.Model):
today = fields.Date.today()
first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, cancel and create a new one.""")
for allocation in self:
level_ids = self.accrual_plan_id.level_ids.sorted('sequence')
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
if not level_ids:
continue
if not allocation.nextcall:
@@ -398,9 +402,21 @@ class HolidaysAllocation(models.Model):
allocation._message_log(body=first_allocation)
days_added_per_level = defaultdict(lambda: 0)
while allocation.nextcall <= today:
current_level = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
nextcall = current_level._get_next_date(allocation.nextcall)
days_added_per_level[current_level] += allocation._process_accrual_plan_level(current_level, allocation.lastcall, allocation.nextcall)
# Since _get_previous_date returns the given date if it corresponds to a call date
# this will always return lastcall except possibly on the first call
# this is used to prorate the first number of days given to the employee
period_start = current_level._get_previous_date(allocation.lastcall)
period_end = current_level._get_next_date(allocation.lastcall)
# Also prorate this accrual in the event that we are passing from one level to another
if current_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
next_level = level_ids[current_level_idx + 1]
current_level_last_date = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type) - relativedelta(days=1)
if allocation.nextcall != current_level_last_date:
nextcall = min(nextcall, current_level_last_date)
days_added_per_level[current_level] += allocation._process_accrual_plan_level(
current_level, period_start, allocation.lastcall, period_end, allocation.nextcall)
allocation.lastcall = allocation.nextcall
allocation.nextcall = nextcall
if days_added_per_level:
@@ -486,7 +502,9 @@ class HolidaysAllocation(models.Model):
if not self._context.get('import_file'):
holiday.activity_update()
if holiday.validation_type == 'no':
holiday.state = 'validate'
if holiday.state == 'draft':
holiday.action_confirm()
holiday.action_validate()
return holidays
def write(self, values):
@@ -520,6 +538,7 @@ class HolidaysAllocation(models.Model):
'number_of_days': self.number_of_days,
'parent_id': self.id,
'employee_id': employee.id,
'employee_ids': [(6, 0, [employee.id])],
'state': 'confirm',
'allocation_type': self.allocation_type,
'date_from': self.date_from,
@@ -550,13 +569,15 @@ class HolidaysAllocation(models.Model):
def action_validate(self):
current_employee = self.env.user.employee_id
if any(holiday.state != 'confirm' for holiday in self):
raise UserError(_('Allocation request must be confirmed in order to approve it.'))
self.write({
'state': 'validate',
'approver_id': current_employee.id
})
for holiday in self:
if holiday.state != 'confirm':
raise UserError(_('Allocation request must be confirmed in order to approve it.'))
holiday.write({'state': 'validate'})
holiday.write({'approver_id': current_employee.id})
holiday._action_validate_create_childs()
self.activity_update()
return True
@@ -582,7 +603,7 @@ class HolidaysAllocation(models.Model):
mail_notify_force_send=False,
mail_activity_automation_skip=True
).create(allocation_create_vals)
if childs and self.validation_type != 'no':
if childs:
childs.action_validate()
return childs
@@ -89,23 +89,37 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2021-09-03',
})
freezer = freeze_time(datetime.date(2021, 9, 5))
freezer.start()
allocation.action_confirm()
allocation.action_validate()
self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.')
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.')
allocation._update_accrual()
nextWeek = self.env['hr.leave.accrual.level']._get_next_weekday(datetime.date.today(), 'mon')
nextWeek = allocation.date_from + relativedelta(days=1, weekday=0)
self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.')
freezer.stop()
freezer = freeze_time(nextWeek)
freezer.start()
allocation._update_accrual()
nextWeek = self.env['hr.leave.accrual.level']._get_next_weekday(datetime.date.today(), 'mon')
self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.')
nextWeek = datetime.date.today() + relativedelta(days=1, weekday=0)
#Prorated
self.assertAlmostEqual(allocation.number_of_days, 0.2857, 4, 'There should be 0.2857 day allocated.')
self.assertEqual(allocation.nextcall, nextWeek, 'The next call date of the cron should be in 2 weeks')
freezer.stop()
freezer = freeze_time(nextWeek)
freezer.start()
allocation._update_accrual()
nextWeek = datetime.date.today() + relativedelta(days=1, weekday=0)
self.assertAlmostEqual(allocation.number_of_days, 1.2857, 4, 'There should be 1.2857 day allocated.')
self.assertEqual(allocation.nextcall, nextWeek, 'The next call date of the cron should be in 2 weeks')
freezer.stop()
def test_frequency_bimonthly(self):
freezer = freeze_time('2021-09-01')
freezer.start()
@@ -129,6 +143,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2021-09-03',
})
self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00')
allocation.action_confirm()
@@ -144,10 +159,18 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
freezer.start()
next_date = datetime.date(2021, 10, 1)
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.')
#Prorated
self.assertAlmostEqual(allocation.number_of_days, 0.7857, 4, 'There should be 0.7857 day allocated.')
self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be October 1st')
freezer.stop()
freezer = freeze_time(next_date)
freezer.start()
allocation._update_accrual()
#Not Prorated
self.assertAlmostEqual(allocation.number_of_days, 1.7857, 4, 'There should be 1.7857 day allocated.')
freezer.stop()
def test_frequency_monthly(self):
freezer = freeze_time('2021-09-01')
freezer.start()
@@ -169,6 +192,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2021-08-31',
})
self.setAllocationCreateDate(allocation.id, '2021-09-01 00:00:00')
allocation.action_confirm()
@@ -184,6 +208,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
freezer.start()
next_date = datetime.date(2021, 11, 1)
allocation._update_accrual()
# Prorata = 1 since a whole month passed
self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.')
self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be November 1st')
freezer.stop()
@@ -225,10 +250,18 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
freezer.start()
next_date = datetime.date(2022, 7, 1)
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.')
# Prorated
self.assertAlmostEqual(allocation.number_of_days, 0.6576, 4, 'There should be 0.6576 day allocated.')
self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be July 1st')
freezer.stop()
freezer = freeze_time(next_date)
freezer.start()
allocation._update_accrual()
# Not Prorated
self.assertAlmostEqual(allocation.number_of_days, 1.6576, 4, 'There should be 1.6576 day allocated.')
freezer.stop()
def test_frequency_yearly(self):
freezer = freeze_time('2021-09-01')
freezer.start()
@@ -266,10 +299,16 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
freezer.start()
next_date = datetime.date(2023, 1, 1)
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, 'There should be 1 day allocated.')
self.assertAlmostEqual(allocation.number_of_days, 0.3315, 4, 'There should be 0.3315 day allocated.')
self.assertEqual(allocation.nextcall, next_date, 'The next call date of the cron should be January 1st 2023')
freezer.stop()
freezer = freeze_time(next_date)
freezer.start()
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 1.3315, 4, 'There should be 1.3315 day allocated.')
freezer.stop()
def test_check_gain(self):
# 2 accruals, one based on worked time, one not
# check gain
@@ -362,9 +401,12 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
freezer.start()
# next_date = datetime.date(2021, 9, 13)
self.env['hr.leave.allocation']._update_accrual()
self.assertEqual(allocation_not_worked_time.number_of_days, 5, 'There should be 5 days allocated.')
# Prorated
self.assertAlmostEqual(allocation_not_worked_time.number_of_days, 4.2857, 4, 'There should be 4.2857 days allocated.')
# 3.75 -> starts 1 day after allocation date -> 31/08-3/09 => 4 days - 1 days time off => (3 / 4) * 5 days
self.assertEqual(allocation_worked_time.number_of_days, 3.75, 'There should be 5 days allocated.')
# ^ result without prorata
# Prorated
self.assertAlmostEqual(allocation_worked_time.number_of_days, 3.42857, 4, 'There should be 3.42857 days allocated.')
self.assertEqual(allocation_not_worked_time.nextcall, datetime.date(2021, 9, 13), 'The next call date of the cron should be the September 13th')
self.assertEqual(allocation_worked_time.nextcall, datetime.date(2021, 9, 13), 'The next call date of the cron should be the September 13th')
freezer.stop()
@@ -373,9 +415,9 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
freezer.start()
next_date = datetime.date(2021, 9, 20)
self.env['hr.leave.allocation']._update_accrual()
self.assertEqual(allocation_not_worked_time.number_of_days, 10, 'There should be 10 days allocated.')
self.assertAlmostEqual(allocation_not_worked_time.number_of_days, 9.2857, 4, 'There should be 9.2857 days allocated.')
self.assertEqual(allocation_not_worked_time.nextcall, next_date, 'The next call date of the cron should be September 20th')
self.assertEqual(allocation_worked_time.number_of_days, 8.75, 'There should be 9 days allocated.')
self.assertAlmostEqual(allocation_worked_time.number_of_days, 8.42857, 4, 'There should be 8.42857 days allocated.')
self.assertEqual(allocation_worked_time.nextcall, next_date, 'The next call date of the cron should be September 20th')
freezer.stop()
@@ -455,7 +497,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_validate()
next_date = datetime.date.today() + relativedelta(days=11)
second_level = self.env['hr.leave.accrual.level'].search([('accrual_plan_id', '=', accrual_plan.id), ('start_count', '=', 10)])
self.assertEqual(allocation._get_current_accrual_plan_level_id(next_date), second_level, 'The second level should be selected')
self.assertEqual(allocation._get_current_accrual_plan_level_id(next_date)[0], second_level, 'The second level should be selected')
def test_accrual_transition_after_period(self):
# 1 accrual with 2 levels and level transition after
@@ -490,7 +532,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_validate()
next_date = datetime.date.today() + relativedelta(days=11)
second_level = self.env['hr.leave.accrual.level'].search([('accrual_plan_id', '=', accrual_plan.id), ('start_count', '=', 10)])
self.assertEqual(allocation._get_current_accrual_plan_level_id(next_date), second_level, 'The second level should be selected')
self.assertEqual(allocation._get_current_accrual_plan_level_id(next_date)[0], second_level, 'The second level should be selected')
def test_unused_accrual_lost(self):
#1 accrual with 2 levels and level transition immediately
@@ -315,7 +315,7 @@ class TestCompanyLeave(TransactionCase):
})
company_leave._compute_date_from_to()
with self.assertQueryCount(__system__=867, admin=865):
with self.assertQueryCount(__system__=870, admin=865):
# Original query count: 1987
# Without tracking/activity context keys: 5154
company_leave.action_validate()
@@ -26,19 +26,29 @@
<label for="frequency"/>
<div class="o_col">
<field name="frequency"/>
<div class="o_row">
<label for="week_day" string="on" attrs="{'invisible': [('frequency', '!=', 'weekly')]}"/><field name="week_day" attrs="{'invisible': [('frequency', '!=', 'weekly')]}"/>
<label for="first_day" string="on the" attrs="{'invisible': [('frequency', '!=', 'monthly')]}"/><field name="first_day" attrs="{'invisible': [('frequency', '!=', 'monthly')]}"/><span attrs="{'invisible': [('frequency', '!=', 'monthly')]}">day of the month</span>
<label for="first_day" string="on the" attrs="{'invisible': [('frequency', '!=', 'bimonthly')]}"/><field name="first_day" attrs="{'invisible': [('frequency', '!=', 'bimonthly')]}"/><span attrs="{'invisible': [('frequency', '!=', 'bimonthly')]}">and on the</span><field name="second_day" attrs="{'invisible': [('frequency', '!=', 'bimonthly')]}"/><span attrs="{'invisible': [('frequency', '!=', 'bimonthly')]}">days of the month</span>
<label for="first_month_day" string="on the" attrs="{'invisible': [('frequency', '!=', 'biyearly')]}"/><field name="first_month_day" attrs="{'invisible': [('frequency', '!=', 'biyearly')]}"/><field name="first_month" attrs="{'invisible': [('frequency', '!=', 'biyearly')]}"/><span attrs="{'invisible': [('frequency', '!=', 'biyearly')]}">and on the</span><field name="second_month_day" attrs="{'invisible': [('frequency', '!=', 'biyearly')]}"/><field name="second_month" attrs="{'invisible': [('frequency', '!=', 'biyearly')]}"/>
<label for="yearly_day" string="on" attrs="{'invisible': [('frequency', '!=', 'yearly')]}"/><field name="yearly_day" attrs="{'invisible': [('frequency', '!=', 'yearly')]}"/><field name="yearly_month" attrs="{'invisible': [('frequency', '!=', 'yearly')]}"/>
<div class="o_row" attrs="{'invisible': [('frequency', '!=', 'weekly')]}">
<label for="week_day" string="on"/><field name="week_day"/>
</div>
<div class="o_row" attrs="{'invisible': [('frequency', '!=', 'monthly')]}">
<label for="first_day_display" string="on the"/><field name="first_day_display" required="1"/> of the month
</div>
<div class="o_row" attrs="{'invisible': [('frequency', '!=', 'bimonthly')]}">
<label for="first_day_display" string="on the"/><field name="first_day_display" required="1"/> and on the <field name="second_day_display" required="1"/> of the month
</div>
<div class="o_row" attrs="{'invisible': [('frequency', '!=', 'biyearly')]}">
<label for="first_month_day_display" string="on the"/><field name="first_month_day_display" required="1"/> of <field name="first_month"/> and on the <field name="second_month_day_display" required="1"/> of <field name="second_month"/>
</div>
<div class="o_row" attrs="{'invisible': [('frequency', '!=', 'yearly')]}">
<label for="yearly_day_display" string="on the"/><field name="yearly_day_display" required="1"/> of <field name="yearly_month"/>
</div>
</div>
<label for="maximum_leave"/>
<div class="o_col">
<div class="o_row">
<field name="maximum_leave"/>
<field name="added_value_type" nolabel="1" readonly="1"/>
<!-- force_save is required here since it would otherwise not be saved due to the readonly
there is an issue when the field is present twice in the view. -->
<field name="added_value_type" nolabel="1" readonly="1" force_save="1"/>
</div>
</div>
<field name="action_with_unused_accruals"/>
@@ -52,9 +62,10 @@
<field name="model">hr.leave.accrual.plan</field>
<field name="arch" type="xml">
<tree string="Accrual Plans">
<field name="name" class="col-3"/>
<field name="time_off_type_id" class="col-3"/>
<field name="employees_count" class="col-6"/>
<field name="name"/>
<field name="level_count"/>
<field name="time_off_type_id"/>
<field name="employees_count"/>
</tree>
</field>
</record>
@@ -166,7 +177,7 @@
</t>
</div>
<div t-if="record.maximum_leave.value">
Limit of <field name="maximum_leave"/> <field name="start_type"/>
Limit of <field name="maximum_leave"/> <field name="added_value_type"/>
</div>
<div t-if="record.action_with_unused_accruals.raw_value">
At the end of the year, unused accruals will be <t t-if="record.action_with_unused_accruals.raw_value == 'postponed'">postponed</t><t t-else="">lost</t>
@@ -168,7 +168,7 @@
<field name="employee_ids" widget="many2many_tags"
groups="hr_holidays.group_hr_holidays_user"
attrs="{'required': [('holiday_type', '=', 'employee'), ('state', 'in', ('draft', 'cancel', 'confirm'))],
'invisible': ['|', ('holiday_type', '!=', 'employee'), ('state', 'not in', ('draft', 'cancel', 'confirm'))]}"/>
'invisible': ['|', ('holiday_type', '!=', 'employee'), '&amp;', ('state', 'not in', ('draft', 'cancel', 'confirm')), ('employee_id', '!=', False)]}"/>
</xpath>
<xpath expr="//field[@name='employee_id']" position="after">
<field name="category_id"
+2 -1
View File
@@ -227,6 +227,7 @@
<div name="title">
<field name="display_name" invisible="1"/>
<field name="employee_id" nolabel="1" readonly="1" force_save="1" invisible="1"/>
<field name="employee_ids" invisible="1"/>
</div>
<group>
<group name="col_left">
@@ -403,7 +404,7 @@
'invisible': ['|', '|', ('holiday_type', '!=', 'employee'), ('state', '!=', 'validate'), ('employee_id', '=', False)]
}" widget="many2one_avatar_employee"/>
<field name="employee_ids" groups="hr_holidays.group_hr_holidays_user" attrs="{
'required': [('holiday_type', '=', 'employee')],
'required': [('holiday_type', '=', 'employee'), ('state', 'in', ('draft', 'cancel', 'refuse'))],
'invisible': ['|', ('holiday_type', '!=', 'employee'), '&amp;', ('state', '=', 'validate'), ('employee_id', '!=', False)],
}" widget="many2many_tags"/>
</label>
@@ -150,7 +150,7 @@ class HrLeave(models.Model):
def create(self, vals_list):
start_dates = [v.get('date_from') for v in vals_list if v.get('date_from')]
stop_dates = [v.get('date_to') for v in vals_list if v.get('date_to')]
if any(vals.get('holiday_type', 'employee') == 'employee' and not vals.get('employee_id', False) for vals in vals_list):
if any(vals.get('holiday_type', 'employee') == 'employee' and not vals.get('multi_employee', False) and not vals.get('employee_id', False) for vals in vals_list):
raise ValidationError(_("There is no employee set on the time off. Please make sure you're logged in the correct company."))
with self.env['hr.work.entry']._error_checking(start=min(start_dates, default=False), stop=max(stop_dates, default=False)):
return super().create(vals_list)
@@ -40,14 +40,14 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
def test_performance_leave_write(self):
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
with self.assertQueryCount(__system__=25, admin=29):
with self.assertQueryCount(__system__=25, admin=30):
leave.date_to = datetime(2018, 1, 1, 19, 0)
leave.action_refuse()
@users('__system__', 'admin')
@warmup
def test_performance_leave_create(self):
with self.assertQueryCount(__system__=26, admin=30):
with self.assertQueryCount(__system__=27, admin=30):
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
leave.action_refuse()