diff --git a/l10n_pe_hr_payroll/data/base.xml b/l10n_pe_hr_payroll/data/base.xml
index fffaf5bf..bdb5a271 100644
--- a/l10n_pe_hr_payroll/data/base.xml
+++ b/l10n_pe_hr_payroll/data/base.xml
@@ -53,4 +53,34 @@
+
+
+ EE: Essalud (rem)
+ EE_PE_ESSALUD
+
+
+
+ EE: Essalud
+ ER_PE_ESSALUD
+
+
+
+
+
+ Bono
+ BONO
+
+
+
+ python
+ result = inputs.BONO.amount > 0.0 if inputs.BONO else False
+ code
+ result = inputs.BONO.amount if inputs.BONO else 0
+ BASIC_BONO
+
+ Bono
+
+
+
+
diff --git a/l10n_pe_hr_payroll/data/er_rules.xml b/l10n_pe_hr_payroll/data/er_rules.xml
index 36733690..169a025f 100644
--- a/l10n_pe_hr_payroll/data/er_rules.xml
+++ b/l10n_pe_hr_payroll/data/er_rules.xml
@@ -22,7 +22,7 @@
-
+
ER: PE Essalud
ER_PE_ESSALUD
python
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 8d293ad7..ea3c3486 100644
--- a/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml
+++ b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml
@@ -47,12 +47,17 @@
result = categories.GROSS
code
-# TODO normalize anual wage based on pay period
+pay_periods_in_year = payslip.pay_periods_in_year
uit = payslip.rule_parameter('pe_uit')
-wage = categories.GROSS
-wage_year = wage * 12.0
-# additional 2 months
-wage_year += wage * 2.0
+
+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_year += wage_year * (1/6) # 2 months 2/12
+wage_year += period_additional_wage
+
over_7uit = wage_year - (7.0 * uit)
if over_7uit <= 0.0:
result = 0.0
@@ -72,7 +77,7 @@ else:
break
last_uit = _uit
tax = -total_tax / 12.0
- result, result_rate = wage, (tax / wage * 100.0)
+ result, result_rate = wage_period, (tax / wage_period * 100.0)
diff --git a/l10n_pe_hr_payroll/models/__init__.py b/l10n_pe_hr_payroll/models/__init__.py
index e45c0ad8..ce62c910 100644
--- a/l10n_pe_hr_payroll/models/__init__.py
+++ b/l10n_pe_hr_payroll/models/__init__.py
@@ -1,6 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from . import browsable_object
from . import hr_contract
from . import hr_payslip
from . import pe_payroll_config
diff --git a/l10n_pe_hr_payroll/models/browsable_object.py b/l10n_pe_hr_payroll/models/browsable_object.py
deleted file mode 100644
index bed067fd..00000000
--- a/l10n_pe_hr_payroll/models/browsable_object.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-
-from odoo import fields
-from odoo.addons.hr_payroll.models import browsable_object
-
-
-class BrowsableObject(object):
- def __init__(self, employee_id, dict, env):
- self.employee_id = employee_id
- self.dict = dict
- self.env = env
- # Customization to allow changing the behavior of the discrete browsable objects.
- # you can think of this as 'compiling' the query based on the configuration.
- sum_field = env['ir.config_parameter'].sudo().get_param('hr_payroll.payslip.sum_behavior', 'date_from')
- if sum_field == 'date' and 'date' not in env['hr.payslip']:
- # missing attribute, closest by definition
- sum_field = 'date_to'
- if not sum_field:
- sum_field = 'date_from'
- self._compile_browsable_query(sum_field)
-
- def __getattr__(self, attr):
- return attr in self.dict and self.dict.__getitem__(attr) or 0.0
-
- def _compile_browsable_query(self, sum_field):
- pass
-
-
-class InputLine(BrowsableObject):
- """a class that will be used into the python code, mainly for usability purposes"""
- def _compile_browsable_query(self, sum_field):
- self.__browsable_query = """
- SELECT sum(amount) as sum
- FROM hr_payslip as hp, hr_payslip_input as pi
- WHERE hp.employee_id = %s AND hp.state = 'done'
- AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field)
-
- def sum(self, code, from_date, to_date=None):
- if to_date is None:
- to_date = fields.Date.today()
- self.env.cr.execute(self.__browsable_query, (self.employee_id, from_date, to_date, code))
- return self.env.cr.fetchone()[0] or 0.0
-
-
-class WorkedDays(BrowsableObject):
- """a class that will be used into the python code, mainly for usability purposes"""
- def _compile_browsable_query(self, sum_field):
- self.__browsable_query = """
- SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours
- FROM hr_payslip as hp, hr_payslip_worked_days as pi
- WHERE hp.employee_id = %s AND hp.state = 'done'
- AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field)
-
- def _sum(self, code, from_date, to_date=None):
- if to_date is None:
- to_date = fields.Date.today()
- self.env.cr.execute(self.__browsable_query, (self.employee_id, from_date, to_date, code))
- return self.env.cr.fetchone()
-
- def sum(self, code, from_date, to_date=None):
- res = self._sum(code, from_date, to_date)
- return res and res[0] or 0.0
-
- def sum_hours(self, code, from_date, to_date=None):
- res = self._sum(code, from_date, to_date)
- return res and res[1] or 0.0
-
-
-class Payslips(BrowsableObject):
- """a class that will be used into the python code, mainly for usability purposes"""
- def _compile_browsable_query(self, sum_field):
- # Note that the core odoo has this as `hp.credit_note = False` but what if it is NULL?
- # reverse of the desired behavior.
- self.__browsable_query_rule = """
- SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
- FROM hr_payslip as hp, hr_payslip_line as pl
- WHERE hp.employee_id = %s AND hp.state = 'done'
- AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""".format(sum_field=sum_field)
- # Original (non-recursive)
- # self.__browsable_query_category = """
- # SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
- # FROM hr_payslip as hp, hr_payslip_line as pl, hr_salary_rule_category as rc
- # WHERE hp.employee_id = %s AND hp.state = 'done'
- # AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
- # AND rc.id = pl.category_id AND rc.code = %s""".format(sum_field=sum_field)
-
- # Hibou Recursive version
- self.__browsable_query_category = """
- WITH RECURSIVE
- category_by_code as (
- SELECT id
- FROM hr_salary_rule_category
- WHERE code = %s
- ),
- category_ids as (
- SELECT COALESCE((SELECT id FROM category_by_code), -1) AS id
- UNION ALL
- SELECT rc.id
- FROM hr_salary_rule_category AS rc
- JOIN category_ids AS rcs ON rcs.id = rc.parent_id
- )
-
- SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end)
- FROM hr_payslip as hp, hr_payslip_line as pl
- WHERE hp.employee_id = %s AND hp.state = 'done'
- AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
- AND pl.category_id in (SELECT id from category_ids)""".format(sum_field=sum_field)
-
- def sum(self, code, from_date, to_date=None):
- if to_date is None:
- to_date = fields.Date.today()
- self.env.cr.execute(self.__browsable_query_rule, (self.employee_id, from_date, to_date, code))
- res = self.env.cr.fetchone()
- return res and res[0] or 0.0
-
- def rule_parameter(self, code):
- return self.env['hr.rule.parameter']._get_parameter_from_code(code, self.dict.date_to)
-
- def sum_category(self, code, from_date, to_date=None):
- if to_date is None:
- to_date = fields.Date.today()
-
- self.env['hr.payslip'].flush(['credit_note', 'employee_id', 'state', 'date_from', 'date_to'])
- self.env['hr.payslip.line'].flush(['total', 'slip_id', 'category_id'])
- self.env['hr.salary.rule.category'].flush(['code'])
-
- # standard version
- # self.env.cr.execute(self.__browsable_query_category, (self.employee_id, from_date, to_date, code))
- # recursive category version
- self.env.cr.execute(self.__browsable_query_category, (code, self.employee_id, from_date, to_date))
- res = self.env.cr.fetchone()
- return res and res[0] or 0.0
-
- @property
- def paid_amount(self):
- return self.dict._get_paid_amount()
-
-
-# Patch over Core
-browsable_object.BrowsableObject.__init__ = BrowsableObject.__init__
-browsable_object.BrowsableObject._compile_browsable_query = BrowsableObject._compile_browsable_query
-browsable_object.InputLine._compile_browsable_query = InputLine._compile_browsable_query
-browsable_object.InputLine.sum = InputLine.sum
-browsable_object.WorkedDays._compile_browsable_query = WorkedDays._compile_browsable_query
-browsable_object.WorkedDays.sum = WorkedDays.sum
-browsable_object.Payslips._compile_browsable_query = Payslips._compile_browsable_query
-browsable_object.Payslips.sum = Payslips.sum
-browsable_object.Payslips.sum_category = Payslips.sum_category
diff --git a/l10n_pe_hr_payroll/tests/__init__.py b/l10n_pe_hr_payroll/tests/__init__.py
index 05472054..7a934474 100644
--- a/l10n_pe_hr_payroll/tests/__init__.py
+++ b/l10n_pe_hr_payroll/tests/__init__.py
@@ -1,9 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-# TODO make/move to l10n_pe_hr_payroll_params
-# Tests moved to `l10n_us_hr_payroll_params`
+# Tests moved to `l10n_pe_hr_payroll_params`
# common remains for site specific tests
from . import common
-from . import test_2020
-# from . import test_2022
diff --git a/l10n_pe_hr_payroll/tests/test_2020.py b/l10n_pe_hr_payroll/tests/test_2020.py
deleted file mode 100644
index 6e72d03e..00000000
--- a/l10n_pe_hr_payroll/tests/test_2020.py
+++ /dev/null
@@ -1,142 +0,0 @@
-# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-
-from .common import TestPePayslip, process_payslip
-
-
-class Test2020(TestPePayslip):
-
- ###
- # 2020 Taxes and Rates
- ###
-
- def test_2020_taxes(self):
- # High salary to hit the maximum for AFP_SEGURO
- salary = 8000.00
-
- employee = self._createEmployee()
-
- contract = self._createContract(employee,
- wage=salary,
- retirement_type='afp',
- afp_type='profuturo',
- afp_comision_type='mixta',
- comp_ss_type='essalud',
- )
- self._log(contract.read())
-
- self._log('2020 tax first payslip:')
- payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- # Employee
- self.assertPayrollEqual(cats['GROSS'], salary)
- self.assertPayrollEqual(rules['EE_PE_AFP_PENSIONES'], -cats['GROSS'] * (10.0 / 100.0))
- self.assertPayrollEqual(rules['EE_PE_AFP_SEGURO'], -cats['GROSS'] * (1.35 / 100.0))
- self.assertPayrollEqual(rules['EE_PE_AFP_COMISION_MIXTA'], -cats['GROSS'] * (0.67 / 100.0))
- # Employer
- self.assertPayrollEqual(rules['ER_PE_ESSALUD'], -cats['GROSS'] * (6.75 / 100.0))
-
- process_payslip(payslip)
-
- self._log('2020 tax second payslip:')
- payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- # Employee
- self.assertPayrollEqual(cats['GROSS'], salary)
- self.assertPayrollEqual(rules['EE_PE_AFP_PENSIONES'], -cats['GROSS'] * (10.0 / 100.0))
-
- self.assertTrue(cats['GROSS'] < 9707.03)
- # Seguro has a wage base.
- second_seguro = -(9707.03 - cats['GROSS']) * (1.35 / 100.0)
- self.assertPayrollEqual(rules['EE_PE_AFP_SEGURO'], second_seguro)
- self.assertPayrollEqual(rules['EE_PE_AFP_COMISION_MIXTA'], -cats['GROSS'] * (0.67 / 100.0))
- # Employer
- self.assertPayrollEqual(rules['ER_PE_ESSALUD'], -cats['GROSS'] * (6.75 / 100.0))
-
- process_payslip(payslip)
-
- self._log('2020 tax third payslip:')
- payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31')
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- # Employee
- self.assertPayrollEqual(cats['GROSS'], salary)
- self.assertPayrollEqual(rules['EE_PE_AFP_PENSIONES'], -cats['GROSS'] * (10.0 / 100.0))
-
- self.assertTrue(cats['GROSS'] < 9707.03)
- self.assertPayrollEqual(rules['EE_PE_AFP_SEGURO'], 0.0)
- self.assertPayrollEqual(rules['EE_PE_AFP_COMISION_MIXTA'], -cats['GROSS'] * (0.67 / 100.0))
- # Employer
- self.assertPayrollEqual(rules['ER_PE_ESSALUD'], -cats['GROSS'] * (6.75 / 100.0))
-
- process_payslip(payslip)
-
- def test_2020_onp(self):
- salary = 3500.00
-
- employee = self._createEmployee()
-
- contract = self._createContract(employee,
- wage=salary,
- retirement_type='onp',
- onp_rule_id=self.env.ref('l10n_pe_hr_payroll.hr_payroll_rule_ee_onp').id,
- comp_ss_type='essalud',
- )
- self._log(contract.read())
-
- self._log('2020 tax first payslip:')
- payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- # Employee
- self.assertPayrollEqual(cats['GROSS'], salary)
- self.assertPayrollEqual(rules['EE_PE_AFP_PENSIONES'], 0.0)
- self.assertPayrollEqual(rules['EE_PE_AFP_SEGURO'], 0.0)
- self.assertPayrollEqual(rules['EE_PE_AFP_COMISION_MIXTA'], 0.0)
- self.assertPayrollEqual(cats['EE_PE_ONP'], -cats['GROSS'] * (13.0 / 100.0))
- # Employer
- self.assertPayrollEqual(rules['ER_PE_ESSALUD'], -cats['GROSS'] * (6.75 / 100.0))
-
- process_payslip(payslip)
-
- def test_2020_ir_5ta_cat(self):
- salary = 1500.00
-
- employee = self._createEmployee()
-
- contract = self._createContract(employee,
- wage=salary,
- retirement_type='onp',
- onp_rule_id=self.env.ref('l10n_pe_hr_payroll.hr_payroll_rule_ee_onp').id,
- comp_ss_type='essalud',
- )
-
- self._log('2020 tax first payslip:')
- payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- self.assertPayrollEqual(cats['GROSS'], salary)
- self.assertPayrollEqual(rules['EE_PE_IR_5TA_CAT'], 0.0)
- payslip.state = 'cancel'
- payslip.unlink()
-
- # larger salary to trigger calculation
- salary = 3000.0
- contract.wage = salary
- payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
- payslip.compute_sheet()
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- self.assertPayrollEqual(cats['GROSS'], salary)
- self.assertPayrollEqual(rules['EE_PE_IR_5TA_CAT'], -74.67)
diff --git a/l10n_pe_hr_payroll/tests/test_2022.py b/l10n_pe_hr_payroll/tests/test_2022.py
deleted file mode 100644
index 67499785..00000000
--- a/l10n_pe_hr_payroll/tests/test_2022.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-
-from .common import TestPePayslip, process_payslip
-
-
-class Test2022(TestPePayslip):
-
- # AFP Constants
- AFP_PENSIONES = 0.1 # 10%
- AFP_SEGURO = 0.0174 # 1.74%
- AFP_COMISION = 0.0018 # 0.18%
-
- # ER ESSALUD
- ER_ESSALUD = 0.0675 # 6.75%
-
- ###
- # 2022 Taxes and Rates
- ###
-
- def test_2022_taxes(self):
- salary = 3290.0
-
- employee = self._createEmployee()
-
- contract = self._createContract(employee, wage=salary)
- self._log(contract.read())
-
- self._log('2022 tax first payslip:')
- payslip = self._createPayslip(employee, '2022-01-01', '2022-01-31')
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
- rules = self._getRules(payslip)
- # Employee
- self.assertPayrollEqual(cats['BASIC'], salary)
- self.assertPayrollEqual(rules['EE_PE_AFP_PENSIONES'], -cats['BASIC'] * self.AFP_PENSIONES)
- self.assertPayrollEqual(rules['EE_PE_AFP_SEGURO'], -cats['BASIC'] * self.AFP_SEGURO)
- self.assertPayrollEqual(rules['EE_PE_AFP_COMISION'], -cats['BASIC'] * self.AFP_COMISION)
- # Employer
- self.assertPayrollEqual(rules['ER_PE_ESSALUD'], -cats['BASIC'] * self.ER_ESSALUD)
-
- process_payslip(payslip)