diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index fb47d741..060f2657 100755
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -29,6 +29,7 @@ USA 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',
'data/final.xml',
'views/hr_contract_views.xml',
diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml
index d266884a..d8756337 100644
--- a/l10n_us_hr_payroll/data/final.xml
+++ b/l10n_us_hr_payroll/data/final.xml
@@ -17,6 +17,9 @@
ref('hr_payroll_rule_ee_fed_941_fit'),
ref('hr_payroll_rule_er_us_fl_suta'),
+ ref('hr_payroll_rule_er_us_mt_suta'),
+ ref('hr_payroll_rule_er_us_mt_suta_aft'),
+ ref('hr_payroll_rule_ee_us_mt_sit'),
ref('hr_payroll_rule_er_us_pa_suta'),
ref('hr_payroll_rule_ee_us_pa_suta'),
ref('hr_payroll_rule_ee_us_pa_sit'),
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..b757e11d
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/mt_montana.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+ US MT Montana SUTA Wage Base
+ us_mt_suta_wage_base
+ 33000.00
+
+
+
+ US MT Montana SUTA Wage Base
+ us_mt_suta_wage_base
+ 34100.00
+
+
+
+
+
+
+
+ US MT Montana SUTA Rate (UI)
+ us_mt_suta_rate
+ 1.18
+
+
+
+ US MT Montana SUTA Rate (UI)
+ us_mt_suta_rate
+ 1.18
+
+
+
+
+
+
+ US MT Montana SUTA Administrative Fund Tax Rate
+ us_mt_suta_aft_rate
+ 0.13
+
+
+
+ US MT Montana SUTA Administrative Fund Tax Rate
+ us_mt_suta_aft_rate
+ 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 Labor & Industries
+
+
+
+
+ US Montana - Department of Revenue - Income Tax
+
+
+
+ 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/migrations/12.0.2020.1.0/post-migration.py b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py
index 95984afb..3f4499ba 100644
--- a/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py
+++ b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py
@@ -1,6 +1,7 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from odoo.addons.l10n_us_hr_payroll.migrations.data import FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020
+from odoo.addons.l10n_us_hr_payroll.migrations.data import FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020, \
+ XMLIDS_COPY_ACCOUNTING_2020
from odoo.addons.l10n_us_hr_payroll.migrations.helper import field_exists, \
temp_field_exists, \
remove_temp_field, \
@@ -68,3 +69,17 @@ def migrate(cr, installed_version):
for field in fields_to_move:
remove_temp_field(cr, 'hr_contract', field)
+
+ # Some added rules should have the same accounting side effects of other migrated rules
+ # To ease the transition, we will copy the accounting fields from one to the other.
+ for source, destinations in XMLIDS_COPY_ACCOUNTING_2020.items():
+ source_rule = env.ref(source, raise_if_not_found=False)
+ if source_rule:
+ for destination in destinations:
+ destination_rule = env.ref(destination, raise_if_not_found=False)
+ if destination_rule:
+ _logger.warn('Mirgrating accounting from rule: ' + source + ' to rule: ' + destination)
+ destination_rule.write({
+ 'account_debit': source_rule.account_debit.id,
+ 'account_credit': source_rule.account_credit.id,
+ })
diff --git a/l10n_us_hr_payroll/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py
index 99534b57..8a1a475c 100644
--- a/l10n_us_hr_payroll/migrations/data.py
+++ b/l10n_us_hr_payroll/migrations/data.py
@@ -9,6 +9,9 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = {
'fica_exempt': 'fed_941_fica_exempt',
'futa_type': 'fed_940_type',
# State
+ 'mt_mw4_additional_withholding': 'state_income_tax_additional_withholding',
+ 'mt_mw4_exemptions': 'mt_mw4_sit_exemptions',
+ 'mt_mw4_exempt': 'mt_mw4_sit_exempt',
'pa_additional_withholding': 'state_income_tax_additional_withholding',
}
@@ -32,6 +35,10 @@ XMLIDS_TO_REMOVE_2020 = [
'l10n_us_fl_hr_payroll.hr_payroll_fl_unemp_wages',
'l10n_us_fl_hr_payroll.hr_payroll_fl_unemp',
'l10n_us_fl_hr_payroll.hr_payroll_rules_fl_unemp_wages_2018',
+ 'l10n_us_mt_hr_payroll.hr_payroll_mt_unemp_wages',
+ 'l10n_us_mt_hr_payroll.hr_payroll_mt_unemp',
+ 'l10n_us_mt_hr_payroll.hr_payroll_mt_income_withhold',
+ 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_unemp_wages',
'l10n_us_pa_hr_payroll.res_partner_pador_unemp_employee',
'l10n_us_pa_hr_payroll.contrib_register_pador_unemp_employee',
'l10n_us_pa_hr_payroll.hr_payroll_pa_unemp_wages',
@@ -59,6 +66,12 @@ XMLIDS_TO_RENAME_2020 = {
'l10n_us_fl_hr_payroll.hr_payroll_rules_fl_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_fl_suta',
'l10n_us_fl_hr_payroll.res_partner_fldor': 'l10n_us_hr_payroll.res_partner_us_fl_dor',
'l10n_us_fl_hr_payroll.contrib_register_fldor': 'l10n_us_hr_payroll.contrib_register_us_fl_dor',
+ 'l10n_us_mt_hr_payroll.res_partner_mtdor_unemp': 'l10n_us_hr_payroll.res_partner_us_mt_dor',
+ 'l10n_us_mt_hr_payroll.res_partner_mtdor_withhold': 'l10n_us_hr_payroll.res_partner_us_mt_dor_sit',
+ 'l10n_us_mt_hr_payroll.contrib_register_mtdor_unemp': 'l10n_us_hr_payroll.contrib_register_us_mt_dor',
+ 'l10n_us_mt_hr_payroll.contrib_register_mtdor_withhold': 'l10n_us_hr_payroll.contrib_register_us_mt_dor_sit',
+ 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_unemp': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta',
+ 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_inc_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_mt_sit',
'l10n_us_pa_hr_payroll.res_partner_pador_unemp_company': 'l10n_us_hr_payroll.res_partner_us_pa_dor',
'l10n_us_pa_hr_payroll.res_partner_pador_withhold': 'l10n_us_hr_payroll.res_partner_us_pa_dor_sit',
'l10n_us_pa_hr_payroll.contrib_register_pador_unemp_company': 'l10n_us_hr_payroll.contrib_register_us_pa_dor',
@@ -66,4 +79,11 @@ XMLIDS_TO_RENAME_2020 = {
'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_unemp_employee_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_pa_suta',
'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_unemp_company_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_pa_suta',
'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_inc_withhold_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_pa_sit',
+
+}
+
+XMLIDS_COPY_ACCOUNTING_2020 = {
+ 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta': [
+ 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta_aft',
+ ],
}
diff --git a/l10n_us_hr_payroll/migrations/helper.py b/l10n_us_hr_payroll/migrations/helper.py
index b41cf304..f5a7a87d 100644
--- a/l10n_us_hr_payroll/migrations/helper.py
+++ b/l10n_us_hr_payroll/migrations/helper.py
@@ -39,7 +39,12 @@ def temp_field_values(cr, table_name, id, field_names):
values = cr.dictfetchone()
if not values:
return {}
- return {k.lstrip(TMP_PREFIX): v for k, v in values.items()}
+
+ def _remove_tmp_prefix(key):
+ if key.startswith(TMP_PREFIX):
+ return key[len(TMP_PREFIX):]
+ return key
+ return {_remove_tmp_prefix(k): v for k, v in values.items()}
"""
diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 90476930..9104c69e 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):
@@ -42,6 +43,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,
}
def get_year(self):
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index fd259f0e..0d11b054 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..fd363301
--- /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.dict.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.dict.contract_id.schedule_pay
+ additional = payslip.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ exemptions = payslip.dict.contract_id.us_payroll_config_value('mt_mw4_sit_exemptions')
+ exemption_rate = payslip.dict.rule_parameter('us_mt_suta_sit_exemption_rate').get(schedule_pay)
+ withholding_rate = payslip.dict.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 4c6e183c..323f1b92 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
+
+
+
+