From 12ec84b44255eb5410aff72ba388d967b884b7d9 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 14 Jan 2020 14:09:41 -0500
Subject: [PATCH] IMP `l10n_us_hr_payroll` Add MO Missouri (unemployment,
income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/mo_missouri.xml | 145 ++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/mo_missouri.py | 52 +++++
.../models/us_payroll_config.py | 10 +-
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_mo_missouri_payslip_2019.py | 188 ++++++++++++++++++
.../tests/test_us_mo_missouri_payslip_2020.py | 105 ++++++++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 511 insertions(+), 1 deletion(-)
create mode 100644 l10n_us_hr_payroll/data/state/mo_missouri.xml
create mode 100644 l10n_us_hr_payroll/models/state/mo_missouri.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 74a9fd12..45a6ae45 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/ga_georgia.xml',
+ 'data/state/mo_missouri.xml',
'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
diff --git a/l10n_us_hr_payroll/data/state/mo_missouri.xml b/l10n_us_hr_payroll/data/state/mo_missouri.xml
new file mode 100644
index 00000000..230e24eb
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/mo_missouri.xml
@@ -0,0 +1,145 @@
+
+
+
+
+ US MO Missouri SUTA Wage Base
+ us_mo_suta_wage_base
+
+
+
+
+ 12000.0
+
+
+
+
+ 11500.0
+
+
+
+
+
+
+
+ US MO Missouri SUTA Rate
+ us_mo_suta_rate
+
+
+
+
+ 2.376
+
+
+
+
+ 2.376
+
+
+
+
+
+
+ US MO Missouri SIT Rate Table
+ us_mo_sit_rate
+
+
+
+
+ [
+ (1053.0, 1.5),
+ (1053.0, 2.0),
+ (1053.0, 2.5),
+ (1053.0, 3.0),
+ (1053.0, 3.5),
+ (1053.0, 4.0),
+ (1053.0, 4.5),
+ (1053.0, 5.0),
+ ( 'inf', 5.4),
+ ]
+
+
+
+
+ [
+ (1073.0, 1.5),
+ (1073.0, 2.0),
+ (1073.0, 2.5),
+ (1073.0, 3.0),
+ (1073.0, 3.5),
+ (1073.0, 4.0),
+ (1073.0, 4.5),
+ (1073.0, 5.0),
+ ( 'inf', 5.4),
+ ]
+
+
+
+
+
+
+ US MO Missouri SIT Deduction
+ us_mo_sit_deduction
+
+
+
+
+ {
+ 'single': 12400.0,
+ 'married': 24800.0,
+ 'head_of_household': 18650.0,
+ }
+
+
+
+
+ {
+ 'single': 12400.0,
+ 'married': 24800.0,
+ 'head_of_household': 18650.0,
+ }
+
+
+
+
+
+
+
+ US Missouri - Department of Taxation - Unemployment Tax
+
+
+
+ US Missouri - Department of Taxation - Income Tax
+
+
+
+
+
+
+
+
+
+ ER: US MO Missouri State Unemployment
+ ER_US_MO_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mo_suta_wage_base', rate='us_mo_suta_rate', state_code='MO')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mo_suta_wage_base', rate='us_mo_suta_rate', state_code='MO')
+
+
+
+
+
+
+
+
+ EE: US MO Missouri State Income Tax Withholding
+ EE_US_MO_SIT
+ python
+ result, _ = mo_missouri_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = mo_missouri_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 b487d6cd..f74cf922 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -13,6 +13,7 @@ from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
from .state.ga_georgia import ga_georgia_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.oh_ohio import oh_ohio_state_income_withholding
@@ -51,6 +52,7 @@ class HRPayslip(models.Model):
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
'ga_georgia_state_income_withholding': ga_georgia_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,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
diff --git a/l10n_us_hr_payroll/models/state/mo_missouri.py b/l10n_us_hr_payroll/models/state/mo_missouri.py
new file mode 100644
index 00000000..c6018df0
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/mo_missouri.py
@@ -0,0 +1,52 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .general import _state_applies
+
+
+def mo_missouri_state_income_withholding(payslip, categories, worked_days, inputs):
+ """
+ Returns SIT eligible wage and rate.
+ WAGE = GROSS + DED_FIT_EXEMPT
+
+ :return: result, result_rate (wage, percent)
+ """
+ state_code = 'MO'
+ if not _state_applies(payslip, state_code):
+ return 0.0, 0.0
+
+ filing_status = payslip.contract_id.us_payroll_config_value('mo_mow4_sit_filing_status')
+ if not filing_status:
+ return 0.0, 0.0
+
+ # Determine Wage
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ reduced_withholding = payslip.contract_id.us_payroll_config_value('mo_mow4_sit_withholding')
+ if reduced_withholding:
+ return wage, -((reduced_withholding / wage) * 100.0)
+
+ pay_periods = payslip.dict.get_pay_periods_in_year()
+ additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ sit_table = payslip.rule_parameter('us_mo_sit_rate')
+ deduction = payslip.rule_parameter('us_mo_sit_deduction')[filing_status]
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ gross_taxable_income = wage * pay_periods
+ gross_taxable_income -= deduction
+
+ remaining_taxable_income = gross_taxable_income
+ withholding = 0.0
+ for amt, rate in sit_table:
+ amt = float(amt)
+ rate = rate / 100.0
+ if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0:
+ withholding += rate * amt
+ else:
+ withholding += rate * remaining_taxable_income
+ break
+ remaining_taxable_income = remaining_taxable_income - amt
+
+ withholding /= pay_periods
+ withholding += additional
+ withholding = round(withholding)
+ 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 b3663dc5..eea9e916 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -64,6 +64,14 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ mo_mow4_sit_filing_status = fields.Selection([
+ ('', 'Exempt'),
+ ('single', 'Single or Married Spouse Works or Married Filing Separate'),
+ ('married', 'Married (Spouse does not work)'),
+ ('head_of_household', 'Head of Household'),
+ ], string='Missouri W-4 Filing Status', help='MO W-4 1.')
+ mo_mow4_sit_withholding = fields.Integer(string='Missouri MO W-4 Reduced Withholding', help='MO W-4 3.')
+
ms_89_350_sit_filing_status = fields.Selection([
('', 'Exempt'),
('single', 'Single'),
@@ -93,4 +101,4 @@ class HRContractUSPayrollConfig(models.Model):
va_va4_sit_exemptions = fields.Integer(string='Virginia VA-4(P) Personal Exemptions',
help='VA-4(P) 1(a)')
va_va4_sit_other_exemptions = fields.Integer(string='Virginia VA-4(P) Age & Blindness Exemptions',
- help='VA-4(P) 1(b)')
+ help='VA-4(P) 1(b)')
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 3e4cb355..6275549a 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -10,6 +10,9 @@ from . import test_us_fl_florida_payslip_2020
from . import test_us_ga_georgia_payslip_2019
from . import test_us_ga_georgia_payslip_2020
+from . import test_us_mo_missouri_payslip_2019
+from . import test_us_mo_missouri_payslip_2020
+
from . import test_us_ms_mississippi_payslip_2019
from . import test_us_ms_mississippi_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py
new file mode 100755
index 00000000..27a0ad93
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py
@@ -0,0 +1,188 @@
+
+from datetime import date
+from .common import TestUsPayslip
+
+
+class TestUsMoPayslip(TestUsPayslip):
+ # Calculations from http://dor.mo.gov/forms/4282_2019.pdf
+ SALARY = 12000.0
+ MO_UNEMP = -2.376 / 100.0
+
+ TAX = [
+ (1053.0, 1.5),
+ (1053.0, 2.0),
+ (1053.0, 2.5),
+ (1053.0, 3.0),
+ (1053.0, 3.5),
+ (1053.0, 4.0),
+ (1053.0, 4.5),
+ (1053.0, 5.0),
+ (999999999.0, 5.4),
+ ]
+
+ def test_2019_taxes_single(self):
+ # Payroll Period Monthly
+ salary = self.SALARY
+ pp = 12.0
+ gross_salary = salary * pp
+ spouse_employed = False
+
+ # Single
+ standard_deduction = 12400.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MO'),
+ mo_mow4_sit_filing_status='single',
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='monthly')
+
+ self._log('2019 Missouri tax single 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.MO_UNEMP)
+
+ mo_taxable_income = gross_salary - standard_deduction
+ self._log('%s = %s - %s -' % (mo_taxable_income, gross_salary, standard_deduction))
+
+ remaining_taxable_income = mo_taxable_income
+ tax = 0.0
+ for amt, rate in self.TAX:
+ amt = float(amt)
+ rate = rate / 100.0
+ self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income))
+ if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0:
+ tax += rate * amt
+ else:
+ tax += rate * remaining_taxable_income
+ break
+ remaining_taxable_income = remaining_taxable_income - amt
+
+ tax = -tax
+ self._log('Computed annual tax: ' + str(tax))
+
+ tax = tax / pp
+ tax = round(tax)
+ self._log('Computed period tax: ' + str(tax))
+ self.assertPayrollEqual(cats['EE_US_SIT'], tax)
+
+ def test_2019_spouse_not_employed(self):
+ # Payroll Period Semi-monthly
+ salary = self.SALARY
+ pp = 24.0
+ gross_salary = salary * pp
+
+ # Married
+ standard_deduction = 24800.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MO'),
+ mo_mow4_sit_filing_status='married',
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 Missouri tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ mo_taxable_income = gross_salary - standard_deduction
+ self._log(mo_taxable_income)
+
+ remaining_taxable_income = mo_taxable_income
+ tax = 0.0
+ for amt, rate in self.TAX:
+ amt = float(amt)
+ rate = rate / 100.0
+ self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income))
+ if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0:
+ tax += rate * amt
+ else:
+ tax += rate * remaining_taxable_income
+ break
+ remaining_taxable_income = remaining_taxable_income - amt
+
+ tax = -tax
+ self._log('Computed annual tax: ' + str(tax))
+
+ tax = tax / pp
+ tax = round(tax)
+ self._log('Computed period tax: ' + str(tax))
+ self.assertPayrollEqual(cats['EE_US_SIT'], tax)
+
+ def test_2019_head_of_household(self):
+ # Payroll Period Weekly
+ salary = self.SALARY
+
+ # Payroll Period Weekly
+ salary = self.SALARY
+ pp = 52.0
+ gross_salary = salary * pp
+
+ # Single HoH
+ standard_deduction = 18650.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MO'),
+ mo_mow4_sit_filing_status='head_of_household',
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='weekly')
+
+ self._log('2019 Missouri tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ mo_taxable_income = gross_salary - standard_deduction
+ self._log(mo_taxable_income)
+
+ remaining_taxable_income = mo_taxable_income
+ tax = 0.0
+ for amt, rate in self.TAX:
+ amt = float(amt)
+ rate = rate / 100.0
+ self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income))
+ if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0:
+ tax += rate * amt
+ else:
+ tax += rate * remaining_taxable_income
+ break
+ remaining_taxable_income = remaining_taxable_income - amt
+ tax = -tax
+ self._log('Computed annual tax: ' + str(tax))
+
+ tax = tax / pp
+ tax = round(tax)
+ self._log('Computed period tax: ' + str(tax))
+ self.assertPayrollEqual(cats['EE_US_SIT'], tax)
+
+ def test_2019_underflow(self):
+ # Payroll Period Weekly
+ salary = 200.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MO'))
+
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['EE_US_SIT'], 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py
new file mode 100755
index 00000000..164b0f0f
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py
@@ -0,0 +1,105 @@
+
+from datetime import date
+from .common import TestUsPayslip
+
+
+class TestUsMoPayslip(TestUsPayslip):
+ # Calculations from http://dor.mo.gov/forms/4282_2020.pdf
+ MO_UNEMP_MAX_WAGE = 11500.0
+ MO_UNEMP = 2.376
+
+ TAX = [
+ (1073.0, 1.5),
+ (1073.0, 2.0),
+ (1073.0, 2.5),
+ (1073.0, 3.0),
+ (1073.0, 3.5),
+ (1073.0, 4.0),
+ (1073.0, 4.5),
+ (1073.0, 5.0),
+ ( 'inf', 5.4),
+ ]
+ STD_DED = {
+ '': 0.0, # Exempt
+ 'single': 12400.0,
+ 'married': 24800.0,
+ 'head_of_household': 18650.0,
+ }
+
+ def _test_sit(self, filing_status, schedule_pay):
+ wage = 5000.0
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ state_id=self.get_us_state('MO'),
+ mo_mow4_sit_filing_status=filing_status,
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay=schedule_pay)
+
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ pp = payslip.get_pay_periods_in_year()
+ gross_salary = wage * pp
+ standard_deduction = self.STD_DED[filing_status]
+
+ mo_taxable_income = gross_salary - standard_deduction
+ self._log('%s = %s - %s -' % (mo_taxable_income, gross_salary, standard_deduction))
+
+ remaining_taxable_income = mo_taxable_income
+ tax = 0.0
+ for amt, rate in self.TAX:
+ amt = float(amt)
+ rate = rate / 100.0
+ self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income))
+ if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0:
+ tax += rate * amt
+ else:
+ tax += rate * remaining_taxable_income
+ break
+ remaining_taxable_income = remaining_taxable_income - amt
+
+ tax = -tax
+ self._log('Computed annual tax: ' + str(tax))
+
+ tax = tax / pp
+ tax = round(tax)
+ self._log('Computed period tax: ' + str(tax))
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), tax if filing_status else 0.0)
+
+ contract.us_payroll_config_id.state_income_tax_additional_withholding = 100.0
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), (tax - 100.0) if filing_status else 0.0)
+
+ contract.us_payroll_config_id.mo_mow4_sit_withholding = 200.0
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), -200.0 if filing_status else 0.0)
+
+ def test_2020_taxes_single(self):
+ self._test_er_suta('MO', self.MO_UNEMP, date(2020, 1, 1), wage_base=self.MO_UNEMP_MAX_WAGE)
+ self._test_sit('single', 'weekly')
+
+ def test_2020_spouse_not_employed(self):
+ self._test_sit('married', 'semi-monthly')
+
+ def test_2020_head_of_household(self):
+ self._test_sit('head_of_household', 'monthly')
+
+ def test_2020_underflow(self):
+ # Payroll Period Weekly
+ salary = 200.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MO'))
+
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['EE_US_SIT'], 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 f94f820b..f38baedc 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -55,6 +55,12 @@
+
+ Form MO W-4 - State Income Tax
+
+
+
+
Form 89-350 - State Income Tax