diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 060f2657..c10cc4e7 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -30,6 +30,7 @@ USA Payroll Rules. 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', 'data/state/mt_montana.xml', + 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', 'data/final.xml', 'views/hr_contract_views.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index d8756337..7eb30e83 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -17,9 +17,14 @@ ref('hr_payroll_rule_ee_fed_941_fit'), ref('hr_payroll_rule_er_us_fl_suta'), + ref('hr_payroll_rule_er_us_mt_suta'), ref('hr_payroll_rule_er_us_mt_suta_aft'), ref('hr_payroll_rule_ee_us_mt_sit'), + + ref('hr_payroll_rule_er_us_oh_suta'), + ref('hr_payroll_rule_ee_us_oh_sit'), + ref('hr_payroll_rule_er_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_sit'), diff --git a/l10n_us_hr_payroll/data/state/mt_montana.xml b/l10n_us_hr_payroll/data/state/mt_montana.xml index b757e11d..7cca142a 100644 --- a/l10n_us_hr_payroll/data/state/mt_montana.xml +++ b/l10n_us_hr_payroll/data/state/mt_montana.xml @@ -50,7 +50,7 @@ US MT Montana SIT Rate Table - us_mt_suta_sit_rate + us_mt_sit_rate { 'weekly': [ ( 135.00, 0.0, 1.80), @@ -90,7 +90,7 @@ US MT Montana SIT Exemption Rate Table - us_mt_suta_sit_exemption_rate + us_mt_sit_exemption_rate { 'weekly': 37.0, 'bi-weekly': 73.0, diff --git a/l10n_us_hr_payroll/data/state/oh_ohio.xml b/l10n_us_hr_payroll/data/state/oh_ohio.xml new file mode 100644 index 00000000..91d16bd8 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/oh_ohio.xml @@ -0,0 +1,150 @@ + + + + + + US OH Ohio SUTA Wage Base + us_oh_suta_wage_base + 9500.00 + + + + US OH Ohio SUTA Wage Base + us_oh_suta_wage_base + 9000.00 + + + + + + + + US OH Ohio SUTA Rate + us_oh_suta_rate + 2.7 + + + + US OH Ohio SUTA Rate + us_oh_suta_rate + 2.7 + + + + + + + US OH Ohio SIT Rate Table + us_oh_sit_rate + + + [ + ( 5000.00, 0.0, 0.005), + ( 10000.00, 25.0, 0.010), + ( 15000.00, 75.0, 0.020), + ( 20000.00, 175.0, 0.025), + ( 40000.00, 300.0, 0.030), + ( 80000.00, 900.0, 0.035), + ( 100000.00, 2300.0, 0.040), + ( 'inf', 3100.0, 0.050), + ] + + + + US OH Ohio SIT Rate Table + us_oh_sit_rate + + + [ + ( 5000.00, 0.0, 0.005), + ( 10000.00, 25.0, 0.010), + ( 15000.00, 75.0, 0.020), + ( 20000.00, 175.0, 0.025), + ( 40000.00, 300.0, 0.030), + ( 80000.00, 900.0, 0.035), + ( 100000.00, 2300.0, 0.040), + ( 'inf', 3100.0, 0.050), + ] + + + + + + + US OH Ohio SIT Exemption Rate + us_oh_sit_exemption_rate + 650.0 + + + + US OH Ohio SIT Exemption Rate + us_oh_sit_exemption_rate + 650.0 + + + + + + + US OH Ohio SIT Multiplier Value + us_oh_sit_multiplier + 1.075 + + + + US OH Ohio SIT Multiplier Value + us_oh_sit_multiplier + 1.032 + + + + + + + US Ohio - OBG - Unemployment + + + + US Ohio - OBG - Unemployment + + + + + US Ohio - OBG - Income Withholding + + + + US Ohio - OBG - Income Withholding + + + + + + + + + + ER: US OH Ohio State Unemployment (JFS-20125) + ER_US_OH_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_oh_suta_wage_base', rate='us_oh_suta_rate', state_code='OH') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_oh_suta_wage_base', rate='us_oh_suta_rate', state_code='OH') + + + + + + + + EE: US OH Ohio State Income Tax Withholding (IT 501) + EE_US_OH_SIT + python + result, _ = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index 8a1a475c..b5d7d15e 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -12,6 +12,10 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { 'mt_mw4_additional_withholding': 'state_income_tax_additional_withholding', 'mt_mw4_exemptions': 'mt_mw4_sit_exemptions', 'mt_mw4_exempt': 'mt_mw4_sit_exempt', + + 'oh_additional_withholding': 'state_income_tax_additional_withholding', + 'oh_income_allowances': 'oh_it4_sit_exemptions', + 'pa_additional_withholding': 'state_income_tax_additional_withholding', } @@ -35,10 +39,17 @@ XMLIDS_TO_REMOVE_2020 = [ 'l10n_us_fl_hr_payroll.hr_payroll_fl_unemp_wages', 'l10n_us_fl_hr_payroll.hr_payroll_fl_unemp', 'l10n_us_fl_hr_payroll.hr_payroll_rules_fl_unemp_wages_2018', + 'l10n_us_mt_hr_payroll.hr_payroll_mt_unemp_wages', 'l10n_us_mt_hr_payroll.hr_payroll_mt_unemp', 'l10n_us_mt_hr_payroll.hr_payroll_mt_income_withhold', 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_unemp_wages', + + 'l10n_us_oh_hr_payroll.hr_payroll_oh_unemp_wages', + 'l10n_us_oh_hr_payroll.hr_payroll_oh_unemp', + 'l10n_us_oh_hr_payroll.hr_payroll_oh_income_withhold', + 'l10n_us_oh_hr_payroll.hr_payroll_rules_oh_unemp_wages_2018', + 'l10n_us_pa_hr_payroll.res_partner_pador_unemp_employee', 'l10n_us_pa_hr_payroll.contrib_register_pador_unemp_employee', 'l10n_us_pa_hr_payroll.hr_payroll_pa_unemp_wages', @@ -66,12 +77,21 @@ XMLIDS_TO_RENAME_2020 = { 'l10n_us_fl_hr_payroll.hr_payroll_rules_fl_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_fl_suta', 'l10n_us_fl_hr_payroll.res_partner_fldor': 'l10n_us_hr_payroll.res_partner_us_fl_dor', 'l10n_us_fl_hr_payroll.contrib_register_fldor': 'l10n_us_hr_payroll.contrib_register_us_fl_dor', + 'l10n_us_mt_hr_payroll.res_partner_mtdor_unemp': 'l10n_us_hr_payroll.res_partner_us_mt_dor', 'l10n_us_mt_hr_payroll.res_partner_mtdor_withhold': 'l10n_us_hr_payroll.res_partner_us_mt_dor_sit', 'l10n_us_mt_hr_payroll.contrib_register_mtdor_unemp': 'l10n_us_hr_payroll.contrib_register_us_mt_dor', 'l10n_us_mt_hr_payroll.contrib_register_mtdor_withhold': 'l10n_us_hr_payroll.contrib_register_us_mt_dor_sit', 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_unemp': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta', 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_inc_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_mt_sit', + + 'l10n_us_oh_hr_payroll.res_partner_ohdor_unemp': 'l10n_us_hr_payroll.res_partner_us_oh_dor', + 'l10n_us_oh_hr_payroll.res_partner_ohdor_withhold': 'l10n_us_hr_payroll.res_partner_us_oh_dor_sit', + 'l10n_us_oh_hr_payroll.res_partner_ohdor_unemp': 'l10n_us_hr_payroll.res_partner_us_oh_dor', + 'l10n_us_oh_hr_payroll.res_partner_ohdor_withhold': 'l10n_us_hr_payroll.res_partner_us_oh_dor_sit', + 'l10n_us_oh_hr_payroll.hr_payroll_rules_oh_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_oh_suta', + 'l10n_us_oh_hr_payroll.hr_payroll_rules_oh_inc_withhold_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_oh_sit', + 'l10n_us_pa_hr_payroll.res_partner_pador_unemp_company': 'l10n_us_hr_payroll.res_partner_us_pa_dor', 'l10n_us_pa_hr_payroll.res_partner_pador_withhold': 'l10n_us_hr_payroll.res_partner_us_pa_dor_sit', 'l10n_us_pa_hr_payroll.contrib_register_pador_unemp_company': 'l10n_us_hr_payroll.contrib_register_us_pa_dor', diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 9104c69e..e2a885f7 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ from .state.general import general_state_unemployment, \ general_state_income_withholding from .state.mt_montana import mt_montana_state_income_withholding +from .state.oh_ohio import oh_ohio_state_income_withholding class HRPayslip(models.Model): @@ -44,6 +45,7 @@ class HRPayslip(models.Model): 'general_state_unemployment': general_state_unemployment, 'general_state_income_withholding': general_state_income_withholding, 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, + 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, } def get_year(self): diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py index fd363301..3816b318 100644 --- a/l10n_us_hr_payroll/models/state/mt_montana.py +++ b/l10n_us_hr_payroll/models/state/mt_montana.py @@ -2,7 +2,6 @@ from .general import _state_applies def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs): - #, wage_base = None, wage_start = None, rate = None, state_code = None """ Returns SIT eligible wage and rate. WAGE = GROSS - WAGE_US_941_FIT_EXEMPT @@ -21,8 +20,8 @@ def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs schedule_pay = payslip.dict.contract_id.schedule_pay additional = payslip.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') exemptions = payslip.dict.contract_id.us_payroll_config_value('mt_mw4_sit_exemptions') - exemption_rate = payslip.dict.rule_parameter('us_mt_suta_sit_exemption_rate').get(schedule_pay) - withholding_rate = payslip.dict.rule_parameter('us_mt_suta_sit_rate').get(schedule_pay) + exemption_rate = payslip.dict.rule_parameter('us_mt_sit_exemption_rate').get(schedule_pay) + withholding_rate = payslip.dict.rule_parameter('us_mt_sit_rate').get(schedule_pay) if not exemption_rate or not withholding_rate or wage == 0.0: return 0.0, 0.0 diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py new file mode 100644 index 00000000..793d4900 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/oh_ohio.py @@ -0,0 +1,44 @@ +from .general import _state_applies + + +def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + WAGE = GROSS - WAGE_US_941_FIT_EXEMPT + + :return: result, result_rate (wage, percent) + """ + state_code = 'OH' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + if payslip.dict.contract_id.us_payroll_config_value('state_income_tax_exempt'): + return 0.0, 0.0 + + # Determine Wage + wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + exemptions = payslip.dict.contract_id.us_payroll_config_value('oh_it4_sit_exemptions') + exemption_rate = payslip.dict.rule_parameter('us_oh_sit_exemption_rate') + withholding_rate = payslip.dict.rule_parameter('us_oh_sit_rate') + multiplier_rate = payslip.dict.rule_parameter('us_oh_sit_multiplier') + if wage == 0.0: + return 0.0, 0.0 + + taxable_wage = (wage * pay_periods) - (exemption_rate * (exemptions or 0)) + withholding = 0.0 + if taxable_wage > 0.0: + prior_wage_cap = 0.0 + for row in withholding_rate: + wage_cap, base, rate = row + wage_cap = float(wage_cap) # e.g. 'inf' + if taxable_wage < wage_cap: + withholding = base + (rate * (taxable_wage - prior_wage_cap)) + break + prior_wage_cap = wage_cap + # Normalize to pay periods + withholding /= pay_periods + withholding *= multiplier_rate + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index b90af562..e0d56a6c 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -58,3 +58,7 @@ class HRContractUSPayrollConfig(models.Model): ('north_dakota', 'North Dakota'), ('montana_for_marriage', 'Montana for Marriage'), ], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2') + + # Ohio will use generic SIT exempt and additional fields + oh_it4_sit_exemptions = fields.Integer(string='Ohio IT-4 Exemptions', + help='Line 4') diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index cc0a4321..a2953520 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -6,7 +6,12 @@ from . import test_us_payslip_2020 from . import test_us_fl_florida_payslip_2019 from . import test_us_fl_florida_payslip_2020 + from . import test_us_mt_montana_payslip_2019 from . import test_us_mt_montana_payslip_2020 + +from . import test_us_oh_ohio_payslip_2019 +from . import test_us_oh_ohio_payslip_2020 + from . import test_us_pa_pennsylvania_payslip_2019 from . import test_us_pa_pennsylvania_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py new file mode 100755 index 00000000..bf38f4d5 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py @@ -0,0 +1,96 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsOhPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + OH_UNEMP_MAX_WAGE = 9500.0 + OH_UNEMP = -2.7 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + # For formula here + # http://www.tax.ohio.gov/Portals/0/employer_withholding/August2015Rates/WTH_OptionalComputerFormula_073015.pdf + tw = salary * 12 # = 60000 + wd = ((tw - 40000) * 0.035 + 900) / 12 * 1.075 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + ) + + self._log('2019 Ohio tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.OH_UNEMP) + self.assertAlmostEqual(cats['EE_US_SIT'], -wd, 1) # Off by 0.6 cents so it rounds off by a penny + #self.assertPayrollEqual(cats['EE_US_SIT'], -wd) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_oh_unemp_wages = self.OH_UNEMP_MAX_WAGE - salary if (self.OH_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Ohio tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_oh_unemp_wages * self.OH_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + external_wages=external_wages, + ) + + self._log('2019 Ohio_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], (self.OH_UNEMP_MAX_WAGE - external_wages) * self.OH_UNEMP) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2019 Ohio exempt tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + # FUTA_TYPE_BASIC + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), salary * 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py new file mode 100755 index 00000000..04256afa --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py @@ -0,0 +1,108 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsOhPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + OH_UNEMP_MAX_WAGE = 9000.0 + OH_UNEMP = 2.7 + + def test_2020_taxes(self): + self._test_er_suta('OH', self.OH_UNEMP, date(2020, 1, 1), wage_base=self.OH_UNEMP_MAX_WAGE) + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=0, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + oh_it4_sit_exemptions=oh_it4_sit_exemptions, + state_id=self.get_us_state('OH'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected, 1) + return payslip + + def test_2020_sit_1(self): + wage = 400.0 + exemptions = 1 + additional = 10.0 + pay_periods = 12.0 + annual_adjusted_wage = (wage * pay_periods) - (650.0 * exemptions) + self.assertPayrollEqual(4150.0, annual_adjusted_wage) + WD = ((annual_adjusted_wage * 0.005) / pay_periods) * 1.032 + self.assertPayrollEqual(WD, 1.7845) + expected = WD + additional + self._run_test_sit(wage=wage, + schedule_pay='monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=additional, + oh_it4_sit_exemptions=exemptions, + expected=expected, + ) + + # the above agrees with online calculator to the penny 0.01 + # below expected coming from calculator to 0.10 + # + # semi-monthly + self._run_test_sit(wage=1200, + schedule_pay='semi-monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=20.0, + oh_it4_sit_exemptions=2, + expected=42.58, + ) + + # bi-weekly + self._run_test_sit(wage=3000, + schedule_pay='bi-weekly', + state_income_tax_exempt=False, + #state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=0, + expected=88.51, + ) + # weekly + self._run_test_sit(wage=355, + schedule_pay='weekly', + state_income_tax_exempt=False, + # state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=1, + expected=4.87, + ) + + # Exempt! + self._run_test_sit(wage=355, + schedule_pay='weekly', + state_income_tax_exempt=True, + # state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=1, + expected=0.0, + ) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index 323f1b92..ee993be7 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -50,6 +50,12 @@ + +

Form IT-4 - State Income Tax

+ + + +