diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 54791cef..9302a934 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/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/final.xml b/l10n_us_hr_payroll/data/final.xml index 9bdeeef6..c571fea5 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -21,6 +21,9 @@ ref('hr_payroll_rule_er_us_ga_suta'), ref('hr_payroll_rule_ee_us_ga_sit'), + ref('hr_payroll_rule_er_us_mo_suta'), + ref('hr_payroll_rule_ee_us_mo_sit'), + ref('hr_payroll_rule_er_us_ms_suta'), ref('hr_payroll_rule_ee_us_ms_sit'), 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..cd6ccc28 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/mo_missouri.xml @@ -0,0 +1,141 @@ + + + + + + US MO Missouri SUTA Wage Base + us_mo_suta_wage_base + 12000.0 + + + + US MO Missouri SUTA Wage Base + us_mo_suta_wage_base + 11500.0 + + + + + + + + US MO Missouri SUTA Rate + us_mo_suta_rate + 2.376 + + + + US MO Missouri SUTA Rate + us_mo_suta_rate + 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), + ] + + + + US MO Missouri SIT Rate Table + us_mo_sit_rate + [ + (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, + } + + + + US MO Missouri SIT Deduction + us_mo_sit_deduction + { + 'single': 12400.0, + 'married': 24800.0, + 'head_of_household': 18650.0, + } + + + + + + + US Missouri - Department of Taxation - Unemployment Tax + + + + US Missouri - Department of Taxation - Unemployment Tax + + + + + US Missouri - Department of Taxation - Income 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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index d6dd22b0..420d864c 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -14,6 +14,9 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { 'ga_g4_additional_allowances': 'ga_g4_sit_additional_allowances', 'ga_g4_additional_wh': 'state_income_tax_additional_withholding', + 'mo_mow4_filing_status': 'mo_mow4_sit_filing_status', + 'mo_mow4_additional_withholding': 'state_income_tax_additional_withholding', + 'ms_89_350_filing_status': 'ms_89_350_sit_filing_status', 'ms_89_350_exemption': 'ms_89_350_sit_exemption_value', 'ms_89_350_additional_withholding': 'state_income_tax_additional_withholding', @@ -57,6 +60,11 @@ XMLIDS_TO_REMOVE_2020 = [ 'l10n_us_ga_hr_payroll.hr_payroll_ga_income_withhold', 'l10n_us_ga_hr_payroll.hr_payroll_rules_ga_unemp_wages', + 'l10n_us_mo_hr_payroll.hr_payroll_mo_unemp_wages', + 'l10n_us_mo_hr_payroll.hr_payroll_mo_unemp', + 'l10n_us_mo_hr_payroll.hr_payroll_mo_income_withhold', + 'l10n_us_mo_hr_payroll.hr_payroll_rules_mo_unemp_wages_2018', + 'l10n_us_ms_hr_payroll.hr_payroll_ms_unemp_wages', 'l10n_us_ms_hr_payroll.hr_payroll_ms_unemp', 'l10n_us_ms_hr_payroll.hr_payroll_ms_income_withhold', @@ -126,6 +134,13 @@ XMLIDS_TO_RENAME_2020 = { 'l10n_us_ga_hr_payroll.hr_payroll_rules_ga_unemp': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_ga_suta', 'l10n_us_ga_hr_payroll.hr_payroll_rules_ga_inc_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_ga_sit', + 'l10n_us_mo_hr_payroll.res_partner_modor_unemp': 'l10n_us_hr_payroll.res_partner_us_mo_dor', + 'l10n_us_mo_hr_payroll.res_partner_modor_withhold': 'l10n_us_hr_payroll.res_partner_us_mo_dor_sit', + 'l10n_us_mo_hr_payroll.contrib_register_modor_unemp': 'l10n_us_hr_payroll.contrib_register_us_mo_dor', + 'l10n_us_mo_hr_payroll.contrib_register_modor_withhold': 'l10n_us_hr_payroll.contrib_register_us_mo_dor_sit', + 'l10n_us_mo_hr_payroll.hr_payroll_rules_mo_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mo_suta', + 'l10n_us_mo_hr_payroll.hr_payroll_rules_mo_inc_withhold_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_mo_sit', + 'l10n_us_ms_hr_payroll.res_partner_msdor_unemp': 'l10n_us_hr_payroll.res_partner_us_ms_dor', 'l10n_us_ms_hr_payroll.res_partner_msdor_withhold': 'l10n_us_hr_payroll.res_partner_us_ms_dor_sit', 'l10n_us_ms_hr_payroll.contrib_register_msdor_unemp': 'l10n_us_hr_payroll.contrib_register_us_ms_dor', diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index cdcb7208..33e263fa 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -15,6 +15,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 @@ -54,6 +55,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..7819fc7b --- /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.dict.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.dict.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.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + sit_table = payslip.dict.rule_parameter('us_mo_sit_rate') + deduction = payslip.dict.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 5ff26656..809fa233 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 247309b5..e46f4a38 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