diff --git a/l10n_pe_hr_payroll/__manifest__.py b/l10n_pe_hr_payroll/__manifest__.py index ee6cb479..bd7cf558 100644 --- a/l10n_pe_hr_payroll/__manifest__.py +++ b/l10n_pe_hr_payroll/__manifest__.py @@ -22,6 +22,7 @@ Peru - Payroll Rules. 'data/integration_rules.xml', 'data/afp_rules.xml', 'data/onp_rules.xml', + 'data/ir_4ta_cat_rules.xml', 'data/ir_5ta_cat_rules.xml', 'data/er_rules.xml', 'views/hr_contract_views.xml', diff --git a/l10n_pe_hr_payroll/data/afp_rules.xml b/l10n_pe_hr_payroll/data/afp_rules.xml index 2f08b5e7..ffeed643 100644 --- a/l10n_pe_hr_payroll/data/afp_rules.xml +++ b/l10n_pe_hr_payroll/data/afp_rules.xml @@ -24,7 +24,7 @@ - EE: PE AFP Pensiones + EE: PE AFP Pensions EE_PE_AFP_PENSIONES python result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' @@ -37,7 +37,7 @@ - EE: PE AFP Seguro + EE: PE AFP Insurance EE_PE_AFP_SEGURO python result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' @@ -60,7 +60,7 @@ result, result_rate = eligible_wage, rate - EE: PE AFP Comisión Mixta + EE: PE AFP Mixed Commission EE_PE_AFP_COMISION_MIXTA python result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' and contract.pe_payroll_config_value('afp_comision_type') == 'mixta' @@ -73,7 +73,7 @@ result, result_rate = eligible_wage, rate - EE: PE AFP Comisión (Non-Mixta) + EE: PE AFP Comission (Non-Mixed) EE_PE_AFP_COMISION_NON_MIXTA python result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' and contract.pe_payroll_config_value('afp_comision_type') == 'non_mixta' diff --git a/l10n_pe_hr_payroll/data/base.xml b/l10n_pe_hr_payroll/data/base.xml index ced1315b..5e3a2efa 100644 --- a/l10n_pe_hr_payroll/data/base.xml +++ b/l10n_pe_hr_payroll/data/base.xml @@ -2,13 +2,13 @@ - Peru Employee + Peru Employee (5ta Cat.) - Peru Employee Standard + Peru Employee (5ta Cat.) @@ -17,6 +17,19 @@ ]"/> + + Peru Employee (4ta Cat.) + + + + + + Peru Employee (4ta Cat.) + + + + + EE: AFP @@ -43,16 +56,23 @@ - EE: IR 5ta Cat. + EE: IR 5th Cat. EE_PE_IR_5TA_CAT - ER: IR 5ta Cat. + ER: IR 5th Cat. ER_PE_IR_5TA_CAT + + + EE: IR 4th Cat. + EE_PE_IR_4TA_CAT + + + EE: Essalud (rem) @@ -67,7 +87,7 @@ - Bono + Bonus BONO result = inputs.BONO.amount if inputs.BONO else 0 BASIC_BONO - Bono + Bonus diff --git a/l10n_pe_hr_payroll/data/ir_4ta_cat_rules.xml b/l10n_pe_hr_payroll/data/ir_4ta_cat_rules.xml new file mode 100644 index 00000000..d18d87f3 --- /dev/null +++ b/l10n_pe_hr_payroll/data/ir_4ta_cat_rules.xml @@ -0,0 +1,30 @@ + + + + + + EE: IR 4ta Cat. + ee_ir_4ta_cat + + + + 8.0 + + + + + + + + + + EE: PE IR 4th Cat. + EE_PE_IR_4TA_CAT + python + result, _ = ir_4ta_cat(payslip, categories, worked_days, inputs) + code + result, result_rate = ir_4ta_cat(payslip, categories, worked_days, inputs) + + + + 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 b2ad02c1..bafd7b5b 100644 --- a/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml +++ b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml @@ -48,7 +48,7 @@ - EE: PE IR 5TA Cat. + EE: PE IR 5th Cat. EE_PE_IR_5TA_CAT python result, _ = ir_5ta_cat(payslip, categories, worked_days, inputs, BASIC) diff --git a/l10n_pe_hr_payroll/models/hr_contract.py b/l10n_pe_hr_payroll/models/hr_contract.py index b7980e38..f6bfbacf 100644 --- a/l10n_pe_hr_payroll/models/hr_contract.py +++ b/l10n_pe_hr_payroll/models/hr_contract.py @@ -7,6 +7,7 @@ class PEHRContract(models.Model): _inherit = 'hr.contract' pe_payroll_config_id = fields.Many2one('hr.contract.pe_payroll_config', 'Payroll Forms') + pe_payroll_ee_4ta_cat_exempt = fields.Boolean(string='Exempt from 4th Cat. withholding.') def pe_payroll_config_value(self, name): return self.pe_payroll_config_id[name] diff --git a/l10n_pe_hr_payroll/models/hr_payslip.py b/l10n_pe_hr_payroll/models/hr_payslip.py index 2788cb61..df66de83 100644 --- a/l10n_pe_hr_payroll/models/hr_payslip.py +++ b/l10n_pe_hr_payroll/models/hr_payslip.py @@ -2,6 +2,7 @@ from odoo import api, fields, models from .rules.general import _general_rate +from .rules.ir_4ta_cat import ir_4ta_cat from .rules.ir_5ta_cat import ir_5ta_cat @@ -12,6 +13,7 @@ class HRPayslip(models.Model): res = super()._get_base_local_dict() res.update({ 'general_rate': _general_rate, + 'ir_4ta_cat': ir_4ta_cat, '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 1f87801d..ebac2dc1 100644 --- a/l10n_pe_hr_payroll/models/pe_payroll_config.py +++ b/l10n_pe_hr_payroll/models/pe_payroll_config.py @@ -1,6 +1,6 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ class HRContractPEPayrollConfig(models.Model): @@ -10,7 +10,7 @@ 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.') + help='For calculations like IR 5TH CAT.') retirement_type = fields.Selection([ ('afp', 'AFP'), @@ -26,8 +26,8 @@ class HRContractPEPayrollConfig(models.Model): ('profuturo', 'Profuturo'), ], string='AFP Type', default='profuturo') afp_comision_type = fields.Selection([ - ('mixta', 'Mixta'), - ('non_mixta', 'Non-Mixta'), + ('mixta', 'Mixed'), + ('non_mixta', 'Non-Mixed'), ], string='AFP Commission Type', default='mixta') comp_ss_type = fields.Selection([ diff --git a/l10n_pe_hr_payroll/models/rules/__init__.py b/l10n_pe_hr_payroll/models/rules/__init__.py index 12ea88cf..9b5a0ca1 100644 --- a/l10n_pe_hr_payroll/models/rules/__init__.py +++ b/l10n_pe_hr_payroll/models/rules/__init__.py @@ -1,4 +1,5 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from . import general +from . import ir_4ta_cat from . import ir_5ta_cat diff --git a/l10n_pe_hr_payroll/models/rules/ir_4ta_cat.py b/l10n_pe_hr_payroll/models/rules/ir_4ta_cat.py new file mode 100644 index 00000000..54bcfcf4 --- /dev/null +++ b/l10n_pe_hr_payroll/models/rules/ir_4ta_cat.py @@ -0,0 +1,10 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +def ir_4ta_cat(payslip, categories, worked_days, inputs): + if payslip.contract_id.pe_payroll_ee_4ta_cat_exempt: + return 0.0, 0.0 + wage = categories.GROSS + assert wage == 11000.00 + rate = payslip.rule_parameter('ee_ir_4ta_cat') + assert rate == 8.0 + 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 index 9e726f55..da802e61 100644 --- a/l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py +++ b/l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py @@ -1,44 +1,60 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. +from datetime import date + 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 + payslip_date_end = payslip.dict.date_to + + # 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 + # NOTE we do NOT currently support 'catch up' in June. Our formula genearlly already catches up. + last_payslip_june = False + last_payslip_december = payslip_date_end.month == 12 and payslip_date_end.day == 31 + + wage_period = categories.GROSS + if not any((basic_wage, wage_period, 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 + + year = payslip_date_end.year + next_year = date(year+1, 1, 1) + prior_wage_year = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01') + pay_periods_at_current = round(((next_year - payslip_date_end).days / 365) * pay_periods_in_year) + 1.0 + + wage_year = (basic_wage * pay_periods_at_current) + prior_wage_year + + # 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 + periods_in_year_eligible = pay_periods_in_year 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 + periods_in_year_eligible = round(((next_year - date_hired).days / 365) * pay_periods_in_year) - # 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 + # normalize 1era Gratification + if hired_in_year and date_hired.month > 6: + wage_gratif_1 = 0.0 + elif hired_in_year: + wage_gratif_1 = basic_wage / 6 * (6 - date_hired.month + 1) + else: + wage_gratif_1 = basic_wage + + # normalize 2da Gratification + if hired_in_year and date_hired.month > 6: + wage_gratif_2 = basic_wage / 6 * (12 - date_hired.month + 1) + else: + wage_gratif_2 = basic_wage + + wage_year += wage_gratif_1 + wage_gratif_2 + cat_ley = (wage_gratif_1 + wage_gratif_2) * (payslip.rule_parameter('ee_ir_5ta_cat_ley_29351') / 100.0) + wage_year += cat_ley wage_year += period_additional_wage - + over_7uit = wage_year - (7.0 * uit) total_tax = 0.0 if over_7uit > 0.0: @@ -57,15 +73,19 @@ def ir_5ta_cat(payslip, categories, worked_days, inputs, basic_wage): 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 + # if total_tax: + ytd_tax = -payslip.sum_category('EE_PE_IR_5TA_CAT', str(year) + '-01-01', str(year+1) + '-01-01') + + if last_payslip_june or last_payslip_december: + if last_payslip_june: + # does not work right because the gratif_2 is already there + total_tax /= 2 + # remaining_tax may flip signs + remaining_tax = -(total_tax - ytd_tax) + if not wage_period: + # to give refund, cannot normalize to wage + return remaining_tax, 100.0 + return wage_period, (remaining_tax / wage_period * 100.0) + + tax = -(total_tax - ytd_tax) / pay_periods_at_current + return wage_period, (tax / wage_period * 100.0) diff --git a/l10n_pe_hr_payroll/views/hr_contract_views.xml b/l10n_pe_hr_payroll/views/hr_contract_views.xml index ece8dc38..a3aa7a7f 100644 --- a/l10n_pe_hr_payroll/views/hr_contract_views.xml +++ b/l10n_pe_hr_payroll/views/hr_contract_views.xml @@ -12,6 +12,8 @@ attrs="{'invisible': [('structure_type_id', '!=', %(l10n_pe_hr_payroll.structure_type_employee)s)], 'required': [('structure_type_id', '=', %(l10n_pe_hr_payroll.structure_type_employee)s)]}" context="{'default_employee_id': employee_id}"/> +