diff options
| author | RomainLibert <rli@odoo.com> | 2018-01-03 09:15:03 +0100 |
|---|---|---|
| committer | RomainLibert <rli@odoo.com> | 2018-06-08 14:41:05 +0200 |
| commit | a8aa0bcbbe48babe877e08d81e5bdcc9e5779fdc (patch) | |
| tree | 93ad69c78531fb2f54b75f5063c92fff81a82cc4 /addons | |
| parent | d49ab568a2cc5231ac12267a9e74eaf16ca45f9e (diff) | |
[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
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/hr_holidays/__manifest__.py | 4 | ||||
| -rw-r--r-- | addons/hr_holidays/data/hr_holidays_data.xml | 2 | ||||
| -rw-r--r-- | addons/hr_holidays/data/hr_holidays_demo.xml | 29 | ||||
| -rw-r--r-- | addons/hr_holidays/data/ir_cron_data.xml | 14 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr.py | 7 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_leave.py | 4 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_leave_allocation.py | 117 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_leave_type.py | 46 | ||||
| -rw-r--r-- | addons/hr_holidays/report/hr_leave_reports.xml | 2 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/hr_holidays/tests/test_accrual_allocations.py | 273 | ||||
| -rw-r--r-- | addons/hr_holidays/views/hr_leave_allocation_views.xml | 29 | ||||
| -rw-r--r-- | addons/hr_holidays/views/hr_leave_type_views.xml | 16 | ||||
| -rw-r--r-- | addons/hr_holidays/views/hr_leave_views.xml | 4 | ||||
| -rw-r--r-- | addons/hr_holidays/views/hr_views.xml | 2 |
15 files changed, 508 insertions, 42 deletions
diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index 9bf1da4a2ed..946035e1309 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -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', diff --git a/addons/hr_holidays/data/hr_holidays_data.xml b/addons/hr_holidays/data/hr_holidays_data.xml index a08a0eddb3d..20ae0403cd6 100644 --- a/addons/hr_holidays/data/hr_holidays_data.xml +++ b/addons/hr_holidays/data/hr_holidays_data.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> diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index f3c7407534a..ae743c74f0d 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -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"> diff --git a/addons/hr_holidays/data/ir_cron_data.xml b/addons/hr_holidays/data/ir_cron_data.xml new file mode 100644 index 00000000000..a162d5c51f5 --- /dev/null +++ b/addons/hr_holidays/data/ir_cron_data.xml @@ -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> diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index 17b7cd93785..d59458c49c3 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -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): diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index f0359744ebb..9be317b69ca 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -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 \ diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index e5b63e4215d..32c656a77a9 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -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}) diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index c31e0355d9f..17d5c9b190f 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -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 diff --git a/addons/hr_holidays/report/hr_leave_reports.xml b/addons/hr_holidays/report/hr_leave_reports.xml index 5afe663eaf5..4694fe392dd 100644 --- a/addons/hr_holidays/report/hr_leave_reports.xml +++ b/addons/hr_holidays/report/hr_leave_reports.xml @@ -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 diff --git a/addons/hr_holidays/tests/__init__.py b/addons/hr_holidays/tests/__init__.py index 1300f22a091..7bc7f93f42f 100644 --- a/addons/hr_holidays/tests/__init__.py +++ b/addons/hr_holidays/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_holidays_flow from . import test_hr_leave_type +from . import test_accrual_allocations diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py new file mode 100644 index 00000000000..3a3d1150f3a --- /dev/null +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -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') diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index c39e12c34d7..d5fc5abe820 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -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> diff --git a/addons/hr_holidays/views/hr_leave_type_views.xml b/addons/hr_holidays/views/hr_leave_type_views.xml index d4c23139c75..d1ddf3b79f6 100644 --- a/addons/hr_holidays/views/hr_leave_type_views.xml +++ b/addons/hr_holidays/views/hr_leave_type_views.xml @@ -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"/> diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 658bf4bac4e..3308f6f645e 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -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> diff --git a/addons/hr_holidays/views/hr_views.xml b/addons/hr_holidays/views/hr_views.xml index 159681154d3..bfdc2e4f51e 100644 --- a/addons/hr_holidays/views/hr_views.xml +++ b/addons/hr_holidays/views/hr_views.xml @@ -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"> |
