[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:
committed by
Yannick Tivisse
parent
09da30fbcd
commit
bd13371781
@@ -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>
|
||||
|
||||
|
||||
@@ -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'), '&', ('state', 'not in', ('draft', 'cancel', 'confirm')), ('employee_id', '!=', False)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='employee_id']" position="after">
|
||||
<field name="category_id"
|
||||
|
||||
@@ -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'), '&', ('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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user