From 8293267dd4de659163e55a0c054759452d8ec533 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Fri, 26 Oct 2018 15:42:16 +0300 Subject: [PATCH] [12.0][ADD] hr_holidays_accrual: Advanced accrual leave allocations --- hr_holidays_accrual/README.rst | 73 +++ hr_holidays_accrual/__init__.py | 4 + hr_holidays_accrual/__manifest__.py | 30 ++ hr_holidays_accrual/models/__init__.py | 3 + .../models/hr_leave_allocation.py | 347 +++++++++++++++ hr_holidays_accrual/readme/CONTRIBUTORS.rst | 1 + hr_holidays_accrual/readme/DESCRIPTION.rst | 1 + hr_holidays_accrual/readme/USAGE.rst | 3 + .../static/description/index.html | 419 ++++++++++++++++++ hr_holidays_accrual/tests/__init__.py | 3 + .../tests/test_hr_holidays_accrual.py | 347 +++++++++++++++ .../views/hr_leave_allocation.xml | 99 +++++ hr_holidays_accrual/wizard/__init__.py | 3 + ...e_allocation_accrual_balance_calculator.py | 55 +++ ..._allocation_accrual_balance_calculator.xml | 45 ++ 15 files changed, 1433 insertions(+) create mode 100644 hr_holidays_accrual/README.rst create mode 100644 hr_holidays_accrual/__init__.py create mode 100644 hr_holidays_accrual/__manifest__.py create mode 100644 hr_holidays_accrual/models/__init__.py create mode 100644 hr_holidays_accrual/models/hr_leave_allocation.py create mode 100644 hr_holidays_accrual/readme/CONTRIBUTORS.rst create mode 100644 hr_holidays_accrual/readme/DESCRIPTION.rst create mode 100644 hr_holidays_accrual/readme/USAGE.rst create mode 100644 hr_holidays_accrual/static/description/index.html create mode 100644 hr_holidays_accrual/tests/__init__.py create mode 100644 hr_holidays_accrual/tests/test_hr_holidays_accrual.py create mode 100644 hr_holidays_accrual/views/hr_leave_allocation.xml create mode 100644 hr_holidays_accrual/wizard/__init__.py create mode 100644 hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.py create mode 100644 hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.xml diff --git a/hr_holidays_accrual/README.rst b/hr_holidays_accrual/README.rst new file mode 100644 index 000000000000..7dc53bba3581 --- /dev/null +++ b/hr_holidays_accrual/README.rst @@ -0,0 +1,73 @@ +=========================== +Advanced Accrual Allocation +=========================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr-lightgray.png?logo=github + :target: https://github.com/OCA/hr/tree/12.0/hr_holidays_accrual + :alt: OCA/hr +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-12-0/hr-12-0-hr_holidays_accrual + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/116/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides advanced accrual leaves allocation. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Braibean Apps + +Contributors +~~~~~~~~~~~~ + +* Alexey Pelykh + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/hr `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_holidays_accrual/__init__.py b/hr_holidays_accrual/__init__.py new file mode 100644 index 000000000000..25aa51eca4cc --- /dev/null +++ b/hr_holidays_accrual/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizard diff --git a/hr_holidays_accrual/__manifest__.py b/hr_holidays_accrual/__manifest__.py new file mode 100644 index 000000000000..8230940acc5e --- /dev/null +++ b/hr_holidays_accrual/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright (C) 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'Advanced Accrual Allocation', + 'version': '12.0.1.0.0', + 'category': 'Human Resources', + 'website': 'https://github.com/OCA/hr', + 'author': + 'Braibean Apps, ' + 'Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'installable': True, + 'application': False, + 'summary': 'Advanced accrual leaves allocation', + 'depends': [ + 'hr', + 'hr_holidays', + 'hr_employee_service', + ], + 'external_dependencies': { + 'python': [ + 'dateutil', + ], + }, + 'data': [ + 'wizard/hr_leave_allocation_accrual_balance_calculator.xml', + 'views/hr_leave_allocation.xml', + ], +} diff --git a/hr_holidays_accrual/models/__init__.py b/hr_holidays_accrual/models/__init__.py new file mode 100644 index 000000000000..262cc28cfcda --- /dev/null +++ b/hr_holidays_accrual/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import hr_leave_allocation diff --git a/hr_holidays_accrual/models/hr_leave_allocation.py b/hr_holidays_accrual/models/hr_leave_allocation.py new file mode 100644 index 000000000000..d64db3a86769 --- /dev/null +++ b/hr_holidays_accrual/models/hr_leave_allocation.py @@ -0,0 +1,347 @@ +# Copyright (C) 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from math import ceil +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo import models, fields, api +from odoo.addons.resource.models.resource import HOURS_PER_DAY + +_logger = logging.getLogger(__name__) + + +class HrLeaveAllocation(models.Model): + _inherit = 'hr.leave.allocation' + + limit_accrued_days = fields.Boolean( + 'Limit Number of Days Accrued', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + help=( + 'Limit total maximum number of days accrued in one accrual' + ' period.' + ), + ) + max_accrued_days = fields.Float( + 'Max Number of Days Accrued', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + help='Total maximum number of days accrued in one accrual period.', + ) + limit_carryover_days = fields.Boolean( + 'Limit Number of Days to Carryover', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + help=( + 'Limit total maximum number of days to carryover to next accrual' + ' period.' + ), + ) + max_carryover_days = fields.Float( + 'Max Number of Days to Carryover', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + help=( + 'Total maximum number of days to carryover to next accrual period.' + ), + ) + limit_accumulated_days = fields.Boolean( + 'Limit Total Balance', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + help='Limit total maximum number of days that can be accumulated.', + ) + max_accumulated_days = fields.Float( + 'Total Balance Limit', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + help='Total maximum number of days that can be accumulated.', + ) + accrual_method = fields.Selection( + selection=[ + ('prorate', 'Prorate'), + ('period_start', 'At the beginning of the period'), + ('period_end', 'At the end of the period'), + ], + string='Accrual Method', + default='prorate', + track_visibility='onchange', + readonly=True, + states={ + 'draft': [('readonly', False)], + 'confirm': [('readonly', False)] + }, + ) + accrual_limit = fields.Integer( + compute='_compute_accrual_limit', + readonly=True, + states={}, + ) + number_per_interval = fields.Float( + 'Allocation per Accrual Period', + default=20.0, + track_visibility='onchange', + help=( + 'Allocation accrued per Accrual Period, measured in' + ' Allocation Units' + ), + ) + interval_number = fields.Integer( + 'Accrual Period Duration', + default=1, + track_visibility='onchange', + help=( + 'Duration of a single accrual period, measured in' + ' Accrual Period Units' + ), + ) + unit_per_interval = fields.Selection( + string='Allocation Unit', + default='days', + track_visibility='onchange', + help='Units in which Allocation per Accrual Period is defined', + ) + interval_unit = fields.Selection( + string='Accrual Period Unit', + default='years', + track_visibility='onchange', + help='Units in which Accrual Period Duration is defined', + ) + + @api.onchange('holiday_type') + def _onchange_holiday_type(self): + if self.holiday_type != 'employee': + self.accrual = False + + @api.multi + @api.depends('max_accumulated_days') + def _compute_accrual_limit(self): + for allocation in self: + allocation.accrual_limit = ceil(allocation.max_accumulated_days) + + @api.model + def action_update_accrual_allocations(self): + _logger.info('Trigerring accrual leave allocations update') + self._update_accrual() + + @api.model + def create(self, values): + if 'holiday_type' in values and values['holiday_type'] != 'employee': + values['accrual'] = False + return super().create(values) + + @api.multi + def write(self, values): + if 'holiday_type' in values and values['holiday_type'] != 'employee': + values['accrual'] = False + return super().write(values) + + def _update_accrual(self): + super()._update_accrual() + + allocations = self.search([ + ('accrual', '=', True), + ('state', '=', 'validate'), + ('holiday_type', '=', 'employee') + ]) + + for allocation in allocations: + number_of_days = allocation._calculate_accrued_amount( + datetime.today() + ) + allocation.write({ + 'number_of_days': number_of_days + }) + + def _calculate_accrued_amount(self, as_of_datetime): + period = self._get_accrual_period() + date_from = self._get_date_from() + date_to = self._get_date_to() or as_of_datetime + + _logger.info( + ( + 'Calculating "%s" leave allocation for employee "%s"' + ' between %s and %s with %s period as of %s' + ), + self.holiday_status_id.name, + self.employee_id.name, + date_from, + date_to, + period, + as_of_datetime, + ) + + number_of_days = 0.0 + while date_from < date_to: + if self.limit_carryover_days: + number_of_days = min( + number_of_days, + self.max_carryover_days + ) + + period_start = date_from + period_end = min(period_start + period, date_to) + + days_worked = self.employee_id.get_work_days_data( + period_start, + period_end, + domain=[ + ('holiday_id.holiday_status_id.unpaid', '=', True), + ('time_type', '=', 'leave') + ] + )['days'] + paid_days_off = self.employee_id.get_leave_days_data( + period_start, + period_end, + domain=[ + ('holiday_id.holiday_status_id.unpaid', '!=', True), + ('time_type', '=', 'leave') + ] + )['days'] + workable_days = self.employee_id.get_work_days_data( + period_start, + period_start + period, + )['days'] + unpaid_days_off = self.employee_id.get_leave_days_data( + period_start, + period_end, + domain=[ + ('holiday_id.holiday_status_id.unpaid', '=', True), + ('time_type', '=', 'leave') + ] + )['days'] + total_workable_days = workable_days + unpaid_days_off + + _logger.info( + ( + 'Employee "%s" / allocation %s (%s - %s):' + ' %s days worked, %s paid days off, %s unpaid days off,' + ' %s workable days, %s total days' + ), + self.employee_id.name, + self.holiday_status_id.name, + period_start, + period_end, + days_worked, + paid_days_off, + unpaid_days_off, + workable_days, + total_workable_days, + ) + + prorata = ( + (days_worked + paid_days_off) / total_workable_days + ) if total_workable_days else 0 + days_to_accrue = self._get_days_to_accrue( + period_start, + period, + as_of_datetime, + prorata + ) + + if self.limit_accrued_days: + days_to_accrue = min( + days_to_accrue, + self.max_accrued_days + ) + number_of_days += days_to_accrue + + date_from += period + + if self.limit_accumulated_days: + number_of_days = min( + number_of_days, + self.max_accumulated_days + ) + + _logger.info( + '%s days of "%s" leave allocated to employee "%s"', + number_of_days, + self.holiday_status_id.name, + self.employee_id.name, + ) + + return number_of_days + + def _get_accrual_period(self): + if self.interval_unit == 'weeks': + return relativedelta(weeks=self.interval_number) + if self.interval_unit == 'months': + return relativedelta(months=self.interval_number) + if self.interval_unit == 'years': + return relativedelta(years=self.interval_number) + + def _get_date_from(self): + if self.date_from: + return self.date_from + + if self.employee_id.service_start_date: + return datetime.combine( + self.employee_id.service_start_date, + datetime.min.time() + ) + + return self.employee_id.create_date + + def _get_date_to(self): + if self.date_to: + return self.date_to + + if self.employee_id.service_termination_date: + return datetime.combine( + self.employee_id.service_termination_date, + datetime.min.time() + ) + + return None + + def _get_days_to_accrue( + self, + period_start, + period, + as_of_datetime, + prorata + ): + days_to_accrue = self.number_per_interval + if self.unit_per_interval == 'hours': + days_to_accrue /= ( + self.employee_id.resource_calendar_id.hours_per_day + ) or HOURS_PER_DAY + + if self.accrual_method == 'period_start': + if period_start < as_of_datetime: + return days_to_accrue + elif self.accrual_method == 'period_end': + if period_start + period < as_of_datetime: + return days_to_accrue + elif self.accrual_method == 'prorate': + return days_to_accrue * prorata + + return 0.0 diff --git a/hr_holidays_accrual/readme/CONTRIBUTORS.rst b/hr_holidays_accrual/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..1c6a35a1e355 --- /dev/null +++ b/hr_holidays_accrual/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Alexey Pelykh diff --git a/hr_holidays_accrual/readme/DESCRIPTION.rst b/hr_holidays_accrual/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..2f09debe85d8 --- /dev/null +++ b/hr_holidays_accrual/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module provides advanced accrual leaves allocation. diff --git a/hr_holidays_accrual/readme/USAGE.rst b/hr_holidays_accrual/readme/USAGE.rst new file mode 100644 index 000000000000..91966082ed5a --- /dev/null +++ b/hr_holidays_accrual/readme/USAGE.rst @@ -0,0 +1,3 @@ +This module is an almost-replacement of accrual feature from the `hr_holidays` +module and its features are configured in the same manner under the Leave Types +menu. diff --git a/hr_holidays_accrual/static/description/index.html b/hr_holidays_accrual/static/description/index.html new file mode 100644 index 000000000000..43e2aa9b0ca6 --- /dev/null +++ b/hr_holidays_accrual/static/description/index.html @@ -0,0 +1,419 @@ + + + + + + +Advanced Accrual Allocation + + + +
+

Advanced Accrual Allocation

+ + +

Beta License: AGPL-3 OCA/hr Translate me on Weblate Try me on Runbot

+

This module provides advanced accrual leaves allocation.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Braibean Apps
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/hr project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/hr_holidays_accrual/tests/__init__.py b/hr_holidays_accrual/tests/__init__.py new file mode 100644 index 000000000000..9a685609e2dd --- /dev/null +++ b/hr_holidays_accrual/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_hr_holidays_accrual diff --git a/hr_holidays_accrual/tests/test_hr_holidays_accrual.py b/hr_holidays_accrual/tests/test_hr_holidays_accrual.py new file mode 100644 index 000000000000..59216e4edcf0 --- /dev/null +++ b/hr_holidays_accrual/tests/test_hr_holidays_accrual.py @@ -0,0 +1,347 @@ +# Copyright (C) 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields +from odoo.tests import common + +from math import fabs +from dateutil.relativedelta import relativedelta + + +class TestHrHolidaysAccrual(common.TransactionCase): + + def setUp(self): + super().setUp() + + self.today = fields.Date.today() + self.now = fields.Datetime.now() + self.Employee = self.env['hr.employee'] + self.SudoEmployee = self.Employee.sudo() + self.LeaveType = self.env['hr.leave.type'] + self.SudoLeaveType = self.LeaveType.sudo() + self.LeaveAllocation = self.env['hr.leave.allocation'] + self.SudoLeaveAllocation = self.LeaveAllocation.sudo() + self.Leave = self.env['hr.leave'] + self.SudoLeave = self.Leave.sudo() + + def test_allocation_1(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #1', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #1', + 'service_hire_date': ( + self.today - relativedelta(years=3) + ), + 'service_start_date': ( + self.today - relativedelta(years=3) + ), + 'service_termination_date': ( + self.today - relativedelta(years=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(allocation.number_of_days, 40.0) + + def test_allocation_2(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #2', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #2', + 'service_hire_date': ( + self.today - relativedelta(years=1) + relativedelta(days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=1) + relativedelta(days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'accrual_method': 'period_end', + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(allocation.number_of_days, 0.0) + + def test_allocation_3(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #3', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #3', + 'service_hire_date': ( + self.today - relativedelta(years=1, days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=1, days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'accrual_method': 'period_end', + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 20) + + def test_allocation_4(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #4', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #4', + 'service_hire_date': ( + self.today - relativedelta(years=1, days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=1, days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'limit_carryover_days': True, + 'max_carryover_days': 5.0, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 5) + + def test_allocation_5(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #5', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #5', + 'service_hire_date': ( + self.today - relativedelta(years=1, days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=1, days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'limit_carryover_days': True, + 'max_carryover_days': 0.0, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 0) + + def test_allocation_6(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #6', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #6', + 'service_hire_date': ( + self.today - relativedelta(years=10, days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=10, days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'limit_carryover_days': True, + 'max_carryover_days': 5.0, + 'limit_accumulated_days': True, + 'max_accumulated_days': 20.0, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 5) + + def test_allocation_7(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #7', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #7', + 'service_hire_date': ( + self.today - relativedelta(years=10, days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=10, days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'limit_carryover_days': True, + 'max_carryover_days': 5.0, + 'limit_accumulated_days': True, + 'max_accumulated_days': 1.0, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 1) + + def test_allocation_8(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #8', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #8', + 'service_hire_date': ( + self.today - relativedelta(years=1, days=1) + ), + 'service_start_date': ( + self.today - relativedelta(years=1, days=1) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + }) + unpaid_leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #8 (unpaid)', + 'allocation_type': 'no', + 'unpaid': True, + }) + unpaid_from = self.now - relativedelta(years=1) + unpaid_to = self.now - relativedelta(months=6) + unpaid_leave = self.SudoLeave.create({ + 'name': 'Leave #8 (unpaid)', + 'employee_id': employee.id, + 'holiday_status_id': unpaid_leave_type.id, + 'date_from': unpaid_from, + 'date_to': unpaid_to, + 'number_of_days': ( + fabs((unpaid_from - unpaid_to).total_seconds() / 86400.0) + ), + }) + unpaid_leave.action_approve() + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 10) + + def test_allocation_9(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #9', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #9', + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(allocation.number_of_days, 0) + + def test_allocation_10(self): + leave_type = self.SudoLeaveType.create({ + 'name': 'Leave Type #10', + 'allocation_type': 'fixed', + }) + employee = self.SudoEmployee.create({ + 'name': 'Employee #10', + 'service_start_date': ( + self.today - relativedelta(years=10) + ), + }) + allocation = self.SudoLeaveAllocation.create({ + 'holiday_type': 'employee', + 'employee_id': employee.id, + 'holiday_status_id': leave_type.id, + 'state': 'validate', + 'accrual': True, + 'limit_carryover_days': True, + 'max_carryover_days': 5.0, + 'limit_accrued_days': True, + 'max_accrued_days': 20.0, + 'limit_accumulated_days': True, + 'max_accumulated_days': 20.0, + }) + + # HACK: https://github.com/odoo/odoo/issues/28306 + allocation.date_from = None + + self.SudoLeaveAllocation._update_accrual() + + self.assertEqual(int(allocation.number_of_days), 5) diff --git a/hr_holidays_accrual/views/hr_leave_allocation.xml b/hr_holidays_accrual/views/hr_leave_allocation.xml new file mode 100644 index 000000000000..3796fe81c8bc --- /dev/null +++ b/hr_holidays_accrual/views/hr_leave_allocation.xml @@ -0,0 +1,99 @@ + + + + + + hr.leave.allocation.view.form.manager.inherit.contract + hr.leave.allocation + + +
+
+ + + { + 'invisible': [('holiday_type', '!=', 'employee')] + } + + + + + { + 'invisible': [('accrual', '=', True)] + } + + + + + { + 'invisible': [('accrual', '=', True)] + } + + + + + + + + + + + +
+
+ + + Recalculate Accrual Allocations + ir.actions.server + + + code + + action = model.action_update_accrual_allocations() + + + + + +
diff --git a/hr_holidays_accrual/wizard/__init__.py b/hr_holidays_accrual/wizard/__init__.py new file mode 100644 index 000000000000..f24aa6e5e551 --- /dev/null +++ b/hr_holidays_accrual/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import hr_leave_allocation_accrual_balance_calculator diff --git a/hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.py b/hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.py new file mode 100644 index 000000000000..f2be1d8fa1f4 --- /dev/null +++ b/hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.py @@ -0,0 +1,55 @@ +# Copyright (C) 2018 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class HrLeaveAllocationAccrualBalanceCalculator(models.TransientModel): + _name = 'hr.leave.allocation.accrual.balance.calculator' + _description = 'HR Leave Allocation Accrual Balance Calculator' + + date = fields.Date( + 'Date', + ) + + balance = fields.Float( + 'Balance', + compute='_compute_balance', + readonly=True, + ) + + @api.multi + @api.depends('date') + def _compute_balance(self): + HrLeaveAllocation = self.env['hr.leave.allocation'] + allocation_id = self.env.context.get('active_id') + + for calculator in self: + calculator.balance = allocation_id +# payslips = self.env['hr.payslip'] +# [data] = self.read() +# active_id = self.env.context.get('active_id') +# if active_id: +# [run_data] = self.env['hr.payslip.run'].browse(active_id).read(['date_start', 'date_end', 'credit_note']) +# from_date = run_data.get('date_start') +# to_date = run_data.get('date_end') +# if not data['employee_ids']: +# raise UserError(_("You must select employee(s) to generate payslip(s).")) +# for employee in self.env['hr.employee'].browse(data['employee_ids']): +# slip_data = self.env['hr.payslip'].onchange_employee_id(from_date, to_date, employee.id, contract_id=False) +# res = { +# 'employee_id': employee.id, +# 'name': slip_data['value'].get('name'), +# 'struct_id': slip_data['value'].get('struct_id'), +# 'contract_id': slip_data['value'].get('contract_id'), +# 'payslip_run_id': active_id, +# 'input_line_ids': [(0, 0, x) for x in slip_data['value'].get('input_line_ids')], +# 'worked_days_line_ids': [(0, 0, x) for x in slip_data['value'].get('worked_days_line_ids')], +# 'date_from': from_date, +# 'date_to': to_date, +# 'credit_note': run_data.get('credit_note'), +# 'company_id': employee.company_id.id, +# } +# payslips += self.env['hr.payslip'].create(res) +# payslips.compute_sheet() +# return {'type': 'ir.actions.act_window_close'} diff --git a/hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.xml b/hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.xml new file mode 100644 index 000000000000..55c0cb3461a0 --- /dev/null +++ b/hr_holidays_accrual/wizard/hr_leave_allocation_accrual_balance_calculator.xml @@ -0,0 +1,45 @@ + + + + + + hr_leave_allocation_accrual_balance_calculator + hr.leave.allocation.accrual.balance.calculator + +
+ + + This wizard will calculate leave allocation balance as of date selected. + + + + + + + + + + + +
+
+
+
+
+ + + Accrual Balance Calculator + hr.leave.allocation.accrual.balance.calculator + form + tree,form + + new + + +