[IMP] hr_holidays: add accrual allocations support

We need to support the accrual allocation for leaves.
This is a special type of allocations that adds some leaves to the
allocation every X amount of time worked.

This kind of allocations is especially used in the USA, but can also be
used in other contexts (eg: In Belgium we could allocate 20 days per
year for the legal leaves)

These allocations have support for :

  * A max balance limit
  * Going in negative (if set on the leave_type and the manager
    approves the leaves)
  * Removing the unpaid leaves from the quantity of days worked
  * Setting an end date after which it does not allocate anymore leaves

Task #46114
Closes #22292
This commit is contained in:
RomainLibert
2018-06-08 14:41:05 +02:00
parent d49ab568a2
commit a8aa0bcbbe
15 changed files with 508 additions and 42 deletions
+3 -1
View File
@@ -29,6 +29,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
'data/report_paperformat.xml',
'data/hr_holidays_data.xml',
'data/mail_data.xml',
'data/ir_cron_data.xml',
'security/hr_holidays_security.xml',
'security/ir.model.access.csv',
@@ -37,7 +38,6 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
'views/hr_leave_views.xml',
'views/hr_leave_type_views.xml',
'views/hr_leave_allocation_views.xml',
'views/hr_views.xml',
'views/mail_activity_views.xml',
'views/res_users_views.xml',
@@ -47,6 +47,8 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
'report/hr_holidays_templates.xml',
'report/hr_holidays_reports.xml',
'report/hr_leave_reports.xml',
'views/hr_views.xml',
],
'demo': [
'data/hr_holidays_demo.xml',
+1 -1
View File
@@ -30,8 +30,8 @@
<!--Unpaid Leave -->
<record id="holiday_status_unpaid" model="hr.leave.type">
<field name="name">Unpaid</field>
<field name="limit" eval="True"/>
<field name="color_name">brown</field>
<field name="unpaid" eval="True"/>
</record>
</data>
</odoo>
+19 -10
View File
@@ -3,19 +3,28 @@
<data noupdate="1">
<record id="hr_holiday_status_hl" model="hr.leave.type">
<field name="name">Overtime Compensation</field>
<field name="limit" eval="False"/>
<field name="color_name">blue</field>
<field name="validity_start" eval="time.strftime('%Y-01-01')"/>
<field name="validity_stop" eval="time.strftime('%Y-12-31')"/>
<field name="request_unit">hour</field>
<field name="name">Overtime Compensation</field>
<field name="limit" eval="False"/>
<field name="color_name">blue</field>
<field name="validity_start" eval="time.strftime('%Y-01-01')"/>
<field name="validity_stop" eval="time.strftime('%Y-12-31')"/>
<field name="request_unit">hour</field>
</record>
<record id="hr_holiday_status_dv" model="hr.leave.type">
<field name="name">Parental Leaves</field>
<field name="limit" eval="True"/>
<field name="color_name">brown</field>
<field name="validation_type">both</field>
<field name="name">Parental Leaves</field>
<field name="limit" eval="True"/>
<field name="color_name">brown</field>
<field name="validation_type">both</field>
</record>
<record id="holiday_status_cla" model="hr.leave.type">
<field name="name" eval="'Paid Time Off ' + time.strftime('%Y')"/>
<field name="limit" eval="False"/>
<field name="color_name">wheat</field>
<field name="validity_start" eval="time.strftime('%Y-01-01')"/>
<field name="validity_stop" eval="time.strftime('%Y-12-31')"/>
<field name="accrual" eval="True"/>
</record>
<record id="hr_holidays_employee1_allocation_cl" model="hr.leave.allocation">
+14
View File
@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='UTF-8' ?>
<odoo>
<record id="hr_leave_allocation_cron_accrual" model="ir.cron">
<field name="name">Accrual Leave: Updates the number of leaves</field>
<field name="model_id" ref="model_hr_leave_allocation"/>
<field name="state">code</field>
<field name="code">model._update_accrual()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
</record>
</odoo>
+4 -3
View File
@@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round
class Department(models.Model):
@@ -112,7 +113,7 @@ class Employee(models.Model):
def _compute_remaining_leaves(self):
remaining = self._get_remaining_leaves()
for employee in self:
employee.remaining_leaves = remaining.get(employee.id, 0.0)
employee.remaining_leaves = float_round(remaining.get(employee.id, 0.0), precision_digits=2)
@api.multi
def _inverse_remaining_leaves(self):
@@ -132,7 +133,7 @@ class Employee(models.Model):
if not status:
continue
# if a status is found, then compute remaing leave for current employee
difference = employee.remaining_leaves - actual_remaining.get(employee.id, 0)
difference = float_round(employee.remaining_leaves - actual_remaining.get(employee.id, 0), precision_digits=2)
if difference > 0:
leave = self.env['hr.leave.allocation'].create({
'name': _('Allocation for %s') % employee.name,
@@ -179,7 +180,7 @@ class Employee(models.Model):
], fields=['number_of_days', 'employee_id'], groupby=['employee_id'])
mapping = dict([(leave['employee_id'][0], leave['number_of_days']) for leave in all_leaves])
for employee in self:
employee.leaves_count = mapping.get(employee.id)
employee.leaves_count = float_round(mapping.get(employee.id, 0), precision_digits=2)
@api.multi
def _compute_show_leaves(self):
+2 -2
View File
@@ -44,7 +44,7 @@ class HolidaysRequest(models.Model):
def default_get(self, fields_list):
defaults = super(HolidaysRequest, self).default_get(fields_list)
LeaveType = self.env['hr.leave.type'].with_context(employee_id=defaults.get('employee_id'), default_date_from=defaults.get('date_from'))
LeaveType = self.env['hr.leave.type'].with_context(employee_id=defaults.get('employee_id'), default_date_from=defaults.get('date_from', fields.Datetime.now()))
lt = LeaveType.search([('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])])
defaults['holiday_status_id'] = lt[0].id if len(lt) > 0 else defaults.get('holiday_status_id')
@@ -325,7 +325,7 @@ class HolidaysRequest(models.Model):
@api.constrains('state', 'number_of_days_temp', 'holiday_status_id')
def _check_holidays(self):
for holiday in self:
if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.limit:
if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.limit or holiday.holiday_status_id.negative:
continue
leave_days = holiday.holiday_status_id.get_days(holiday.employee_id.id)[holiday.holiday_status_id.id]
if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \
@@ -5,10 +5,17 @@
import logging
import pytz
from datetime import datetime, time
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.translate import _
from odoo.addons.resource.models.resource import HOURS_PER_DAY
_logger = logging.getLogger(__name__)
@@ -54,6 +61,10 @@ class HolidaysAllocation(models.Model):
"\nThe status is 'To Approve', when leave request is confirmed by user." +
"\nThe status is 'Refused', when leave request is refused by manager." +
"\nThe status is 'Approved', when leave request is approved by manager.")
date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False,
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange')
date_to = fields.Datetime('End Date', readonly=True, copy=False,
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, track_visibility='onchange')
holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", required=True, readonly=True,
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
domain="[('valid', '=', True), ('employee_applicability', 'in', ['allocation', 'both']), ('limit', '=', False)]", default=_default_holiday_status_id)
@@ -86,6 +97,19 @@ class HolidaysAllocation(models.Model):
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')
accrual = fields.Boolean("Accrual", related='holiday_status_id.accrual', store=True, readonly=True)
number_per_interval = fields.Float("Number of unit per interval", readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
interval_number = fields.Integer("Number of unit between two intervals", readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
unit_per_interval = fields.Selection([
('hours', 'Hour(s)'),
('days', 'Day(s)')
], string="Unit of time added at each interval", default='hours', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
interval_unit = fields.Selection([
('weeks', 'Week(s)'),
('months', 'Month(s)'),
('years', 'Year(s)')
], string="Unit of time between two intervals", default='weeks', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True)
_sql_constraints = [
('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL) or (holiday_type='department' AND department_id IS NOT NULL) )",
@@ -93,14 +117,80 @@ class HolidaysAllocation(models.Model):
('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."),
]
@api.model
def _update_accrual(self):
"""
Method called by the cron task in order to increment the number_of_days when
necessary.
"""
today = fields.Date.from_string(fields.Date.today())
holidays = self.search([('accrual', '=', True), ('state', '=', 'validate'),
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
for holiday in holidays:
values = {}
delta = relativedelta(days=0)
if holiday.interval_unit == 'weeks':
delta = relativedelta(weeks=holiday.interval_number)
if holiday.interval_unit == 'months':
delta = relativedelta(months=holiday.interval_number)
if holiday.interval_unit == 'years':
delta = relativedelta(years=holiday.interval_number)
values['nextcall'] = (holiday.nextcall if holiday.nextcall else today) + delta
period_start = datetime.combine(today, time(0, 0, 0)) - delta
period_end = datetime.combine(today, time(0, 0, 0))
# We have to check when the employee has been created
# in order to not allocate him/her too much leaves
creation_date = fields.Datetime.from_string(holiday.employee_id.create_date)
# If employee is created after the period, we cancel the computation
if period_end <= creation_date:
holiday.write(values)
continue
# If employee created during the period, taking the date at which he has been created
if period_start <= creation_date:
period_start = creation_date
worked = holiday.employee_id.get_work_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days']
left = holiday.employee_id.get_leave_days_data(period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])['days']
prorata = worked / (left + worked) if worked else 0
days_to_give = holiday.number_per_interval
if holiday.unit_per_interval == 'hours':
# As we encode everything in days in the database we need to convert
# the number of hours into days for this we use the
# mean number of hours set on the employee's calendar
days_to_give = days_to_give / holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY
values['number_of_days_temp'] = holiday.number_of_days_temp + days_to_give * prorata
if holiday.holiday_status_id.balance_limit > 0:
values['number_of_days_temp'] = min(values['number_of_days_temp'], holiday.holiday_status_id.balance_limit)
values['number_of_hours'] = values['number_of_days_temp'] * holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY
holiday.write(values)
@api.multi
@api.depends('number_of_days_temp', 'type_request_unit', 'number_of_hours')
@api.depends('number_of_days_temp', 'type_request_unit', 'number_of_hours', 'holiday_status_id', 'employee_id')
def _compute_number_of_days(self):
for holiday in self:
number_of_days = holiday.number_of_days_temp
if holiday.type_request_unit == 'hour':
holiday.number_of_days = holiday.number_of_hours / holiday.employee_id.resource_calendar_id.hours_per_day
else:
holiday.number_of_days = holiday.number_of_days_temp
# In this case we need the number of days to reflect the number of hours taken
number_of_days = holiday.number_of_hours / holiday.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY
if holiday.holiday_status_id.balance_limit > 0:
number_of_days = min(number_of_days, holiday.holiday_status_id.balance_limit)
holiday.number_of_days = number_of_days
@api.multi
def _compute_can_reset(self):
@@ -149,6 +239,23 @@ class HolidaysAllocation(models.Model):
def _onchange_number_of_hours(self):
self.number_of_days_temp = self.number_of_hours / self.employee_id.resource_calendar_id.hours_per_day
@api.onchange('holiday_status_id')
def _onchange_holiday_status_id(self):
self.date_to = self.holiday_status_id.validity_stop
if self.accrual:
self.number_of_days_temp = 0
if self.holiday_status_id.request_unit == 'hour':
self.unit_per_interval = 'hours'
else:
self.unit_per_interval = 'days'
else:
self.interval_number = 0
self.interval_unit = 'weeks'
self.number_per_interval = 0
self.unit_per_interval = 'hours'
####################################################
# ORM Overrides methods
####################################################
@@ -185,6 +292,8 @@ class HolidaysAllocation(models.Model):
@api.model
def create(self, values):
""" Override to avoid automatic logging of creation """
if values.get('accrual', False):
values['date_from'] = fields.Datetime.now()
employee_id = values.get('employee_id', False)
if not values.get('department_id'):
values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
+40 -6
View File
@@ -8,6 +8,7 @@ import logging
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.translate import _
from odoo.tools.float_utils import float_round
_logger = logging.getLogger(__name__)
@@ -76,29 +77,61 @@ class HolidaysType(models.Model):
valid = fields.Boolean(compute='_compute_valid', search='_search_valid', help='This indicates if it is still possible to use this type of leave')
time_type = fields.Selection([('leave', 'Leave'), ('other', 'Other')], default='leave', string="Kind of Leave",
help="Whether this should be computed as a holiday or as work time (eg: formation)")
request_unit = fields.Selection([('day', 'Day'),
('half', 'Half-day'),
('hour', 'Hours')], default='day', string='Take Leaves in', required=True)
accrual = fields.Boolean('Is Accrual', default=False,
help='This option forces this type of leave to be allocated accrually')
unpaid = fields.Boolean('Is Unpaid', default=False)
negative = fields.Boolean('Allow Negative', help="This option allows to take more leaves than allocated")
balance_limit = fields.Float('Max Balance Limit', default=0, help="The maximum quantity of allocated days on this allocation, zero meaning infinite amount")
_sql_constraints = [
('no_negative_balance_limit', "CHECK(balance_limit >= 0)", "The max balance limit cannot be negative"),
('no_accrual_unpaid', 'CHECK(NOT (accrual AND unpaid))', "A leave type cannot be accrual and considered as unpaid leaves")
]
@api.multi
@api.constrains('validity_start', 'validity_stop')
def _check_validity_dates(self):
for htype in self:
if htype.validity_start and htype.validity_stop and \
htype.validity_start > htype.validity_stop:
for leave_type in self:
if leave_type.validity_start and leave_type.validity_stop and \
leave_type.validity_start > leave_type.validity_stop:
raise ValidationError(_("End of validity period should be greater than start of validity period"))
@api.multi
@api.constrains('balance_limit', 'accrual')
def _check_balance_limit(self):
for leave_type in self:
if not leave_type.accrual and leave_type.balance_limit > 0:
raise ValidationError(_("Max balance limit can only be set for accrual leaves"))
@api.onchange('limit')
def _onchange_limit(self):
if self.limit:
self.employee_applicability = 'leave'
self.accrual = False
@api.onchange('accrual')
def _onchange_accrual(self):
if self.accrual:
self.limit = False
self.employee_applicability = 'both'
else:
self.negative = False
self.balance_limit = 0
@api.multi
@api.depends('validity_start', 'validity_stop', 'limit')
def _compute_valid(self):
dt = self._context.get('default_date_from', fields.Date.today())
dt = self._context.get('default_date_from') or fields.Datetime.now()
for holiday_type in self:
if holiday_type.validity_start and holiday_type.validity_stop:
@@ -109,7 +142,8 @@ class HolidaysType(models.Model):
holiday_type.valid = True
def _search_valid(self, operator, value):
dt = self._context.get('default_date_from', fields.Date.today()) or fields.Date.today()
dt = self._context.get('default_date_from') or fields.Datetime.now()
signs = ['>=', '<='] if operator == '=' else ['<=', '>=']
return ['|', ('validity_stop', operator, False), '&',
@@ -181,7 +215,7 @@ class HolidaysType(models.Model):
if not record.limit:
name = "%(name)s (%(count)s)" % {
'name': name,
'count': _('%g remaining out of %g') % (record.virtual_remaining_leaves or 0.0, record.max_leaves or 0.0)
'count': _('%g remaining out of %g') % (float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, float_round(record.max_leaves, precision_digits=2) or 0.0)
}
res.append((record.id, name))
return res
@@ -150,7 +150,7 @@
<field name="context">{'search_default_employee_id': [active_id], 'default_employee_id': active_id, 'search_default_group_type': 1,
'search_default_year': 1}</field>
<field name="domain">[('holiday_type','=','employee'), ('holiday_status_id.limit', '=', False), ('state','!=', 'refuse')]</field>
<field name="search_view_id" ref="view_hr_leave_allocation_filter"/>
<field name="search_view_id" ref="view_hr_holidays_filter_report"/>
</record>
<menuitem
+1
View File
@@ -3,3 +3,4 @@
from . import test_holidays_flow
from . import test_hr_leave_type
from . import test_accrual_allocations
@@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.tools import mute_logger
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
class TestAccrualAllocations(TestHrHolidaysBase):
def setUp(self):
super(TestAccrualAllocations, self).setUp()
# Make sure we have the rights to create, validate and delete the leaves, leave types and allocations
LeaveType = self.env['hr.leave.type'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
self.accrual_type = LeaveType.create({
'name': 'accrual',
'limit': False,
'accrual': True,
})
self.unpaid_type = LeaveType.create({
'name': 'unpaid',
'limit': True,
'unpaid': True,
})
self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00')
self.set_employee_create_date(self.employee_hruser_id, '2010-02-03 00:00:00')
def set_employee_create_date(self, id, newdate):
""" This method is a hack in order to be able to define/redefine the create_date
of the employees.
This is done in SQL because ORM does not allow to write onto the create_date field.
"""
self.env.cr.execute("""
UPDATE
hr_employee
SET create_date = '%s'
WHERE id = %s
""" % (newdate, id))
def test_accrual_base_no_leaves(self):
"""
Test if we can allocate some leaves accrually to an employee
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
alloc_0 = Allocation.create({
'name': 'Accrual allocation for employee',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 1,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'weeks',
})
alloc_0.action_approve()
Allocation._update_accrual()
self.assertEqual(alloc_0.number_of_days, 1, 'Employee should have been allocated one leave day')
def test_accrual_base_leaves(self):
"""
Test if the accrual allocation take the unpaid leaves into account
when allocating leaves
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
Leave = self.env['hr.leave'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
alloc_0 = Allocation.create({
'name': 'Accrual allocation for employee with leaves',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 1,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'weeks',
})
alloc_0.action_approve()
employee = self.env['hr.employee'].browse(self.employee_hruser_id)
# Getting the previous work date
df = employee.resource_calendar_id.plan_days(-2, fields.Datetime.from_string(fields.Datetime.now())).date()
leave_0 = Leave.create({
'name': 'Leave for hruser',
'employee_id': self.employee_hruser_id,
'holiday_status_id': self.unpaid_type.id,
'date_from': datetime.combine(df, time(0, 0, 0)),
'date_to': datetime.combine(df + relativedelta(days=1), time(0, 0, 0)),
'number_of_days_temp': 1,
})
leave_0.action_approve()
Allocation._update_accrual()
self.assertEqual(alloc_0.number_of_days, .8, 'As employee took some unpaid leaves last week, he should be allocated only .8 days')
def test_accrual_many(self):
"""
Test different configuration of accrual allocations
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
alloc_0 = Allocation.create({
'name': '1 day per 2 weeks',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 1,
'interval_number': 2,
'unit_per_interval': 'days',
'interval_unit': 'weeks',
})
alloc_1 = Allocation.create({
'name': '4 hours per week',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 4,
'interval_number': 1,
'unit_per_interval': 'hours',
'interval_unit': 'weeks',
})
alloc_2 = Allocation.create({
'name': '2 day per 1 month',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 2,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'months',
})
alloc_3 = Allocation.create({
'name': '20 days per year',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 20,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'years',
})
(alloc_0 | alloc_1 | alloc_2 | alloc_3).action_approve()
Allocation._update_accrual()
self.assertEqual(alloc_0.number_of_days, 1)
self.assertEqual(alloc_1.number_of_days, .5)
self.assertEqual(alloc_2.number_of_days, 2)
self.assertEqual(alloc_3.number_of_days, 20)
def test_accrual_new_employee(self):
"""
Test if accrual allocation takes into account the creation date
of an employee
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
self.set_employee_create_date(self.employee_emp_id, fields.Datetime.now())
alloc_0 = Allocation.create({
'name': 'one shot one kill',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 1,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'weeks',
})
alloc_0.action_approve()
Allocation._update_accrual()
self.assertEqual(alloc_0.number_of_days, 0, 'Employee is new he should not get any accrual leaves')
def test_accrual_multi(self):
"""
Test if the cron does not allocate leaves every time it's called but only
when necessary
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
alloc_0 = Allocation.create({
'name': '2 days per week',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 1,
'interval_number': 2,
'unit_per_interval': 'days',
'interval_unit': 'weeks',
})
alloc_0.action_approve()
Allocation._update_accrual()
Allocation._update_accrual()
self.assertEqual(alloc_0.number_of_days, 1, 'Cron only allocates 1 days every two weeks')
def test_accrual_validation(self):
"""
Test if cron does not allocate past it's validity date
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
alloc_0 = Allocation.create({
'name': '20 days per year',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.accrual_type.id,
'number_of_days_temp': 0,
'number_per_interval': 20,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'years',
'date_to': fields.Datetime.from_string('2015-02-03 00:00:00'),
})
alloc_0.action_approve()
Allocation._update_accrual()
self.assertEqual(alloc_0.number_of_days, 0, 'Cron validity passed, should not allocate any leave')
def test_accrual_balance_limit(self):
"""
Test if accrual allocation does not allocate more than the balance limit
"""
Allocation = self.env['hr.leave.allocation'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
LeaveType = self.env['hr.leave.type'].sudo(self.user_hrmanager_id).with_context(tracking_disable=True)
accrual_max_five = LeaveType.create({
'name': 'max 5 days',
'limit': False,
'accrual': True,
'balance_limit': 5,
})
allocation = Allocation.create({
'name': 'accrual 5 max',
'employee_id': self.employee_emp_id,
'holiday_status_id': accrual_max_five.id,
'number_of_days_temp': 0,
'number_per_interval': 6,
'interval_number': 1,
'unit_per_interval': 'days',
'interval_unit': 'weeks',
})
allocation.action_approve()
Allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 5, 'Should have allocated only 5 days as balance limit is 5')
@@ -7,8 +7,10 @@
<field name="arch" type="xml">
<search string="Search allocations">
<field name="name"/>
<field name="accrual"/>
<filter domain="[('state','in',('confirm', 'validate1'))]" string="To Approve" name="approve"/>
<filter string="Approved Allocations" domain="[('state', '=', 'validate')]" name="validated"/>
<filter string="Accrual Allocations" domain="[('accrual', '=', True)]" name="accrual_allocation"/>
<separator/>
<filter name="active_types" string="Active Types" domain="[('holiday_status_id.active', '=', True)]" help="Filters only on allocations that belong to an leave type that is 'active' (active field is True)"/>
<separator/>
@@ -52,6 +54,7 @@
<form string="Allocation Request">
<field name="can_reset" invisible="1"/>
<field name="can_approve" invisible="1"/>
<field name="accrual" invisible="1"/>
<header>
<button string="Confirm" name="action_confirm" states="draft" type="object" class="oe_highlight"/>
<button string="Approve" name="action_approve" states="confirm" type="object" groups="hr_holidays.group_hr_holidays_user" class="oe_highlight"
@@ -72,12 +75,12 @@
<field name="type_request_unit" invisible="1"/>
<field name="name" attrs="{'readonly':[('state','!=','draft'),('state','!=','confirm')]}"/>
<field name="holiday_status_id" context="{'employee_id':employee_id}"/>
<label string="Duration"/>
<label string="Duration" attrs="{'invisible': [('accrual', '=', True)]}"/>
<div>
<div attrs="{'invisible': [('type_request_unit', '=', 'hour')]}">
<div attrs="{'invisible': ['|', ('type_request_unit', '=', 'hour'), ('accrual', '=', True)]}">
<field name="number_of_days_temp" class="oe_inline" attrs="{'readonly': [('state', '=', 'validate')]}"/> days
</div>
<div attrs="{'invisible': [('type_request_unit', '!=', 'hour')]}">
<div attrs="{'invisible': ['|', ('type_request_unit', '!=', 'hour'), ('accrual', '=', True)]}">
<field name="number_of_hours" class="oe_inline" attrs="{'readonly': [('state', '=', 'validate')]}"/> hours
</div>
</div>
@@ -88,6 +91,22 @@
<field name="category_id" attrs="{'required':[('holiday_type','=','category')], 'invisible':[('holiday_type','!=','category')]}"/>
<field name="department_id" groups="hr_holidays.group_hr_holidays_user" attrs="{'invisible':[('holiday_type','=','category')]}"/>
</group>
<group attrs="{'invisible': [('accrual', '=', True)]}">
<label for="number_of_days_temp" string="Duration"/>
<div class="o_row">
<field name="number_of_days_temp"/> days
</div>
</group>
<group attrs="{'invisible': [('accrual', '=', False)]}">
<field name="date_to" string="Run Until" help="If no value set, runs indefinitely" class="oe_inline"/>
<div class="o_row" colspan="2">
Add <field name="number_per_interval" attrs="{'required': [('accrual', '=', True)]}"/>
<field name="unit_per_interval" attrs="{'required': [('accrual', '=', True)]}"/> of leaves every
<field name="interval_number" attrs="{'required': [('accrual', '=', True)]}"/>
<field name="interval_unit" attrs="{'required': [('accrual', '=', True)]}"/>
</div>
</group>
<field name="notes" nolabel="1" colspan="4" placeholder="Add a reason..."/>
</group>
</sheet>
@@ -110,7 +129,7 @@
<field name="department_id" invisible="1"/>
<field name="holiday_status_id"/>
<field name="name"/>
<field name="number_of_days" string="Allocated Days" sum="Remaining Days"/>
<field name="number_of_days" string="Allocated Days" sum="Total allocated days"/>
<field name="message_needaction" invisible="1"/>
<field name="state"/>
</tree>
@@ -126,7 +145,7 @@
<field name="employee_id"/>
<field name="holiday_status_id"/>
<field name="name"/>
<field name="number_of_days" string="Allocated Days" sum="Remaining Days"/>
<field name="number_of_days" string="Allocated Days" sum="Total allocated days"/>
<field name="message_needaction" invisible="1"/>
<field name="state"/>
</tree>
@@ -28,18 +28,22 @@
<group>
<group name="description" string="Description">
<field name="name"/>
</group>
<group name="leave_request" string="Leave Request">
<field name="time_type" groups="base.group_no_one"/>
<field name="limit"/>
<field name="sequence" attrs="{'invisible': [('limit', '=', False)]}" groups="base.group_no_one"/>
<field name="unpaid"/>
<field name="limit" attrs="{'invisible': [('accrual', '=', True)]}"/>
<field name="request_unit"/>
<field name="sequence" attrs="{'invisible': [('limit', '!=', False)]}" groups="base.group_no_one"/>
<field name="time_type" groups="base.group_no_one"/>
</group>
<group name="allocation_request" string="Allocation" attrs="{'invisible': [('unpaid', '=', True)]}">
<field name="accrual"/>
<field name="negative" attrs="{'invisible': [('accrual', '=', False)]}"/>
<field name="balance_limit" attrs="{'invisible': [('accrual', '=', False)]}"/>
</group>
<group name="validation" string="Validation">
<field name="validation_type" widget="radio"/>
</group>
<group name="visibility" string="Visibility">
<field name="employee_applicability" widget="radio" attrs="{'readonly': [('limit', '=', True)]}" force_save="1"/>
<field name="employee_applicability" widget="radio" attrs="{'readonly': ['|', ('limit', '=', True), ('accrual', '=', True)]}" force_save="1"/>
</group>
<group name="calendar" string="Calendar">
<field name="categ_id"/>
+2 -2
View File
@@ -170,7 +170,7 @@
<field name="name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="number_of_days" string="Number of Days" sum="Remaining Days"/>
<field name="number_of_days" string="Number of Days" sum="Total leaves"/>
<field name="state"/>
<field name="payslip_status" widget="toggle_button" options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}' groups="hr_holidays.group_hr_holidays_manager"/>
<field name="category_id" invisible="1"/>
@@ -335,7 +335,7 @@
<field name="holiday_type"/>
<field name="holiday_status_id"/>
<field name="name" readonly="1"/>
<field name="number_of_days_temp" string="Allocated Days" sum="Remaining Days"/>
<field name="number_of_days_temp" string="Allocated Days" sum="Total leaves"/>
<field name="state"/>
</tree>
</field>
+1 -1
View File
@@ -11,7 +11,7 @@
<field name="context">{'search_default_employee_id': [active_id], 'default_employee_id': active_id, 'search_default_group_type': 1,
'search_default_year': 1}</field>
<field name="domain">[('holiday_type','=','employee'), ('holiday_status_id.limit', '=', False), ('state','!=', 'refuse')]</field>
<field name="search_view_id" ref="view_hr_leave_allocation_filter"/>
<field name="search_view_id" ref="view_hr_holidays_filter_report"/>
</record>
<record id="hr_holidays_leaves_assign_tree_view" model="ir.ui.view">