From f788d0f0fa651eebe742f26508cd9edeeaf8e30f Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 8 Jan 2020 11:22:19 -0800
Subject: [PATCH] IMP `l10n_us_hr_payroll` Add OH Ohio (unemployment, income
tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/mt_montana.xml | 4 +-
l10n_us_hr_payroll/data/state/oh_ohio.xml | 157 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/mt_montana.py | 5 +-
l10n_us_hr_payroll/models/state/oh_ohio.py | 44 +++++
.../models/us_payroll_config.py | 4 +
l10n_us_hr_payroll/tests/__init__.py | 5 +
.../tests/test_us_oh_ohio_payslip_2019.py | 96 +++++++++++
.../tests/test_us_oh_ohio_payslip_2020.py | 108 ++++++++++++
.../views/us_payroll_config_views.xml | 6 +
11 files changed, 427 insertions(+), 5 deletions(-)
create mode 100644 l10n_us_hr_payroll/data/state/oh_ohio.xml
create mode 100644 l10n_us_hr_payroll/models/state/oh_ohio.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 4c9df42a..cca06ffe 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -27,6 +27,7 @@ United States of America - 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',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
diff --git a/l10n_us_hr_payroll/data/state/mt_montana.xml b/l10n_us_hr_payroll/data/state/mt_montana.xml
index ec18a955..b420c4fe 100644
--- a/l10n_us_hr_payroll/data/state/mt_montana.xml
+++ b/l10n_us_hr_payroll/data/state/mt_montana.xml
@@ -58,7 +58,7 @@
US MT Montana SIT Rate Table
- us_mt_suta_sit_rate
+ us_mt_sit_rate
@@ -102,7 +102,7 @@
US MT Montana SIT Exemption Rate Table
- us_mt_suta_sit_exemption_rate
+ us_mt_sit_exemption_rate
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..e6db8eb8
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/oh_ohio.xml
@@ -0,0 +1,157 @@
+
+
+
+
+ US OH Ohio SUTA Wage Base
+ us_oh_suta_wage_base
+
+
+
+
+ 9500.00
+
+
+
+
+ 9000.00
+
+
+
+
+
+
+
+ US OH Ohio SUTA Rate
+ us_oh_suta_rate
+
+
+
+
+ 2.7
+
+
+
+
+ 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),
+ ]
+
+
+
+
+
+
+ [
+ ( 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
+
+
+
+
+ 650.0
+
+
+
+
+
+
+ US OH Ohio SIT Multiplier Value
+ us_oh_sit_multiplier
+
+
+
+
+ 1.075
+
+
+
+
+ 1.032
+
+
+
+
+
+
+
+ US Ohio - OBG - Unemployment
+
+
+
+ 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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 93e4f64b..2abc4afa 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):
@@ -43,6 +44,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,
})
return res
diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py
index 727c56e9..b46380b1 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.contract_id.schedule_pay
additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
exemptions = payslip.contract_id.us_payroll_config_value('mt_mw4_sit_exemptions')
- exemption_rate = payslip.rule_parameter('us_mt_suta_sit_exemption_rate').get(schedule_pay)
- withholding_rate = payslip.rule_parameter('us_mt_suta_sit_rate').get(schedule_pay)
+ exemption_rate = payslip.rule_parameter('us_mt_sit_exemption_rate').get(schedule_pay)
+ withholding_rate = payslip.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..d279ce1f
--- /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.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.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ exemptions = payslip.contract_id.us_payroll_config_value('oh_it4_sit_exemptions')
+ exemption_rate = payslip.rule_parameter('us_oh_sit_exemption_rate')
+ withholding_rate = payslip.rule_parameter('us_oh_sit_rate')
+ multiplier_rate = payslip.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 846f5ca4..d9222493 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
+
+
+
+