diff --git a/l10n_us_mn_hr_payroll/__init__.py b/l10n_us_mn_hr_payroll/__init__.py new file mode 100755 index 00000000..0650744f --- /dev/null +++ b/l10n_us_mn_hr_payroll/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/l10n_us_mn_hr_payroll/__manifest__.py b/l10n_us_mn_hr_payroll/__manifest__.py new file mode 100755 index 00000000..0f6fc07b --- /dev/null +++ b/l10n_us_mn_hr_payroll/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'USA - Minnesota - Payroll', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Localization', + 'depends': ['l10n_us_hr_payroll'], + 'version': '11.0.2019.0.0', + 'description': """ +USA - Minnesota Payroll Rules +============================= + +* Contribution register and partner for Minnesota Department of Revenue (MDOR) - Income Tax Withholding +* Contribution register and partner for Minnesota Unemployment Insurance (MUI) - Unemployment Taxes +* Contract level Minnesota Exemptions +* Company level Minnesota Unemployment Rate + + """, + 'auto_install': False, + 'website': 'https://hibou.io/', + 'data': [ + 'views/hr_payroll_views.xml', + 'data/base.xml', + 'data/rates.xml', + 'data/rules.xml', + 'data/final.xml', + ], + 'installable': True +} diff --git a/l10n_us_mn_hr_payroll/data/base.xml b/l10n_us_mn_hr_payroll/data/base.xml new file mode 100755 index 00000000..6eb152b7 --- /dev/null +++ b/l10n_us_mn_hr_payroll/data/base.xml @@ -0,0 +1,49 @@ + + + + + + Minnesota Unemployment Insurance (MUI) - Unemployment Tax + 1 + + + + + Minnesota Department of Revenue (MDOR) - Income Tax Withholding + 1 + + + + + + Minnesota Unemployment + Minnesota Unemployment Insurance (MUI) - Unemployment + + + + + Minnesota Income Tax Withholding + Minnesota Department of Revenue (MDOR) - Income Tax Withholding + + + + + + + Wage: US-MN Unemployment + WAGE_US_MN_UNEMP + + + + ER: US-MN Unemployment + ER_US_MN_UNEMP + + + + + EE: US-MN Income Tax Withholding + EE_US_MN_INC_WITHHOLD + + + + diff --git a/l10n_us_mn_hr_payroll/data/final.xml b/l10n_us_mn_hr_payroll/data/final.xml new file mode 100755 index 00000000..7b1cd9ab --- /dev/null +++ b/l10n_us_mn_hr_payroll/data/final.xml @@ -0,0 +1,17 @@ + + + + + + US_MN_EMP + USA Minnesota Employee + + + + + + diff --git a/l10n_us_mn_hr_payroll/data/rates.xml b/l10n_us_mn_hr_payroll/data/rates.xml new file mode 100755 index 00000000..e2aa35da --- /dev/null +++ b/l10n_us_mn_hr_payroll/data/rates.xml @@ -0,0 +1,15 @@ + + + + + + + US MN Unemployment + US_MN_UNEMP + 1.11 + 2019-01-01 + + + + + diff --git a/l10n_us_mn_hr_payroll/data/rules.xml b/l10n_us_mn_hr_payroll/data/rules.xml new file mode 100755 index 00000000..af769d2d --- /dev/null +++ b/l10n_us_mn_hr_payroll/data/rules.xml @@ -0,0 +1,123 @@ + + + + + + + + Wage: US-MN Unemployment + WAGE_US_MN_UNEMP + python + result = (contract.futa_type != contract.FUTA_TYPE_BASIC) + code + +rate = payslip.dict.get_rate('US_MN_UNEMP') +year = int(payslip.dict.date_to[:4]) +ytd = payslip.sum('WAGE_US_MN_UNEMP', str(year) + '-01-01', str(year+1) + '-01-01') +ytd += contract.external_wages +remaining = rate.wage_limit_year - ytd +if remaining <= 0.0: + result = 0 +elif remaining < categories.BASIC: + result = remaining +else: + result = categories.BASIC + + + + + + + + ER: US-MN Unemployment + ER_US_MN_UNEMP + python + result = (contract.futa_type != contract.FUTA_TYPE_BASIC) + code + +rate = payslip.dict.get_rate('US_MN_UNEMP') +result_rate = -rate.rate +result = categories.WAGE_US_MN_UNEMP + +# result_rate of 0 implies 100% due to bug +if result_rate == 0.0: + result = 0.0 + + + + + + + + + EE: US-MN Income Tax Withholding + EE_US_MN_INC_WITHHOLD + python + result = not contract.mn_w4mn_tax_exempt + code + +# Step 1 - Determine Employee's Total Wages for one payroll period. +wages = categories.GROSS +allowances = contract.mn_w4mn_allowances +schedule_pay = contract.schedule_pay + +# Step 2 - Multiply the wages from step by number of payroll periods per year. This gives annual wage. +pay_period = 0.0 +pay_periods = { + 'weekly': 52.0, + 'bi-weekly': 26.0, + 'semi-monthly': 24.0, + 'monthly': 12.0 + } +if schedule_pay in pay_periods: + pay_period = pay_periods[schedule_pay] +else: + raise Exception('Invalid schedule_pay="' + schedule_pay + '" for AR Income Withholding calculation') + +annual_wages = wages * pay_period + +# Step 3 - Multiple the number of employee's withholding allowances by $4,250 +allowance_amount = 4250.00 * allowances + +# Step 4 - Subtract the result in step 3 from the result in step 2. +taxable_wages = annual_wages - allowance_amount + +# Step 5 - Use the result in step 4 (taxable wages) and the tax chart to calculate step 5 amount. +mn_w4_filing_status = contract.mn_w4mn_filing_status +tax_table = [] +if mn_w4_filing_status == 'single': + tax_table = [ + (28920, 2400, 5.35, 0.00), + (89510, 28920, 7.05, 1418.82), + (166290, 89510, 7.85, 5690.42), + (float('inf'), 166290, 9.85, 11717.65) + ] +elif mn_w4_filing_status == 'married': + tax_table = [ + (47820, 9050, 5.35, 0.00), + (163070, 47820, 7.05, 2074.20), + (282200, 163070, 7.85, 10199.33), + (float('inf'), 282200, 9.85, 19551.04) + ] +else: + raise Exception('Invalid w4_filing_status="' + mn_w4_filing_status + '" for MN Income Withholding calculation') + +last = 0.0 +result = 0.0 +for row in tax_table: + cap, subtract_amt, rate, flat_fee = row + if cap > taxable_wages: + taxed_amount = taxable_wages - subtract_amt + result = ((rate / 100.00) * taxed_amount) + flat_fee + break + +# Make the result per pay period once more. +result = result / pay_period + +# Round Result - Make sign negative as well - Add additional withholding here as well +result = -round(result) - contract.mn_w4mn_additional_wh + + + + + diff --git a/l10n_us_mn_hr_payroll/models/__init__.py b/l10n_us_mn_hr_payroll/models/__init__.py new file mode 100644 index 00000000..e99aa24a --- /dev/null +++ b/l10n_us_mn_hr_payroll/models/__init__.py @@ -0,0 +1 @@ +from . import hr_payroll diff --git a/l10n_us_mn_hr_payroll/models/hr_payroll.py b/l10n_us_mn_hr_payroll/models/hr_payroll.py new file mode 100755 index 00000000..a85e19e2 --- /dev/null +++ b/l10n_us_mn_hr_payroll/models/hr_payroll.py @@ -0,0 +1,14 @@ +from odoo import models, fields, api + + +class USMNHrContract(models.Model): + _inherit = 'hr.contract' + + mn_w4mn_allowances = fields.Integer(string="MN Allowances") + mn_w4mn_additional_wh = fields.Float(string="MN Additional Withholding") + mn_w4mn_tax_exempt = fields.Boolean(string="MN Tax Exempt") + mn_w4mn_filing_status = fields.Selection([ + # ('exempt', 'Exempt'), + ('single', 'Single'), + ('married', 'Married'), + ], string='MN Filing Status', default='single') diff --git a/l10n_us_mn_hr_payroll/tests/__init__.py b/l10n_us_mn_hr_payroll/tests/__init__.py new file mode 100755 index 00000000..b6900e72 --- /dev/null +++ b/l10n_us_mn_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_us_mn_payslip diff --git a/l10n_us_mn_hr_payroll/tests/test_us_mn_payslip.py b/l10n_us_mn_hr_payroll/tests/test_us_mn_payslip.py new file mode 100755 index 00000000..cf55afab --- /dev/null +++ b/l10n_us_mn_hr_payroll/tests/test_us_mn_payslip.py @@ -0,0 +1,182 @@ +from odoo.addons.l10n_us_hr_payroll.tests.test_us_payslip import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.l10n_us_hr_payroll import USHrContract + + +class TestUsMNPayslip(TestUsPayslip): + + # TAXES AND RATES + MN_UNEMP_MAX_WAGE = 34000.00 + MN_UNEMP = -1.11 / 100.0 + + def test_taxes_weekly(self): + salary = 30000.00 + schedule_pay = 'weekly' + allowances = 1 + # Hand Calculated Amount to Test + # Step 1 -> 30000.00 for wages per period Step 2 -> 52.0 for weekly -> 30000 * 52 -> 1560000 + # Step 3 -> allowances * 4250.0 -> 4250.00 in this case. + # Step 4 -> Step 2 - Step 3 -> 1560000 - 4250.00 -> 1555750 + # Step 5 -> using chart -> we have last row -> ((1555750 - 166290) * (9.85 / 100)) + 11717.65 -> 148579.46 + # Step 6 -> Convert back to pay period amount and round - > 2857.297 - > 2857.0 + # wh = 2857.0 + wh = -2857.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + struct_id=self.ref('l10n_us_mn_hr_payroll.hr_payroll_salary_structure_us_mn_employee'), + schedule_pay=schedule_pay) + contract.mn_w4mn_allowances = allowances + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 Minnesota tax first payslip weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['WAGE_US_MN_UNEMP'], salary) + self.assertPayrollEqual(cats['ER_US_MN_UNEMP'], cats['WAGE_US_MN_UNEMP'] * self.MN_UNEMP) + self.assertPayrollEqual(cats['EE_US_MN_INC_WITHHOLD'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + remaining_MN_UNEMP_wages = self.MN_UNEMP_MAX_WAGE - salary if (self.MN_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Minnesota tax second payslip weekly:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['WAGE_US_MN_UNEMP'], remaining_MN_UNEMP_wages) + self.assertPayrollEqual(cats['ER_US_MN_UNEMP'], remaining_MN_UNEMP_wages * self.MN_UNEMP) + + def test_taxes_married(self): + salary = 5000.00 + schedule_pay = 'weekly' + allowances = 1 + filing_status = 'married' + + # Hand Calculated Amount to Test + # Step 1 -> 5000.0 for wages per period Step 2 -> 52.0 for weekly -> 5000 * 52 -> 260,000 + # Step 3 -> allowances * 4250.0 -> 4250.00 in this case. + # Step 4 -> Step 2 - Step 3 -> 260,000 - 4250.00 -> 255750.0 + # For step five we used the married section + # Step 5 -> using chart -> we have 2nd last row -> ((255750 - 163070) * (7.85 / 100)) + 10199.33 -> + # Step 6 -> Convert back to pay period amount and round + # wh = 336.0 + wh = -336.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + struct_id=self.ref('l10n_us_mn_hr_payroll.hr_payroll_salary_structure_us_mn_employee'), + schedule_pay=schedule_pay) + contract.mn_w4mn_allowances = allowances + contract.mn_w4mn_filing_status = filing_status + + self.assertEqual(contract.schedule_pay, 'weekly') + self.assertEqual(contract.mn_w4mn_filing_status, 'married') + + self._log('2019 Minnesota tax first payslip married:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['WAGE_US_MN_UNEMP'], salary) + self.assertPayrollEqual(cats['ER_US_MN_UNEMP'], cats['WAGE_US_MN_UNEMP'] * self.MN_UNEMP) + self.assertPayrollEqual(cats['EE_US_MN_INC_WITHHOLD'], wh) + + def test_taxes_semimonthly(self): + salary = 6500.00 + schedule_pay = 'semi-monthly' + mn_w4mn_filing_status = 'single' + allowances = 1 + # Hand Calculated Amount to Test + # Step 1 -> 6500.00 for wages per period Step 2 -> 24 for semi-monthly -> 6500.00 * 24 -> 156000.00 + # Step 3 -> allowances * 4250.0 -> 4250.00 in this case. + # Step 4 -> Step 2 - Step 3 -> 156000.00 - 4250.00 -> 151750.0 + # Step 5 -> using chart -> we have 2nd last row -> ((151750.0- 89510) * (7.85 / 100)) + 5690.42 -> 10576.26 + # Step 6 -> Convert back to pay period amount and round + # wh = -441 + wh = -441.00 + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + struct_id=self.ref('l10n_us_mn_hr_payroll.hr_payroll_salary_structure_us_mn_employee'), + schedule_pay=schedule_pay) + contract.mn_w4mn_allowances = allowances + contract.mn_w4mn_filing_status = mn_w4mn_filing_status + + self.assertEqual(contract.schedule_pay, 'semi-monthly') + + self._log('2019 Minnesota tax first payslip semimonthly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['WAGE_US_MN_UNEMP'], salary) + self.assertPayrollEqual(cats['ER_US_MN_UNEMP'], cats['WAGE_US_MN_UNEMP'] * self.MN_UNEMP) + self.assertPayrollEqual(cats['EE_US_MN_INC_WITHHOLD'], wh) + + def test_tax_exempt(self): + salary = 5500.00 + wh = 0 + schedule_pay = 'weekly' + allowances = 2 + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, struct_id=self.ref('l10n_us_mn_hr_payroll.hr_payroll_salary_structure_us_mn_employee'), + schedule_pay=schedule_pay) + contract.mn_w4mn_allowances = allowances + contract.mn_w4mn_tax_exempt = True + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 Minnesota tax first payslip exempt:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['WAGE_US_MN_UNEMP'], salary) + self.assertPayrollEqual(cats['ER_US_MN_UNEMP'], cats['WAGE_US_MN_UNEMP'] * self.MN_UNEMP) + self.assertPayrollEqual(cats.get('EE_US_MN_INC_WITHHOLD', 0.0), wh) + + def test_additional_withholding(self): + salary = 5500.0 + schedule_pay = 'weekly' + additional_wh = 40.0 + allowances = 2 + # Hand Calculated Amount to Test + # Step 1 -> 5500 for wages per period Step 2 -> 52 for weekly -> 5500 * 52 -> 286000.00 + # Step 3 -> allowances * 4250.0 -> 8500 in this case. + # Step 4 -> Step 2 - Step 3 -> 286000.00 - 8500 -> 277500 + # Step 5 -> using chart -> we have last row -> ((277500- 166290) * (9.85 / 100)) + 11717.65 -> 22671.835 + # Step 6 -> Convert back to pay period amount and round + # wh = -436.0 + # Add additional_withholding + # wh = -436.0 - 40.0 + wh = -436.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + struct_id=self.ref('l10n_us_mn_hr_payroll.hr_payroll_salary_structure_us_mn_employee'), + schedule_pay=schedule_pay) + contract.mn_w4mn_allowances = allowances + contract.mn_w4mn_additional_wh = additional_wh + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 Minnesota tax first payslip additional withholding:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['WAGE_US_MN_UNEMP'], salary) + self.assertPayrollEqual(cats['ER_US_MN_UNEMP'], cats['WAGE_US_MN_UNEMP'] * self.MN_UNEMP) + self.assertPayrollEqual(cats['EE_US_MN_INC_WITHHOLD'], wh - additional_wh) diff --git a/l10n_us_mn_hr_payroll/views/hr_payroll_views.xml b/l10n_us_mn_hr_payroll/views/hr_payroll_views.xml new file mode 100755 index 00000000..14477732 --- /dev/null +++ b/l10n_us_mn_hr_payroll/views/hr_payroll_views.xml @@ -0,0 +1,21 @@ + + + + + hr.contract.form.inherit + hr.contract + 128 + + + + + + + + + + + + + +