From 18ee1e315dec587e97c927d2e0823e90fa0ea2be Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 19 Apr 2022 19:52:14 +0000 Subject: [PATCH 1/9] [WIP] l10n_pe_hr_payroll: for 13.0 --- l10n_pe_hr_payroll/__init__.py | 12 ++ l10n_pe_hr_payroll/__manifest__.py | 32 ++++ l10n_pe_hr_payroll/data/afp_rules.xml | 86 ++++++++++ l10n_pe_hr_payroll/data/base.xml | 32 ++++ l10n_pe_hr_payroll/data/er_rules.xml | 36 +++++ l10n_pe_hr_payroll/data/integration_rules.xml | 29 ++++ l10n_pe_hr_payroll/models/__init__.py | 7 + l10n_pe_hr_payroll/models/browsable_object.py | 148 ++++++++++++++++++ l10n_pe_hr_payroll/models/hr_contract.py | 33 ++++ l10n_pe_hr_payroll/models/hr_payslip.py | 134 ++++++++++++++++ .../models/res_config_settings.py | 24 +++ l10n_pe_hr_payroll/models/update.py | 32 ++++ .../static/description/icon.png | Bin 0 -> 8221 bytes l10n_pe_hr_payroll/tests/__init__.py | 8 + l10n_pe_hr_payroll/tests/common.py | 143 +++++++++++++++++ l10n_pe_hr_payroll/tests/test_2022.py | 60 +++++++ .../views/hr_contract_views.xml | 19 +++ .../views/res_config_settings_views.xml | 32 ++++ 18 files changed, 867 insertions(+) create mode 100644 l10n_pe_hr_payroll/__init__.py create mode 100644 l10n_pe_hr_payroll/__manifest__.py create mode 100644 l10n_pe_hr_payroll/data/afp_rules.xml create mode 100644 l10n_pe_hr_payroll/data/base.xml create mode 100644 l10n_pe_hr_payroll/data/er_rules.xml create mode 100644 l10n_pe_hr_payroll/data/integration_rules.xml create mode 100644 l10n_pe_hr_payroll/models/__init__.py create mode 100644 l10n_pe_hr_payroll/models/browsable_object.py create mode 100644 l10n_pe_hr_payroll/models/hr_contract.py create mode 100644 l10n_pe_hr_payroll/models/hr_payslip.py create mode 100644 l10n_pe_hr_payroll/models/res_config_settings.py create mode 100644 l10n_pe_hr_payroll/models/update.py create mode 100644 l10n_pe_hr_payroll/static/description/icon.png create mode 100644 l10n_pe_hr_payroll/tests/__init__.py create mode 100755 l10n_pe_hr_payroll/tests/common.py create mode 100644 l10n_pe_hr_payroll/tests/test_2022.py create mode 100644 l10n_pe_hr_payroll/views/hr_contract_views.xml create mode 100644 l10n_pe_hr_payroll/views/res_config_settings_views.xml diff --git a/l10n_pe_hr_payroll/__init__.py b/l10n_pe_hr_payroll/__init__.py new file mode 100644 index 00000000..013f4e73 --- /dev/null +++ b/l10n_pe_hr_payroll/__init__.py @@ -0,0 +1,12 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models + +def _post_install_hook(cr, registry): + """ + This method will set the default for the Payslip Sum Behavior + """ + cr.execute("SELECT id FROM ir_config_parameter WHERE key = 'hr_payroll.payslip.sum_behavior';") + existing = cr.fetchall() + if not existing: + cr.execute("INSERT INTO ir_config_parameter (key, value) VALUES ('hr_payroll.payslip.sum_behavior', 'date');") diff --git a/l10n_pe_hr_payroll/__manifest__.py b/l10n_pe_hr_payroll/__manifest__.py new file mode 100644 index 00000000..65927b42 --- /dev/null +++ b/l10n_pe_hr_payroll/__manifest__.py @@ -0,0 +1,32 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Peru - Payroll', + 'author': 'Hibou Corp. ', + 'version': '13.0.2022.0.0', + 'category': 'Payroll Localization', + 'depends': [ + 'hr_payroll', + 'hr_contract_reports', + 'hibou_professional', + ], + 'description': """ +Peru - Payroll Rules. +===================== + + """, + + 'data': [ + 'data/base.xml', + 'data/integration_rules.xml', + 'data/afp_rules.xml', + 'data/er_rules.xml', + # 'views/hr_contract_views.xml', + 'views/res_config_settings_views.xml', + ], + 'demo': [ + ], + 'auto_install': False, + 'post_init_hook': '_post_install_hook', + 'license': 'OPL-1', +} diff --git a/l10n_pe_hr_payroll/data/afp_rules.xml b/l10n_pe_hr_payroll/data/afp_rules.xml new file mode 100644 index 00000000..d99d13bd --- /dev/null +++ b/l10n_pe_hr_payroll/data/afp_rules.xml @@ -0,0 +1,86 @@ + + + + + + EE: AFP Pensiones + ee_afp_pensiones + + + + 10.0 + + + + + + EE: AFP Seguro + ee_afp_seguro + + + + 1.74 + + + + + + EE: AFP Comisión + ee_afp_comision + + + + 0.18 + + + + + + + AFP + + + + + + + + EE: PE AFP Pensiones + EE_PE_AFP_PENSIONES + python + result = categories.BASIC + code + result, result_rate = categories.BASIC, payslip.rule_parameter('ee_afp_pensiones') + + + + + + + + + EE: PE AFP Seguro + EE_PE_AFP_SEGURO + python + result = categories.BASIC + code + result, result_rate = categories.BASIC, payslip.rule_parameter('ee_afp_seguro') + + + + + + + + + EE: PE AFP Comisión + EE_PE_AFP_COMISION + python + result = categories.BASIC + code + result, result_rate = categories.BASIC, payslip.rule_parameter('ee_afp_comision') + + + + + diff --git a/l10n_pe_hr_payroll/data/base.xml b/l10n_pe_hr_payroll/data/base.xml new file mode 100644 index 00000000..1a4d2236 --- /dev/null +++ b/l10n_pe_hr_payroll/data/base.xml @@ -0,0 +1,32 @@ + + + + + Peru Employee + + + + + + Peru Employee Standard + + + + + + + + + EE: AFP + EE_PE_AFP + + + + ER: AFP + ER_PE_AFP + + + + diff --git a/l10n_pe_hr_payroll/data/er_rules.xml b/l10n_pe_hr_payroll/data/er_rules.xml new file mode 100644 index 00000000..0c782704 --- /dev/null +++ b/l10n_pe_hr_payroll/data/er_rules.xml @@ -0,0 +1,36 @@ + + + + + + ER: Essalud + er_essalud + + + + 6.75 + + + + + + + Essalud + + + + + + + + ER: PE Essalud + ER_PE_ESSALUD + python + result = categories.BASIC + code + result, result_rate = categories.BASIC, payslip.rule_parameter('er_essalud') + + + + + diff --git a/l10n_pe_hr_payroll/data/integration_rules.xml b/l10n_pe_hr_payroll/data/integration_rules.xml new file mode 100644 index 00000000..af2684ed --- /dev/null +++ b/l10n_pe_hr_payroll/data/integration_rules.xml @@ -0,0 +1,29 @@ + + + + + python + result = inputs.COMMISSION.amount > 0.0 if inputs.COMMISSION else False + code + result = inputs.COMMISSION.amount if inputs.COMMISSION else 0 + BASIC_COM + + Commissions + + + + + + + python + result = inputs.BADGES.amount > 0.0 if inputs.BADGES else False + code + result = inputs.BADGES.amount if inputs.BADGES else 0 + BASIC_BADGES + + Badges + + + + + \ No newline at end of file diff --git a/l10n_pe_hr_payroll/models/__init__.py b/l10n_pe_hr_payroll/models/__init__.py new file mode 100644 index 00000000..34b51524 --- /dev/null +++ b/l10n_pe_hr_payroll/models/__init__.py @@ -0,0 +1,7 @@ +# 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 res_config_settings +from . import update diff --git a/l10n_pe_hr_payroll/models/browsable_object.py b/l10n_pe_hr_payroll/models/browsable_object.py new file mode 100644 index 00000000..bed067fd --- /dev/null +++ b/l10n_pe_hr_payroll/models/browsable_object.py @@ -0,0 +1,148 @@ +# 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/models/hr_contract.py b/l10n_pe_hr_payroll/models/hr_contract.py new file mode 100644 index 00000000..316411f1 --- /dev/null +++ b/l10n_pe_hr_payroll/models/hr_contract.py @@ -0,0 +1,33 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models +# from .us_payroll_config import FUTA_TYPE_NORMAL, \ +# FUTA_TYPE_BASIC, \ +# FUTA_TYPE_EXEMPT + + +# class HrPayrollStructureType(models.Model): +# _inherit = 'hr.payroll.structure.type' +# default_schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')]) + + +# class HrPayrollStructure(models.Model): +# _inherit = 'hr.payroll.structure' +# schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')]) + + +class PEHRContract(models.Model): + _inherit = 'hr.contract' + + # FUTA_TYPE_NORMAL = FUTA_TYPE_NORMAL + # FUTA_TYPE_BASIC = FUTA_TYPE_BASIC + # FUTA_TYPE_EXEMPT = FUTA_TYPE_EXEMPT + + # us_payroll_config_id = fields.Many2one('hr.contract.us_payroll_config', 'Payroll Forms') + # external_wages = fields.Float(string='External Existing Wages') + + # Simplified fields for easier rules, state code will exempt based on contract's futa_type + # futa_type = fields.Selection(related='us_payroll_config_id.fed_940_type') + + # def us_payroll_config_value(self, name): + # return self.us_payroll_config_id[name] diff --git a/l10n_pe_hr_payroll/models/hr_payslip.py b/l10n_pe_hr_payroll/models/hr_payslip.py new file mode 100644 index 00000000..90a27025 --- /dev/null +++ b/l10n_pe_hr_payroll/models/hr_payslip.py @@ -0,0 +1,134 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + +# from .federal.fed_940 import er_us_940_futa +# from .federal.fed_941 import ee_us_941_fica_ss, \ +# ee_us_941_fica_m, \ +# ee_us_941_fica_m_add,\ +# er_us_941_fica_ss, \ +# er_us_941_fica_m, \ +# ee_us_941_fit +# from .state.general import general_state_unemployment, \ +# general_state_income_withholding, \ +# is_us_state +# from .state.al_alabama import al_alabama_state_income_withholding +# from .state.ar_arkansas import ar_arkansas_state_income_withholding +# from .state.az_arizona import az_arizona_state_income_withholding +# from .state.ca_california import ca_california_state_income_withholding +# from .state.co_colorado import co_colorado_state_income_withholding +# from .state.ct_connecticut import ct_connecticut_state_income_withholding +# from .state.de_delaware import de_delaware_state_income_withholding +# from .state.ga_georgia import ga_georgia_state_income_withholding +# from .state.hi_hawaii import hi_hawaii_state_income_withholding +# from .state.ia_iowa import ia_iowa_state_income_withholding +# from .state.id_idaho import id_idaho_state_income_withholding +# from .state.il_illinois import il_illinois_state_income_withholding +# from .state.in_indiana import in_indiana_state_income_withholding +# from .state.ks_kansas import ks_kansas_state_income_withholding +# from .state.ky_kentucky import ky_kentucky_state_income_withholding +# from .state.la_louisiana import la_louisiana_state_income_withholding +# from .state.me_maine import me_maine_state_income_withholding +# from .state.mi_michigan import mi_michigan_state_income_withholding +# from .state.mn_minnesota import mn_minnesota_state_income_withholding +# from .state.mo_missouri import mo_missouri_state_income_withholding +# from .state.ms_mississippi import ms_mississippi_state_income_withholding +# from .state.mt_montana import mt_montana_state_income_withholding +# from .state.nc_northcarolina import nc_northcarolina_state_income_withholding +# from .state.nd_north_dakota import nd_north_dakota_state_income_withholding +# from .state.ne_nebraska import ne_nebraska_state_income_withholding +# from .state.nj_newjersey import nj_newjersey_state_income_withholding +# from .state.nm_new_mexico import nm_new_mexico_state_income_withholding +# from .state.ny_new_york import ny_new_york_state_income_withholding +# from .state.oh_ohio import oh_ohio_state_income_withholding +# from .state.ok_oklahoma import ok_oklahoma_state_income_withholding +# from .state.ri_rhode_island import ri_rhode_island_state_income_withholding +# from .state.sc_south_carolina import sc_south_carolina_state_income_withholding +# from .state.ut_utah import ut_utah_state_income_withholding +# from .state.vt_vermont import vt_vermont_state_income_withholding +# from .state.va_virginia import va_virginia_state_income_withholding +# from .state.wa_washington import wa_washington_fml_er, \ +# wa_washington_fml_ee, \ +# wa_washington_cares_ee +# from .state.wi_wisconsin import wi_wisconsin_state_income_withholding +# from .state.wv_west_virginia import wv_west_virginia_state_income_withholding + + +class HRPayslip(models.Model): + _inherit = 'hr.payslip' + + # From IRS Publication 15-T or logically (annually, bi-monthly) + PAY_PERIODS_IN_YEAR = { + 'annually': 1, + 'semi-annually': 2, + 'quarterly': 4, + 'bi-monthly': 6, + 'monthly': 12, + 'semi-monthly': 24, + 'bi-weekly': 26, + 'weekly': 52, + 'daily': 260, + } + + def _get_base_local_dict(self): + res = super()._get_base_local_dict() + res.update({ + # 'er_us_940_futa': er_us_940_futa, + # 'ee_us_941_fica_ss': ee_us_941_fica_ss, + # 'ee_us_941_fica_m': ee_us_941_fica_m, + # 'ee_us_941_fica_m_add': ee_us_941_fica_m_add, + # 'er_us_941_fica_ss': er_us_941_fica_ss, + # 'er_us_941_fica_m': er_us_941_fica_m, + # 'ee_us_941_fit': ee_us_941_fit, + # 'general_state_unemployment': general_state_unemployment, + # 'general_state_income_withholding': general_state_income_withholding, + # 'is_us_state': is_us_state, + # 'al_alabama_state_income_withholding': al_alabama_state_income_withholding, + # 'ar_arkansas_state_income_withholding': ar_arkansas_state_income_withholding, + # 'az_arizona_state_income_withholding': az_arizona_state_income_withholding, + # 'ca_california_state_income_withholding': ca_california_state_income_withholding, + # 'co_colorado_state_income_withholding': co_colorado_state_income_withholding, + # 'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding, + # 'de_delaware_state_income_withholding': de_delaware_state_income_withholding, + # 'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding, + # 'hi_hawaii_state_income_withholding': hi_hawaii_state_income_withholding, + # 'ia_iowa_state_income_withholding': ia_iowa_state_income_withholding, + # 'id_idaho_state_income_withholding': id_idaho_state_income_withholding, + # 'il_illinois_state_income_withholding': il_illinois_state_income_withholding, + # 'in_indiana_state_income_withholding': in_indiana_state_income_withholding, + # 'ks_kansas_state_income_withholding': ks_kansas_state_income_withholding, + # 'ky_kentucky_state_income_withholding':ky_kentucky_state_income_withholding, + # 'la_louisiana_state_income_withholding': la_louisiana_state_income_withholding, + # 'me_maine_state_income_withholding': me_maine_state_income_withholding, + # 'mi_michigan_state_income_withholding': mi_michigan_state_income_withholding, + # 'mn_minnesota_state_income_withholding': mn_minnesota_state_income_withholding, + # 'mo_missouri_state_income_withholding': mo_missouri_state_income_withholding, + # 'ms_mississippi_state_income_withholding': ms_mississippi_state_income_withholding, + # 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, + # 'nc_northcarolina_state_income_withholding': nc_northcarolina_state_income_withholding, + # 'nd_north_dakota_state_income_withholding': nd_north_dakota_state_income_withholding, + # 'ne_nebraska_state_income_withholding': ne_nebraska_state_income_withholding, + # 'nj_newjersey_state_income_withholding': nj_newjersey_state_income_withholding, + # 'nm_new_mexico_state_income_withholding': nm_new_mexico_state_income_withholding, + # 'ny_new_york_state_income_withholding': ny_new_york_state_income_withholding, + # 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, + # 'ok_oklahoma_state_income_withholding': ok_oklahoma_state_income_withholding, + # 'ri_rhode_island_state_income_withholding': ri_rhode_island_state_income_withholding, + # 'sc_south_carolina_state_income_withholding': sc_south_carolina_state_income_withholding, + # 'ut_utah_state_income_withholding': ut_utah_state_income_withholding, + # 'vt_vermont_state_income_withholding': vt_vermont_state_income_withholding, + # 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, + # 'wa_washington_fml_er': wa_washington_fml_er, + # 'wa_washington_fml_ee': wa_washington_fml_ee, + # 'wa_washington_cares_ee': wa_washington_cares_ee, + # 'wi_wisconsin_state_income_withholding': wi_wisconsin_state_income_withholding, + # 'wv_west_virginia_state_income_withholding': wv_west_virginia_state_income_withholding, + }) + return res + + def get_year(self): + # Helper method to get the year (normalized between Odoo Versions) + return self.date_to.year + + def get_pay_periods_in_year(self): + return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0) diff --git a/l10n_pe_hr_payroll/models/res_config_settings.py b/l10n_pe_hr_payroll/models/res_config_settings.py new file mode 100644 index 00000000..05af9430 --- /dev/null +++ b/l10n_pe_hr_payroll/models/res_config_settings.py @@ -0,0 +1,24 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + payslip_sum_type = fields.Selection([ + ('date_from', 'Date From'), + ('date_to', 'Date To'), + ('date', 'Accounting Date'), + ], 'Payslip Sum Behavior', help="Behavior for what payslips are considered " + "during rule execution. Stock Odoo behavior " + "would not consider a payslip starting on 2019-12-30 " + "ending on 2020-01-07 when summing a 2020 payslip category.\n\n" + "Accounting Date requires Payroll Accounting and will " + "fall back to date_to as the 'closest behavior'.", + config_parameter='hr_payroll.payslip.sum_behavior') + + def set_values(self): + super(ResConfigSettings, self).set_values() + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', + self.payslip_sum_type or 'date_from') diff --git a/l10n_pe_hr_payroll/models/update.py b/l10n_pe_hr_payroll/models/update.py new file mode 100644 index 00000000..b901db8a --- /dev/null +++ b/l10n_pe_hr_payroll/models/update.py @@ -0,0 +1,32 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +import datetime +from odoo import api, fields, models + + +class PublisherWarrantyContract(models.AbstractModel): + _inherit = 'publisher_warranty.contract' + + def _get_hibou_modules(self): + modules = super(PublisherWarrantyContract, self)._get_hibou_modules() + try: + today_date = fields.Date.today() + last_thirty_date = today_date - datetime.timedelta(days=30) + today = fields.Date.to_string(today_date + datetime.timedelta(days=1)) # Dates vs Datetimes, pad out a day + last_thirty = fields.Date.to_string(last_thirty_date) + self.env.cr.execute( + 'SELECT COUNT(DISTINCT(employee_id)) FROM hr_payslip WHERE create_date BETWEEN %s AND %s', + (last_thirty, today)) + employee_count = self.env.cr.fetchone()[0] or 0 + modules.update({ + 'l10n_pe_hr_payroll': employee_count, + }) + except: + pass + return modules + + @api.model + def hibou_payroll_modules_to_update(self): + res = super().hibou_payroll_modules_to_update() + res.append('l10n_pe_hr_payroll') + return res diff --git a/l10n_pe_hr_payroll/static/description/icon.png b/l10n_pe_hr_payroll/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..586dd9e83651c9d9a5bb9b0ab08e1a4dcf033034 GIT binary patch literal 8221 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 literal 0 HcmV?d00001 diff --git a/l10n_pe_hr_payroll/tests/__init__.py b/l10n_pe_hr_payroll/tests/__init__.py new file mode 100644 index 00000000..761fda6c --- /dev/null +++ b/l10n_pe_hr_payroll/tests/__init__.py @@ -0,0 +1,8 @@ +# 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` +# common remains for site specific tests + +from . import common +from . import test_2022 diff --git a/l10n_pe_hr_payroll/tests/common.py b/l10n_pe_hr_payroll/tests/common.py new file mode 100755 index 00000000..a47a2cfd --- /dev/null +++ b/l10n_pe_hr_payroll/tests/common.py @@ -0,0 +1,143 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from logging import getLogger +from sys import float_info as sys_float_info +from collections import defaultdict +from datetime import timedelta + +from odoo.tests import common +from odoo.tools.float_utils import float_round as odoo_float_round + + +def process_payslip(payslip): + try: + payslip.action_payslip_done() + except AttributeError: + # v9 + payslip.process_sheet() + + +class TestPePayslip(common.TransactionCase): + debug = False + _logger = getLogger(__name__) + + def setUp(self): + super().setUp() + # TODO Question, is this the correct summing behavior for Peru? + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to') + self.structure_type_id = self.ref('l10n_pe_hr_payroll.structure_type_employee') + self.resource_calendar_id = self.ref('resource.resource_calendar_std') + + float_info = sys_float_info + + def float_round(self, value, digits): + return odoo_float_round(value, digits) + + _payroll_digits = -1 + + @property + def payroll_digits(self): + if self._payroll_digits == -1: + self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll') + return self._payroll_digits + + def _log(self, message): + if self.debug: + self._logger.warn(message) + + def _createEmployee(self): + return self.env['hr.employee'].create({ + 'birthday': '1985-03-14', + 'country_id': self.ref('base.pe'), + 'department_id': self.ref('hr.dep_rd'), + 'gender': 'male', + 'name': 'Jared' + }) + + def _createContract(self, employee, **kwargs): + if not 'schedule_pay' in kwargs: + kwargs['schedule_pay'] = 'monthly' + schedule_pay = kwargs['schedule_pay'] + contract_model = self.env['hr.contract'] + contract_values = { + 'name': 'Test Contract', + 'employee_id': employee.id, + } + + for key, val in kwargs.items(): + # Assume any Odoo object is in a Many2one + if hasattr(val, 'id'): + val = val.id + found = False + if hasattr(contract_model, key): + contract_values[key] = val + found = True + if not found: + self._logger.warn('cannot locate attribute names "%s" on contract' % (key, )) + + + # Some Basic Defaults + if not contract_values.get('state'): + contract_values['state'] = 'open' # Running + if not contract_values.get('structure_type_id'): + contract_values['structure_type_id'] = self.structure_type_id + if not contract_values.get('date_start'): + contract_values['date_start'] = '2016-01-01' + if not contract_values.get('date_end'): + contract_values['date_end'] = '2030-12-31' + if not contract_values.get('resource_calendar_id'): + contract_values['resource_calendar_id'] = self.resource_calendar_id + + # Compatibility with earlier Odoo versions + if not contract_values.get('journal_id') and hasattr(contract_model, 'journal_id'): + try: + contract_values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id + except KeyError: + # Accounting not installed + pass + + contract = contract_model.create(contract_values) + + # Compatibility with Odoo 13 + contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay + return contract + + def _createPayslip(self, employee, date_from, date_to): + slip = self.env['hr.payslip'].create({ + 'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to), + 'employee_id': employee.id, + 'date_from': date_from, + 'date_to': date_to + }) + slip._onchange_employee() + slip._onchange_worked_days_inputs() + self.assertTrue(slip.contract_id) + return slip + + def _getCategories(self, payslip): + categories = defaultdict(float) + for line in payslip.line_ids: + self._log(' line code: ' + str(line.code) + + ' category code: ' + line.category_id.code + + ' total: ' + str(line.total) + + ' rate: ' + str(line.rate) + + ' amount: ' + str(line.amount)) + category_id = line.category_id + category_code = line.category_id.code + while category_code: + categories[category_code] += line.total + category_id = category_id.parent_id + category_code = category_id.code + return categories + + def _getRules(self, payslip): + rules = defaultdict(float) + for line in payslip.line_ids: + rules[line.code] += line.total + return rules + + def assertPayrollEqual(self, first, second): + self.assertAlmostEqual(first, second, self.payroll_digits) + + def assertPayrollAlmostEqual(self, first, second): + self.assertAlmostEqual(first, second, self.payroll_digits-1) diff --git a/l10n_pe_hr_payroll/tests/test_2022.py b/l10n_pe_hr_payroll/tests/test_2022.py new file mode 100644 index 00000000..ee388352 --- /dev/null +++ b/l10n_pe_hr_payroll/tests/test_2022.py @@ -0,0 +1,60 @@ +# 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% + + # # FUTA Constants + # FUTA_RATE_NORMAL = 0.6 + # FUTA_RATE_BASIC = 6.0 + # FUTA_RATE_EXEMPT = 0.0 + + # # Wage caps + # FICA_SS_MAX_WAGE = 147000.0 + # FICA_M_MAX_WAGE = float_info.max + # FICA_M_ADD_START_WAGE = 200000.0 + # FUTA_MAX_WAGE = 7000.0 + + # # Rates + # FICA_SS = 6.2 / -100.0 + # FICA_M = 1.45 / -100.0 + # FUTA = FUTA_RATE_NORMAL / -100.0 + # FICA_M_ADD = 0.9 / -100.0 + + ### + # 2022 Taxes and Rates + ### + + def test_2022_taxes(self): + self.debug = True + 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) diff --git a/l10n_pe_hr_payroll/views/hr_contract_views.xml b/l10n_pe_hr_payroll/views/hr_contract_views.xml new file mode 100644 index 00000000..b7532699 --- /dev/null +++ b/l10n_pe_hr_payroll/views/hr_contract_views.xml @@ -0,0 +1,19 @@ + + + + + hr.contract.form.inherit + hr.contract + + + + + + + + + \ No newline at end of file diff --git a/l10n_pe_hr_payroll/views/res_config_settings_views.xml b/l10n_pe_hr_payroll/views/res_config_settings_views.xml new file mode 100644 index 00000000..3c69b42f --- /dev/null +++ b/l10n_pe_hr_payroll/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + + res.config.settings.view.form.inherit + res.config.settings + + + +
+
+
+ Payslip Sum Behavior +
+ Customize the behavior of what payslips are eligible when summing over date ranges in rules. + Generally, "Date To" or "Accounting Date" would be preferred in the United States and anywhere + else where the ending date on the payslip is used to calculate wage bases. +
+
+
+
+
+
+
+
+
+
+
+ +
From 6f7bbabfe1927ad7876dcd65ce7eb3cfac0dc922 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 19 Apr 2022 20:03:45 +0000 Subject: [PATCH 2/9] [FIX] l10n_pe_hr_payroll: sign on deductions --- l10n_pe_hr_payroll/data/afp_rules.xml | 6 +++--- l10n_pe_hr_payroll/data/er_rules.xml | 2 +- l10n_pe_hr_payroll/tests/test_2022.py | 26 ++++---------------------- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/l10n_pe_hr_payroll/data/afp_rules.xml b/l10n_pe_hr_payroll/data/afp_rules.xml index d99d13bd..aa991d9f 100644 --- a/l10n_pe_hr_payroll/data/afp_rules.xml +++ b/l10n_pe_hr_payroll/data/afp_rules.xml @@ -50,7 +50,7 @@ python result = categories.BASIC code - result, result_rate = categories.BASIC, payslip.rule_parameter('ee_afp_pensiones') + result, result_rate = categories.BASIC, -payslip.rule_parameter('ee_afp_pensiones') @@ -64,7 +64,7 @@ python result = categories.BASIC code - result, result_rate = categories.BASIC, payslip.rule_parameter('ee_afp_seguro') + result, result_rate = categories.BASIC, -payslip.rule_parameter('ee_afp_seguro') @@ -78,7 +78,7 @@ python result = categories.BASIC code - result, result_rate = categories.BASIC, payslip.rule_parameter('ee_afp_comision') + result, result_rate = categories.BASIC, -payslip.rule_parameter('ee_afp_comision') diff --git a/l10n_pe_hr_payroll/data/er_rules.xml b/l10n_pe_hr_payroll/data/er_rules.xml index 0c782704..36733690 100644 --- a/l10n_pe_hr_payroll/data/er_rules.xml +++ b/l10n_pe_hr_payroll/data/er_rules.xml @@ -28,7 +28,7 @@ python result = categories.BASIC code - result, result_rate = categories.BASIC, payslip.rule_parameter('er_essalud') + result, result_rate = categories.BASIC, -payslip.rule_parameter('er_essalud') diff --git a/l10n_pe_hr_payroll/tests/test_2022.py b/l10n_pe_hr_payroll/tests/test_2022.py index ee388352..67499785 100644 --- a/l10n_pe_hr_payroll/tests/test_2022.py +++ b/l10n_pe_hr_payroll/tests/test_2022.py @@ -12,30 +12,12 @@ class Test2022(TestPePayslip): # ER ESSALUD ER_ESSALUD = 0.0675 # 6.75% - - # # FUTA Constants - # FUTA_RATE_NORMAL = 0.6 - # FUTA_RATE_BASIC = 6.0 - # FUTA_RATE_EXEMPT = 0.0 - - # # Wage caps - # FICA_SS_MAX_WAGE = 147000.0 - # FICA_M_MAX_WAGE = float_info.max - # FICA_M_ADD_START_WAGE = 200000.0 - # FUTA_MAX_WAGE = 7000.0 - - # # Rates - # FICA_SS = 6.2 / -100.0 - # FICA_M = 1.45 / -100.0 - # FUTA = FUTA_RATE_NORMAL / -100.0 - # FICA_M_ADD = 0.9 / -100.0 ### # 2022 Taxes and Rates ### def test_2022_taxes(self): - self.debug = True salary = 3290.0 employee = self._createEmployee() @@ -51,10 +33,10 @@ class Test2022(TestPePayslip): 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) + 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) + self.assertPayrollEqual(rules['ER_PE_ESSALUD'], -cats['BASIC'] * self.ER_ESSALUD) process_payslip(payslip) From cabadf0c42e0acdc29c5567eff5f54f6269a3a2f Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 22 Apr 2022 21:45:25 +0000 Subject: [PATCH 3/9] [IMP] l10n_pe_hr_payroll: retirement type and other contract fields --- l10n_pe_hr_payroll/__manifest__.py | 8 +- l10n_pe_hr_payroll/data/afp_rules.xml | 79 ++++---- l10n_pe_hr_payroll/data/base.xml | 24 +++ l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml | 80 ++++++++ l10n_pe_hr_payroll/data/onp_rules.xml | 36 ++++ l10n_pe_hr_payroll/models/__init__.py | 1 + l10n_pe_hr_payroll/models/hr_contract.py | 31 +-- l10n_pe_hr_payroll/models/hr_payslip.py | 178 ++++++------------ .../models/pe_payroll_config.py | 39 ++++ .../security/ir.model.access.csv | 2 + l10n_pe_hr_payroll/tests/__init__.py | 3 +- l10n_pe_hr_payroll/tests/common.py | 11 ++ l10n_pe_hr_payroll/tests/test_2020.py | 142 ++++++++++++++ .../views/hr_contract_views.xml | 10 +- .../views/pe_payroll_config_views.xml | 71 +++++++ 15 files changed, 524 insertions(+), 191 deletions(-) create mode 100644 l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml create mode 100644 l10n_pe_hr_payroll/data/onp_rules.xml create mode 100644 l10n_pe_hr_payroll/models/pe_payroll_config.py create mode 100644 l10n_pe_hr_payroll/security/ir.model.access.csv create mode 100644 l10n_pe_hr_payroll/tests/test_2020.py create mode 100644 l10n_pe_hr_payroll/views/pe_payroll_config_views.xml diff --git a/l10n_pe_hr_payroll/__manifest__.py b/l10n_pe_hr_payroll/__manifest__.py index 65927b42..0ae186ac 100644 --- a/l10n_pe_hr_payroll/__manifest__.py +++ b/l10n_pe_hr_payroll/__manifest__.py @@ -6,7 +6,7 @@ 'version': '13.0.2022.0.0', 'category': 'Payroll Localization', 'depends': [ - 'hr_payroll', + 'hr_payroll_hibou', 'hr_contract_reports', 'hibou_professional', ], @@ -17,11 +17,15 @@ Peru - Payroll Rules. """, 'data': [ + 'security/ir.model.access.csv', 'data/base.xml', 'data/integration_rules.xml', 'data/afp_rules.xml', + 'data/onp_rules.xml', + 'data/ir_5ta_cat_rules.xml', 'data/er_rules.xml', - # 'views/hr_contract_views.xml', + 'views/hr_contract_views.xml', + 'views/pe_payroll_config_views.xml', 'views/res_config_settings_views.xml', ], 'demo': [ diff --git a/l10n_pe_hr_payroll/data/afp_rules.xml b/l10n_pe_hr_payroll/data/afp_rules.xml index aa991d9f..9b5fc102 100644 --- a/l10n_pe_hr_payroll/data/afp_rules.xml +++ b/l10n_pe_hr_payroll/data/afp_rules.xml @@ -2,36 +2,20 @@ - - EE: AFP Pensiones - ee_afp_pensiones + + EE: AFP + ee_afp - - 10.0 - - - - - - EE: AFP Seguro - ee_afp_seguro - - - - 1.74 - - - - - - EE: AFP Comisión - ee_afp_comision - - - - 0.18 - + + { + # non-mixta, mixta_monthly, mixta_annually, iss, ley, maximum (iss wage base) + 'habitat': (1.47, 0.38, 1.25, 1.35, 10.0, 9707.03), + 'integra': (1.55, 0.00, 1.82, 1.35, 10.0, 9707.03), + 'prima': (1.60, 0.18, 1.25, 1.35, 10.0, 9707.03), + 'profuturo': (1.69, 0.67, 1.20, 1.35, 10.0, 9707.03), + } + @@ -48,9 +32,9 @@ EE: PE AFP Pensiones EE_PE_AFP_PENSIONES python - result = categories.BASIC + result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' code - result, result_rate = categories.BASIC, -payslip.rule_parameter('ee_afp_pensiones') + result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][4] @@ -62,23 +46,44 @@ EE: PE AFP Seguro EE_PE_AFP_SEGURO python - result = categories.BASIC + result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' code - result, result_rate = categories.BASIC, -payslip.rule_parameter('ee_afp_seguro') + +year = payslip.dict.get_year() +ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01') +wage = categories.GROSS +wage_base = payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][5] +rate = payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][3] +result, result_rate = general_rate(payslip, wage, ytd_wage, wage_base=wage_base, rate=rate) + - + - EE: PE AFP Comisión - EE_PE_AFP_COMISION + EE: PE AFP Comisión Mixta + EE_PE_AFP_COMISION_MIXTA python - result = categories.BASIC + result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' and contract.pe_payroll_config_value('afp_comision_type') == 'mixta' code - result, result_rate = categories.BASIC, -payslip.rule_parameter('ee_afp_comision') + result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][1] + + + + + + + + + EE: PE AFP Comisión (Non-Mixta) + 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' + code + result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][0] diff --git a/l10n_pe_hr_payroll/data/base.xml b/l10n_pe_hr_payroll/data/base.xml index 1a4d2236..fffaf5bf 100644 --- a/l10n_pe_hr_payroll/data/base.xml +++ b/l10n_pe_hr_payroll/data/base.xml @@ -29,4 +29,28 @@ + + + EE: ONP + EE_PE_ONP + + + + ER: ONP + ER_PE_ONP + + + + + + EE: IR 5ta Cat. + EE_PE_IR_5TA_CAT + + + + ER: IR 5ta Cat. + ER_PE_IR_5TA_CAT + + + diff --git a/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml new file mode 100644 index 00000000..8d293ad7 --- /dev/null +++ b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml @@ -0,0 +1,80 @@ + + + + + + Peru UIT + pe_uit + + + + 4400.0 + + + + + + EE: IR 5ta Cat. + ee_ir_5ta_cat + + + + + [ + ( 5.0, 8.0), + ( 20.0, 14.0), + ( 35.0, 17.0), + ( 45.0, 20.0), + ('inf', 30.0), + ] + + + + + + + IR 5ta Cat. + + + + + + + + EE: PE IR 5TA Cat. + EE_PE_IR_5TA_CAT + python + result = categories.GROSS + code + +# TODO normalize anual wage based on pay period +uit = payslip.rule_parameter('pe_uit') +wage = categories.GROSS +wage_year = wage * 12.0 +# additional 2 months +wage_year += wage * 2.0 +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 / 12.0 + result, result_rate = wage, (tax / wage * 100.0) + + + + + diff --git a/l10n_pe_hr_payroll/data/onp_rules.xml b/l10n_pe_hr_payroll/data/onp_rules.xml new file mode 100644 index 00000000..b1d9df0c --- /dev/null +++ b/l10n_pe_hr_payroll/data/onp_rules.xml @@ -0,0 +1,36 @@ + + + + + + EE: ONP + ee_onp + + + + 13.0 + + + + + + + ONP + + + + + + + + EE: PE ONP/SNP + EE_PE_ONP + python + result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'onp' and rule == contract.pe_payroll_config_value('onp_rule_id') + code + result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_onp') + + + + + diff --git a/l10n_pe_hr_payroll/models/__init__.py b/l10n_pe_hr_payroll/models/__init__.py index 34b51524..e45c0ad8 100644 --- a/l10n_pe_hr_payroll/models/__init__.py +++ b/l10n_pe_hr_payroll/models/__init__.py @@ -3,5 +3,6 @@ from . import browsable_object from . import hr_contract from . import hr_payslip +from . import pe_payroll_config from . import res_config_settings from . import update diff --git a/l10n_pe_hr_payroll/models/hr_contract.py b/l10n_pe_hr_payroll/models/hr_contract.py index 316411f1..b7980e38 100644 --- a/l10n_pe_hr_payroll/models/hr_contract.py +++ b/l10n_pe_hr_payroll/models/hr_contract.py @@ -1,33 +1,12 @@ # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. from odoo import api, fields, models -# from .us_payroll_config import FUTA_TYPE_NORMAL, \ -# FUTA_TYPE_BASIC, \ -# FUTA_TYPE_EXEMPT - - -# class HrPayrollStructureType(models.Model): -# _inherit = 'hr.payroll.structure.type' -# default_schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')]) - - -# class HrPayrollStructure(models.Model): -# _inherit = 'hr.payroll.structure' -# schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')]) class PEHRContract(models.Model): _inherit = 'hr.contract' - - # FUTA_TYPE_NORMAL = FUTA_TYPE_NORMAL - # FUTA_TYPE_BASIC = FUTA_TYPE_BASIC - # FUTA_TYPE_EXEMPT = FUTA_TYPE_EXEMPT - - # us_payroll_config_id = fields.Many2one('hr.contract.us_payroll_config', 'Payroll Forms') - # external_wages = fields.Float(string='External Existing Wages') - - # Simplified fields for easier rules, state code will exempt based on contract's futa_type - # futa_type = fields.Selection(related='us_payroll_config_id.fed_940_type') - - # def us_payroll_config_value(self, name): - # return self.us_payroll_config_id[name] + + pe_payroll_config_id = fields.Many2one('hr.contract.pe_payroll_config', 'Payroll Forms') + + 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 90a27025..432337e2 100644 --- a/l10n_pe_hr_payroll/models/hr_payslip.py +++ b/l10n_pe_hr_payroll/models/hr_payslip.py @@ -2,133 +2,71 @@ from odoo import api, fields, models -# from .federal.fed_940 import er_us_940_futa -# from .federal.fed_941 import ee_us_941_fica_ss, \ -# ee_us_941_fica_m, \ -# ee_us_941_fica_m_add,\ -# er_us_941_fica_ss, \ -# er_us_941_fica_m, \ -# ee_us_941_fit -# from .state.general import general_state_unemployment, \ -# general_state_income_withholding, \ -# is_us_state -# from .state.al_alabama import al_alabama_state_income_withholding -# from .state.ar_arkansas import ar_arkansas_state_income_withholding -# from .state.az_arizona import az_arizona_state_income_withholding -# from .state.ca_california import ca_california_state_income_withholding -# from .state.co_colorado import co_colorado_state_income_withholding -# from .state.ct_connecticut import ct_connecticut_state_income_withholding -# from .state.de_delaware import de_delaware_state_income_withholding -# from .state.ga_georgia import ga_georgia_state_income_withholding -# from .state.hi_hawaii import hi_hawaii_state_income_withholding -# from .state.ia_iowa import ia_iowa_state_income_withholding -# from .state.id_idaho import id_idaho_state_income_withholding -# from .state.il_illinois import il_illinois_state_income_withholding -# from .state.in_indiana import in_indiana_state_income_withholding -# from .state.ks_kansas import ks_kansas_state_income_withholding -# from .state.ky_kentucky import ky_kentucky_state_income_withholding -# from .state.la_louisiana import la_louisiana_state_income_withholding -# from .state.me_maine import me_maine_state_income_withholding -# from .state.mi_michigan import mi_michigan_state_income_withholding -# from .state.mn_minnesota import mn_minnesota_state_income_withholding -# from .state.mo_missouri import mo_missouri_state_income_withholding -# from .state.ms_mississippi import ms_mississippi_state_income_withholding -# from .state.mt_montana import mt_montana_state_income_withholding -# from .state.nc_northcarolina import nc_northcarolina_state_income_withholding -# from .state.nd_north_dakota import nd_north_dakota_state_income_withholding -# from .state.ne_nebraska import ne_nebraska_state_income_withholding -# from .state.nj_newjersey import nj_newjersey_state_income_withholding -# from .state.nm_new_mexico import nm_new_mexico_state_income_withholding -# from .state.ny_new_york import ny_new_york_state_income_withholding -# from .state.oh_ohio import oh_ohio_state_income_withholding -# from .state.ok_oklahoma import ok_oklahoma_state_income_withholding -# from .state.ri_rhode_island import ri_rhode_island_state_income_withholding -# from .state.sc_south_carolina import sc_south_carolina_state_income_withholding -# from .state.ut_utah import ut_utah_state_income_withholding -# from .state.vt_vermont import vt_vermont_state_income_withholding -# from .state.va_virginia import va_virginia_state_income_withholding -# from .state.wa_washington import wa_washington_fml_er, \ -# wa_washington_fml_ee, \ -# wa_washington_cares_ee -# from .state.wi_wisconsin import wi_wisconsin_state_income_withholding -# from .state.wv_west_virginia import wv_west_virginia_state_income_withholding + +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 class HRPayslip(models.Model): _inherit = 'hr.payslip' - # From IRS Publication 15-T or logically (annually, bi-monthly) - PAY_PERIODS_IN_YEAR = { - 'annually': 1, - 'semi-annually': 2, - 'quarterly': 4, - 'bi-monthly': 6, - 'monthly': 12, - 'semi-monthly': 24, - 'bi-weekly': 26, - 'weekly': 52, - 'daily': 260, - } - def _get_base_local_dict(self): res = super()._get_base_local_dict() res.update({ - # 'er_us_940_futa': er_us_940_futa, - # 'ee_us_941_fica_ss': ee_us_941_fica_ss, - # 'ee_us_941_fica_m': ee_us_941_fica_m, - # 'ee_us_941_fica_m_add': ee_us_941_fica_m_add, - # 'er_us_941_fica_ss': er_us_941_fica_ss, - # 'er_us_941_fica_m': er_us_941_fica_m, - # 'ee_us_941_fit': ee_us_941_fit, - # 'general_state_unemployment': general_state_unemployment, - # 'general_state_income_withholding': general_state_income_withholding, - # 'is_us_state': is_us_state, - # 'al_alabama_state_income_withholding': al_alabama_state_income_withholding, - # 'ar_arkansas_state_income_withholding': ar_arkansas_state_income_withholding, - # 'az_arizona_state_income_withholding': az_arizona_state_income_withholding, - # 'ca_california_state_income_withholding': ca_california_state_income_withholding, - # 'co_colorado_state_income_withholding': co_colorado_state_income_withholding, - # 'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding, - # 'de_delaware_state_income_withholding': de_delaware_state_income_withholding, - # 'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding, - # 'hi_hawaii_state_income_withholding': hi_hawaii_state_income_withholding, - # 'ia_iowa_state_income_withholding': ia_iowa_state_income_withholding, - # 'id_idaho_state_income_withholding': id_idaho_state_income_withholding, - # 'il_illinois_state_income_withholding': il_illinois_state_income_withholding, - # 'in_indiana_state_income_withholding': in_indiana_state_income_withholding, - # 'ks_kansas_state_income_withholding': ks_kansas_state_income_withholding, - # 'ky_kentucky_state_income_withholding':ky_kentucky_state_income_withholding, - # 'la_louisiana_state_income_withholding': la_louisiana_state_income_withholding, - # 'me_maine_state_income_withholding': me_maine_state_income_withholding, - # 'mi_michigan_state_income_withholding': mi_michigan_state_income_withholding, - # 'mn_minnesota_state_income_withholding': mn_minnesota_state_income_withholding, - # 'mo_missouri_state_income_withholding': mo_missouri_state_income_withholding, - # 'ms_mississippi_state_income_withholding': ms_mississippi_state_income_withholding, - # 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, - # 'nc_northcarolina_state_income_withholding': nc_northcarolina_state_income_withholding, - # 'nd_north_dakota_state_income_withholding': nd_north_dakota_state_income_withholding, - # 'ne_nebraska_state_income_withholding': ne_nebraska_state_income_withholding, - # 'nj_newjersey_state_income_withholding': nj_newjersey_state_income_withholding, - # 'nm_new_mexico_state_income_withholding': nm_new_mexico_state_income_withholding, - # 'ny_new_york_state_income_withholding': ny_new_york_state_income_withholding, - # 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, - # 'ok_oklahoma_state_income_withholding': ok_oklahoma_state_income_withholding, - # 'ri_rhode_island_state_income_withholding': ri_rhode_island_state_income_withholding, - # 'sc_south_carolina_state_income_withholding': sc_south_carolina_state_income_withholding, - # 'ut_utah_state_income_withholding': ut_utah_state_income_withholding, - # 'vt_vermont_state_income_withholding': vt_vermont_state_income_withholding, - # 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, - # 'wa_washington_fml_er': wa_washington_fml_er, - # 'wa_washington_fml_ee': wa_washington_fml_ee, - # 'wa_washington_cares_ee': wa_washington_cares_ee, - # 'wi_wisconsin_state_income_withholding': wi_wisconsin_state_income_withholding, - # 'wv_west_virginia_state_income_withholding': wv_west_virginia_state_income_withholding, + 'general_rate': _general_rate, }) return res - - def get_year(self): - # Helper method to get the year (normalized between Odoo Versions) - return self.date_to.year - - def get_pay_periods_in_year(self): - return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0) diff --git a/l10n_pe_hr_payroll/models/pe_payroll_config.py b/l10n_pe_hr_payroll/models/pe_payroll_config.py new file mode 100644 index 00000000..ec7da289 --- /dev/null +++ b/l10n_pe_hr_payroll/models/pe_payroll_config.py @@ -0,0 +1,39 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + + +class HRContractPEPayrollConfig(models.Model): + _name = 'hr.contract.pe_payroll_config' + _description = 'Contract PE Payroll Forms' + + name = fields.Char(string="Description") + employee_id = fields.Many2one('hr.employee', string="Employee", required=True) + + retirement_type = fields.Selection([ + ('afp', 'AFP'), + ('onp', 'ONP'), + ('retired', 'Retired'), + ], string='Retirement Type', required=True, default='afp') + + onp_rule_id = fields.Many2one('hr.salary.rule', string='ONP Rule', domain=[('code', '=like', 'EE_PE_ONP%')]) + + # AFP Type may actually be company specific.... + afp_type = fields.Selection([ + ('habitat', 'Habitat'), + ('integra', 'Integra'), + ('prima', 'Prima'), + ('profuturo', 'Profuturo'), + ], string='AFP Type', default='profuturo') + afp_comision_type = fields.Selection([ + ('mixta', 'Mixta'), + ('non_mixta', 'Non-Mixta'), + ], string='AFP Commission Type', default='mixta') + + comp_ss_type = fields.Selection([ + ('essalud', 'Essalud'), + ('eps', 'EPS'), + ], string='Company Social Services', default='essalud') + comp_ss_eps_rule_id = fields.Many2one('hr.salary.rule', string='Company Social Security EPS Rule') + comp_life_insurance_rule_id = fields.Many2one('hr.salary.rule', string='Company Life Insurance Rule') + comp_risk_insurance_rule_id = fields.Many2one('hr.salary.rule', string='Company Risk Insurance Rule') diff --git a/l10n_pe_hr_payroll/security/ir.model.access.csv b/l10n_pe_hr_payroll/security/ir.model.access.csv new file mode 100644 index 00000000..2ccba490 --- /dev/null +++ b/l10n_pe_hr_payroll/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_contract_pe_payroll_config,hr.contract.pe_payroll_config,model_hr_contract_pe_payroll_config,hr_payroll.group_hr_payroll_manager,1,1,1,1 diff --git a/l10n_pe_hr_payroll/tests/__init__.py b/l10n_pe_hr_payroll/tests/__init__.py index 761fda6c..05472054 100644 --- a/l10n_pe_hr_payroll/tests/__init__.py +++ b/l10n_pe_hr_payroll/tests/__init__.py @@ -5,4 +5,5 @@ # common remains for site specific tests from . import common -from . import test_2022 +from . import test_2020 +# from . import test_2022 diff --git a/l10n_pe_hr_payroll/tests/common.py b/l10n_pe_hr_payroll/tests/common.py index a47a2cfd..2eb46fa7 100755 --- a/l10n_pe_hr_payroll/tests/common.py +++ b/l10n_pe_hr_payroll/tests/common.py @@ -58,7 +58,12 @@ class TestPePayslip(common.TransactionCase): if not 'schedule_pay' in kwargs: kwargs['schedule_pay'] = 'monthly' schedule_pay = kwargs['schedule_pay'] + config_model = self.env['hr.contract.pe_payroll_config'] contract_model = self.env['hr.contract'] + config_values = { + 'name': 'Test Config Values', + 'employee_id': employee.id, + } contract_values = { 'name': 'Test Contract', 'employee_id': employee.id, @@ -69,12 +74,18 @@ class TestPePayslip(common.TransactionCase): if hasattr(val, 'id'): val = val.id found = False + if hasattr(config_model, key): + config_values[key] = val + found = True if hasattr(contract_model, key): contract_values[key] = val found = True if not found: self._logger.warn('cannot locate attribute names "%s" on contract' % (key, )) + # PE Payroll Config Defaults Should be set on the Model + config = config_model.create(config_values) + contract_values['pe_payroll_config_id'] = config.id # Some Basic Defaults if not contract_values.get('state'): diff --git a/l10n_pe_hr_payroll/tests/test_2020.py b/l10n_pe_hr_payroll/tests/test_2020.py new file mode 100644 index 00000000..6e72d03e --- /dev/null +++ b/l10n_pe_hr_payroll/tests/test_2020.py @@ -0,0 +1,142 @@ +# 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/views/hr_contract_views.xml b/l10n_pe_hr_payroll/views/hr_contract_views.xml index b7532699..ece8dc38 100644 --- a/l10n_pe_hr_payroll/views/hr_contract_views.xml +++ b/l10n_pe_hr_payroll/views/hr_contract_views.xml @@ -1,17 +1,17 @@ - + hr.contract.form.inherit hr.contract - + 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}"/> diff --git a/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml b/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml new file mode 100644 index 00000000..c2e86f30 --- /dev/null +++ b/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml @@ -0,0 +1,71 @@ + + + + hr.contract.pe_payroll_config.tree + hr.contract.pe_payroll_config + + + + + + + + + + + + + hr.contract.pe_payroll_config.form + hr.contract.pe_payroll_config + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + hr.contract.pe_payroll_config.search + hr.contract.pe_payroll_config + + + + + + + + + + Peru Employee Payroll Forms + hr.contract.pe_payroll_config + tree,form + +

+ No Forms +

+
+
+ + +
From 88957a77410b88b6ad73bbecc1b519bc39a90e08 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 28 Apr 2022 19:26:28 +0000 Subject: [PATCH 4/9] [IMP] l10n_pe_hr_payroll: move tests and improve --- l10n_pe_hr_payroll/data/base.xml | 30 ++++ l10n_pe_hr_payroll/data/er_rules.xml | 2 +- l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml | 17 +- l10n_pe_hr_payroll/models/__init__.py | 1 - l10n_pe_hr_payroll/models/browsable_object.py | 148 ------------------ l10n_pe_hr_payroll/tests/__init__.py | 5 +- l10n_pe_hr_payroll/tests/test_2020.py | 142 ----------------- l10n_pe_hr_payroll/tests/test_2022.py | 42 ----- 8 files changed, 43 insertions(+), 344 deletions(-) delete mode 100644 l10n_pe_hr_payroll/models/browsable_object.py delete mode 100644 l10n_pe_hr_payroll/tests/test_2020.py delete mode 100644 l10n_pe_hr_payroll/tests/test_2022.py 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) From f71a2ca6b7e46f0afdb7829a5a4fd0e5321405c0 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 28 Apr 2022 23:37:10 +0000 Subject: [PATCH 5/9] [IMP] l10n_pe_hr_payroll: numerous improvements and simplifications (list) Removed partners as chart of accounts is detailed enough to allow reconciliation. No support for EPS as discussed as legacy. No wage base, but rather 'monthly maximum' on Seguo. NEEDS IR 5TH CAT. refactor to use partial pay period and true up on month 6 and 12 --- l10n_pe_hr_payroll/__manifest__.py | 1 - l10n_pe_hr_payroll/data/afp_rules.xml | 22 +++++-------- l10n_pe_hr_payroll/data/base.xml | 4 +++ l10n_pe_hr_payroll/data/er_rules.xml | 6 ---- l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml | 12 +++---- l10n_pe_hr_payroll/data/onp_rules.xml | 8 +---- l10n_pe_hr_payroll/models/__init__.py | 1 - .../models/pe_payroll_config.py | 8 ++--- .../models/res_config_settings.py | 24 -------------- .../views/pe_payroll_config_views.xml | 6 ++-- .../views/res_config_settings_views.xml | 32 ------------------- 11 files changed, 22 insertions(+), 102 deletions(-) delete mode 100644 l10n_pe_hr_payroll/models/res_config_settings.py delete mode 100644 l10n_pe_hr_payroll/views/res_config_settings_views.xml diff --git a/l10n_pe_hr_payroll/__manifest__.py b/l10n_pe_hr_payroll/__manifest__.py index 0ae186ac..ee6cb479 100644 --- a/l10n_pe_hr_payroll/__manifest__.py +++ b/l10n_pe_hr_payroll/__manifest__.py @@ -26,7 +26,6 @@ Peru - Payroll Rules. 'data/er_rules.xml', 'views/hr_contract_views.xml', 'views/pe_payroll_config_views.xml', - 'views/res_config_settings_views.xml', ], 'demo': [ ], diff --git a/l10n_pe_hr_payroll/data/afp_rules.xml b/l10n_pe_hr_payroll/data/afp_rules.xml index 9b5fc102..2f08b5e7 100644 --- a/l10n_pe_hr_payroll/data/afp_rules.xml +++ b/l10n_pe_hr_payroll/data/afp_rules.xml @@ -19,11 +19,6 @@ - - - AFP - - @@ -35,7 +30,6 @@ result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' code result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][4] - @@ -49,14 +43,16 @@ result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' code -year = payslip.dict.get_year() -ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01') wage = categories.GROSS -wage_base = payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][5] -rate = payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][3] -result, result_rate = general_rate(payslip, wage, ytd_wage, wage_base=wage_base, rate=rate) +# wage_max is monthly +wage_max = payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][5] +# normalize wage_max to pay period +pay_periods_in_year = payslip.pay_periods_in_year +wage_max = (wage_max * 12.0) / pay_periods_in_year +eligible_wage = min(wage, wage_max) +rate = -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][3] +result, result_rate = eligible_wage, rate - @@ -70,7 +66,6 @@ result, result_rate = general_rate(payslip, wage, ytd_wage, wage_base=wage_base, result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' and contract.pe_payroll_config_value('afp_comision_type') == 'mixta' code result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][1] - @@ -84,7 +79,6 @@ result, result_rate = general_rate(payslip, wage, ytd_wage, wage_base=wage_base, result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'afp' and contract.pe_payroll_config_value('afp_comision_type') == 'non_mixta' code result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_afp')[contract.pe_payroll_config_value('afp_type')][0] - diff --git a/l10n_pe_hr_payroll/data/base.xml b/l10n_pe_hr_payroll/data/base.xml index bdb5a271..ced1315b 100644 --- a/l10n_pe_hr_payroll/data/base.xml +++ b/l10n_pe_hr_payroll/data/base.xml @@ -69,6 +69,10 @@ Bono BONO + + diff --git a/l10n_pe_hr_payroll/data/er_rules.xml b/l10n_pe_hr_payroll/data/er_rules.xml index 169a025f..a5d97b44 100644 --- a/l10n_pe_hr_payroll/data/er_rules.xml +++ b/l10n_pe_hr_payroll/data/er_rules.xml @@ -13,11 +13,6 @@ - - - Essalud - - @@ -29,7 +24,6 @@ result = categories.BASIC code result, result_rate = categories.BASIC, -payslip.rule_parameter('er_essalud') - 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 ea3c3486..b0a11869 100644 --- a/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml +++ b/l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml @@ -31,11 +31,6 @@ - - - IR 5ta Cat. - - @@ -55,7 +50,9 @@ 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_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) @@ -76,9 +73,8 @@ else: else: break last_uit = _uit - tax = -total_tax / 12.0 + tax = -total_tax / pay_periods_in_year result, result_rate = wage_period, (tax / wage_period * 100.0) - diff --git a/l10n_pe_hr_payroll/data/onp_rules.xml b/l10n_pe_hr_payroll/data/onp_rules.xml index b1d9df0c..b2f81e34 100644 --- a/l10n_pe_hr_payroll/data/onp_rules.xml +++ b/l10n_pe_hr_payroll/data/onp_rules.xml @@ -13,11 +13,6 @@ - - - ONP - - @@ -26,10 +21,9 @@ EE: PE ONP/SNP EE_PE_ONP python - result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'onp' and rule == contract.pe_payroll_config_value('onp_rule_id') + result = categories.GROSS and contract.pe_payroll_config_value('retirement_type') == 'onp' code result, result_rate = categories.GROSS, -payslip.rule_parameter('ee_onp') - diff --git a/l10n_pe_hr_payroll/models/__init__.py b/l10n_pe_hr_payroll/models/__init__.py index ce62c910..30e1b8cc 100644 --- a/l10n_pe_hr_payroll/models/__init__.py +++ b/l10n_pe_hr_payroll/models/__init__.py @@ -3,5 +3,4 @@ from . import hr_contract from . import hr_payslip from . import pe_payroll_config -from . import res_config_settings from . import update diff --git a/l10n_pe_hr_payroll/models/pe_payroll_config.py b/l10n_pe_hr_payroll/models/pe_payroll_config.py index ec7da289..93adf0f0 100644 --- a/l10n_pe_hr_payroll/models/pe_payroll_config.py +++ b/l10n_pe_hr_payroll/models/pe_payroll_config.py @@ -16,8 +16,6 @@ class HRContractPEPayrollConfig(models.Model): ('retired', 'Retired'), ], string='Retirement Type', required=True, default='afp') - onp_rule_id = fields.Many2one('hr.salary.rule', string='ONP Rule', domain=[('code', '=like', 'EE_PE_ONP%')]) - # AFP Type may actually be company specific.... afp_type = fields.Selection([ ('habitat', 'Habitat'), @@ -34,6 +32,6 @@ class HRContractPEPayrollConfig(models.Model): ('essalud', 'Essalud'), ('eps', 'EPS'), ], string='Company Social Services', default='essalud') - comp_ss_eps_rule_id = fields.Many2one('hr.salary.rule', string='Company Social Security EPS Rule') - comp_life_insurance_rule_id = fields.Many2one('hr.salary.rule', string='Company Life Insurance Rule') - comp_risk_insurance_rule_id = fields.Many2one('hr.salary.rule', string='Company Risk Insurance Rule') + comp_ss_eps_rule_id = fields.Many2one('hr.salary.rule', string='Company Social Security EPS Rule', + domain=[('code', '=like', 'ER_PE_EPS%')], + help="Rule code prefix 'ER_PE_EPS' to select here.") diff --git a/l10n_pe_hr_payroll/models/res_config_settings.py b/l10n_pe_hr_payroll/models/res_config_settings.py deleted file mode 100644 index 05af9430..00000000 --- a/l10n_pe_hr_payroll/models/res_config_settings.py +++ /dev/null @@ -1,24 +0,0 @@ -# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. - -from odoo import fields, models - - -class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' - - payslip_sum_type = fields.Selection([ - ('date_from', 'Date From'), - ('date_to', 'Date To'), - ('date', 'Accounting Date'), - ], 'Payslip Sum Behavior', help="Behavior for what payslips are considered " - "during rule execution. Stock Odoo behavior " - "would not consider a payslip starting on 2019-12-30 " - "ending on 2020-01-07 when summing a 2020 payslip category.\n\n" - "Accounting Date requires Payroll Accounting and will " - "fall back to date_to as the 'closest behavior'.", - config_parameter='hr_payroll.payslip.sum_behavior') - - def set_values(self): - super(ResConfigSettings, self).set_values() - self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', - self.payslip_sum_type or 'date_from') 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 c2e86f30..4ac41567 100644 --- a/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml +++ b/l10n_pe_hr_payroll/views/pe_payroll_config_views.xml @@ -27,15 +27,13 @@ - - - - +

Not supported. Specify rule.

+
diff --git a/l10n_pe_hr_payroll/views/res_config_settings_views.xml b/l10n_pe_hr_payroll/views/res_config_settings_views.xml deleted file mode 100644 index 3c69b42f..00000000 --- a/l10n_pe_hr_payroll/views/res_config_settings_views.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - res.config.settings.view.form.inherit - res.config.settings - - - -
-
-
- Payslip Sum Behavior -
- Customize the behavior of what payslips are eligible when summing over date ranges in rules. - Generally, "Date To" or "Accounting Date" would be preferred in the United States and anywhere - else where the ending date on the payslip is used to calculate wage bases. -
-
-
-
-
-
-
-
-
-
-
- -
From 24629e56e7422fec69f0aad27d93f8bc000b0402 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 29 Apr 2022 19:12:30 +0000 Subject: [PATCH 6/9] [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 @@ + From 16d4d5ca2b69ad1a4a52fa2f85e1f61a9b00924d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 6 May 2022 14:57:49 +0000 Subject: [PATCH 7/9] [IMP] l10n_pe_hr_payroll: refactor 5th Cat. add 4th Cat. --- l10n_pe_hr_payroll/__manifest__.py | 1 + l10n_pe_hr_payroll/data/afp_rules.xml | 8 +- l10n_pe_hr_payroll/data/base.xml | 32 ++++-- l10n_pe_hr_payroll/data/ir_4ta_cat_rules.xml | 30 ++++++ l10n_pe_hr_payroll/data/ir_5ta_cat_rules.xml | 2 +- l10n_pe_hr_payroll/models/hr_contract.py | 1 + l10n_pe_hr_payroll/models/hr_payslip.py | 2 + .../models/pe_payroll_config.py | 8 +- l10n_pe_hr_payroll/models/rules/__init__.py | 1 + l10n_pe_hr_payroll/models/rules/ir_4ta_cat.py | 10 ++ l10n_pe_hr_payroll/models/rules/ir_5ta_cat.py | 102 +++++++++++------- .../views/hr_contract_views.xml | 2 + 12 files changed, 143 insertions(+), 56 deletions(-) create mode 100644 l10n_pe_hr_payroll/data/ir_4ta_cat_rules.xml create mode 100644 l10n_pe_hr_payroll/models/rules/ir_4ta_cat.py 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}"/> + From 36148e5c255d1d04bbe0c2c936a3f5cae88e7783 Mon Sep 17 00:00:00 2001 From: Mishael Navarro Date: Fri, 6 May 2022 15:00:58 +0000 Subject: [PATCH 8/9] [IMP] l10n_pe_hr_payroll,l10n_pe_hr_payroll_account: translate to spanish --- l10n_pe_hr_payroll/i18n/es.po | 323 ++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 l10n_pe_hr_payroll/i18n/es.po diff --git a/l10n_pe_hr_payroll/i18n/es.po b/l10n_pe_hr_payroll/i18n/es.po new file mode 100644 index 00000000..da3d014e --- /dev/null +++ b/l10n_pe_hr_payroll/i18n/es.po @@ -0,0 +1,323 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_pe_hr_payroll +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-05-06 01:45+0000\n" +"PO-Revision-Date: 2022-05-06 01:45+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__retirement_type__afp +msgid "AFP" +msgstr "AFP" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__afp_comision_type +msgid "AFP Commission Type" +msgstr "Tipo de Comisión de AFP" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__afp_type +msgid "AFP Type" +msgstr "Tipo de AFP" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_salary_rule_gamification +msgid "Badges" +msgstr "Insignias" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_salary_rule_bonus +msgid "Bonus" +msgstr "Bono" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_salary_rule_commission +msgid "Commissions" +msgstr "Comisiones" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__comp_ss_eps_rule_id +msgid "Company Social Security EPS Rule" +msgstr "Regla EPS de la Seguridad Social de la Empresa" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__comp_ss_type +msgid "Company Social Services" +msgstr "Servicios Social de la Empresa" + +#. module: l10n_pe_hr_payroll +#: model:ir.model,name:l10n_pe_hr_payroll.model_hr_contract_pe_payroll_config +msgid "Contract PE Payroll Forms" +msgstr "Formularios de Nómina del Contrato PE" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__create_uid +msgid "Created by" +msgstr "Creado Por" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__create_date +msgid "Created on" +msgstr "Creado En" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__date_hired +msgid "Date Hired" +msgstr "Fecha de Contratación" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__name +msgid "Description" +msgstr "Descripción" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__display_name +msgid "Display Name" +msgstr "Nombre para Mostrar" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_ee_pe_afp +msgid "EE: AFP" +msgstr "EE: AFP" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_er_essalud +msgid "EE: Essalud" +msgstr "EE: Essalud" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_ee_essalud +msgid "EE: Essalud (rem)" +msgstr "EE: Essalud (rem)" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_ee_ir_4ta_cat +msgid "EE: IR 4th Cat." +msgstr "EE: IR 4ta Cat." + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_ee_ir_5ta_cat +msgid "EE: IR 5th Cat." +msgstr "EE: IR 5ta Cat." + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_ee_pe_onp +msgid "EE: ONP" +msgstr "EE: ONP" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_afp_comision_non_mixta +msgid "EE: PE AFP Comission (Non-Mixed)" +msgstr "EE: PE AFP Comisión (No-Mixta)" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_afp_seguro +msgid "EE: PE AFP Insurance" +msgstr "EE: PE AFP Seguro" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_afp_comision_mixta +msgid "EE: PE AFP Mixed Commission" +msgstr "EE: PE AFP Comisión Mixta" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_afp_pensiones +msgid "EE: PE AFP Pensions" +msgstr "EE: PE AFP Pensiones" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_ir_4ta_cat +msgid "EE: PE IR 4th Cat." +msgstr "EE: PE IR 4ta Cat." + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_ir_5ta_cat +msgid "EE: PE IR 5th Cat." +msgstr "EE: PE IR 5ta Cat." + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_ee_onp +msgid "EE: PE ONP/SNP" +msgstr "EE: PE ONP/SNP" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__comp_ss_type__eps +msgid "EPS" +msgstr "EPS" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_er_pe_afp +msgid "ER: AFP" +msgstr "ER: AFP" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_er_ir_5ta_cat +msgid "ER: IR 5th Cat." +msgstr "ER: IR 5ta Cat." + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule.category,name:l10n_pe_hr_payroll.hr_payroll_category_er_pe_onp +msgid "ER: ONP" +msgstr "ER: ONP" + +#. module: l10n_pe_hr_payroll +#: model:hr.salary.rule,name:l10n_pe_hr_payroll.hr_payroll_rule_er_essalud +msgid "ER: PE Essalud" +msgstr "ER: PE Essalud" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__employee_id +#: model_terms:ir.ui.view,arch_db:l10n_pe_hr_payroll.pe_payroll_config_form +msgid "Employee" +msgstr "Empleado" + +#. module: l10n_pe_hr_payroll +#: model:ir.model,name:l10n_pe_hr_payroll.model_hr_contract +msgid "Employee Contract" +msgstr "Contrato de Empleado" + +#. module: l10n_pe_hr_payroll +#: model_terms:ir.ui.view,arch_db:l10n_pe_hr_payroll.pe_payroll_config_form +msgid "Employee Payroll Forms" +msgstr "Formularios de Nómina del Empleado" + +#. module: l10n_pe_hr_payroll +#: model_terms:ir.ui.view,arch_db:l10n_pe_hr_payroll.pe_payroll_config_form +msgid "Employer" +msgstr "Empleador" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__comp_ss_type__essalud +msgid "Essalud" +msgstr "Essalud" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract__pe_payroll_ee_4ta_cat_exempt +msgid "Exempt from 4th Cat. withholding." +msgstr "Exento de Retención de la 4ta Cat." + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,help:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__date_hired +msgid "For calculations like IR 5TH CAT." +msgstr "Para Cálculos como IR 5TA CAT." + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__afp_type__habitat +msgid "Habitat" +msgstr "Habitat" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__id +msgid "ID" +msgstr "ID" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__afp_type__integra +msgid "Integra" +msgstr "Integra" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config____last_update +msgid "Last Modified on" +msgstr "Última Modificación En" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__write_uid +msgid "Last Updated by" +msgstr "Última Actualización Por" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__write_date +msgid "Last Updated on" +msgstr "Ultima Actualización En" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__afp_comision_type__mixta +msgid "Mixed" +msgstr "Mixta" + +#. module: l10n_pe_hr_payroll +#: model_terms:ir.actions.act_window,help:l10n_pe_hr_payroll.pe_payroll_config_action_main +msgid "No Forms" +msgstr "Sin Formularios" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__afp_comision_type__non_mixta +msgid "Non-Mixed" +msgstr "No-Mixta" + +#. module: l10n_pe_hr_payroll +#: model_terms:ir.ui.view,arch_db:l10n_pe_hr_payroll.pe_payroll_config_form +msgid "Not supported. Specify rule." +msgstr "No Soportado. Indique una Regla" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__retirement_type__onp +msgid "ONP" +msgstr "ONP" + +#. module: l10n_pe_hr_payroll +#: model:ir.model,name:l10n_pe_hr_payroll.model_hr_payslip +msgid "Pay Slip" +msgstr "Recibo de Nómina" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract__pe_payroll_config_id +msgid "Payroll Forms" +msgstr "Formularios de Nómina" + +#. module: l10n_pe_hr_payroll +#: model:ir.actions.act_window,name:l10n_pe_hr_payroll.pe_payroll_config_action_main +#: model_terms:ir.ui.view,arch_db:l10n_pe_hr_payroll.pe_payroll_config_tree +msgid "Peru Employee Payroll Forms" +msgstr "Formularios de Nómina del Empleado de Perú" + +#. module: l10n_pe_hr_payroll +#: model_terms:ir.ui.view,arch_db:l10n_pe_hr_payroll.pe_payroll_config_search +msgid "Peru Employee Payroll Forms Search" +msgstr "Búsqueda de Formularios de Nómina del Empleado de Perú" + +#. module: l10n_pe_hr_payroll +#: model:ir.ui.menu,name:l10n_pe_hr_payroll.pe_payroll_config_menu_main +msgid "Peru Payroll Forms" +msgstr "Formularios de Nómina de Perú" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__afp_type__prima +msgid "Prima" +msgstr "Prima" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__afp_type__profuturo +msgid "Profuturo" +msgstr "Profuturo" + +#. module: l10n_pe_hr_payroll +#: model:ir.model,name:l10n_pe_hr_payroll.model_publisher_warranty_contract +msgid "Publisher Warranty Contract" +msgstr "Contrato de Garantía del Editorial" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields.selection,name:l10n_pe_hr_payroll.selection__hr_contract_pe_payroll_config__retirement_type__retired +msgid "Retired" +msgstr "Jubilado" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,field_description:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__retirement_type +msgid "Retirement Type" +msgstr "Tipo de Jubilación" + +#. module: l10n_pe_hr_payroll +#: model:ir.model.fields,help:l10n_pe_hr_payroll.field_hr_contract_pe_payroll_config__comp_ss_eps_rule_id +msgid "Rule code prefix 'ER_PE_EPS' to select here." +msgstr "Prefijo del Código de la Regla 'ER_PE_EPS' a Seleccionar Aquí." From 4f6091e2115e5d2c8a517a62b03313b1d17c2335 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 10 May 2022 19:31:16 +0000 Subject: [PATCH 9/9] [MIG] l10n_pe_hr_payroll: to 15.0 --- l10n_pe_hr_payroll/__manifest__.py | 2 +- l10n_pe_hr_payroll/data/base.xml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/l10n_pe_hr_payroll/__manifest__.py b/l10n_pe_hr_payroll/__manifest__.py index bd7cf558..6530714a 100644 --- a/l10n_pe_hr_payroll/__manifest__.py +++ b/l10n_pe_hr_payroll/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Peru - Payroll', 'author': 'Hibou Corp. ', - 'version': '13.0.2022.0.0', + 'version': '15.0.2022.0.0', 'category': 'Payroll Localization', 'depends': [ 'hr_payroll_hibou', diff --git a/l10n_pe_hr_payroll/data/base.xml b/l10n_pe_hr_payroll/data/base.xml index 5e3a2efa..e4416df3 100644 --- a/l10n_pe_hr_payroll/data/base.xml +++ b/l10n_pe_hr_payroll/data/base.xml @@ -11,9 +11,8 @@ Peru Employee (5ta Cat.) - @@ -27,7 +26,6 @@ Peru Employee (4ta Cat.) -