diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 3aaa137b..5e752f94 100644 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -30,6 +30,7 @@ United States of America - Payroll Rules. 'data/state/mo_missouri.xml', 'data/state/ms_mississippi.xml', 'data/state/mt_montana.xml', + 'data/state/nc_northcarolina.xml', 'data/state/nj_newjersey.xml', 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', diff --git a/l10n_us_hr_payroll/data/state/nc_northcarolina.xml b/l10n_us_hr_payroll/data/state/nc_northcarolina.xml new file mode 100644 index 00000000..597fd543 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/nc_northcarolina.xml @@ -0,0 +1,109 @@ + + + + + US NC North Carolina SUTA Wage Base + us_nc_suta_wage_base + + + + + 24300.0 + + + + + 25200.0 + + + + + + + + US NC North Carolina SUTA Rate + us_nc_suta_rate + + + + + 1.0 + + + + + 1.0 + + + + + + + US NC North Carolina Allowance Rate + us_nc_sit_allowance_rate + + + + + { + 'weekly': {'allowance': 48.08, 'standard_deduction': 192.31, 'standard_deduction_hh': 288.46}, + 'bi-weekly': {'allowance': 96.15, 'standard_deduction': 384.62, 'standard_deduction_hh': 576.92}, + 'semi-monthly': {'allowance': 104.17, 'standard_deduction': 416.67, 'standard_deduction_hh': 625.00}, + 'monthly': {'allowance': 208.33, 'standard_deduction': 833.33, 'standard_deduction_hh': 1250.00}, + } + + + + + { + 'weekly': {'allowance': 48.08, 'standard_deduction': 206.73, 'standard_deduction_hh': 310.10}, + 'bi-weekly': {'allowance': 96.15, 'standard_deduction': 413.46, 'standard_deduction_hh': 620.19}, + 'semi-monthly': {'allowance': 104.17, 'standard_deduction': 447.92, 'standard_deduction_hh': 671.88}, + 'monthly': {'allowance': 208.33, 'standard_deduction': 895.83, 'standard_deduction_hh': 1343.75}, + } + + + + + + + + US North Carolina - Department of Taxation - Unemployment Tax + + + + US North Carolina - Department of Taxation - Income Tax + + + + + + + + + + ER: US NC North Carolina State Unemployment + ER_US_NC_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nc_suta_wage_base', rate='us_nc_suta_rate', state_code='NC') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nc_suta_wage_base', rate='us_nc_suta_rate', state_code='NC') + + + + + + + + + EE: US NC North Carolina State Income Tax Withholding + EE_US_NC_SIT + python + result, _ = nc_northcarolina_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = nc_northcarolina_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 c3dffe34..7e4a32d0 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -16,6 +16,7 @@ from .state.ga_georgia import ga_georgia_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 +from .state.nc_northcarolina import nc_northcarolina_state_income_withholding from .state.nj_newjersey import nj_newjersey_state_income_withholding from .state.oh_ohio import oh_ohio_state_income_withholding from .state.va_virginia import va_virginia_state_income_withholding @@ -56,6 +57,7 @@ class HRPayslip(models.Model): '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, + 'nc_northcarolina_state_income_withholding': nc_northcarolina_state_income_withholding, 'nj_newjersey_state_income_withholding': nj_newjersey_state_income_withholding, 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, diff --git a/l10n_us_hr_payroll/models/state/nc_northcarolina.py b/l10n_us_hr_payroll/models/state/nc_northcarolina.py new file mode 100644 index 00000000..8b9be103 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/nc_northcarolina.py @@ -0,0 +1,37 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies + + +def nc_northcarolina_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 = 'NC' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('nc_nc4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + # Determine Wage + wage = categories.GROSS + categories.DED_FIT_EXEMPT + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + allowances = payslip.contract_id.us_payroll_config_value('nc_nc4_sit_allowances') + allowances_rate = payslip.rule_parameter('us_nc_sit_allowance_rate').get(schedule_pay)['allowance'] + deduction = payslip.rule_parameter('us_nc_sit_allowance_rate').get(schedule_pay)['standard_deduction'] if filing_status != 'head_household' else payslip.rule_parameter('us_nc_sit_allowance_rate').get(schedule_pay)['standard_deduction_hh'] + + if wage == 0.0: + return 0.0, 0.0 + taxable_wage = round((wage - (deduction + (allowances * allowances_rate))) * 0.0535) + withholding = 0.0 + if taxable_wage < 0.0: + withholding -= taxable_wage + withholding = taxable_wage + 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 26aec499..49d1169b 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -94,6 +94,15 @@ class HRContractUSPayrollConfig(models.Model): ('montana_for_marriage', 'Montana for Marriage'), ], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2') + nc_nc4_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single'), + ('married', 'Married'), + ('surviving_spouse', 'Surviving Spouse'), + ('head_household', 'Head of Household') + ], string='North Carolina NC-4 Filing Status', help='NC-4') + nc_nc4_sit_allowances = fields.Integer(string='North Carolina NC-4 Allowances', help='NC-4 1.') + nj_njw4_sit_filing_status = fields.Selection([ ('', 'Exempt'), ('single', 'Single'), diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index f0e84ff2..3844fbcf 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -19,6 +19,9 @@ from . import test_us_ms_mississippi_payslip_2020 from . import test_us_mt_montana_payslip_2019 from . import test_us_mt_montana_payslip_2020 +from . import test_us_nc_northcarolina_payslip_2019 +from . import test_us_nc_northcarolina_payslip_2020 + from . import test_us_nj_newjersey_payslip_2019 from . import test_us_nj_newjersey_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py new file mode 100755 index 00000000..14c1c5b2 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py @@ -0,0 +1,270 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsNCPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + NC_UNEMP_MAX_WAGE = 24300.0 + NC_UNEMP = -1.0 / 100.0 + NC_INC_TAX = -0.0535 + + + def test_2019_taxes_weekly(self): + salary = 20000.0 + # allowance_multiplier and Portion of Standard Deduction for weekly + allowance_multiplier = 48.08 + PST = 192.31 + exemption = 1 + # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf + wh = -round((salary - (PST + (allowance_multiplier * exemption))) * -self.NC_INC_TAX) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + nc_nc4_sit_allowances=1.0, + schedule_pay='weekly') + + self._log('2019 North Carolina tax first payslip weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + self._log('2019 North Carolina 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_NC_UNEMP_wages * self.NC_UNEMP) + + def test_2019_taxes_with_external_weekly(self): + salary = 5000.0 + schedule_pay = 'weekly' + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + nc_nc4_sit_allowances=1.0, + schedule_pay='weekly') + + self._log('2019 NorthCarolina_external 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.NC_UNEMP) + + def test_2019_taxes_biweekly(self): + salary = 5000.0 + schedule_pay = 'bi-weekly' + # allowance_multiplier and Portion of Standard Deduction for weekly + allowance_multiplier = 96.15 + PST = 384.62 + + allowances = 2 + # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf + + wh = -round((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX) + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + nc_nc4_sit_allowances=2.0, + schedule_pay='bi-weekly') + + self._log('2019 North Carolina 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.NC_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 North Carolina 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_NC_UNEMP_wages * self.NC_UNEMP) + + def test_2019_taxes_semimonthly(self): + salary = 4000.0 + # allowance_multiplier and Portion of Standard Deduction for weekly + allowance_multiplier = 104.17 + PST = 625.00 + + allowances = 1 + # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf + + wh = -round((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX) + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status='head_household', + state_income_tax_additional_withholding=0.0, + nc_nc4_sit_allowances=1.0, + schedule_pay='semi-monthly') + + self._log('2019 North Carolina tax 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.NC_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 North Carolina tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP) + + def test_2019_taxes_monthly(self): + salary = 4000.0 + schedule_pay = 'monthly' + # allowance_multiplier and Portion of Standard Deduction for weekly + allowance_multiplier = 208.33 + PST = 833.33 + + allowances = 1 + # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf + + wh = -round((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX) + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status='single', + state_income_tax_additional_withholding=0.0, + nc_nc4_sit_allowances=1.0, + schedule_pay='monthly') + + self._log('2019 North Carolina tax 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.NC_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if ( + self.NC_UNEMP_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 North Carolina tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP) + + def test_additional_withholding(self): + salary = 4000.0 + # allowance_multiplier and Portion of Standard Deduction for weekly + allowance_multiplier = 48.08 + PST = 192.31 + additional_wh = 40.0 + + #4000 - (168.27 + (48.08 * 1) + + allowances = 1 + # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf + + wh = -round(((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX) + additional_wh) + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status='married', + state_income_tax_additional_withholding=40.0, + nc_nc4_sit_allowances=1.0, + schedule_pay='weekly') + + self._log('2019 North Carolina tax first payslip weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 North Carolina 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_NC_UNEMP_wages * self.NC_UNEMP) diff --git a/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py new file mode 100755 index 00000000..2c484ac4 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py @@ -0,0 +1,36 @@ +# 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 TestUsNCPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + NC_UNEMP_MAX_WAGE = 25200.0 + NC_UNEMP = 1.0 + NC_INC_TAX = 0.0535 + + def _test_sit(self, wage, filing_status, allowances, additional_withholding, schedule_pay, date_start, expected_withholding): + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('NC'), + nc_nc4_sit_filing_status=filing_status, + nc_nc4_sit_allowances=allowances, + 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 if filing_status else 0.0) + + def test_2020_taxes_example(self): + self._test_er_suta('NC', self.NC_UNEMP, date(2020, 1, 1), wage_base=self.NC_UNEMP_MAX_WAGE) + self._test_sit(20000.0, 'single', 1, 100.0, 'weekly', date(2020, 1, 1), 1156.0) + self._test_sit(5000.0, 'married', 1, 0.0, 'weekly', date(2020, 1, 1), 254.0) + self._test_sit(4000.0, 'head_household', 1, 5.0, 'semi-monthly', date(2020, 1, 1), 177.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 a96d5f57..394f3e80 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -73,6 +73,12 @@ + +

Form NC-4 - State Income Tax

+ + + +

Form NJ-W4 - State Income Tax