diff --git a/l10n_us_ar_hr_payroll/__init__.py b/l10n_us_ar_hr_payroll/__init__.py new file mode 100755 index 00000000..0650744f --- /dev/null +++ b/l10n_us_ar_hr_payroll/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/l10n_us_ar_hr_payroll/__manifest__.py b/l10n_us_ar_hr_payroll/__manifest__.py new file mode 100755 index 00000000..1bb42dad --- /dev/null +++ b/l10n_us_ar_hr_payroll/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'USA - Arkansas - Payroll', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Localization', + 'depends': ['l10n_us_hr_payroll'], + 'version': '11.0.2019.0.0', + 'description': """ +USA::Arkansas Payroll Rules. +================================== + +* Contribution register and partner for Arkansas Department of Financial Administration - Income Tax Withholding +* Contribution register and partner for Arkansas Department of Workforce Solutions - Unemployment +* Contract level Arkansas Exemptions +* Company level Arkansas Unemployment Rate +* Salary Structure for Arkansas + """, + '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_ar_hr_payroll/data/base.xml b/l10n_us_ar_hr_payroll/data/base.xml new file mode 100755 index 00000000..2e004924 --- /dev/null +++ b/l10n_us_ar_hr_payroll/data/base.xml @@ -0,0 +1,47 @@ + + + + + + Arkansas Department of Workforce Solutions - Unemployment Tax + 1 + + + + Arkansas Department of Financial Administration- Income Tax Withholding + 1 + + + + + + Arkansas Unemployment + Arkansas Department of Workforce Solutions - Unemployment + + + + Arkansas Income Tax Withholding + Arkansas Department of Financial Administration - Income Tax Withholding + + + + + + + Wage: US-AR Unemployment + WAGE_US_AR_UNEMP + + + + ER: US-AR Unemployment + ER_US_AR_UNEMP + + + + + EE: US-AR Income Tax Withholding + EE_US_AR_INC_WITHHOLD + + + + diff --git a/l10n_us_ar_hr_payroll/data/final.xml b/l10n_us_ar_hr_payroll/data/final.xml new file mode 100755 index 00000000..6bbf8f32 --- /dev/null +++ b/l10n_us_ar_hr_payroll/data/final.xml @@ -0,0 +1,17 @@ + + + + + + US_AR_EMP + USA Arkansas Employee + + + + + + diff --git a/l10n_us_ar_hr_payroll/data/rates.xml b/l10n_us_ar_hr_payroll/data/rates.xml new file mode 100755 index 00000000..00c3b202 --- /dev/null +++ b/l10n_us_ar_hr_payroll/data/rates.xml @@ -0,0 +1,14 @@ + + + + + + US AR Unemployment + US_AR_UNEMP + 3.2 + 2019-01-01 + + + + + diff --git a/l10n_us_ar_hr_payroll/data/rules.xml b/l10n_us_ar_hr_payroll/data/rules.xml new file mode 100755 index 00000000..00fed6f5 --- /dev/null +++ b/l10n_us_ar_hr_payroll/data/rules.xml @@ -0,0 +1,113 @@ + + + + + + + Wage: US-AR Unemployment + WAGE_US_AR_UNEMP + python + result = (contract.futa_type != contract.FUTA_TYPE_BASIC) + code + +rate = payslip.dict.get_rate('US_AR_UNEMP') +year = int(payslip.dict.date_to[:4]) +ytd = payslip.sum('WAGE_US_AR_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-AR Unemployment + ER_US_AR_UNEMP + python + result = (contract.futa_type != contract.FUTA_TYPE_BASIC) + code + +rate = payslip.dict.get_rate('US_AR_UNEMP') +result_rate = -rate.rate +result = categories.WAGE_US_AR_UNEMP + +# result_rate of 0 implies 100% due to bug +if result_rate == 0.0: + result = 0.0 + + + + + + + + + EE: US-AR Income Tax Withholding + EE_US_AR_INC_WITHHOLD + python + result = not (contract.ar_w4_texarkana_exemption or contract.ar_w4_tax_exempt) + code + +wages = categories.GROSS +annual_gross_pay = 0.00 +allowance_amt = contract.ar_w4_allowances * 26.00 +schedule_pay = contract.schedule_pay +standard_deduction = 2200 +additional_withholding = contract.ar_w4_additional_wh + +if contract.w4_filing_status == 'married': + standard_deduction = standard_deduction * 2 + +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_gross_pay = (wages * pay_period) +net_taxable_income = annual_gross_pay - standard_deduction - allowance_amt +if (net_taxable_income < 50000.00): + # This formula will round the number to the nearest 50 if under 50000 + net_taxable_income = (net_taxable_income // 50) * 50.0 + 50.0 + +tax_rate_table = [(4299, 0.90), + (8499, 2.50), + (12699, 3.50), + (21199, 4.50), + (35099, 6.0), + (float('inf'), 6.9)] + +result = 0.0 +last = 0.0 + +for row in tax_rate_table: + cap, rate = row + if cap <= net_taxable_income: + taxed = cap - last + result = result + (taxed * (rate / 100.0)) + last = cap + elif cap > net_taxable_income: + taxed = net_taxable_income - last + result = result + (taxed * (rate / 100.0)) + break + +result = (result / pay_period) + additional_withholding +result = -result + + + + + diff --git a/l10n_us_ar_hr_payroll/models/__init__.py b/l10n_us_ar_hr_payroll/models/__init__.py new file mode 100644 index 00000000..e99aa24a --- /dev/null +++ b/l10n_us_ar_hr_payroll/models/__init__.py @@ -0,0 +1 @@ +from . import hr_payroll diff --git a/l10n_us_ar_hr_payroll/models/hr_payroll.py b/l10n_us_ar_hr_payroll/models/hr_payroll.py new file mode 100755 index 00000000..b6b51866 --- /dev/null +++ b/l10n_us_ar_hr_payroll/models/hr_payroll.py @@ -0,0 +1,10 @@ +from odoo import models, fields, api + + +class USARHrContract(models.Model): + _inherit = 'hr.contract' + + ar_w4_allowances = fields.Integer(string='Arkansas W-4 allowances', default=0) + ar_w4_additional_wh = fields.Float(string="Arkansas Additional Withholding", default=0.0) + ar_w4_tax_exempt = fields.Boolean(string="Tax Exempt") + ar_w4_texarkana_exemption = fields.Boolean(string="Texarkana Exemption") diff --git a/l10n_us_ar_hr_payroll/tests/__init__.py b/l10n_us_ar_hr_payroll/tests/__init__.py new file mode 100755 index 00000000..7d2ca3e4 --- /dev/null +++ b/l10n_us_ar_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_us_ar_payslip_2019 diff --git a/l10n_us_ar_hr_payroll/tests/test_us_ar_payslip_2019.py b/l10n_us_ar_hr_payroll/tests/test_us_ar_payslip_2019.py new file mode 100755 index 00000000..44a4a058 --- /dev/null +++ b/l10n_us_ar_hr_payroll/tests/test_us_ar_payslip_2019.py @@ -0,0 +1,292 @@ +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 TestUsARPayslip(TestUsPayslip): + + AR_UNEMP_MAX_WAGE = 10000.00 + AR_UNEMP = -3.2 / 100.0 + AR_INC_TAX = -0.0535 + + def test_taxes_monthly(self): + salary = 10000.0 + schedule_pay = 'monthly' + + employee = self._createEmployee() + contract = self._createContract(employee, salary, + struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + schedule_pay=schedule_pay) + + self.assertEqual(contract.schedule_pay, 'monthly') + + self._log('2019 Arkansas tax first payslip weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + # Not exempt from rule 1 or rule 2 - unemployment wages., and actual unemployment. + self.assertPayrollEqual(cats['WAGE_US_AR_UNEMP'], salary) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + remaining_AR_UNEMP_wages = self.AR_UNEMP_MAX_WAGE - salary if (self.AR_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + # We reached the cap of 10000.0 in the first payslip. + self.assertEqual(0.0, remaining_AR_UNEMP_wages) + self._log('2019 Arkansas 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_AR_UNEMP'], remaining_AR_UNEMP_wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], remaining_AR_UNEMP_wages * self.AR_UNEMP) + + def test_taxes_with_state_exempt(self): + salary = 50000.0 + tax_exempt = True # State withholding should be zero. + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + ) + contract.ar_w4_tax_exempt = tax_exempt + + self._log('2019 Arkansas exempt 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_AR_UNEMP'], self.AR_UNEMP_MAX_WAGE) + self.assertPayrollEqual(cats.get('ER_US_AR_UNEMP', 0.0), cats.get('WAGE_US_AR_UNEMP', 0.0) * self.AR_UNEMP) + self.assertPayrollEqual(cats.get('EE_US_AR_INC_WITHHOLD', 0.0), 0.0) + + process_payslip(payslip) + + def test_taxes_with_texarkana_exempt(self): + salary = 40000.00 + texarkana_exemption = True # State withholding should be zero. + + employee = self._createEmployee() + contract = self._createContract(employee, + salary, + struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee')) + contract.ar_w4_texarkana_exemption = texarkana_exemption + + self._log('2019 Arkansas tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('WAGE_US_AR_UNEMP', 0.0), self.AR_UNEMP_MAX_WAGE) + self.assertPayrollEqual(cats.get('ER_US_AR_UNEMP', 0.0), cats.get('WAGE_US_AR_UNEMP', 0.0) * self.AR_UNEMP) + + process_payslip(payslip) + + def test_additional_withholding(self): + wages = 5000.0 + schedule_pay = 'monthly' + pay_periods = 12 + additional_wh = 150.0 + exemptions = 2 + # TODO: comment on how it was calculated + test_ar_amt = 3069.97 + + employee = self._createEmployee() + contract = self._createContract(employee, + wages, + struct_id=self.ref('l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + schedule_pay=schedule_pay) + contract.ar_w4_additional_wh = 0.0 + contract.ar_w4_allowances = exemptions + + self.assertEqual(contract.schedule_pay, 'monthly') + + self._log('2019 Arkansas 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_AR_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP) + # TODO: change to hand the test_ar_amt already be divided by pay periods + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods)) + + contract.ar_w4_additional_wh = additional_wh + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh) + + process_payslip(payslip) + + def test_under_fifty_thousand(self): + wages = 2500.00 + schedule_pay = 'monthly' + pay_periods = 12 + additional_wh = 150.0 + exemptions = 2 + # TODO: comment calc. + test_ar_amt = 1066.151 + + employee = self._createEmployee() + contract = self._createContract(employee, + wages, + struct_id=self.ref( + 'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + schedule_pay=schedule_pay) + contract.ar_w4_additional_wh = 0.0 + contract.ar_w4_allowances = exemptions + + self.assertEqual(contract.schedule_pay, 'monthly') + + self._log('2019 Arkansas 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_AR_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods)) + + contract.ar_w4_additional_wh = additional_wh + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_AR_UNEMP_wages = self.AR_UNEMP_MAX_WAGE - wages if (self.AR_UNEMP_MAX_WAGE - 2 * wages < wages) \ + else wages + + self._log('2019 Arkansas 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_AR_UNEMP'], remaining_AR_UNEMP_wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], remaining_AR_UNEMP_wages * self.AR_UNEMP) + + def test_over_fifty_thousand(self): + wages = 10000.00 # 10000.00 monthly is over 50,000 annually. + schedule_pay = 'monthly' + pay_periods = 12 + additional_wh = 150.0 + exemptions = 2 + # TODO: comment on how it was calculated + test_ar_amt = 7209.97 + + employee = self._createEmployee() + contract = self._createContract(employee, + wages, + struct_id=self.ref( + 'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + schedule_pay=schedule_pay) + contract.ar_w4_additional_wh = 0.0 + contract.ar_w4_allowances = exemptions + + self.assertEqual(contract.schedule_pay, 'monthly') + + self._log('2019 Arkansas 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_AR_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods)) + + contract.ar_w4_additional_wh = additional_wh + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_AR_UNEMP_wages = self.AR_UNEMP_MAX_WAGE - wages if (self.AR_UNEMP_MAX_WAGE - 2 * wages < wages) \ + else wages + + self._log('2019 Arkansas 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_AR_UNEMP'], remaining_AR_UNEMP_wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], remaining_AR_UNEMP_wages * self.AR_UNEMP) + + def test_married(self): + wages = 5500.00 + schedule_pay = 'monthly' + pay_periods = 12 + additional_wh = 150.0 + exemptions = 2 + w4_filing_status = 'married' + # TODO: explain calc. + # Yearly -> 3332.17. Monthly -> 427.681 + test_ar_amt = 3332.17 + + employee = self._createEmployee() + contract = self._createContract(employee, + wages, + struct_id=self.ref( + 'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + schedule_pay=schedule_pay) + contract.ar_w4_additional_wh = additional_wh + contract.ar_w4_allowances = exemptions + contract.w4_filing_status = w4_filing_status + + self.assertEqual(contract.w4_filing_status, 'married') + + self._log('2019 Arkansas 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_AR_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh) + + def test_single(self): + wages = 5500.00 + schedule_pay = 'monthly' + pay_periods = 12 + additional_wh = 150.0 + exemptions = 2 + w4_filling_status = 'single' + # TODO: explain calc. + # Yearly -> 3483.972 Monthly -> 298.331 + test_ar_amt = 3483.972 + + employee = self._createEmployee() + contract = self._createContract(employee, + wages, + struct_id=self.ref( + 'l10n_us_ar_hr_payroll.hr_payroll_salary_structure_us_ar_employee'), + schedule_pay=schedule_pay) + contract.ar_w4_additional_wh = 0 + contract.ar_w4_allowances = exemptions + contract.w4_filling_status = w4_filling_status + + self.assertEqual(contract.w4_filling_status, 'single') + + self._log('2019 Arkansas 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_AR_UNEMP'], wages) + self.assertPayrollEqual(cats['ER_US_AR_UNEMP'], cats['WAGE_US_AR_UNEMP'] * self.AR_UNEMP) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods)) + + contract.ar_w4_additional_wh = additional_wh + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_AR_INC_WITHHOLD'], -(test_ar_amt / pay_periods) - additional_wh) + + process_payslip(payslip) diff --git a/l10n_us_ar_hr_payroll/views/hr_payroll_views.xml b/l10n_us_ar_hr_payroll/views/hr_payroll_views.xml new file mode 100755 index 00000000..d1ed5d9c --- /dev/null +++ b/l10n_us_ar_hr_payroll/views/hr_payroll_views.xml @@ -0,0 +1,23 @@ + + + + + hr.contract.form.inherit + hr.contract + 106 + + + + + + + + + + + + + + + +