IMP l10n_us_hr_payroll Add OH Ohio (unemployment, income tax)

This commit is contained in:
Jared Kipe
2020-01-08 11:22:19 -08:00
parent dc2e895b40
commit f788d0f0fa
11 changed files with 427 additions and 5 deletions

View File

@@ -27,6 +27,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_rules.xml', 'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml', 'data/state/fl_florida.xml',
'data/state/mt_montana.xml', 'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml', 'data/state/pa_pennsylvania.xml',
'views/hr_contract_views.xml', 'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml', 'views/us_payroll_config_views.xml',

View File

@@ -58,7 +58,7 @@
<record id="rule_parameter_us_mt_sit_rate" model="hr.rule.parameter"> <record id="rule_parameter_us_mt_sit_rate" model="hr.rule.parameter">
<field name="name">US MT Montana SIT Rate Table</field> <field name="name">US MT Montana SIT Rate Table</field>
<field name="code">us_mt_suta_sit_rate</field> <field name="code">us_mt_sit_rate</field>
<field name="country_id" ref="base.us"/> <field name="country_id" ref="base.us"/>
</record> </record>
<data noupdate="1"> <data noupdate="1">
@@ -102,7 +102,7 @@
<record id="rule_parameter_us_mt_sit_exemption_rate" model="hr.rule.parameter"> <record id="rule_parameter_us_mt_sit_exemption_rate" model="hr.rule.parameter">
<field name="name">US MT Montana SIT Exemption Rate Table</field> <field name="name">US MT Montana SIT Exemption Rate Table</field>
<field name="code">us_mt_suta_sit_exemption_rate</field> <field name="code">us_mt_sit_exemption_rate</field>
<field name="country_id" ref="base.us"/> <field name="country_id" ref="base.us"/>
</record> </record>
<data noupdate="1"> <data noupdate="1">

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Wage Base -->
<record id="rule_parameter_us_oh_suta_wage_base" model="hr.rule.parameter">
<field name="name">US OH Ohio SUTA Wage Base</field>
<field name="code">us_oh_suta_wage_base</field>
<field name="country_id" ref="base.us"/>
</record>
<data noupdate="1">
<record id="rule_parameter_us_oh_suta_wage_base_2019" model="hr.rule.parameter.value">
<field name="parameter_value">9500.00</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_suta_wage_base"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_us_oh_suta_wage_base_2020" model="hr.rule.parameter.value">
<field name="parameter_value">9000.00</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_suta_wage_base"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</data>
<!-- Rate -->
<record id="rule_parameter_us_oh_suta_rate" model="hr.rule.parameter">
<field name="name">US OH Ohio SUTA Rate</field>
<field name="code">us_oh_suta_rate</field>
<field name="country_id" ref="base.us"/>
</record>
<data noupdate="1">
<record id="rule_parameter_us_oh_suta_rate_2019" model="hr.rule.parameter.value">
<field name="parameter_value">2.7</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_suta_rate"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_us_oh_suta_rate_2020" model="hr.rule.parameter.value">
<field name="parameter_value">2.7</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_suta_rate"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</data>
<record id="rule_parameter_us_oh_sit_rate" model="hr.rule.parameter">
<field name="name">US OH Ohio SIT Rate Table</field>
<field name="code">us_oh_sit_rate</field>
<field name="country_id" ref="base.us"/>
</record>
<data noupdate="1">
<record id="rule_parameter_us_oh_sit_rate_2019" model="hr.rule.parameter.value">
<!-- https://www.tax.ohio.gov/Portals/0/employer_withholding/2019%20tables/WTH_OptionalComputerFormula_2019.pdf -->
<!-- wage_less_than, base_amount, rate_over -->
<field name="parameter_value">[
( 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),
]</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_sit_rate"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_us_oh_sit_rate_2020" model="hr.rule.parameter.value">
<!-- https://www.tax.ohio.gov/Portals/0/employer_withholding/2020%20tables/WTH_OptionalComputerFormula_2020.pdf -->
<!-- wage_less_than, base_amount, rate_over -->
<field name="parameter_value">[
( 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),
]</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_sit_rate"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</data>
<record id="rule_parameter_us_oh_sit_exemption_rate" model="hr.rule.parameter">
<field name="name">US OH Ohio SIT Exemption Rate</field>
<field name="code">us_oh_sit_exemption_rate</field>
<field name="country_id" ref="base.us"/>
</record>
<data noupdate="1">
<record id="rule_parameter_us_oh_sit_exemption_rate_2019" model="hr.rule.parameter.value">
<field name="parameter_value">650.0</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_sit_exemption_rate"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_us_oh_sit_exemption_rate_2020" model="hr.rule.parameter.value">
<field name="parameter_value">650.0</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_sit_exemption_rate"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</data>
<record id="rule_parameter_us_oh_sit_multiplier" model="hr.rule.parameter">
<field name="name">US OH Ohio SIT Multiplier Value</field>
<field name="code">us_oh_sit_multiplier</field>
<field name="country_id" ref="base.us"/>
</record>
<data noupdate="1">
<record id="rule_parameter_us_oh_sit_multiplier_2019" model="hr.rule.parameter.value">
<field name="parameter_value">1.075</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_sit_multiplier"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_us_oh_sit_multiplier_2020" model="hr.rule.parameter.value">
<field name="parameter_value">1.032</field>
<field name="rule_parameter_id" ref="rule_parameter_us_oh_sit_multiplier"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</data>
<!-- Partners and Contribution Registers -->
<record id="res_partner_us_oh_dor" model="res.partner">
<field name="name">US Ohio - OBG - Unemployment</field>
</record>
<record id="res_partner_us_oh_dor_sit" model="res.partner">
<field name="name">US Ohio - OBG - Income Withholding</field>
</record>
<!-- Categories -->
<!-- Rules -->
<record id="hr_payroll_rule_er_us_oh_suta" model="hr.salary.rule">
<field name="sequence" eval="450"/>
<field name="struct_id" ref="hr_payroll_structure"/>
<field name="category_id" ref="hr_payroll_category_er_us_suta"/>
<field name="name">ER: US OH Ohio State Unemployment (JFS-20125)</field>
<field name="code">ER_US_OH_SUTA</field>
<field name="condition_select">python</field>
<field name="condition_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')</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">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')</field>
<field name="partner_id" ref="res_partner_us_oh_dor"/>
<field name="appears_on_payslip" eval="False"/>
</record>
<record id="hr_payroll_rule_ee_us_oh_sit" model="hr.salary.rule">
<field name="sequence" eval="195"/>
<field name="struct_id" ref="hr_payroll_structure"/>
<field name="category_id" ref="hr_payroll_category_ee_us_sit"/>
<field name="name">EE: US OH Ohio State Income Tax Withholding (IT 501)</field>
<field name="code">EE_US_OH_SIT</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs)</field>
<field name="partner_id" ref="res_partner_us_oh_dor_sit"/>
<field name="appears_on_payslip" eval="True"/>
</record>
</odoo>

View File

@@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
from .state.general import general_state_unemployment, \ from .state.general import general_state_unemployment, \
general_state_income_withholding general_state_income_withholding
from .state.mt_montana import mt_montana_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): class HRPayslip(models.Model):
@@ -43,6 +44,7 @@ class HRPayslip(models.Model):
'general_state_unemployment': general_state_unemployment, 'general_state_unemployment': general_state_unemployment,
'general_state_income_withholding': general_state_income_withholding, 'general_state_income_withholding': general_state_income_withholding,
'mt_montana_state_income_withholding': mt_montana_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 return res

View File

@@ -2,7 +2,6 @@ from .general import _state_applies
def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs): 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. Returns SIT eligible wage and rate.
WAGE = GROSS - WAGE_US_941_FIT_EXEMPT 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 schedule_pay = payslip.contract_id.schedule_pay
additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') 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') 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) exemption_rate = payslip.rule_parameter('us_mt_sit_exemption_rate').get(schedule_pay)
withholding_rate = payslip.rule_parameter('us_mt_suta_sit_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: if not exemption_rate or not withholding_rate or wage == 0.0:
return 0.0, 0.0 return 0.0, 0.0

View File

@@ -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)

View File

@@ -58,3 +58,7 @@ class HRContractUSPayrollConfig(models.Model):
('north_dakota', 'North Dakota'), ('north_dakota', 'North Dakota'),
('montana_for_marriage', 'Montana for Marriage'), ('montana_for_marriage', 'Montana for Marriage'),
], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2') ], 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')

View File

@@ -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_2019
from . import test_us_fl_florida_payslip_2020 from . import test_us_fl_florida_payslip_2020
from . import test_us_mt_montana_payslip_2019 from . import test_us_mt_montana_payslip_2019
from . import test_us_mt_montana_payslip_2020 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_2019
from . import test_us_pa_pennsylvania_payslip_2020 from . import test_us_pa_pennsylvania_payslip_2020

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -50,6 +50,12 @@
<field name="mt_mw4_sit_exemptions" string="Exemptions"/> <field name="mt_mw4_sit_exemptions" string="Exemptions"/>
<field name="state_income_tax_additional_withholding" string="Additional Withholding (Box H)"/> <field name="state_income_tax_additional_withholding" string="Additional Withholding (Box H)"/>
</group> </group>
<group name="state_oh_ohio" string="OH Ohio" attrs="{'invisible':[('state_id', '!=', %(base.state_us_30)s)]}">
<p colspan="2"><h3>Form IT-4 - State Income Tax</h3></p>
<field name="state_income_tax_exempt"/>
<field name="oh_it4_sit_exemptions" string="Exemptions"/>
<field name="state_income_tax_additional_withholding" string="Additional Withholding (Line 5)"/>
</group>
<group name="state_pa_pennsylvania" string="PA Pennsylvania" attrs="{'invisible':[('state_id', '!=', %(base.state_us_39)s)]}"> <group name="state_pa_pennsylvania" string="PA Pennsylvania" attrs="{'invisible':[('state_id', '!=', %(base.state_us_39)s)]}">
<field name="state_income_tax_exempt"/> <field name="state_income_tax_exempt"/>
<field name="state_income_tax_additional_withholding"/> <field name="state_income_tax_additional_withholding"/>