From 24629e56e7422fec69f0aad27d93f8bc000b0402 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 29 Apr 2022 19:12:30 +0000 Subject: [PATCH] [IMP] l10n_pe_hr_payroll: ir_5ta_cat catch up and refactors --- l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml | 49 ++++--------- l10n_pe_hr_payroll/models/hr_payslip.py | 61 +--------------- .../models/pe_payroll_config.py | 2 + l10n_pe_hr_payroll/models/rules/__init__.py | 4 ++ l10n_pe_hr_payroll/models/rules/general.py | 58 +++++++++++++++ l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py | 71 +++++++++++++++++++ l10n_pe_hr_payroll/tests/common.py | 2 + .../views/pe_payroll_config_views.xml | 2 + 8 files changed, 156 insertions(+), 93 deletions(-) create mode 100644 l10n_pe_hr_payroll/models/rules/__init__.py create mode 100644 l10n_pe_hr_payroll/models/rules/general.py create mode 100644 l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py diff --git a/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml index b0a11869..b2ad02c1 100644 --- a/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml +++ b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml @@ -31,6 +31,18 @@ + + EE: IR 5ta Cat. Bonificación Extraordinaria (Ley 29351) + ee_ir_5ta_cat_ley_29351 + + + + 9.0 + + + + + @@ -39,42 +51,9 @@ EE: PE IR 5TA Cat. EE_PE_IR_5TA_CAT python - result = categories.GROSS + result, _ = ir_5ta_cat(payslip, categories, worked_days, inputs, BASIC) code - -pay_periods_in_year = payslip.pay_periods_in_year -uit = payslip.rule_parameter('pe_uit') - -basic_wage = BASIC -wage_period = categories.GROSS -period_additional_wage = max(wage_period - basic_wage, 0.0) -wage_year = basic_wage * pay_periods_in_year -# additional 2 months (July and December) -wage_2 = wage_year * (1/6) -wage_3 = wage_2 * 0.09 # TODO paramatarize 9% # 2 months 2/12 -wage_year += wage_2 + wage_3 -wage_year += period_additional_wage - -over_7uit = wage_year - (7.0 * uit) -if over_7uit <= 0.0: - result = 0.0 -else: - total_tax = 0.0 - last_uit = 0.0 - for _uit, rate in payslip.rule_parameter('ee_ir_5ta_cat'): - # marginal brackets - _uit = float(_uit) - if over_7uit > (last_uit * uit): - eligible_wage = min(over_7uit, _uit * uit) - (last_uit * uit) - if eligible_wage > 0.0: - total_tax += eligible_wage * (rate / 100.0) - else: - break - else: - break - last_uit = _uit - tax = -total_tax / pay_periods_in_year - result, result_rate = wage_period, (tax / wage_period * 100.0) + result, result_rate = ir_5ta_cat(payslip, categories, worked_days, inputs, BASIC) diff --git a/l10n_pe_hr_payroll/models/hr_payslip.py b/l10n_pe_hr_payroll/models/hr_payslip.py index 432337e2..2788cb61 100644 --- a/l10n_pe_hr_payroll/models/hr_payslip.py +++ b/l10n_pe_hr_payroll/models/hr_payslip.py @@ -1,64 +1,8 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from odoo import api, fields, models - - -def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, rate=None): - """ - Function parameters: - wage_base, wage_start, rate can either be strings (rule_parameters) or floats - :return: result, result_rate(wage, percent) - """ - - # Resolve parameters. On exception, return (probably missing a year, would rather not have exception) - if wage_base and isinstance(wage_base, str): - try: - wage_base = payslip.rule_parameter(wage_base) - except (KeyError, UserError): - return 0.0, 0.0 - - if wage_start and isinstance(wage_start, str): - try: - wage_start = payslip.rule_parameter(wage_start) - except (KeyError, UserError): - return 0.0, 0.0 - - if rate and isinstance(rate, str): - try: - rate = payslip.rule_parameter(rate) - except (KeyError, UserError): - return 0.0, 0.0 - - if not rate: - return 0.0, 0.0 - else: - # Rate assumed positive percentage! - rate = -rate - - if wage_base: - remaining = wage_base - ytd_wage - if remaining < 0.0: - result = 0.0 - elif remaining < wage: - result = remaining - else: - result = wage - - # _logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) - return result, rate - if wage_start: - if ytd_wage >= wage_start: - # _logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) - return wage, rate - if ytd_wage + wage <= wage_start: - # _logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) - return 0.0, 0.0 - # _logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) - return (wage - (wage_start - ytd_wage)), rate - - # If the wage doesn't have a start or a base - # _logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) - return wage, rate +from .rules.general import _general_rate +from .rules.ir_5ta_cat import ir_5ta_cat class HRPayslip(models.Model): @@ -68,5 +12,6 @@ class HRPayslip(models.Model): res = super()._get_base_local_dict() res.update({ 'general_rate': _general_rate, + 'ir_5ta_cat': ir_5ta_cat, }) return res diff --git a/l10n_pe_hr_payroll/models/pe_payroll_config.py b/l10n_pe_hr_payroll/models/pe_payroll_config.py index 93adf0f0..1f87801d 100644 --- a/l10n_pe_hr_payroll/models/pe_payroll_config.py +++ b/l10n_pe_hr_payroll/models/pe_payroll_config.py @@ -9,6 +9,8 @@ class HRContractPEPayrollConfig(models.Model): name = fields.Char(string="Description") employee_id = fields.Many2one('hr.employee', string="Employee", required=True) + date_hired = fields.Date(string='Date Hired', required=True, default=fields.Date.today, + help='For calculations like IR 5TA CAT.') retirement_type = fields.Selection([ ('afp', 'AFP'), diff --git a/l10n_pe_hr_payroll/models/rules/__init__.py b/l10n_pe_hr_payroll/models/rules/__init__.py new file mode 100644 index 00000000..12ea88cf --- /dev/null +++ b/l10n_pe_hr_payroll/models/rules/__init__.py @@ -0,0 +1,4 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import general +from . import ir_5ta_cat diff --git a/l10n_pe_hr_payroll/models/rules/general.py b/l10n_pe_hr_payroll/models/rules/general.py new file mode 100644 index 00000000..a061b934 --- /dev/null +++ b/l10n_pe_hr_payroll/models/rules/general.py @@ -0,0 +1,58 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, rate=None): + """ + Function parameters: + wage_base, wage_start, rate can either be strings (rule_parameters) or floats + :return: result, result_rate(wage, percent) + """ + + # Resolve parameters. On exception, return (probably missing a year, would rather not have exception) + if wage_base and isinstance(wage_base, str): + try: + wage_base = payslip.rule_parameter(wage_base) + except (KeyError, UserError): + return 0.0, 0.0 + + if wage_start and isinstance(wage_start, str): + try: + wage_start = payslip.rule_parameter(wage_start) + except (KeyError, UserError): + return 0.0, 0.0 + + if rate and isinstance(rate, str): + try: + rate = payslip.rule_parameter(rate) + except (KeyError, UserError): + return 0.0, 0.0 + + if not rate: + return 0.0, 0.0 + else: + # Rate assumed positive percentage! + rate = -rate + + if wage_base: + remaining = wage_base - ytd_wage + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + # _logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) + return result, rate + if wage_start: + if ytd_wage >= wage_start: + # _logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate + if ytd_wage + wage <= wage_start: + # _logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) + return 0.0, 0.0 + # _logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) + return (wage - (wage_start - ytd_wage)), rate + + # If the wage doesn't have a start or a base + # _logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate diff --git a/l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py b/l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py new file mode 100644 index 00000000..9e726f55 --- /dev/null +++ b/l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py @@ -0,0 +1,71 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +def ir_5ta_cat(payslip, categories, worked_days, inputs, basic_wage): + pay_periods_in_year = payslip.pay_periods_in_year + uit = payslip.rule_parameter('pe_uit') + + # there are two special scenarios + # 1. IF employee's `date_hired` is in current year + # THEN we can pro-rate the period (reduce withholding) + date_hired = payslip.dict.contract_id.pe_payroll_config_value('date_hired') + payslip_date_end = payslip.dict.date_to + hired_in_year = date_hired.year == payslip_date_end.year + + # 2. IF this is the last payroll in June or December + # THEN we need to 'true up' the last two quarters of withholding (e.g. give a refund) + last_payslip_june = payslip_date_end.month == 6 and payslip_date_end.day == 30 + last_payslip_december = payslip_date_end.month == 12 and payslip_date_end.day == 31 + + # basic_wage = BASIC # must be paramatarized as we will not have locals + wage_period = categories.GROSS + if not all((basic_wage, wage_period)) and not any((last_payslip_june, last_payslip_december)): + return 0.0, 0.0 + + period_additional_wage = max(wage_period - basic_wage, 0.0) # 0.0 or positive + wage_year = basic_wage * pay_periods_in_year + # this can be reduced + remaining_months = 12 + if hired_in_year: + # e.g. hired in March (3) gives us 12 - 3 + 1 = 10 (Jan, Feb are the 2 missing from 12) + remaining_months = 12 - date_hired.month + 1 + + # additional 2 months (July and December) (note 2/12 == 1/6) + wage_2 = wage_year * (1/6) + if remaining_months < 6: + wage_2 = wage_year * (1/12) + wage_3 = wage_2 * (payslip.rule_parameter('ee_ir_5ta_cat_ley_29351') / 100.0) + wage_year += wage_2 + wage_3 + # now that we have wage_year, we may need to adjust it by remaining months + wage_year = wage_year * remaining_months / 12 # could be 12/12 + wage_year += period_additional_wage + + over_7uit = wage_year - (7.0 * uit) + total_tax = 0.0 + if over_7uit > 0.0: + total_tax = 0.0 + last_uit = 0.0 + for _uit, rate in payslip.rule_parameter('ee_ir_5ta_cat'): + # marginal brackets + _uit = float(_uit) + if over_7uit > (last_uit * uit): + eligible_wage = min(over_7uit, _uit * uit) - (last_uit * uit) + if eligible_wage > 0.0: + total_tax += eligible_wage * (rate / 100.0) + else: + break + else: + break + last_uit = _uit + + if total_tax: + if last_payslip_june or last_payslip_december: + year = payslip_date_end.year + ytd_tax = -payslip.sum_category('EE_PE_IR_5TA_CAT', str(year) + '-01-01', str(year+1) + '-01-01') + if last_payslip_june: + total_tax /= 2 + # remaining_tax may flip signs + remaining_tax = -(total_tax - ytd_tax) + return wage_period, (remaining_tax / wage_period * 100.0) + tax = -total_tax / remaining_months # TODO needs to be normalized to periods in year if not monthly... + return wage_period, (tax / wage_period * 100.0) + return 0.0, 0.0 diff --git a/l10n_pe_hr_payroll/tests/common.py b/l10n_pe_hr_payroll/tests/common.py index 2eb46fa7..5b0a35aa 100755 --- a/l10n_pe_hr_payroll/tests/common.py +++ b/l10n_pe_hr_payroll/tests/common.py @@ -84,6 +84,8 @@ class TestPePayslip(common.TransactionCase): self._logger.warn('cannot locate attribute names "%s" on contract' % (key, )) # PE Payroll Config Defaults Should be set on the Model + if 'date_hired' not in config_values: + config_values['date_hired'] = '2016-01-01' config = config_model.create(config_values) contract_values['pe_payroll_config_id'] = config.id diff --git a/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml b/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml index 4ac41567..4ae8e587 100644 --- a/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml +++ b/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml @@ -8,6 +8,7 @@ + @@ -23,6 +24,7 @@ +