From dc2e895b40d9bc9354f99410048491977d7e1e9c Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 8 Jan 2020 08:04:07 -0800 Subject: [PATCH] IMP `l10n_us_hr_payroll` Add MT Montana (unemployment (with AFT), income tax) --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/state/mt_montana.xml | 176 ++++++++++++++++++ l10n_us_hr_payroll/models/hr_payslip.py | 2 + l10n_us_hr_payroll/models/state/general.py | 2 +- l10n_us_hr_payroll/models/state/mt_montana.py | 41 ++++ .../models/us_payroll_config.py | 12 ++ l10n_us_hr_payroll/tests/__init__.py | 2 + .../tests/test_us_mt_montana_payslip_2019.py | 139 ++++++++++++++ .../tests/test_us_mt_montana_payslip_2020.py | 17 ++ .../views/us_payroll_config_views.xml | 6 + 10 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 l10n_us_hr_payroll/data/state/mt_montana.xml create mode 100644 l10n_us_hr_payroll/models/state/mt_montana.py create mode 100755 l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 6c3e9aa2..4c9df42a 100644 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -26,6 +26,7 @@ United States of America - Payroll Rules. 'data/federal/fed_941_fit_parameters.xml', 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', + 'data/state/mt_montana.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 new file mode 100644 index 00000000..ec18a955 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/mt_montana.xml @@ -0,0 +1,176 @@ + + + + + US MT Montana SUTA Wage Base + us_mt_suta_wage_base + + + + + 33000.00 + + + + + 34100.00 + + + + + + + + US MT Montana SUTA Rate (UI) + us_mt_suta_rate + + + + + 1.18 + + + + + 1.18 + + + + + + + US MT Montana SUTA Administrative Fund Tax Rate + us_mt_suta_aft_rate + + + + + 0.13 + + + + + 0.13 + + + + + + + US MT Montana SIT Rate Table + us_mt_suta_sit_rate + + + + + { + 'weekly': [ + ( 135.00, 0.0, 1.80), + ( 288.00, 2.0, 4.40), + ( 2308.00, 9.0, 6.00), + ( 'inf', 130.0, 6.60), + ], + 'bi-weekly': [ + ( 269.00, 0.0, 1.80), + ( 577.00, 5.0, 4.40), + ( 4615.00, 18.0, 6.00), + ( 'inf', 261.0, 6.60), + ], + 'semi-monthly': [ + ( 292.00, 0.0, 1.80), + ( 625.00, 5.0, 4.40), + ( 5000.00, 20.0, 6.00), + ( 'inf', 282.0, 6.60), + ], + 'monthly': [ + ( 583.00, 0.0, 1.80), + ( 1250.00, 11.0, 4.40), + ( 10000.00, 40.0, 6.00), + ( 'inf', 565.0, 6.60), + ], + 'annually': [ + ( 7000.00, 0.0, 1.80), + ( 15000.00, 126.0, 4.40), + ( 120000.00, 478.0, 6.00), + ( 'inf', 6778.0, 6.60), + ], + } + + + + + + + US MT Montana SIT Exemption Rate Table + us_mt_suta_sit_exemption_rate + + + + + { + 'weekly': 37.0, + 'bi-weekly': 73.0, + 'semi-monthly': 79.0, + 'monthly': 158.0, + 'annually': 1900.0, + } + + + + + + + + US Montana - Department of Labor & Industries + + + + US Montana - Department of Revenue - Income Tax + + + + + + + + + + ER: US MT Montana State Unemployment (UI-5) + ER_US_MT_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_rate', state_code='MT') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_rate', state_code='MT') + + + + + + + + + ER: US MT Montana State Unemployment Administrative Fund Tax (AFT) (UI-5) + ER_US_MT_SUTA_AFT + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_aft_rate', state_code='MT') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_aft_rate', state_code='MT') + + + + + + + + + EE: US MT Montana State Income Tax Withholding (MW-3) + EE_US_MT_SIT + python + result, _ = mt_montana_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = mt_montana_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 7d92f5b4..93e4f64b 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -11,6 +11,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ ee_us_941_fit from .state.general import general_state_unemployment, \ general_state_income_withholding +from .state.mt_montana import mt_montana_state_income_withholding class HRPayslip(models.Model): @@ -41,6 +42,7 @@ class HRPayslip(models.Model): 'ee_us_941_fit': ee_us_941_fit, 'general_state_unemployment': general_state_unemployment, 'general_state_income_withholding': general_state_income_withholding, + 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, }) return res diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py index eff29da8..faf3f477 100644 --- a/l10n_us_hr_payroll/models/state/general.py +++ b/l10n_us_hr_payroll/models/state/general.py @@ -96,7 +96,7 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba def general_state_income_withholding(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): """ - Returns SUTA eligible wage and rate. + Returns SIT eligible wage and rate. WAGE = GROSS - WAGE_US_941_FIT_EXEMPT :return: result, result_rate (wage, percent) diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py new file mode 100644 index 00000000..727c56e9 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/mt_montana.py @@ -0,0 +1,41 @@ +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 + + :return: result, result_rate (wage, percent) + """ + state_code = 'MT' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + if payslip.contract_id.us_payroll_config_value('mt_mw4_sit_exempt'): + return 0.0, 0.0 + + # Determine Wage + wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT + 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) + if not exemption_rate or not withholding_rate or wage == 0.0: + return 0.0, 0.0 + + adjusted_wage = wage - (exemption_rate * (exemptions or 0)) + withholding = 0.0 + if adjusted_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 adjusted_wage < wage_cap: + withholding = round(base + ((rate / 100.0) * (adjusted_wage - prior_wage_cap))) + break + prior_wage_cap = wage_cap + 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 e75b5210..b90af562 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -46,3 +46,15 @@ class HRContractUSPayrollConfig(models.Model): help='Form W4 (2020+) 4(b)') fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]', help='Form W4 (2020+) 4(c)') + + mt_mw4_sit_exemptions = fields.Integer(string='Montana MW-4 Exemptions', + help='MW-4 Box G') + # Don't use the main state_income_tax_exempt because of special meaning and reporting + # Use additional withholding but name it on the form 'MW-4 Box H' + mt_mw4_sit_exempt = fields.Selection([ + ('', 'Not Exempt'), + ('tribe', 'Registered Tribe'), + ('reserve', 'Reserve or National Guard'), + ('north_dakota', 'North Dakota'), + ('montana_for_marriage', 'Montana for Marriage'), + ], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2') diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 57f8e5e1..cc0a4321 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -6,5 +6,7 @@ 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_pa_pennsylvania_payslip_2019 from . import test_us_pa_pennsylvania_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py new file mode 100755 index 00000000..ff6e2daf --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py @@ -0,0 +1,139 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsMtPayslip(TestUsPayslip): + # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705 + MT_UNEMP = -1.18 / 100.0 + MT_UNEMP_AFT = -0.13 / 100.0 + + def test_2019_taxes_one(self): + # Payroll Period Semi-Monthly example + salary = 550 + mt_mw4_exemptions = 5 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='semi-monthly') + + self._log('2019 Montana 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.MT_UNEMP + self.MT_UNEMP_AFT)) # New non-combined... + + mt_taxable_income = salary - (79.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 155.0) + self.assertPayrollEqual(mt_withhold, 3.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_two(self): + # Payroll Period Bi-Weekly example + salary = 2950 + mt_mw4_exemptions = 2 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='bi-weekly') + + self._log('2019 Montana 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'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2)) + + # Note!! + # The example calculation uses A = 16 but the actual table describes this as A = 18 + mt_taxable_income = salary - (73.0 * mt_mw4_exemptions) + mt_withhold = round(18 + (0.06 * (mt_taxable_income - 577))) + self.assertPayrollEqual(mt_taxable_income, 2804.0) + self.assertPayrollEqual(mt_withhold, 152.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_three(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='weekly') + + self._log('2019 Montana 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'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2)) + + mt_taxable_income = salary - (37.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 98.0) + self.assertPayrollEqual(mt_withhold, 2.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_three_exempt(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + mt_mw4_sit_exempt='reserve', + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2019_taxes_three_additional(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + mt_mw4_additional_withholding = 20.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + state_income_tax_additional_withholding=mt_mw4_additional_withholding, + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + cats = self._getCategories(payslip) + + mt_taxable_income = salary - (37.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 98.0) + self.assertPayrollEqual(mt_withhold, 2.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold + -mt_mw4_additional_withholding) diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py new file mode 100755 index 00000000..ec861a0d --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py @@ -0,0 +1,17 @@ +# 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 TestUsMtPayslip(TestUsPayslip): + # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705 + MT_UNEMP_WAGE_MAX = 34100.0 + MT_UNEMP = 1.18 + MT_UNEMP_AFT = 0.13 + + def test_2020_taxes_one(self): + combined_rate = self.MT_UNEMP + self.MT_UNEMP_AFT # Combined for test as they both go to the same category and have the same cap + self._test_er_suta('MT', combined_rate, date(2020, 1, 1), wage_base=self.MT_UNEMP_WAGE_MAX) + + # TODO Montana Incometax rates for 2020 when released 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 701920b3..846f5ca4 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -44,6 +44,12 @@

No additional fields.

+ +

Form MT-4 - State Income Tax

+ + + +