diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 5e752f94..8a54c1e5 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/mi_michigan.xml',
'data/state/mo_missouri.xml',
'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
diff --git a/l10n_us_hr_payroll/data/state/mi_michigan.xml b/l10n_us_hr_payroll/data/state/mi_michigan.xml
new file mode 100644
index 00000000..1ce32483
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/mi_michigan.xml
@@ -0,0 +1,99 @@
+
+
+
+
+ US MI Michigan SUTA Wage Base
+ us_mi_suta_wage_base
+
+
+
+
+ 9500.0
+
+
+
+
+ 9000.0
+
+
+
+
+
+
+
+ US MI Michigan SUTA Rate
+ us_mi_suta_rate
+
+
+
+
+ 2.7
+
+
+
+
+ 2.7
+
+
+
+
+
+
+ US MI Michigan Exemption Rate
+ us_mi_sit_exemption_rate
+
+
+
+
+ 4400.0
+
+
+
+
+ 4750.0
+
+
+
+
+
+
+
+ US Michigan - Unemployment Insurance Agency - Unemployment Tax
+
+
+
+ US Michigan - Department of Treasury - Income Tax
+
+
+
+
+
+
+
+
+
+ ER: US MI Michigan State Unemployment
+ ER_US_MI_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mi_suta_wage_base', rate='us_mi_suta_rate', state_code='MI')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mi_suta_wage_base', rate='us_mi_suta_rate', state_code='MI')
+
+
+
+
+
+
+
+
+ EE: US MI Michigan State Income Tax Withholding
+ EE_US_MI_SIT
+ python
+ result, _ = mi_michigan_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = mi_michigan_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 7e4a32d0..56fd8981 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.mi_michigan import mi_michigan_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
@@ -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,
+ 'mi_michigan_state_income_withholding': mi_michigan_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,
diff --git a/l10n_us_hr_payroll/models/state/mi_michigan.py b/l10n_us_hr_payroll/models/state/mi_michigan.py
new file mode 100644
index 00000000..ba556c06
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/mi_michigan.py
@@ -0,0 +1,35 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .general import _state_applies
+
+
+def mi_michigan_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 = 'MI'
+ if not _state_applies(payslip, state_code):
+ return 0.0, 0.0
+
+ if payslip.contract_id.us_payroll_config_value('state_income_tax_exempt'):
+ return 0.0, 0.0
+
+ # Determine Wage
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ pay_periods = payslip.dict.get_pay_periods_in_year()
+ additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ exemption_rate = payslip.rule_parameter('us_mi_sit_exemption_rate')
+ exemption = payslip.contract_id.us_payroll_config_value('mi_w4_sit_exemptions')
+
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ annual_exemption = (exemption * exemption_rate) / pay_periods
+ withholding = ((wage - annual_exemption) * 0.0425)
+ if withholding < 0.0:
+ withholding = 0.0
+ 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 49d1169b..3e6950d4 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -64,6 +64,8 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ mi_w4_sit_exemptions = fields.Integer(string='Michigan MI W-4 Exemptions', help='MI-W4 6.')
+
mo_mow4_sit_filing_status = fields.Selection([
('', 'Exempt'),
('single', 'Single or Married Spouse Works or Married Filing Separate'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 3844fbcf..65616a6a 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_mi_michigan_payslip_2019
+from . import test_us_mi_michigan_payslip_2020
+
from . import test_us_mo_missouri_payslip_2019
from . import test_us_mo_missouri_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2019.py
new file mode 100755
index 00000000..b12baed2
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2019.py
@@ -0,0 +1,194 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsMIPayslip(TestUsPayslip):
+ # Taxes and Rates
+ MI_UNEMP_MAX_WAGE = 9500.0
+ MI_UNEMP = - 2.7 / 100.0
+ MI_INC_TAX = - 4.25 / 100.0
+ ANNUAL_EXEMPTION_AMOUNT = 4400.00
+ PAY_PERIOD_DIVISOR = {
+ 'weekly': 52.0,
+ 'bi-weekly': 26.0,
+ 'semi-monthly': 24.0,
+ 'monthly': 12.0
+ }
+
+ def test_2019_taxes_weekly(self):
+ salary = 5000.0
+ schedule_pay = 'weekly'
+ exemptions = 1
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MI'),
+ state_income_tax_additional_withholding=0.0,
+ mi_w4_sit_exemptions=1.0,
+ schedule_pay='weekly')
+
+ allowance_amount = self.ANNUAL_EXEMPTION_AMOUNT / self.PAY_PERIOD_DIVISOR[schedule_pay]
+ wh = -((salary - (allowance_amount * exemptions)) * -self.MI_INC_TAX)
+
+ self._log('2019 Michigan tax first payslip weekly:')
+ 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.MI_UNEMP)
+ self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh)
+ #
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+ remaining_MI_UNEMP_wages = self.MI_UNEMP_MAX_WAGE - salary if (self.MI_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 Michigan tax second payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_MI_UNEMP_wages * self.MI_UNEMP)
+
+ def test_2019_taxes_biweekly(self):
+ salary = 5000.0
+ schedule_pay = 'bi-weekly'
+ allowance_amount = self.ANNUAL_EXEMPTION_AMOUNT / self.PAY_PERIOD_DIVISOR[schedule_pay]
+ exemption = 2
+
+ wh = -((salary - (allowance_amount * exemption)) * -self.MI_INC_TAX)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MI'),
+ state_income_tax_additional_withholding=0.0,
+ mi_w4_sit_exemptions=2.0,
+ schedule_pay='bi-weekly')
+
+ self._log('2019 Michigan tax first payslip bi-weekly:')
+ 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.MI_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+ remaining_MI_UNEMP_wages = self.MI_UNEMP_MAX_WAGE - salary if (self.MI_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 Michigan tax second payslip bi-weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_MI_UNEMP_wages * self.MI_UNEMP)
+
+ def test_2019_taxes_semimonthly(self):
+ salary = 5000.0
+ schedule_pay = 'semi-monthly'
+ allowance_amount = self.ANNUAL_EXEMPTION_AMOUNT / self.PAY_PERIOD_DIVISOR[schedule_pay]
+ exemption = 1
+
+ wh = -((salary - (allowance_amount * exemption)) * -self.MI_INC_TAX)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MI'),
+ state_income_tax_additional_withholding=0.0,
+ mi_w4_sit_exemptions=1.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 Michigan tax first payslip semi-monthly:')
+ 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.MI_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+ remaining_MI_UNEMP_wages = self.MI_UNEMP_MAX_WAGE - salary if (self.MI_UNEMP_MAX_WAGE - 2 * salary < salary) \
+ else salary
+
+ self._log('2019 Michigan tax second payslip semi-monthly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_MI_UNEMP_wages * self.MI_UNEMP)
+
+ def test_2019_taxes_monthly(self):
+ salary = 5000.0
+ schedule_pay = 'monthly'
+ allowance_amount = self.ANNUAL_EXEMPTION_AMOUNT / self.PAY_PERIOD_DIVISOR[schedule_pay]
+ exemption = 1
+
+ wh = -((salary - (allowance_amount * exemption)) * -self.MI_INC_TAX)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MI'),
+ state_income_tax_additional_withholding=0.0,
+ mi_w4_sit_exemptions=1.0,
+ schedule_pay='monthly')
+
+ self._log('2019 Michigan tax first payslip monthly:')
+ 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.MI_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+ remaining_MI_UNEMP_wages = self.MI_UNEMP_MAX_WAGE - salary if (
+ self.MI_UNEMP_MAX_WAGE - (2 * salary) < salary) \
+ else salary
+
+ self._log('2019 Michigan tax second payslip monthly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_MI_UNEMP_wages * self.MI_UNEMP)
+
+ def test_additional_withholding(self):
+ salary = 5000.0
+ schedule_pay = 'weekly'
+ allowance_amount = 0.0
+ allowance_amount = self.ANNUAL_EXEMPTION_AMOUNT / self.PAY_PERIOD_DIVISOR[schedule_pay]
+ additional_wh = 40.0
+ exemption = 1
+
+ wh = -(((salary - (allowance_amount * exemption)) * -self.MI_INC_TAX) + additional_wh)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MI'),
+ state_income_tax_additional_withholding=40.0,
+ mi_w4_sit_exemptions=1.0,
+ schedule_pay='weekly')
+
+ self._log('2019 Michigan tax first payslip with additional withholding:')
+ 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.MI_UNEMP)
+ self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
diff --git a/l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2020.py
new file mode 100755
index 00000000..7d178d76
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2020.py
@@ -0,0 +1,35 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date, timedelta
+from .common import TestUsPayslip
+
+
+class TestUsMIPayslip(TestUsPayslip):
+ # Taxes and Rates
+ MI_UNEMP_MAX_WAGE = 9000.0
+ MI_UNEMP = 2.7
+
+ def _test_sit(self, wage, exemptions, additional_withholding, schedule_pay, date_start, expected_withholding):
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ state_id=self.get_us_state('MI'),
+ mi_w4_sit_exemptions=exemptions,
+ state_income_tax_additional_withholding=additional_withholding,
+ schedule_pay=schedule_pay)
+ payslip = self._createPayslip(employee, date_start, date_start + timedelta(days=7))
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self._log('Computed period tax: ' + str(expected_withholding))
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), -expected_withholding)
+
+ def test_2020_taxes_example(self):
+ self._test_er_suta('MI', self.MI_UNEMP, date(2020, 1, 1), wage_base=self.MI_UNEMP_MAX_WAGE)
+ self._test_sit(5000.0, 1, 100.0, 'weekly', date(2020, 1, 1), 308.62)
+ self._test_sit(5000.0, 1, 0.0, 'weekly', date(2020, 1, 1), 208.62)
+ self._test_sit(5000.0, 1, 5.0, 'semi-monthly', date(2020, 1, 1), 209.09)
+ self._test_sit(5000.0, 1, 5.0, 'monthly', date(2020, 1, 1), 200.68)
+ self._test_sit(5000.0, 200, 0.0, 'monthly', date(2020, 1, 1), 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 394f3e80..62dfe6b9 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 MI-W4 - State Income Tax
+
+
+
+
Form MO W-4 - State Income Tax