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
+
+
+
+