[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:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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):
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user