[IMP] hr_holiday: accrual refactoring.

Refactoring of the accrual computation methods and views. There are also
2 new features: custom carry-over date and allocation time:

Custom carry-over date:
Previously, an accrual plan was always evaluated on the 1st of january
to decide if days were lost or postponed to next year. Now the user can
choose any recurring date in the year for that evaluation

Allocation time:
Previously the days were given at the end of the accrual period, meaning
an employee had to work the whole period to get his days. Now there is a
choice between accruing at the start or at the end of a given period.

Simulation:
On an allocation of type accrual, changing date_from or
accrual_plan launches the calculation of the days to allocate. This lets
the user know how many days the allocation would give to an employee and
lets him eventually change that number. After saving the record the flow
resumes like before. There is no more need to create fake allocations.

Additional tests written and some existing tests have corrected values /
parameters to still test same feature but be correct with the new
functioning.

task 3060724

closes odoo/odoo#112383

Related: odoo/upgrade#4597
Signed-off-by: Sofie Gvaladze (sgv) <sgv@odoo.com>
This commit is contained in:
dasz
2023-08-25 10:59:13 +02:00
committed by sofiagvaladze
parent a2c222520d
commit cf33b80aca
10 changed files with 773 additions and 285 deletions
@@ -3,6 +3,11 @@
from odoo import api, fields, models, _
from odoo.addons.hr_holidays.models.hr_leave_accrual_plan_level import _get_selection_days
DAY_SELECT_VALUES = [str(i) for i in range(1, 29)] + ['last']
DAY_SELECT_SELECTION_NO_LAST = tuple(zip(DAY_SELECT_VALUES, (str(i) for i in range(1, 29))))
class AccrualPlan(models.Model):
_name = "hr.leave.accrual.plan"
@@ -15,19 +20,48 @@ class AccrualPlan(models.Model):
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)
level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id', copy=True, string="Milestone")
allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id')
company_id = fields.Many2one('res.company', string='Company',
compute="_compute_company_id", store="True", readonly=False)
transition_mode = fields.Selection([
('immediately', 'Immediately'),
('end_of_accrual', "After this accrual's period")],
string="Level Transition", default="immediately", required=True,
string="Milestone Transition", default="immediately", required=True,
help="""Specify what occurs if a level transition takes place in the middle of a pay period.\n
'Immediately' will switch the employee to the new accrual level on the exact date during the ongoing pay period.\n
'After this accrual's period' will keep the employee on the same accrual level until the ongoing pay period is complete.
After it is complete, the new level will take effect when the next pay period begins.""")
show_transition_mode = fields.Boolean(compute='_compute_show_transition_mode')
is_based_on_worked_time = fields.Boolean("Based on worked time", compute="_compute_is_based_on_worked_time", store=True, readonly=False,
help="If checked, the accrual period will be calculated according to the work days, not calendar days.")
accrued_gain_time = fields.Selection([
("start", "At the start of the accrual period"),
("end", "At the end of the accrual period")],
default="end", required=True)
carryover_date = fields.Selection([
("year_start", "At the start of the year"),
("allocation", "At the allocation date"),
("other", "Other")],
default="year_start", required=True, string="Carry-Over Time")
carryover_day = fields.Integer(default=1)
carryover_day_display = fields.Selection(
_get_selection_days, compute='_compute_carryover_day_display', inverse='_inverse_carryover_day_display')
carryover_month = fields.Selection([
("jan", "January"),
("feb", "February"),
("mar", "March"),
("apr", "April"),
("may", "May"),
("jun", "June"),
("jul", "July"),
("aug", "August"),
("sep", "September"),
("oct", "October"),
("nov", "November"),
("dec", "December")
], default="jan")
added_value_type = fields.Selection([('day', 'Days'), ('hour', 'Hours')], compute='_compute_added_value_type', store=True)
@api.depends('level_ids')
def _compute_show_transition_mode(self):
@@ -66,6 +100,31 @@ class AccrualPlan(models.Model):
else:
accrual_plan.company_id = self.env.company
@api.depends("accrued_gain_time")
def _compute_is_based_on_worked_time(self):
for plan in self:
if plan.accrued_gain_time == "start":
plan.is_based_on_worked_time = False
@api.depends("level_ids")
def _compute_added_value_type(self):
for plan in self:
if plan.level_ids:
plan.added_value_type = plan.level_ids[0].added_value_type
@api.depends("carryover_day")
def _compute_carryover_day_display(self):
days_select = _get_selection_days(self)
for plan in self:
plan.carryover_day_display = days_select[min(plan.carryover_day - 1, 28)][0]
def _inverse_carryover_day_display(self):
for plan in self:
if plan.carryover_day_display == 'last':
plan.carryover_day = 31
else:
plan.carryover_day = DAY_SELECT_VALUES.index(plan.carryover_day_display) + 1
def action_open_accrual_plan_employees(self):
self.ensure_one()
@@ -1,13 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
import calendar
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.tools.date_utils import get_timedelta
DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
@@ -19,6 +15,7 @@ DAY_SELECT_SELECTION_NO_LAST = tuple(zip(DAY_SELECT_VALUES, (str(i) for i in ran
def _get_selection_days(self):
return DAY_SELECT_SELECTION_NO_LAST + (("last", _("last day")),)
class AccrualPlanLevel(models.Model):
_name = "hr.leave.accrual.level"
_description = "Accrual Plan Level"
@@ -27,27 +24,25 @@ class AccrualPlanLevel(models.Model):
sequence = fields.Integer(
string='sequence', compute='_compute_sequence', store=True,
help='Sequence is generated automatically by start time delta.')
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True)
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True, ondelete="cascade")
start_count = fields.Integer(
"Start after",
help="The accrual starts after a defined period from the allocation 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)')],
[('day', 'Days'),
('month', 'Months'),
('year', 'Years')],
default='day', string=" ", required=True,
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="If checked, the rate will be prorated on time off type where type is set on Working Time in the configuration.")
# Accrue of
added_value = fields.Float(
"Rate", digits=(16, 5), required=True,
"Rate", digits=(16, 5), required=True, default=1,
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')],
default='days', required=True)
added_value_type = fields.Selection([
('day', 'Days'),
('hour', 'Hours')
], compute="_compute_added_value_type", store=True, required=True, default="day")
frequency = fields.Selection([
('daily', 'Daily'),
('weekly', 'Weekly'),
@@ -110,19 +105,19 @@ class AccrualPlanLevel(models.Model):
yearly_day = fields.Integer(default=1)
yearly_day_display = fields.Selection(
_get_selection_days, compute='_compute_days_display', inverse='_inverse_yearly_day_display')
cap_accrued_time = fields.Boolean("Cap accrued time", default=True)
maximum_leave = fields.Float(
'Limit to', required=False, default=100,
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.")
'Limit to', digits=(16, 2), compute="_compute_maximum_leave", readonly=False, store=True,
help="Choose a cap for this accrual.")
action_with_unused_accruals = fields.Selection(
[('postponed', 'Transferred to the next year'),
('lost', 'Lost')],
string="At the end of the calendar year, unused accruals will be",
default='postponed', required=True)
[('lost', 'None. Accrued time reset to 0'),
('all', 'All accrued time carried over'),
('maximum', 'Carry over with a maximum')],
string="Carry over",
default='all', required=True)
postpone_max_days = fields.Integer("Maximum amount of accruals to transfer",
help="Set a maximum of days an allocation keeps at the end of the year. 0 for no limit.")
help="Set a maximum of accruals an allocation keeps at the end of the year.")
can_modify_value_type = fields.Boolean(compute="_compute_can_modify_value_type")
_sql_constraints = [
('check_dates',
@@ -148,6 +143,19 @@ class AccrualPlanLevel(models.Model):
for level in self:
level.sequence = level.start_count * start_type_multipliers[level.start_type]
@api.depends('accrual_plan_id', 'accrual_plan_id.level_ids', 'accrual_plan_id.time_off_type_id')
def _compute_can_modify_value_type(self):
for level in self:
level.can_modify_value_type = not level.accrual_plan_id.time_off_type_id and level.accrual_plan_id.level_ids and level.accrual_plan_id.level_ids[0] == level
@api.depends('accrual_plan_id', 'accrual_plan_id.level_ids', 'accrual_plan_id.time_off_type_id')
def _compute_added_value_type(self):
for level in self:
if level.accrual_plan_id.time_off_type_id:
level.added_value_type = "day" if level.accrual_plan_id.time_off_type_id.request_unit in ["day", "half_day"] else "hour"
elif level.accrual_plan_id.level_ids and level.accrual_plan_id.level_ids[0] != level:
level.added_value_type = level.accrual_plan_id.level_ids[0]._origin.added_value_type
@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)
@@ -158,6 +166,11 @@ class AccrualPlanLevel(models.Model):
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]
@api.depends('cap_accrued_time')
def _compute_maximum_leave(self):
for level in self:
level.maximum_leave = 100 if level.cap_accrued_time else 0
def _inverse_first_day_display(self):
for level in self:
if level.first_day_display == 'last':
+122 -91
View File
@@ -3,23 +3,19 @@
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
from collections import defaultdict
import logging
from datetime import datetime, time
from datetime import datetime, date, time
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.addons.resource.models.utils import HOURS_PER_DAY
from odoo.addons.hr_holidays.models.hr_leave import get_employee_from_context
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.translate import _
from odoo.exceptions import AccessError, UserError
from odoo.tools.float_utils import float_round
from odoo.tools.date_utils import get_timedelta
from odoo.osv import expression
_logger = logging.getLogger(__name__)
MONTHS_TO_INTEGER = {"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12}
class HolidaysAllocation(models.Model):
""" Allocation Requests Access specifications: similar to leave requests """
@@ -75,11 +71,10 @@ class HolidaysAllocation(models.Model):
help='Duration in days. Reference field to use when necessary.')
number_of_days_display = fields.Float(
'Duration (days)', compute='_compute_number_of_days_display',
readonly=False,
help="If Accrual Allocation: Number of days allocated in addition to the ones you will get via the accrual' system.")
help="For an Accrual Allocation, this field contains the theorical amount of time given to the employee, due to a previous start date, on the first run of the plan. This can be manually edited.")
number_of_hours_display = fields.Float(
'Duration (hours)', compute='_compute_number_of_hours_display',
help="If Accrual Allocation: Number of hours allocated in addition to the ones you will get via the accrual' system.")
help="For an Accrual Allocation, this field contains the theorical amount of time given to the employee, due to a previous start date, on the first run of the plan. This can be manually edited.")
duration_display = fields.Char('Allocated (Days/Hours)', compute='_compute_duration_display',
help="Field allowing to see the allocation duration in days or hours depending on the type_request_unit")
# details
@@ -91,7 +86,11 @@ class HolidaysAllocation(models.Model):
validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True)
can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True)
type_request_unit = fields.Selection([
('hour', 'Hours'),
('half_day', 'Half Day'),
('day', 'Day'),
], compute="_compute_type_request_unit")
# mode
holiday_type = fields.Selection([
('employee', 'By Employee'),
@@ -117,12 +116,12 @@ class HolidaysAllocation(models.Model):
'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', readonly=False)
# accrual configuration
lastcall = fields.Date("Date of the last accrual allocation", readonly=True, default=fields.Date.context_today)
nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True)
allocation_type = fields.Selection(
[
('regular', 'Regular Allocation'),
('accrual', 'Accrual Allocation')
], string="Allocation Type", default="regular", required=True, readonly=False)
nextcall = fields.Date("Date of the next accrual allocation", readonly=True, default=False)
already_accrued = fields.Boolean()
allocation_type = fields.Selection([
('regular', 'Regular Allocation'),
('accrual', 'Accrual Allocation')
], string="Allocation Type", default="regular", required=True, readonly=True)
is_officer = fields.Boolean(compute='_compute_is_officer')
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', compute="_compute_from_holiday_status_id", store=True, readonly=False, domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]", tracking=True)
max_leaves = fields.Float(compute='_compute_leaves')
@@ -222,7 +221,6 @@ class HolidaysAllocation(models.Model):
allocation.number_of_hours_display = allocation.number_of_days * (allocation_calendar.hours_per_day or HOURS_PER_DAY)
@api.depends('number_of_hours_display', 'number_of_days_display')
def _compute_duration_display(self):
for allocation in self:
@@ -336,37 +334,42 @@ class HolidaysAllocation(models.Model):
if allocation.holiday_status_id:
allocation.accrual_plan_id = accruals_dict.get(allocation.holiday_status_id.id, [False])[0]
def _end_of_year_accrual(self):
# to override in payroll
today = fields.Date.today()
last_day_last_year = today + relativedelta(years=-1, month=12, day=31)
first_day_this_year = today + relativedelta(month=1, day=1)
@api.depends("allocation_type", "holiday_status_id", "accrual_plan_id")
def _compute_type_request_unit(self):
for allocation in self:
current_level = allocation._get_current_accrual_plan_level_id(first_day_this_year)[0]
if not current_level:
continue
# lastcall has two cases:
# 1. The period was fully ran until the last day of last year
# 2. The period was not fully ran until the last day of last year
# For case 2, we need to prorata the number of days so need to check if the lastcall within the current level period
lastcall = current_level._get_previous_date(last_day_last_year) if allocation.lastcall < current_level._get_previous_date(last_day_last_year) else allocation.lastcall
nextcall = current_level._get_next_date(last_day_last_year)
if 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': lastcall, 'nextcall': nextcall})
elif current_level.action_with_unused_accruals == 'postponed' and current_level.postpone_max_days:
# Make sure the period was ran until the last day of last year
if allocation.nextcall:
allocation.nextcall = first_day_this_year
# date_to should be first day of this year so the prorata amount is computed correctly
allocation._process_accrual_plans(first_day_this_year, True)
number_of_days = min(allocation.number_of_days - allocation.leaves_taken, current_level.postpone_max_days) + allocation.leaves_taken
allocation.write({'number_of_days': number_of_days, 'lastcall': lastcall, 'nextcall': nextcall})
if allocation.allocation_type == "accrual" and allocation.accrual_plan_id:
allocation.type_request_unit = allocation.accrual_plan_id.added_value_type
elif allocation.allocation_type == "regular":
allocation.type_request_unit = allocation.holiday_status_id.request_unit
else:
allocation.type_request_unit = "day"
def _get_carryover_date(self, date_from):
self.ensure_one()
carryover_time = self.accrual_plan_id.carryover_date
accrual_plan = self.accrual_plan_id
carryover_date = False
if carryover_time == 'year_start':
carryover_date = date(date_from.year, 1, 1)
elif carryover_time == 'allocation':
carryover_date = date(date_from.year, self.date_from.month, self.date_from.day)
else:
carryover_date = date(date_from.year, MONTHS_TO_INTEGER[accrual_plan.carryover_month], accrual_plan.carryover_day)
if date_from > carryover_date:
carryover_date += relativedelta(years=1)
return carryover_date
def _add_days_to_allocation(self, current_level, current_level_maximum_leave, leaves_taken, period_start, period_end):
days_to_add = self._process_accrual_plan_level(
current_level, period_start, self.lastcall, period_end, self.nextcall)
self.number_of_days += days_to_add
if current_level.cap_accrued_time:
self.number_of_days = min(self.number_of_days, current_level_maximum_leave + leaves_taken)
def _get_current_accrual_plan_level_id(self, date, level_ids=False):
"""
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
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:
@@ -398,14 +401,15 @@ class HolidaysAllocation(models.Model):
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())
if level.accrual_plan_id.is_based_on_worked_time:
datetime_min_time = datetime.min.time()
start_dt = datetime.combine(start_date, datetime_min_time)
end_dt = datetime.combine(end_date, datetime_min_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]['hours']
if start_period != start_date or end_period != end_date:
start_dt = datetime.combine(start_period, datetime.min.time())
end_dt = datetime.combine(end_period, datetime.min.time())
start_dt = datetime.combine(start_period, datetime_min_time)
end_dt = datetime.combine(end_period, datetime_min_time)
planned_worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
[self.employee_id.id]['hours']
else:
@@ -417,10 +421,10 @@ class HolidaysAllocation(models.Model):
else:
added_value = level.added_value
# Convert time in hours to time in days in case the level is encoded in hours
if level.added_value_type == 'hours':
if level.added_value_type == 'hour':
added_value = added_value / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
period_prorata = 1
if (start_period != start_date or end_period != end_date) and not level.is_based_on_worked_time:
if (start_period != start_date or end_period != end_date) and not level.accrual_plan_id.is_based_on_worked_time:
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
@@ -438,83 +442,94 @@ class HolidaysAllocation(models.Model):
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
if not level_ids:
continue
# "cache" leaves taken, as it gets recomputed every time allocation.number_of_days is assigned to. Without this,
# every loop will take 1+ second. It can be removed if computes don't chain in a way to always reassign accrual plan
# even if the value doesn't change. This is the best performance atm.
first_level = level_ids[0]
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
leaves_taken = allocation.leaves_taken if first_level.added_value_type == "day" else allocation.leaves_taken / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
# first time the plan is run, initialize nextcall and take carryover / level transition into account
if not allocation.nextcall:
first_level = level_ids[0]
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
# Accrual plan is not configured properly or has not started
if date_to < first_level_start_date:
# Accrual plan is not configured properly or has not started
continue
allocation.lastcall = max(allocation.lastcall, first_level_start_date)
allocation.nextcall = first_level._get_next_date(allocation.lastcall)
# adjust nextcall for carryover
carryover_date = allocation._get_carryover_date(allocation.nextcall)
allocation.nextcall = min(carryover_date, allocation.nextcall)
# adjust nextcall for level_transition
if len(level_ids) > 1:
second_level_start_date = allocation.date_from + get_timedelta(level_ids[1].start_count, level_ids[1].start_type)
allocation.nextcall = min(second_level_start_date, allocation.nextcall)
allocation._message_log(body=first_allocation)
days_added_per_level = defaultdict(lambda: 0)
(current_level, current_level_idx) = (False, 0)
current_level_maximum_leave = 0.0
# all subsequent runs, at every loop:
# get current level and normal period boundaries, then set nextcall, adjusted for level transition and carryover
# add days, trimmed if there is a maximum_leave
while allocation.nextcall <= date_to:
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
if not current_level:
break
current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "days" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
if current_level.cap_accrued_time:
current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "day" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
nextcall = current_level._get_next_date(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
# There are 2 cases where nextcall could be closer than the normal period:
# 1. Passing from one level to another, if mode is set to 'immediately'
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)
if allocation.nextcall != current_level_last_date:
nextcall = min(nextcall, current_level_last_date)
# We have to check for end of year actions if it is within our period
# since we can create retroactive allocations.
if allocation.lastcall.year < allocation.nextcall.year and\
current_level.action_with_unused_accruals == 'postponed' and\
current_level.postpone_max_days > 0:
# Compute number of days kept
allocation_days = allocation.number_of_days - allocation.leaves_taken
allowed_to_keep = max(0, current_level.postpone_max_days - allocation_days)
number_of_days = min(allocation_days, current_level.postpone_max_days)
allocation.number_of_days = number_of_days + allocation.leaves_taken
total_gained_days = sum(days_added_per_level.values())
days_added_per_level.clear()
days_added_per_level[current_level] = min(total_gained_days, allowed_to_keep)
gained_days = allocation._process_accrual_plan_level(
current_level, period_start, allocation.lastcall, period_end, allocation.nextcall)
days_added_per_level[current_level] += gained_days
if current_level_maximum_leave > 0 and sum(days_added_per_level.values()) > current_level_maximum_leave:
days_added_per_level[current_level] -= sum(days_added_per_level.values()) - current_level_maximum_leave
# 2. On carry-over date
carryover_date = allocation._get_carryover_date(allocation.nextcall)
if allocation.nextcall < carryover_date < nextcall:
nextcall = min(nextcall, carryover_date)
if not allocation.already_accrued:
allocation._add_days_to_allocation(current_level, current_level_maximum_leave, leaves_taken, period_start, period_end)
# if it's the carry-over date, adjust days using current level's carry-over policy, then continue
if allocation.nextcall == carryover_date:
if current_level.action_with_unused_accruals in ['lost', 'maximum']:
allocation_days = allocation.number_of_days + leaves_taken
allocation_max_days = current_level.postpone_max_days + leaves_taken
allocation.number_of_days = min(allocation_days, allocation_max_days)
allocation.lastcall = allocation.nextcall
allocation.nextcall = nextcall
allocation.already_accrued = False
if force_period and allocation.nextcall > date_to:
allocation.nextcall = date_to
force_period = False
if days_added_per_level:
number_of_days_to_add = allocation.number_of_days + sum(days_added_per_level.values())
max_allocation_days = current_level_maximum_leave + (allocation.leaves_taken if allocation.type_request_unit != "hour" else allocation.leaves_taken / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY))
# Let's assume the limit of the last level is the correct one
allocation.number_of_days = min(number_of_days_to_add, max_allocation_days) if current_level_maximum_leave > 0 else number_of_days_to_add
# if plan.accrued_gain_time == 'start', process next period and set flag 'already_accrued', this will skip adding days
# once, preventing double allocation.
if allocation.accrual_plan_id.accrued_gain_time == 'start':
# check that we are at the start of a period, not on a carry-over or level transition date
current_level = current_level or allocation.accrual_plan_id.level_ids[0]
period_start = current_level._get_previous_date(allocation.lastcall)
if allocation.lastcall != period_start:
continue
if current_level.cap_accrued_time:
current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "day" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
allocation._add_days_to_allocation(current_level, current_level_maximum_leave, leaves_taken, allocation.lastcall, allocation.nextcall)
allocation.already_accrued = True
@api.model
def _update_accrual(self):
"""
Method called by the cron task in order to increment the number_of_days when
necessary.
Method called by the cron task in order to increment the number_of_days when
necessary.
"""
# Get the current date to determine the start and end of the accrual period
today = datetime.combine(fields.Date.today(), time(0, 0, 0))
this_year_first_day = (today + relativedelta(day=1, month=1)).date()
end_of_year_allocations = self.search(
[('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()), ('lastcall', '<', this_year_first_day)])
end_of_year_allocations._end_of_year_accrual()
end_of_year_allocations.flush_model()
allocations = self.search(
[('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
allocations = self.search([
('allocation_type', '=', 'accrual'), ('state', '=', 'validate'),
('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
allocations._process_accrual_plans()
@@ -757,6 +772,22 @@ class HolidaysAllocation(models.Model):
elif not self.number_of_days_display:
self.number_of_days = 1.0
# Allows user to simulate how many days an accrual plan would give from a certain start date.
# it uses the actual computation function but resets values of lastcall, nextcall and nbr of days
# before every run, as if it was run from date_from, after an optional change in the allocation value
# the user can simply confirm and validate the allocation. The record is in correct state for the next
# call of the cron job.
@api.onchange('date_from', 'accrual_plan_id')
def _onchange_date_from(self):
now = date.today()
if self.allocation_type != 'accrual' or self.state == 'validate' or not self.accrual_plan_id\
or not self.employee_id or not (not self.date_to or self.date_to > now):
return
self.lastcall = self.date_from
self.nextcall = False
self.number_of_days_display = 0.0
self._process_accrual_plans()
# ------------------------------------------------------------
# Activity methods
# ------------------------------------------------------------
@@ -101,7 +101,6 @@ class HolidaysType(models.Model):
accruals_ids = fields.One2many('hr.leave.accrual.plan', 'time_off_type_id')
accrual_count = fields.Float(compute="_compute_accrual_count", string="Accruals count")
@api.model
def _search_valid(self, operator, value):
""" Returns leave_type ids for which a valid allocation exists
@@ -134,7 +133,6 @@ class HolidaysType(models.Model):
return [('id', new_operator, [x['holiday_status_id'] for x in self._cr.dictfetchall()])]
@api.depends('requires_allocation')
def _compute_valid(self):
date_to = self._context.get('default_date_to', fields.Datetime.today())
@@ -0,0 +1,14 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
export class AccrualLevelsX2ManyField extends X2ManyField {};
AccrualLevelsX2ManyField.template = "hr_holidays.AccrualLevelsX2ManyField";
export const accrualLevelsX2ManyField = {
...x2ManyField,
component: AccrualLevelsX2ManyField,
};
registry.category("fields").add("accrual_levels_one2many", accrualLevelsX2ManyField);
@@ -112,3 +112,6 @@ $o-hr-holidays-border-color: map-get($grays, '300');
}
}
}
.o_radio_item .o_form_label {
font-weight: normal;
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.AccrualLevelsX2ManyField" t-inherit="web.X2ManyField" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_x2m_control_panel')]/.." position="before">
<xpath expr="//ListRenderer" position="move"/>
<xpath expr="//KanbanRenderer" position="move"/>
</xpath>
</t>
</templates>
@@ -21,6 +21,13 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'requires_allocation': 'yes',
'allocation_validation_type': 'officer',
})
cls.leave_type_hour = cls.env['hr.leave.type'].create({
'name': 'Paid Time Off',
'time_type': 'leave',
'requires_allocation': 'yes',
'allocation_validation_type': 'officer',
'request_unit': 'hour',
})
def setAllocationCreateDate(self, allocation_id, date):
""" This method is a hack in order to be able to define/redefine the create_date
@@ -35,15 +42,16 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
""" % (date, allocation_id))
def test_frequency_daily(self):
with freeze_time(datetime.date(2017, 12, 5)):
with freeze_time("2017-12-5"):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'added_value_type': 'day',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 10000
})],
})
@@ -72,15 +80,16 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
self.assertEqual(allocation.number_of_days, 1, 'There should be only 1 day allocated.')
def test_frequency_weekly(self):
with freeze_time(datetime.date(2017, 12, 5)):
with freeze_time("2017-12-5"):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 10000
})],
})
@@ -105,7 +114,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
with freeze_time(nextWeek):
allocation._update_accrual()
nextWeek = datetime.date.today() + relativedelta(days=1, weekday=0)
#Prorated
# 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')
@@ -120,13 +129,14 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'bimonthly',
'first_day': 1,
'second_day': 15,
'cap_accrued_time': True,
'maximum_leave': 10000,
})],
})
@@ -151,13 +161,13 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
with freeze_time(next_date):
next_date = datetime.date(2021, 10, 1)
allocation._update_accrual()
#Prorated
# 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')
with freeze_time(next_date):
allocation._update_accrual()
#Not Prorated
# Not Prorated
self.assertAlmostEqual(allocation.number_of_days, 1.7857, 4, 'There should be 1.7857 day allocated.')
def test_frequency_monthly(self):
@@ -165,11 +175,12 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 10000
})],
})
@@ -203,15 +214,16 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'biyearly',
'cap_accrued_time': True,
'maximum_leave': 10000,
})],
})
#this sets up an accrual on the 1st of January and the 1st of July
# this sets up an accrual on the 1st of January and the 1st of July
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
@@ -246,15 +258,16 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 10000,
})],
})
#this sets up an accrual on the 1st of January
# this sets up an accrual on the 1st of January
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
@@ -319,24 +332,26 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan_not_based_on_worked_time = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 5,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 10000,
})],
})
accrual_plan_based_on_worked_time = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'is_based_on_worked_time': True,
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 5,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 10000,
'is_based_on_worked_time': True,
})],
})
allocation_not_worked_time = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
@@ -401,15 +416,16 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
self.assertEqual(allocation_worked_time.nextcall, next_date, 'The next call date of the cron should be September 20th')
def test_check_max_value(self):
with freeze_time(datetime.date(2017, 12, 5)):
with freeze_time("2017-12-5"):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 1,
})],
})
@@ -436,20 +452,21 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
with freeze_time(nextcall):
allocation._update_accrual()
nextcall = datetime.date.today() + relativedelta(days=1)
#The maximum value is 1 so this shouldn't change anything
# The maximum value is 1 so this shouldn't change anything
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, 'There should be only 1 day allocated.')
def test_check_max_value_hours(self):
with freeze_time(datetime.date(2017, 12, 5)):
with freeze_time("2017-12-5"):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'hour',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'hours',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 4,
})],
})
@@ -476,29 +493,30 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
with freeze_time(nextcall):
allocation._update_accrual()
nextcall = datetime.date.today() + relativedelta(days=1)
#The maximum value is 1 so this shouldn't change anything
# The maximum value is 1 so this shouldn't change anything
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 0.5, 'There should be only 0.5 days allocated.')
def test_accrual_transition_immediately(self):
with freeze_time(datetime.date(2017, 12, 5)):
#1 accrual with 2 levels and level transition immediately
with freeze_time("2017-12-5"):
# 1 accrual with 2 levels and level transition immediately
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'transition_mode': 'immediately',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 1,
}), (0, 0, {
'start_count': 10,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 1,
})],
})
@@ -517,24 +535,25 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
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):
with freeze_time(datetime.date(2017, 12, 5)):
with freeze_time("2017-12-5"):
# 1 accrual with 2 levels and level transition after
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'transition_mode': 'end_of_accrual',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 1,
}), (0, 0, {
'start_count': 10,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'weekly',
'cap_accrued_time': True,
'maximum_leave': 1,
})],
})
@@ -553,16 +572,17 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
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
with freeze_time('2021-09-01'):
# 1 accrual with 2 levels and level transition immediately
with freeze_time('2021-12-15'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 1,
'action_with_unused_accruals': 'lost',
})],
@@ -580,25 +600,26 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
# Reset the cron's lastcall
accrual_cron = self.env['ir.cron'].sudo().env.ref('hr_holidays.hr_leave_allocation_cron_accrual')
accrual_cron.lastcall = datetime.date(2021, 9, 1)
accrual_cron.lastcall = datetime.date(2021, 12, 15)
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, 'The number of days should be reset')
self.assertEqual(allocation.number_of_days, 0, 'The number of days should be reset')
def test_unused_accrual_postponed(self):
# 1 accrual with 2 levels and level transition after
# This also tests retroactivity
with freeze_time('2021-09-01'):
with freeze_time('2021-12-15'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 25,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'all',
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
@@ -614,22 +635,24 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
# Reset the cron's lastcall
accrual_cron = self.env['ir.cron'].sudo().env.ref('hr_holidays.hr_leave_allocation_cron_accrual')
accrual_cron.lastcall = datetime.date(2021, 9, 1)
accrual_cron.lastcall = datetime.date(2021, 12, 15)
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 25, 'The maximum number of days should be reached and kept.')
def test_unused_accrual_postponed_2(self):
with freeze_time('2021-01-01'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 2,
'added_value_type': 'days',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 100,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'maximum',
'postpone_max_days': 10,
})],
})
@@ -649,22 +672,24 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_cron.lastcall = datetime.date(2021, 1, 1)
with freeze_time('2023-01-26'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 6, 'The maximum number of days should be reached and kept.')
self.assertEqual(allocation.number_of_days, 4, 'The maximum number of days should be reached and kept.')
def test_unused_accrual_postponed_limit(self):
# 1 accrual with 2 levels and level transition after
# This also tests retroactivity
with freeze_time('2021-09-01'):
with freeze_time('2021-12-15'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 25,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'maximum',
'postpone_max_days': 15,
})],
})
@@ -681,22 +706,24 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
# Reset the cron's lastcall
accrual_cron = self.env['ir.cron'].sudo().env.ref('hr_holidays.hr_leave_allocation_cron_accrual')
accrual_cron.lastcall = datetime.date(2021, 9, 1)
accrual_cron.lastcall = datetime.date(2021, 12, 15)
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 16, 'The maximum number of days should be reached and kept.')
def test_unused_accrual_postponed_limit_2(self):
with freeze_time('2021-01-01'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 15,
'added_value_type': 'days',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 100,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'maximum',
'postpone_max_days': 7,
})],
})
@@ -716,7 +743,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_cron.lastcall = datetime.date(2021, 1, 1)
with freeze_time('2023-01-26'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 22, 'The maximum number of days should be reached and kept.')
self.assertEqual(allocation.number_of_days, 7, 'The maximum number of days should be reached and kept.')
def test_accrual_skipped_period(self):
# Test that when an allocation is made in the past and the second level is technically reached
@@ -724,21 +751,22 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 15,
'added_value_type': 'days',
'frequency': 'biyearly',
'cap_accrued_time': True,
'maximum_leave': 100,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'all',
}), (0, 0, {
'start_count': 4,
'start_type': 'month',
'added_value': 10,
'added_value_type': 'days',
'frequency': 'biyearly',
'cap_accrued_time': True,
'maximum_leave': 500,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'all',
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
@@ -760,31 +788,32 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 2,
'start_type': 'month',
'added_value': 3,
'added_value_type': 'days',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 3,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'all',
'first_day': 31,
}), (0, 0, {
'start_count': 3,
'start_type': 'month',
'added_value': 6,
'added_value_type': 'days',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 6,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'all',
'first_day': 31,
}), (0, 0, {
'start_count': 4,
'start_type': 'month',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 100,
'action_with_unused_accruals': 'postponed',
'action_with_unused_accruals': 'all',
'first_day': 31,
})],
})
@@ -814,11 +843,12 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'name': 'Accrual Plan For Test',
'level_ids': [
(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 12,
'action_with_unused_accruals': 'lost',
}),
@@ -826,8 +856,8 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'start_count': 1,
'start_type': 'year',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'monthly',
'cap_accrued_time': True,
'maximum_leave': 12,
'action_with_unused_accruals': 'lost',
}),
@@ -846,18 +876,20 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_validate()
with freeze_time('2022-4-4'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 4, "Invalid number of days")
self.assertEqual(allocation.number_of_days, 3, "Invalid number of days")
def test_accrual_lost_first_january(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'level_ids': [
(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 3,
'added_value_type': 'days',
'frequency': 'yearly',
'cap_accrued_time': True,
'maximum_leave': 12,
'action_with_unused_accruals': 'lost',
})
@@ -882,11 +914,12 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'cap_accrued_time': True,
'maximum_leave': 5,
})],
})
@@ -900,7 +933,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'date_from': '2021-09-03',
})
with freeze_time(datetime.date(2021, 10, 3)):
with freeze_time("2021-10-3"):
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -911,12 +944,12 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'maximum_leave': 0,
'cap_accrued_time': False,
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
@@ -929,7 +962,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'date_from': '2021-09-03',
})
with freeze_time(datetime.date(2021, 10, 3)):
with freeze_time("2021-10-3"):
allocation.action_confirm()
allocation.action_validate()
allocation._update_accrual()
@@ -940,12 +973,13 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'weekly',
'week_day': 'mon',
'cap_accrued_time': True,
'maximum_leave': 5,
})],
})
@@ -961,7 +995,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_confirm()
allocation.action_validate()
with freeze_time(datetime.date(2022, 3, 2)):
with freeze_time("2022-3-2"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 5, "Maximum of 5 days accrued")
@@ -975,20 +1009,21 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
})
leave.action_validate()
with freeze_time(datetime.date(2022, 6, 1)):
with freeze_time("2022-6-1"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 10, "Should accrue 5 additional days")
def test_accrual_leaves_taken_maximum_hours(self):
def test_aaaccrual_leaves_taken_maximum_hours(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'added_value_type': 'hour',
'start_count': 0,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'hours',
'frequency': 'weekly',
'week_day': 'mon',
'cap_accrued_time': True,
'maximum_leave': 10,
})],
})
@@ -996,7 +1031,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'holiday_status_id': self.leave_type_hour.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2022-01-01',
@@ -1012,7 +1047,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
leave = self.env['hr.leave'].create({
'name': 'leave',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'holiday_status_id': self.leave_type_hour.id,
'date_from': '2022-03-07 00:00:00',
'date_to': '2022-03-07 23:59:59'
})
@@ -1021,3 +1056,287 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
with freeze_time(datetime.date(2022, 6, 1)):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 18 / self.hours_per_day, "Should accrue 8 additional hours")
def test_accrual_period_start(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'end',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 1,
'frequency': 'weekly',
'week_day': 'mon',
'cap_accrued_time': True,
'maximum_leave': 5,
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2023-04-24',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2023-4-24"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 0, "Should accrue 0 days, because the period is not done yet.")
accrual_plan.accrued_gain_time = 'start'
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2023-04-24',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2023-4-24"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, "Should accrue 1 day, at the start of the period.")
def test_aaaccrual_period_start_multiple_runs(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 1.5,
'frequency': 'monthly',
'first_day': 13,
'cap_accrued_time': True,
'maximum_leave': 15,
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2023-04-13',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2023-4-13"):
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 1.5, 2)
with freeze_time("2023-9-13"):
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 9, 2)
def test_accrual_period_start_level_transfer(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'level_ids': [
(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 1,
'frequency': 'weekly',
'week_day': 'wed',
'cap_accrued_time': True,
'maximum_leave': 10,
}),
(0, 0, {
'start_count': 3,
'start_type': 'month',
'added_value': 2,
'frequency': 'weekly',
'week_day': 'wed',
'cap_accrued_time': True,
'maximum_leave': 5,
})
],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2023-04-26',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2023-4-26"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 1, "Should accrue 1 day, at the start of the period.")
with freeze_time("2023-7-5"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 10, "Should accrue 10 days, days received, but not over limit.")
# first wednesday at the second level
with freeze_time("2023-8-02"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 5, "Should accrue 5 days, after level transfer 10 are cut to 5")
def test_accrual_carryover_at_allocation(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'carryover_date': 'allocation',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 1,
'frequency': 'monthly',
'first_day': 27,
'cap_accrued_time': False,
'action_with_unused_accruals': 'lost',
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2023-04-26',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2023-4-26"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 0, "Should accrue 0 days, days are added on 27th.")
with freeze_time("2023-4-27"):
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 1.03, 2, "Should accrue 1 day, days are added on 27th.")
with freeze_time("2023-12-27"):
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 9.03, 2, "Should accrue 9 day, after 8 months.")
with freeze_time("2024-04-26"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 0, "Allocations not lost on 1st of January, but on allocation date.")
with freeze_time("2024-04-27"):
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 1.03, 2, "Allocations lost, then 1 accrued.")
def test_accrual_carryover_at_other(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'start',
'carryover_date': 'other',
'carryover_day': 20,
'carryover_month': 'apr',
'level_ids': [(0, 0, {
'added_value_type': 'day',
'start_count': 0,
'start_type': 'day',
'added_value': 10,
'frequency': 'monthly',
'first_day': 11,
'cap_accrued_time': False,
'action_with_unused_accruals': 'maximum',
'postpone_max_days': 69,
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 0,
'allocation_type': 'accrual',
'date_from': '2023-04-20',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2024-04-20"):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 69, "Carryover at other date, level's maximum leave is 69.")
def test_accrual_carrover_other_period_end_multi_level(self):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'accrued_gain_time': 'end',
'carryover_date': 'other',
'carryover_day': 5,
'carryover_month': 'jun',
'level_ids': [
(0, 0, {
'added_value_type': 'day',
'start_count': 5,
'start_type': 'day',
'added_value': 1,
'frequency': 'monthly',
'first_day': 9,
'cap_accrued_time': True,
'maximum_leave': 15,
'action_with_unused_accruals': 'maximum',
'postpone_max_days': 13,
}),
(0, 0, {
'start_count': 9,
'start_type': 'month',
'added_value': 2,
'frequency': 'biyearly',
'first_month_day': 17,
'first_month': 'feb',
'second_month_day': 29,
'second_month': 'oct',
'cap_accrued_time': True,
'maximum_leave': 10,
'action_with_unused_accruals': 'maximum',
'postpone_max_days': 20,
}),
(0, 0, {
'start_count': 17,
'start_type': 'month',
'added_value': 12,
'frequency': 'yearly',
'yearly_month': 'jul',
'yearly_day': 15,
'cap_accrued_time': True,
'maximum_leave': 21,
'action_with_unused_accruals': 'lost',
}),
],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 9,
'allocation_type': 'accrual',
'date_from': '2023-04-4',
})
allocation.action_confirm()
allocation.action_validate()
with freeze_time("2026-08-01"):
allocation._update_accrual()
self.assertAlmostEqual(allocation.number_of_days, 1.315, 3)
@@ -6,49 +6,73 @@
<field name="arch" type="xml">
<form string="Accrual Level">
<sheet>
<group>
<field name="sequence" invisible="1" force_save="1"/>
<label for="start_count"/>
<div class="o_col">
<div class="o_row">
<field name="start_count"/>
<field name="start_type" nolabel="1"/><label for="start_type" string="after allocation date" class="o_form_label"/>
</div>
<group name="accrue" col="1" width="800px">
<div class="o_td_label">
<label for="added_value" string="Employee accrue"/>
</div>
<field name="is_based_on_worked_time"/>
<label for="added_value"/>
<div class="o_col">
<div class="o_row">
<field name="added_value"/>
<field name="added_value_type" nolabel="1"/>
</div>
<div>
<field name="can_modify_value_type" invisible="1"/>
<field name="added_value" widget="FloatWithoutTrailingZeros" style="width: 4rem" class="me-1"/>
<field name="added_value_type" style="width: 3.4rem" nolabel="1" readonly="not can_modify_value_type"/>
</div>
<label for="frequency"/>
<div class="o_col">
<field name="frequency" class="w-25"/>
<div class="o_row" invisible="frequency != 'weekly'">
<label for="week_day" string="on"/><field name="week_day"/>
</div>
<div class="o_row" 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" 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" 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" invisible="frequency != 'yearly'">
<label for="yearly_day_display" string="on the"/><field name="yearly_day_display" required="1"/> of <field name="yearly_month" class="w-25"/>
</div>
<div style="width: 5rem"/>
<div name="daily" invisible="frequency != 'daily'">
<field name="frequency" style="width: 5rem"/>
</div>
<label for="maximum_leave"/>
<div class="o_col">
<div class="o_row">
<field name="maximum_leave"/>
<!-- 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 name="weekly" invisible="frequency != 'weekly'">
<field name="frequency" style="width: 4.5rem;"/>
<label for="week_day" string="on" class="me-1"/><field name="week_day" style="width: 6.6rem"/>
</div>
<div name="monthly" invisible="frequency != 'monthly'">
<field name="frequency" style="width: 4.5rem"/>
<label for="first_day_display" string="on the" class="me-1"/>
<field name="first_day_display" required="1" style="width: 4rem"/>
of the month
</div>
<div name="bimonthly" invisible="frequency != 'bimonthly'" style="width: 100%">
<field name="frequency" style="width: 7rem"/>
<label for="first_day_display" string="on the" class="me-1"/><field name="first_day_display" required="1" style="width: 4rem"/>
<label for="second_day_display" string="and on the" class="me-1"/><field name="second_day_display" required="1" style="width: 4.1rem"/>
of the month
</div>
<div name="biyearly" invisible="frequency != 'biyearly'">
<field name="frequency" style="width: 6rem"/>
<label for="first_month_day_display" string="on the" class="me-1"/>
<field name="first_month_day_display" required="1" style="width: 4.1rem"/>of <field name="first_month" required="1" style="width: 4.55rem"/>
<label for="second_month_day_display" string="and" class="me-1"/>
<field name="second_month_day_display" required="1" style="width: 4.1rem"/>of <field name="second_month" required="1" style="width: 5.4rem"/>
</div>
<div name="yearly" invisible="frequency != 'yearly'">
<field name="frequency" style="width: 4rem"/>
<label for="yearly_day_display" string="on the" class="me-1"/>
<field name="yearly_day_display" required="1" style="width: 4rem"/> of <field name="yearly_month" required="1" style="width: 5.4rem"/>
</div>
</group>
<group name="maximum_leave">
<field name="cap_accrued_time" style="width: 9.35rem"/>
<div style="width: 9.35rem"/>
<div invisible="not cap_accrued_time">
<field name="maximum_leave" style="width: 5rem" widget="FloatWithoutTrailingZeros"/>
<field name="added_value_type" style="white-space: nowrap;width: 5rem"/>
</div>
</group>
<group name="milestone">
<div class="o_td_label">
<label for="start_count" string="Milestone reached"/>
</div>
<div>
<field name="start_count" style="width: 2rem"/>
<field name="start_type" style="width: 4.75rem"/> after allocation start date
</div>
<div class="o_td_label">
<label for="action_with_unused_accruals" string="Carry over"/>
</div>
<div>
<field name="action_with_unused_accruals" class="o_light_label" style="font-weight: 0" widget="radio"/><br/>
<div invisible="action_with_unused_accruals != 'maximum'">
<label for="postpone_max_days" string="Up to"/>
<field name="postpone_max_days" style="width: 3rem"/>
<field name="added_value_type" nolabel="1" readonly="1" force_save="1" style="width: 5rem"/>
</div>
</div>
<field name="action_with_unused_accruals" class="w-25"/>
@@ -78,8 +102,8 @@
<field name="active" invisible="1"/>
<field name="show_transition_mode" invisible="1" />
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_accrual_plan_employees" type="object" class="oe_stat_button" icon="fa-users">
<div class="oe_button_box" name="button_box" invisible="not id" >
<button name="action_open_accrual_plan_employees" type="object" class="oe_stat_button" icon="fa-users" invisible="employees_count == 0">
<field name="employees_count" widget="statinfo"/>
</button>
</div>
@@ -87,15 +111,18 @@
<group>
<group>
<field name="name"/>
<field name="accrued_gain_time" widget="radio"/>
<field name="carryover_date" widget="radio"/>
<label for="carryover_month" string="Carry-Over Date" invisible="carryover_date != 'other'"/>
<div name="carryover" initially="carryover_date != 'other'">
<field name="carryover_day" invisible="1"/>
<field name="carryover_day_display" required="1" style="width: 4.75rem;"/> of
<field name="carryover_month" required="1" style="width: 6rem;"/>
</div>
</group>
<group>
<field name="company_id" invisible="1"/>
<field name="time_off_type_id"/>
</group>
<group>
<field name="is_based_on_worked_time" readonly="accrued_gain_time == 'start'"/>
<field name="transition_mode" widget="radio" invisible="not show_transition_mode"/>
</group>
<group>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
</group>
@@ -109,8 +136,9 @@
</p>
</div>
<field name="level_ids" mode="kanban" nolabel="1"
class="o_hr_holidays_plan_level_container o_hr_holidays_plan_level_hierarchy"
add-label="Add a new level"
class="o_hr_holidays_plan_level_container"
widget="accrual_levels_one2many"
add-label="New Milestone"
>
<kanban default_order="sequence">
<field name="sequence"/>
@@ -130,12 +158,17 @@
<field name="yearly_month"/>
<field name="maximum_leave"/>
<field name="action_with_unused_accruals"/>
<field name="is_based_on_worked_time"/>
<field name="cap_accrued_time"/>
<templates>
<div t-name="kanban-box" class="border-0 bg-transparent">
<div class="o_hr_holidays_body oe_kanban_global_click">
<div class="o_hr_holidays_timeline text-center">
Level <span class="o_hr_holidays_plan_level_level"/>
<t t-if="record.start_count.value > 0">
after <t t-esc="record.start_count.value"/> <t t-esc="record.start_type.value"/>
</t>
<t t-else="">
initially
</t>
</div>
<t t-if="!read_only_mode">
<a type="edit" t-attf-class="oe_kanban_action oe_kanban_action_a text-black">
@@ -149,42 +182,52 @@
</div>
<t t-name="level_content">
<div class="o_hr_holidays_card">
<div class="content">
<div>
<t t-if="record.start_count.value > 0">
Starts <field name="start_count" widget="integer"/> <field name="start_type"/> after allocation start date
</t>
<t t-else="">
Starts immediately after allocation start date
</t>
<div class="content container" style="width: 560px;">
<div class="row w-100">
<div class="pe-0 me-0" style="width: 6rem;">
<field name="added_value" widget="FloatWithoutTrailingZeros"/> <field name="added_value_type"/>,
</div>
<div class="col-auto m-0 p-0">
<field name="frequency" class="ms-1"/>
<t t-if="record.frequency.raw_value === 'weekly'">
on <field name="week_day"/>
</t>
<t t-elif="record.frequency.raw_value === 'monthly'">
on the <field name="first_day"/> day of the month
</t>
<t t-elif="record.frequency.raw_value === 'bimonthly'">
on the <field name="first_day"/> and on the <field name="second_day"/> days of the months
</t>
<t t-elif="record.frequency.raw_value === 'biyearly'">
on the <field name="first_month_day"/> <field name="first_month"/> and on the <field name="second_month_day"/> <field name="second_month"/>
</t>
<t t-elif="record.frequency.raw_value === 'yearly'">
on <field name="yearly_day"/> <field name="yearly_month"/>
</t>
</div>
</div>
<div>
Adds <field name="added_value" widget="float_without_trailing_zeros"/> <field name="added_value_type"/>
<t t-if="record.is_based_on_worked_time.raw_value">(based on worked time)</t>
<div class="row text-muted">
<div class="pe-0 me-0" style="width: 6rem;">
Cap:
</div>
<div class="col-3 m-0 ps-1">
<t t-if="record.cap_accrued_time.value &amp;&amp; record.maximum_leave.value > 0">
<field name="maximum_leave" widget="FloatWithoutTrailingZeros"/> <field name="added_value_type"/>
</t>
<t t-else="">
Unlimited
</t>
</div>
</div>
<div>
<field name="frequency"/>
<t t-if="record.frequency.raw_value == 'weekly'">
on <field name="week_day"/>
</t>
<t t-elif="record.frequency.raw_value === 'monthly'">
on the <field name="first_day"/> day of the month
</t>
<t t-elif="record.frequency.raw_value === 'bimonthly'">
on the <field name="first_day"/> and on the <field name="second_day"/> days of the months
</t>
<t t-elif="record.frequency.raw_value === 'biyearly'">
on the <field name="first_month_day"/> <field name="first_month"/> and on the <field name="second_month_day"/> <field name="second_month"/>
</t>
<t t-elif="record.frequency.raw_value === 'yearly'">
on <field name="yearly_day"/> <field name="yearly_month"/>
</t>
</div>
<div t-if="record.maximum_leave.value">
Limit of <field name="maximum_leave" widget="float_without_trailing_zeros"/> <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>
<div class="row text-muted">
<div class="pe-0 me-0" style="width: 6rem;">
Carry over:
</div>
<div class="col-3 m-0 ps-1">
<t t-if="record.action_with_unused_accruals.raw_value === 'all'">all</t>
<t t-elif="record.action_with_unused_accruals.raw_value == 'maximum'">up to <field name="postpone_max_days" /> <t t-esc="record.added_value_type.raw_value" /></t>
<t t-else="">no</t>
</div>
</div>
</div>
</div>
@@ -70,7 +70,7 @@
<field name="can_approve" invisible="1"/>
<field name="holiday_type" invisible="1" readonly="state not in ['confirm', 'draft']"/>
<header>
<button string="Confirm" name="action_confirm" invisible="state != 'draft'" type="object" class="oe_highlight"/>
<button string="Confirm" name="action_confirm" invisible="state != 'draft' and 'allocation_type' != 'accrual' and not employee_id" type="object" class="oe_highlight"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,validate"/>
</header>
<sheet>
@@ -101,19 +101,18 @@
<field name="date_to" widget="date" nolabel="1" placeholder="No Limit" readonly="1" invisible="allocation_type == 'regular' and state not in ('draft', 'confirm')"/>
<div id="no_limit_label" class="oe_read_only" invisible="not id or date_to or state not in ('draft', 'confirm')">No limit</div>
</div>
<field name="number_of_days" invisible="1"/>
<div class="o_td_label">
<label for="number_of_days_display" string="Amount"
<label for="number_of_days_display" string="Allocation"
readonly="allocation_type == 'accrual' and state not in ('draft', 'confirm')"/>
</div>
<div name="duration_display">
<field name="number_of_days_display" nolabel="1" style="width: 5rem;"
invisible="type_request_unit == 'hour'"
readonly="type_request_unit == 'hour' or state not in ('draft', 'confirm') or allocation_type == 'accrual'"/>
readonly="state not in ('draft', 'confirm')"/>
<field name="number_of_hours_display" nolabel="1" style="width: 5rem;"
invisible="type_request_unit != 'hour'"
readonly="type_request_unit != 'hour' or state not in ('draft', 'confirm') or allocation_type == 'accrual'"/>
readonly="state not in ('draft', 'confirm')"/>
<span class="ml8" invisible="type_request_unit == 'hour'">Days</span>
<span class="ml8" invisible="type_request_unit != 'hour'">Hours</span>
</div>