From 0cd34d7e499a3da195a09037d4ecbdb02146770e Mon Sep 17 00:00:00 2001 From: David Frick Date: Thu, 11 Jul 2019 16:46:42 -0400 Subject: [PATCH 1/2] NEW `l10n_us_ia_hr_payroll` for 11.0 --- l10n_us_ia_hr_payroll/__init__.py | 1 + l10n_us_ia_hr_payroll/__manifest__.py | 29 +++ l10n_us_ia_hr_payroll/data/base.xml | 47 +++++ l10n_us_ia_hr_payroll/data/final.xml | 17 ++ l10n_us_ia_hr_payroll/data/rates.xml | 14 ++ l10n_us_ia_hr_payroll/data/rules.xml | 159 +++++++++++++++ l10n_us_ia_hr_payroll/models/__init__.py | 1 + l10n_us_ia_hr_payroll/models/hr_payroll.py | 9 + l10n_us_ia_hr_payroll/tests/__init__.py | 1 + .../tests/test_us_ia_payslip.py | 192 ++++++++++++++++++ .../views/hr_payroll_views.xml | 22 ++ 11 files changed, 492 insertions(+) create mode 100755 l10n_us_ia_hr_payroll/__init__.py create mode 100755 l10n_us_ia_hr_payroll/__manifest__.py create mode 100755 l10n_us_ia_hr_payroll/data/base.xml create mode 100755 l10n_us_ia_hr_payroll/data/final.xml create mode 100755 l10n_us_ia_hr_payroll/data/rates.xml create mode 100755 l10n_us_ia_hr_payroll/data/rules.xml create mode 100644 l10n_us_ia_hr_payroll/models/__init__.py create mode 100755 l10n_us_ia_hr_payroll/models/hr_payroll.py create mode 100755 l10n_us_ia_hr_payroll/tests/__init__.py create mode 100755 l10n_us_ia_hr_payroll/tests/test_us_ia_payslip.py create mode 100755 l10n_us_ia_hr_payroll/views/hr_payroll_views.xml diff --git a/l10n_us_ia_hr_payroll/__init__.py b/l10n_us_ia_hr_payroll/__init__.py new file mode 100755 index 00000000..0650744f --- /dev/null +++ b/l10n_us_ia_hr_payroll/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/l10n_us_ia_hr_payroll/__manifest__.py b/l10n_us_ia_hr_payroll/__manifest__.py new file mode 100755 index 00000000..28dc125d --- /dev/null +++ b/l10n_us_ia_hr_payroll/__manifest__.py @@ -0,0 +1,29 @@ +{ + 'name': 'USA - Iowa - Payroll', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Localization', + 'depends': ['l10n_us_hr_payroll'], + 'version': '11.0.2019.0.0', + 'description': """ +USA::Iowa Payroll Rules. +================================== + +* Contribution register and partner for Additional WithholdingTaxaction - Income Tax Withholding +* Contribution register and partner for Iowa Workforce Development- Unemployment +* Contract level Iowa Exemptions +* Company level Iowa Unemployment Rate +* Salary Structure for Iowa + """, + + '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_ia_hr_payroll/data/base.xml b/l10n_us_ia_hr_payroll/data/base.xml new file mode 100755 index 00000000..6e5a2391 --- /dev/null +++ b/l10n_us_ia_hr_payroll/data/base.xml @@ -0,0 +1,47 @@ + + + + + + Iowa Workforce Development- Unemployment Tax + 1 + + + + Iowa Department of Revenue - Income Tax Withholding + 1 + + + + + + Iowa Unemployment + Iowa Workforce Development - Unemployment + + + + Iowa Income Tax Withholding + Iowa Department of Revenue - Income Tax Withholding + + + + + + + Wage: US-IA Unemployment + WAGE_US_IA_UNEMP + + + + ER: US-IA Unemployment + ER_US_IA_UNEMP + + + + + EE: US-IA Income Tax Withholding + EE_US_IA_INC_WITHHOLD + + + + diff --git a/l10n_us_ia_hr_payroll/data/final.xml b/l10n_us_ia_hr_payroll/data/final.xml new file mode 100755 index 00000000..58b73b5a --- /dev/null +++ b/l10n_us_ia_hr_payroll/data/final.xml @@ -0,0 +1,17 @@ + + + + + + US_IA_EMP + USA Iowa Employee + + + + + + diff --git a/l10n_us_ia_hr_payroll/data/rates.xml b/l10n_us_ia_hr_payroll/data/rates.xml new file mode 100755 index 00000000..6c910f0c --- /dev/null +++ b/l10n_us_ia_hr_payroll/data/rates.xml @@ -0,0 +1,14 @@ + + + + + + US IA Unemployment + US_IA_UNEMP + 1.0 + 2019-01-01 + + + + + diff --git a/l10n_us_ia_hr_payroll/data/rules.xml b/l10n_us_ia_hr_payroll/data/rules.xml new file mode 100755 index 00000000..eebebd47 --- /dev/null +++ b/l10n_us_ia_hr_payroll/data/rules.xml @@ -0,0 +1,159 @@ + + + + + + + + Wage: US-IA Unemployment + WAGE_US_IA_UNEMP + python + result = (contract.futa_type != contract.FUTA_TYPE_BASIC) + code + +rate = payslip.dict.get_rate('US_IA_UNEMP') +year = int(payslip.dict.date_to[:4]) +ytd = payslip.sum('WAGE_US_IA_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-IA Unemployment + ER_US_IA_UNEMP + python + result = (contract.futa_type != contract.FUTA_TYPE_BASIC) + code + +rate = payslip.dict.get_rate('US_IA_UNEMP') +result_rate = -rate.rate +result = categories.WAGE_US_IA_UNEMP + +# result_rate of 0 implies 100% due to bug +if result_rate == 0.0: + result = 0.0 + + + + + + + + + EE: US-IA Income Tax Withholding + EE_US_IA_INC_WITHHOLD + python + result = not contract.ia_w4_tax_exempt + code + +wages = categories.GROSS +federal_withholding = categories.EE_US_FED_INC_WITHHOLD +schedule_pay = contract.schedule_pay +allowances = contract.ia_w4_allowances +# It is + federal_withholding because federal_withholding is negative. +t1 = wages + federal_withholding +standard_deduction_table = { + 'daily': (6.50, 16.00), + 'weekly': (32.50, 80.00), + 'bi-weekly': (65.00, 160.00), + 'semi-monthly': (70.42, 173.33), + 'monthly': (140.83, 346.67), + 'annually': (1690.00, 4160.00)} +t2 = t1 - standard_deduction_table[schedule_pay][0] if allowances < 2 else standard_deduction_table[schedule_pay][1] +# IMPORTANT -- ALL RATES ARE ALREADY DIVIDED BY 100 -> 8.53% is in the table as 0.0853 +if schedule_pay == 'weekly': + tax_rate_table = [ + (25.63, 0.0033, 0.0), + (51.27, 0.0067, 0.08), + (102.52, 0.0225, 0.025), + (230.67, 0.0414, 1.40), + (384.46, 0.0563, 6.71), + (512.62, 0.0596, 15.37), + (768.92, 0.0625, 23.01), + (1153.38, 0.0744, 39.03), + (float('inf'), 0.0853, 67.63), + ] +elif schedule_pay == 'bi-weekly': + tax_rate_table = [ + (51.27, 0.0033, 0.00), + (102.54, 0.0067, 0.17), + (205.04, 0.00225, 0.51), + (461.35, 0.0414, 2.82), + (768.92, 0.0563, 13.43), + (1025.23, 0.0596, 30.75), + (1537.85, 0.0625, 46.03), + (2306.77, 0.0744, 78.07), + (float('inf'), 0.0853, 135.28) + ] +elif schedule_pay == 'semi-monthly': + tax_rate_table = [ + (55.54, 0.0033, 0.00), + (111.08, 0.0067, 0.18), + (222.13, 0.0225, 0.55), + (499.79, 0.0414, 3.05), + (833.00, 0.0563, 14.59), + (1110.67, 0.0596, 33.31), + (1666.00, 0.0625, 49.86), + (2499.00, 0.0744, 84.57), + (float('inf'), 0.0853, 146.55) + ] +elif schedule_pay == 'monthly': + tax_rate_table = [ + (111.08, 0.0033, 0.00), + (222.17, 0.0067, 0.37), + (444.25, 0.0225, 1.11), + (999.58, 0.0414, 6.11), + (1666.00, 0.0563, 29.10), + (2221.33, 0.0596, 62.66), + (3332.00, 0.0625, 99.72), + (4998.00, 0.0744, 169.14), + (float('inf'), 0.0853, 293.09) + ] +elif schedule_pay == 'annual': + tax_rate_table = [ + (1333.00, 0.0033, 0.00), + (2666.00, 0.0067, 4.40), + (5331.00, 0.0225, 13.33), + (11995.00, 0.0414, 73.29), + (19992.00, 0.0563, 349.19), + (26656.00, 0.0596, 799.41), + (39984.00, 0.0625, 1196.58), + (59976.00, 0.0744, 2029.58), + (float('inf'), 0.0853, 3516.98) + ] + +t3 = 0.0 +last = 0.0 +for row in tax_rate_table: + cap, rate, flat_fee = row + if cap > t2: + taxed_amount = t2 - last + t3 = flat_fee + (rate * taxed_amount) + break + last = cap + +deduction_per_allowance = { + 'daily': 0.15, + 'weekly': 0.77, + 'bi-weekly': 1.54, + 'semi-monthly': 3.33, + 'annually': 40.00 + } +t4 = t3 - (deduction_per_allowance[schedule_pay] * allowances) +t5 = t4 + contract.ia_w4_additional_wh +result = -t5 + + + + + diff --git a/l10n_us_ia_hr_payroll/models/__init__.py b/l10n_us_ia_hr_payroll/models/__init__.py new file mode 100644 index 00000000..e99aa24a --- /dev/null +++ b/l10n_us_ia_hr_payroll/models/__init__.py @@ -0,0 +1 @@ +from . import hr_payroll diff --git a/l10n_us_ia_hr_payroll/models/hr_payroll.py b/l10n_us_ia_hr_payroll/models/hr_payroll.py new file mode 100755 index 00000000..c171154a --- /dev/null +++ b/l10n_us_ia_hr_payroll/models/hr_payroll.py @@ -0,0 +1,9 @@ +from odoo import models, fields, api + + +class USIAHrContract(models.Model): + _inherit = 'hr.contract' + + ia_w4_allowances = fields.Integer(string='Iowa W-4 allowances', default=0) + ia_w4_additional_wh = fields.Float(string="Iowa Additional Withholding", default=0.0) + ia_w4_tax_exempt = fields.Boolean(string="Tax Exempt") diff --git a/l10n_us_ia_hr_payroll/tests/__init__.py b/l10n_us_ia_hr_payroll/tests/__init__.py new file mode 100755 index 00000000..4f771ce0 --- /dev/null +++ b/l10n_us_ia_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_us_ia_payslip diff --git a/l10n_us_ia_hr_payroll/tests/test_us_ia_payslip.py b/l10n_us_ia_hr_payroll/tests/test_us_ia_payslip.py new file mode 100755 index 00000000..53d1fd44 --- /dev/null +++ b/l10n_us_ia_hr_payroll/tests/test_us_ia_payslip.py @@ -0,0 +1,192 @@ +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 TestUsIAPayslip(TestUsPayslip): + IA_UNEMP_MAX_WAGE = 30600 + IA_UNEMP = -1.0 / 100.0 + IA_INC_TAX = -0.0535 + + def test_taxes_weekly(self): + wages = 30000.00 + schedule_pay = 'weekly' + allowances = 1 + additional_wh = 0.00 + employee = self._createEmployee() + contract = self._createContract(employee, wages, + struct_id=self.ref('l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'), + schedule_pay=schedule_pay) + contract.ia_w4_allowances = allowances + contract.ia_w4_additional_wh = additional_wh + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 Iowa tax first payslip weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + # T1 is the gross taxable wages for the pay period minus the Federal withholding amount. We add the federal + # withholding amount because it is calculated in the base US payroll module as a negative + # t1 = 30000 - (10399.66) = 19600.34 + t1_to_test = wages + cats['EE_US_FED_INC_WITHHOLD'] + self.assertPayrollEqual(t1_to_test, 19600.34) + + # T2 is T1 minus our standard deduction which is a table of flat rates dependent on the number of allowances. + # In our case, we have a weekly period which on the table has a std deduct. of $32.50 for 0 or 1 allowances, + # and 80.00 of 2 or more allowances. + standard_deduction = 32.50 # The allowance tells us what standard_deduction amount to use. + # t2 = 19600.34 - 32.50 = 19567.84 + t2_to_test = t1_to_test - standard_deduction + self.assertPayrollEqual(t2_to_test, 19567.84) + # T3 is T2 multiplied by the income rates in the large table plus a flat fee for that bracket. + # 1153.38 is the bracket floor. 8.53 is the rate, and 67.63 is the flat fee. + # t3 = 1638.38 + t3_to_test = ((t2_to_test - 1153.38) * (8.53 / 100)) + 67.63 + self.assertPayrollEqual(t3_to_test, 1638.38) + # T4 is T3 minus a flat amount determined by pay period * the number of deductions. For 2019, our weekly + # deduction amount per allowance is 0.77 + # t4 = 1638.38 - 0.77 = 155.03 + t4_to_test = t3_to_test - (0.77 * allowances) + self.assertPayrollEqual(t4_to_test, 1637.61) + # t5 is our T4 plus the additional withholding per period + # t5 = 1637.61 + 0.0 + # Convert to negative as well. + t5_to_test = -t4_to_test - additional_wh + self.assertPayrollEqual(t5_to_test, -1637.61) + + self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP) + self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test) + + # Test additional + additional_wh = 15.00 + contract.ia_w4_additional_wh = additional_wh + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test - additional_wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_IA_UNEMP_wages = self.IA_UNEMP_MAX_WAGE - wages if (self.IA_UNEMP_MAX_WAGE - 2*wages < wages) \ + else wages + + self._log('2019 Iowa 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_IA_UNEMP'], remaining_IA_UNEMP_wages) + self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], remaining_IA_UNEMP_wages * self.IA_UNEMP) + + def test_taxes_biweekly(self): + wages = 3000.00 + schedule_pay = 'bi-weekly' + allowances = 1 + additional_wh = 0.00 + employee = self._createEmployee() + contract = self._createContract(employee, wages, + struct_id=self.ref( + 'l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'), + schedule_pay=schedule_pay) + contract.ia_w4_allowances = allowances + contract.ia_w4_additional_wh = additional_wh + + self.assertEqual(contract.schedule_pay, 'bi-weekly') + + self._log('2019 Iowa tax first payslip bi-weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + # T1 is the gross taxable wages for the pay period minus the Federal withholding amount. We add the federal + # withholding amount because it is calculated in the base US payroll module as a negative + t1_to_test = wages + cats['EE_US_FED_INC_WITHHOLD'] + # T2 is T1 minus our standard deduction which is a table of flat rates dependent on the number of allowances. + # In our case, we have a biweekly period which on the table has a std deduct. of $65.00 for 0 or 1 allowances, + # and $160.00 of 2 or more allowances. + standard_deduction = 65.00 # The allowance tells us what standard_deduction amount to use. + t2_to_test = t1_to_test - standard_deduction + # T3 is T2 multiplied by the income rates in the large table plus a flat fee for that bracket. + t3_to_test = ((t2_to_test - 2306.77) * (8.53 / 100)) + 135.28 + # T4 is T3 minus a flat amount determined by pay period * the number of deductions. For 2019, our weekly + # deduction amount per allowance is 0.77 + t4_to_test = t3_to_test - (1.54 * allowances) + # t5 is our T4 plus the additional withholding per period + t5_to_test = -t4_to_test - additional_wh + + self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP) + self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test - additional_wh) + + process_payslip(payslip) + + def test_taxes_with_external_weekly(self): + wages = 2500.00 + schedule_pay = 'weekly' + allowances = 1 + additional_wh = 0.00 + external_wages = 500.0 + + employee = self._createEmployee() + contract = self._createContract(employee, wages, external_wages=external_wages, + struct_id=self.ref('l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'), + schedule_pay=schedule_pay) + contract.ia_w4_additional_wh = additional_wh + contract.ia_w4_allowances = allowances + + self._log('2019 Iowa external tax first payslip external weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + + # T1 is the gross taxable wages for the pay period minus the Federal withholding amount. We add the federal + # withholding amount because it is calculated in the base US payroll module as a negative + t1_to_test = wages + cats['EE_US_FED_INC_WITHHOLD'] + # T2 is T1 minus our standard deduction which is a table of flat rates dependent on the number of allowances. + # In our case, we have a weekly period which on the table has a std deduct. of $32.50 for 0 or 1 allowances, + # and 80.00 of 2 or more allowances. + standard_deduction = 32.50 # The allowance tells us what standard_deduction amount to use. + t2_to_test = t1_to_test - standard_deduction + # T3 is T2 multiplied by the income rates in the large table plus a flat fee for that bracket. + t3_to_test = ((t2_to_test - 1153.38) * (8.53 / 100)) + 67.63 + # T4 is T3 minus a flat amount determined by pay period * the number of deductions. For 2019, our weekly + # deduction amount per allowance is 0.77 + t4_to_test = t3_to_test - (0.77 * allowances) + # t5 is our T4 plus the additional withholding per period + t5_to_test = -t4_to_test - additional_wh + + self.assertPayrollEqual(cats['WAGE_US_IA_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_IA_UNEMP'], cats['WAGE_US_IA_UNEMP'] * self.IA_UNEMP) + self.assertPayrollEqual(cats['EE_US_IA_INC_WITHHOLD'], t5_to_test) + + process_payslip(payslip) + + def test_taxes_with_state_exempt_weekly(self): + salary = 5000.0 + external_wages = 10000.0 + schedule_pay = 'weekly' + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + external_wages=external_wages, + struct_id=self.ref('l10n_us_ia_hr_payroll.hr_payroll_salary_structure_us_ia_employee'), + futa_type=USHrContract.FUTA_TYPE_BASIC, + schedule_pay=schedule_pay) + contract.ia_w4_tax_exempt = True + + self._log('2019 Iowa exempt tax first payslip exempt weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('WAGE_US_IA_UNEMP', 0.0), 0.0) + self.assertPayrollEqual(cats.get('ER_US_IA_UNEMP', 0.0), cats.get('WAGE_US_IA_UNEMP', 0.0) * self.IA_UNEMP) + + + + diff --git a/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml b/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml new file mode 100755 index 00000000..f061cceb --- /dev/null +++ b/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml @@ -0,0 +1,22 @@ + + + + + hr.contract.form.inherit + hr.contract + 119 + + + + + + + + + + + + + + + From 42bb9efa2bdbf26d7aa6a6f3873e45d8072a0e37 Mon Sep 17 00:00:00 2001 From: David Frick Date: Mon, 22 Jul 2019 15:41:47 -0400 Subject: [PATCH 2/2] FIX 'l10n_us_ia_hr_payroll` missing mothnly allowance per standard deduction --- l10n_us_ia_hr_payroll/data/rules.xml | 9 +++++---- l10n_us_ia_hr_payroll/views/hr_payroll_views.xml | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/l10n_us_ia_hr_payroll/data/rules.xml b/l10n_us_ia_hr_payroll/data/rules.xml index eebebd47..2281d900 100755 --- a/l10n_us_ia_hr_payroll/data/rules.xml +++ b/l10n_us_ia_hr_payroll/data/rules.xml @@ -69,7 +69,7 @@ standard_deduction_table = { 'semi-monthly': (70.42, 173.33), 'monthly': (140.83, 346.67), 'annually': (1690.00, 4160.00)} -t2 = t1 - standard_deduction_table[schedule_pay][0] if allowances < 2 else standard_deduction_table[schedule_pay][1] +t2 = t1 - standard_deduction_table[schedule_pay][0] if (allowances < 2) else standard_deduction_table[schedule_pay][1] # IMPORTANT -- ALL RATES ARE ALREADY DIVIDED BY 100 -> 8.53% is in the table as 0.0853 if schedule_pay == 'weekly': tax_rate_table = [ @@ -136,7 +136,7 @@ t3 = 0.0 last = 0.0 for row in tax_rate_table: cap, rate, flat_fee = row - if cap > t2: + if cap > t2: taxed_amount = t2 - last t3 = flat_fee + (rate * taxed_amount) break @@ -146,8 +146,9 @@ deduction_per_allowance = { 'daily': 0.15, 'weekly': 0.77, 'bi-weekly': 1.54, - 'semi-monthly': 3.33, - 'annually': 40.00 + 'semi-monthly': 1.67, + 'monthly': 3.33, + 'annually': 40.00, } t4 = t3 - (deduction_per_allowance[schedule_pay] * allowances) t5 = t4 + contract.ia_w4_additional_wh diff --git a/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml b/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml index f061cceb..4b822364 100755 --- a/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml +++ b/l10n_us_ia_hr_payroll/views/hr_payroll_views.xml @@ -1,5 +1,6 @@ + hr.contract.form.inherit @@ -19,4 +20,5 @@ +