diff --git a/l10n_us_hr_payroll/__init__.py b/l10n_us_hr_payroll/__init__.py new file mode 100644 index 00000000..013f4e73 --- /dev/null +++ b/l10n_us_hr_payroll/__init__.py @@ -0,0 +1,12 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models + +def _post_install_hook(cr, registry): + """ + This method will set the default for the Payslip Sum Behavior + """ + cr.execute("SELECT id FROM ir_config_parameter WHERE key = 'hr_payroll.payslip.sum_behavior';") + existing = cr.fetchall() + if not existing: + cr.execute("INSERT INTO ir_config_parameter (key, value) VALUES ('hr_payroll.payslip.sum_behavior', 'date');") diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py new file mode 100644 index 00000000..0084f427 --- /dev/null +++ b/l10n_us_hr_payroll/__manifest__.py @@ -0,0 +1,65 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'United States of America - Payroll', + 'author': 'Hibou Corp. ', + 'version': '13.0.2020.0.0', + 'category': 'Payroll Localization', + 'depends': [ + 'hr_payroll', + 'hr_contract_reports', + ], + 'description': """ +United States of America - Payroll Rules. +========================================= + + """, + + 'data': [ + 'security/ir.model.access.csv', + 'data/base.xml', + 'data/integration_rules.xml', + 'data/federal/fed_940_futa_parameters.xml', + 'data/federal/fed_940_futa_rules.xml', + 'data/federal/fed_941_fica_parameters.xml', + 'data/federal/fed_941_fica_rules.xml', + 'data/federal/fed_941_fit_parameters.xml', + 'data/federal/fed_941_fit_rules.xml', + 'data/state/ak_alaska.xml', + 'data/state/al_alabama.xml', + 'data/state/ar_arkansas.xml', + 'data/state/az_arizona.xml', + 'data/state/ca_california.xml', + 'data/state/co_colorado.xml', + 'data/state/ct_connecticut.xml', + 'data/state/de_delaware.xml', + 'data/state/fl_florida.xml', + 'data/state/ga_georgia.xml', + 'data/state/hi_hawaii.xml', + 'data/state/ia_iowa.xml', + 'data/state/id_idaho.xml', + 'data/state/il_illinois.xml', + 'data/state/mi_michigan.xml', + 'data/state/mn_minnesota.xml', + 'data/state/mo_missouri.xml', + 'data/state/ms_mississippi.xml', + 'data/state/mt_montana.xml', + 'data/state/nc_northcarolina.xml', + 'data/state/nh_new_hampshire.xml', + 'data/state/nj_newjersey.xml', + 'data/state/nm_new_mexico.xml', + 'data/state/oh_ohio.xml', + 'data/state/pa_pennsylvania.xml', + 'data/state/tx_texas.xml', + 'data/state/va_virginia.xml', + 'data/state/wa_washington.xml', + 'views/hr_contract_views.xml', + 'views/res_config_settings_views.xml', + 'views/us_payroll_config_views.xml', + ], + 'demo': [ + ], + 'auto_install': False, + 'post_init_hook': '_post_install_hook', + 'license': 'OPL-1', +} diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml new file mode 100644 index 00000000..2e29934e --- /dev/null +++ b/l10n_us_hr_payroll/data/base.xml @@ -0,0 +1,128 @@ + + + + + USA Employee + + + + + + USA Employee Standard + + + + + + + + + EE: State Unemployment SUTA + EE_US_SUTA + + + + ER: State Unemployment SUTA + ER_US_SUTA + + + + + + EE: State Income Tax Withholding + EE_US_SIT + + + + + + + + Wage: US FIT Exempt + ALW_FIT_EXEMPT + + + + + Wage: US FIT & FICA Exempt + ALW_FIT_FICA_EXEMPT + + + + + Wage: US FIT & FUTA Exempt + ALW_FIT_FUTA_EXEMPT + + + + + Wage: US FIT & FICA & FUTA Exempt + ALW_FIT_FICA_FUTA_EXEMPT + + + + + Wage: US FICA Exempt + ALW_FICA_EXEMPT + + + + + Wage: US FICA & FUTA Exempt + ALW_FICA_FUTA_EXEMPT + + + + + Wage: US FUTA Exempt + ALW_FUTA_EXEMPT + + + + + + + Deduction: US FIT Exempt + DED_FIT_EXEMPT + + + + + Deduction: US FIT & FICA Exempt + DED_FIT_FICA_EXEMPT + + + + + Deduction: US FIT & FUTA Exempt + DED_FIT_FUTA_EXEMPT + + + + + Deduction: US FIT & FICA & FUTA Exempt + DED_FIT_FICA_FUTA_EXEMPT + + + + + Deduction: US FICA Exempt + DED_FICA_EXEMPT + + + + + Deduction: US FICA & FUTA Exempt + DED_FICA_FUTA_EXEMPT + + + + + Deduction: US FUTA Exempt + DED_FUTA_EXEMPT + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/federal/fed_940_futa_parameters.xml b/l10n_us_hr_payroll/data/federal/fed_940_futa_parameters.xml new file mode 100644 index 00000000..9c34afb6 --- /dev/null +++ b/l10n_us_hr_payroll/data/federal/fed_940_futa_parameters.xml @@ -0,0 +1,38 @@ + + + + + Federal 940 FUTA Wage Base + fed_940_futa_wage_base + + + + 7000.00 + + + + + + + Federal 940 FUTA Rate Basic + fed_940_futa_rate_basic + + + + 6.0 + + + + + + Federal 940 FUTA Rate Normal + fed_940_futa_rate_normal + + + + 0.6 + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml b/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml new file mode 100644 index 00000000..6a153efb --- /dev/null +++ b/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml @@ -0,0 +1,28 @@ + + + + + US Federal 940 - EFTPS + + + + ER: Federal 940 FUTA + ER_US_940_FUTA + + + + + + + + ER: US FUTA Federal Unemployment + ER_US_940_FUTA + python + result, _ = er_us_940_futa(payslip, categories, worked_days, inputs) + code + result, result_rate = er_us_940_futa(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/federal/fed_941_fica_parameters.xml b/l10n_us_hr_payroll/data/federal/fed_941_fica_parameters.xml new file mode 100644 index 00000000..1be404bb --- /dev/null +++ b/l10n_us_hr_payroll/data/federal/fed_941_fica_parameters.xml @@ -0,0 +1,88 @@ + + + + + + Federal 941 FICA Social Security Wage Base + fed_941_fica_ss_wage_base + + + + 128400.0 + + + + + 132900.0 + + + + + 137700.0 + + + + + + + Federal 941 FICA Rate + fed_941_fica_ss_rate + + + + 6.2 + + + + + + + + Federal 941 FICA Medicare Wage Base + fed_941_fica_m_wage_base + + + + "inf" + + + + + + + Federal 941 FICA Rate + fed_941_fica_m_rate + + + + 1.45 + + + + + + + + Federal 941 FICA Medicare Additional Wage Start + fed_941_fica_m_add_wage_start + + + + 200000.0 + + + + + + + Federal 941 FICA Medicare Additional Rate + fed_941_fica_m_add_rate + + + + 0.9 + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml b/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml new file mode 100644 index 00000000..64c91607 --- /dev/null +++ b/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml @@ -0,0 +1,94 @@ + + + + + US Federal 941 - EFTPS + + + + EE: Federal 941 FICA + EE_US_941_FICA + + + + + ER: Federal 941 FICA + ER_US_941_FICA + + + + + + + + + + EE: US FICA Social Security + EE_US_941_FICA_SS + python + result, _ = ee_us_941_fica_ss(payslip, categories, worked_days, inputs) + code + result, result_rate = ee_us_941_fica_ss(payslip, categories, worked_days, inputs) + + + + + + + + + ER: US FICA Social Security + ER_US_941_FICA_SS + python + result, _ = er_us_941_fica_ss(payslip, categories, worked_days, inputs) + code + result, result_rate = er_us_941_fica_ss(payslip, categories, worked_days, inputs) + + + + + + + + + + EE: US FICA Medicare + EE_US_941_FICA_M + python + result, _ = ee_us_941_fica_m(payslip, categories, worked_days, inputs) + code + result, result_rate = ee_us_941_fica_m(payslip, categories, worked_days, inputs) + + + + + + + + + ER: US FICA Medicare + ER_US_941_FICA_M + python + result, _ = er_us_941_fica_m(payslip, categories, worked_days, inputs) + code + result, result_rate = er_us_941_fica_m(payslip, categories, worked_days, inputs) + + + + + + + + + + EE: US FICA Medicare Additional + EE_US_941_FICA_M_ADD + python + result, _ = ee_us_941_fica_m_add(payslip, categories, worked_days, inputs) + code + result, result_rate = ee_us_941_fica_m_add(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/federal/fed_941_fit_parameters.xml b/l10n_us_hr_payroll/data/federal/fed_941_fit_parameters.xml new file mode 100644 index 00000000..97753e5a --- /dev/null +++ b/l10n_us_hr_payroll/data/federal/fed_941_fit_parameters.xml @@ -0,0 +1,504 @@ + + + + + + Federal 941 FIT Allowance + fed_941_fit_allowance + + + + + { + 'weekly': 80.80, + 'bi-weekly': 161.50, + 'semi-monthly': 175.00, + 'monthly': 350.00, + 'quarterly': 1050.00, + 'semi-annually': 2100.00, + 'annually': 4200.00, + } + + + + + { + 'weekly': 80.80, + 'bi-weekly': 161.50, + 'semi-monthly': 175.00, + 'monthly': 350.00, + 'quarterly': 1050.00, + 'semi-annually': 2100.00, + 'annually': 4200.00, + } + + + + + + 4300.0 + + + + + + Federal 941 FIT NRA Additional + fed_941_fit_nra_additional + + + + + { + 'weekly': 153.80, + 'bi-weekly': 307.70, + 'semi-monthly': 333.30, + 'monthly': 666.70, + 'quarterly': 2000.00, + 'semi-annually': 4000.00, + 'annually': 8000.00, + } + + + + + { + 'weekly': 153.80, + 'bi-weekly': 307.70, + 'semi-monthly': 333.30, + 'monthly': 666.70, + 'quarterly': 2000.00, + 'semi-annually': 4000.00, + 'annually': 8000.00, + } + + + + + { + 'weekly': 238.50, + 'bi-weekly': 476.90, + 'semi-monthly': 516.70, + 'monthly': 1033.30, + 'quarterly': 3100.00, + 'semi-annually': 6200.00, + 'annually': 12400.00, + } + + + + + + + Federal 941 FIT Table Single + fed_941_fit_table_single + + + + + + { + 'weekly': [ + ( 73.00, 0.00, 0), + ( 260.00, 0.00, 10), + ( 832.00, 18.70, 12), + ( 1692.00, 87.34, 22), + ( 3164.00, 276.54, 24), + ( 3998.00, 629.82, 32), + ( 9887.00, 896.70, 35), + ( 'inf', 2957.85, 37), + ], + 'bi-weekly': [ + ( 146.00, 0.00, 0), + ( 519.00, 0.00, 10), + ( 1664.00, 37.30, 12), + ( 3385.00, 174.70, 22), + ( 6328.00, 553.32, 24), + ( 7996.00, 1259.64, 32), + ( 19773.00, 1793.40, 35), + ( 'inf', 5915.35, 37), + ], + 'semi-monthly': [ + ( 158.00, 0.00, 0), + ( 563.00, 0.00, 10), + ( 1803.00, 40.50, 12), + ( 3667.00, 189.30, 22), + ( 6855.00, 599.38, 24), + ( 8663.00, 1364.50, 32), + ( 21421.00, 1943.06, 35), + ( 'inf', 6408.36, 37), + ], + 'monthly': [ + ( 317.00, 0.00, 0), + ( 1125.00, 0.00, 10), + ( 3606.00, 80.80, 12), + ( 7333.00, 378.52, 22), + ( 13710.00, 1198.46, 24), + ( 17325.00, 2728.94, 32), + ( 42842.00, 3885.74, 35), + ( 'inf', 12816.69, 37), + ], + 'quarterly': [ + ( 950.00, 0.00, 0), + ( 3375.00, 0.00, 10), + ( 10819.00, 242.50, 12), + ( 22000.00, 1135.78, 22), + ( 41131.00, 3595.60, 24), + ( 51975.00, 8187.04, 32), + ( 128525.00, 11657.12, 35), + ( 'inf', 38449.62, 37), + ], + 'semi-annually': [ + ( 1900.00, 0.00, 0), + ( 6750.00, 0.00, 10), + ( 21638.00, 485.00, 12), + ( 44000.00, 2271.56, 22), + ( 82263.00, 7191.20, 24), + ( 103950.00, 16374.32, 32), + ( 257050.00, 23314.16, 35), + ( 'inf', 76899.16, 37), + ], + 'annually': [ + ( 3800.00, 0.00, 0), + ( 13500.00, 0.00, 10), + ( 43275.00, 970.00, 12), + ( 88000.00, 4543.00, 22), + ( 164525.00, 14382.50, 24), + ( 207900.00, 32748.50, 32), + ( 514100.00, 46628.50, 35), + ( 'inf', 153798.50, 37), + ], + } + + + + + + { + 'weekly': [ + ( 73.00, 0.00, 0), + ( 260.00, 0.00, 10), + ( 832.00, 18.70, 12), + ( 1692.00, 87.34, 22), + ( 3164.00, 276.54, 24), + ( 3998.00, 629.82, 32), + ( 9887.00, 896.70, 35), + ( 'inf', 2957.85, 37), + ], + 'bi-weekly': [ + ( 146.00, 0.00, 0), + ( 519.00, 0.00, 10), + ( 1664.00, 37.30, 12), + ( 3385.00, 174.70, 22), + ( 6328.00, 553.32, 24), + ( 7996.00, 1259.64, 32), + ( 19773.00, 1793.40, 35), + ( 'inf', 5915.35, 37), + ], + 'semi-monthly': [ + ( 158.00, 0.00, 0), + ( 563.00, 0.00, 10), + ( 1803.00, 40.50, 12), + ( 3667.00, 189.30, 22), + ( 6855.00, 599.38, 24), + ( 8663.00, 1364.50, 32), + ( 21421.00, 1943.06, 35), + ( 'inf', 6408.36, 37), + ], + 'monthly': [ + ( 317.00, 0.00, 0), + ( 1125.00, 0.00, 10), + ( 3606.00, 80.80, 12), + ( 7333.00, 378.52, 22), + ( 13710.00, 1198.46, 24), + ( 17325.00, 2728.94, 32), + ( 42842.00, 3885.74, 35), + ( 'inf', 12816.69, 37), + ], + 'quarterly': [ + ( 950.00, 0.00, 0), + ( 3375.00, 0.00, 10), + ( 10819.00, 242.50, 12), + ( 22000.00, 1135.78, 22), + ( 41131.00, 3595.60, 24), + ( 51975.00, 8187.04, 32), + ( 128525.00, 11657.12, 35), + ( 'inf', 38449.62, 37), + ], + 'semi-annually': [ + ( 1900.00, 0.00, 0), + ( 6750.00, 0.00, 10), + ( 21638.00, 485.00, 12), + ( 44000.00, 2271.56, 22), + ( 82263.00, 7191.20, 24), + ( 103950.00, 16374.32, 32), + ( 257050.00, 23314.16, 35), + ( 'inf', 76899.16, 37), + ], + 'annually': [ + ( 3800.00, 0.00, 0), + ( 13500.00, 0.00, 10), + ( 43275.00, 970.00, 12), + ( 88000.00, 4543.00, 22), + ( 164525.00, 14382.50, 24), + ( 207900.00, 32748.50, 32), + ( 514100.00, 46628.50, 35), + ( 'inf', 153798.50, 37), + ], + } + + + + + + + { + 'standard': [ + ( 0.00, 0.00, 0.00), + ( 3800.00, 0.00, 0.10), + ( 13675.00, 987.50, 0.12), + ( 43925.00, 4617.50, 0.22), + ( 89325.00, 14605.50, 0.24), + ( 167100.00, 33271.50, 0.32), + ( 211150.00, 47367.50, 0.35), + ( 522200.00, 156235.00, 0.37), + ], + 'higher': [ + ( 0.00, 0.00, 0.00), + ( 6200.00, 0.00, 0.10), + ( 11138.00, 493.75, 0.12), + ( 26263.00, 2308.75, 0.22), + ( 48963.00, 7302.75, 0.24), + ( 87850.00, 16635.75, 0.32), + ( 109875.00, 23683.75, 0.35), + ( 265400.00, 78117.50, 0.37), + ], + } + + + + + + + Federal 941 FIT Table Married + fed_941_fit_table_married + + + + + + { + 'weekly': [ + ( 227.00, 0.00, 0), + ( 600.00, 0.00, 10), + ( 1745.00, 37.30, 12), + ( 3465.00, 174.70, 22), + ( 6409.00, 553.10, 24), + ( 8077.00, 1259.66, 32), + ( 12003.00, 1793.42, 35), + ( 'inf', 3167.52, 37), + ], + 'bi-weekly': [ + ( 454.00, 0.00, 0), + ( 1200.00, 0.00, 10), + ( 3490.00, 74.60, 12), + ( 6931.00, 349.40, 22), + ( 12817.00, 1106.42, 24), + ( 16154.00, 2519.06, 32), + ( 24006.00, 3586.90, 35), + ( 'inf', 6335.10, 37), + ], + 'semi-monthly': [ + ( 492.00, 0.00, 0), + ( 1300.00, 0.00, 10), + ( 3781.00, 80.80, 12), + ( 7508.00, 378.52, 22), + ( 13885.00, 1198.46, 24), + ( 17500.00, 2728.94, 32), + ( 26006.00, 3885.74, 35), + ( 'inf', 6862.84, 37), + ], + 'monthly': [ + ( 983.00, 0.00, 0), + ( 2600.00, 0.00, 10), + ( 7563.00, 161.70, 12), + ( 15017.00, 757.26, 22), + ( 27771.00, 2397.14, 24), + ( 35000.00, 5458.10, 32), + ( 52013.00, 7771.38, 35), + ( 'inf', 13725.93, 37), + ], + 'quarterly': [ + ( 2950.00, 0.00, 0), + ( 7800.00, 0.00, 10), + ( 22688.00, 485.00, 12), + ( 45050.00, 2271.56, 22), + ( 83313.00, 7191.20, 24), + ( 105000.00, 16374.32, 32), + ( 156038.00, 23314.16, 35), + ( 'inf', 41177.46, 37), + ], + 'semi-annually': [ + ( 5900.00, 0.00, 0), + ( 15600.00, 0.00, 10), + ( 45375.00, 970.00, 12), + ( 90100.00, 4543.00, 22), + ( 166625.00, 14382.50, 24), + ( 210000.00, 32748.50, 32), + ( 312075.00, 46628.50, 35), + ( 'inf', 82354.75, 37), + ], + 'annually': [ + ( 11800.00, 0.00, 0), + ( 31200.00, 0.00, 10), + ( 90750.00, 1940.00, 12), + ( 180200.00, 9086.00, 22), + ( 333250.00, 28765.00, 24), + ( 420000.00, 65497.00, 32), + ( 624150.00, 93257.00, 35), + ( 'inf', 164709.50, 37), + ], + } + + + + + + { + 'weekly': [ + ( 227.00, 0.00, 0), + ( 600.00, 0.00, 10), + ( 1745.00, 37.30, 12), + ( 3465.00, 174.70, 22), + ( 6409.00, 553.10, 24), + ( 8077.00, 1259.66, 32), + ( 12003.00, 1793.42, 35), + ( 'inf', 3167.52, 37), + ], + 'bi-weekly': [ + ( 454.00, 0.00, 0), + ( 1200.00, 0.00, 10), + ( 3490.00, 74.60, 12), + ( 6931.00, 349.40, 22), + ( 12817.00, 1106.42, 24), + ( 16154.00, 2519.06, 32), + ( 24006.00, 3586.90, 35), + ( 'inf', 6335.10, 37), + ], + 'semi-monthly': [ + ( 492.00, 0.00, 0), + ( 1300.00, 0.00, 10), + ( 3781.00, 80.80, 12), + ( 7508.00, 378.52, 22), + ( 13885.00, 1198.46, 24), + ( 17500.00, 2728.94, 32), + ( 26006.00, 3885.74, 35), + ( 'inf', 6862.84, 37), + ], + 'monthly': [ + ( 983.00, 0.00, 0), + ( 2600.00, 0.00, 10), + ( 7563.00, 161.70, 12), + ( 15017.00, 757.26, 22), + ( 27771.00, 2397.14, 24), + ( 35000.00, 5458.10, 32), + ( 52013.00, 7771.38, 35), + ( 'inf', 13725.93, 37), + ], + 'quarterly': [ + ( 2950.00, 0.00, 0), + ( 7800.00, 0.00, 10), + ( 22688.00, 485.00, 12), + ( 45050.00, 2271.56, 22), + ( 83313.00, 7191.20, 24), + ( 105000.00, 16374.32, 32), + ( 156038.00, 23314.16, 35), + ( 'inf', 41177.46, 37), + ], + 'semi-annually': [ + ( 5900.00, 0.00, 0), + ( 15600.00, 0.00, 10), + ( 45375.00, 970.00, 12), + ( 90100.00, 4543.00, 22), + ( 166625.00, 14382.50, 24), + ( 210000.00, 32748.50, 32), + ( 312075.00, 46628.50, 35), + ( 'inf', 82354.75, 37), + ], + 'annually': [ + ( 11800.00, 0.00, 0), + ( 31200.00, 0.00, 10), + ( 90750.00, 1940.00, 12), + ( 180200.00, 9086.00, 22), + ( 333250.00, 28765.00, 24), + ( 420000.00, 65497.00, 32), + ( 624150.00, 93257.00, 35), + ( 'inf', 164709.50, 37), + ], + } + + + + + + + { + 'standard': [ + ( 0.00, 0.00, 0.00), + ( 11900.00, 0.00, 0.10), + ( 31650.00, 1975.00, 0.12), + ( 92150.00, 9235.00, 0.22), + ( 182950.00, 29211.00, 0.24), + ( 338500.00, 66543.00, 0.32), + ( 426600.00, 94735.00, 0.35), + ( 633950.00, 167307.50, 0.37), + ], + 'higher': [ + ( 0.00, 0.00, 0.00), + ( 12400.00, 0.00, 0.10), + ( 22275.00, 987.50, 0.12), + ( 52525.00, 4617.50, 0.22), + ( 97925.00, 14605.50, 0.24), + ( 175700.00, 33271.50, 0.32), + ( 219750.00, 47367.50, 0.35), + ( 323425.00, 83653.75, 0.37), + ], + } + + + + + + Federal 941 FIT Table Head of Household + fed_941_fit_table_hh + + + + + + { + 'standard': [ + ( 0.00, 0.00, 0.00), + ( 10050.00, 0.00, 0.10), + ( 24150.00, 1410.00, 0.12), + ( 63750.00, 6162.00, 0.22), + ( 95550.00, 13158.00, 0.24), + ( 173350.00, 31830.00, 0.32), + ( 217400.00, 45926.00, 0.35), + ( 528450.00, 154793.50, 0.37), + ], + 'higher': [ + ( 0.00, 0.00, 0.00), + ( 9325.00, 0.00, 0.10), + ( 16375.00, 705.00, 0.12), + ( 36175.00, 3081.00, 0.22), + ( 52075.00, 6579.00, 0.24), + ( 90975.00, 15915.00, 0.32), + ( 113000.00, 22963.00, 0.35), + ( 268525.00, 77396.75, 0.37), + ], + } + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml b/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml new file mode 100644 index 00000000..4e3cb28c --- /dev/null +++ b/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml @@ -0,0 +1,24 @@ + + + + + EE: Federal 941 Income Tax Withholding + EE_US_941_FIT + + + + + + + + EE: US Federal Income Tax Withholding + EE_US_941_FIT + python + result, _ = ee_us_941_fit(payslip, categories, worked_days, inputs) + code + result, result_rate = ee_us_941_fit(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/integration_rules.xml b/l10n_us_hr_payroll/data/integration_rules.xml new file mode 100644 index 00000000..9991a3aa --- /dev/null +++ b/l10n_us_hr_payroll/data/integration_rules.xml @@ -0,0 +1,29 @@ + + + + + python + result = inputs.COMMISSION.amount > 0.0 if inputs.COMMISSION else False + code + result = inputs.COMMISSION.amount if inputs.COMMISSION else 0 + BASIC_COM + + Commissions + + + + + + + python + result = inputs.BADGES.amount > 0.0 if inputs.BADGES else False + code + result = inputs.BADGES.amount if inputs.BADGES else 0 + BASIC_BADGES + + Badges + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ak_alaska.xml b/l10n_us_hr_payroll/data/state/ak_alaska.xml new file mode 100644 index 00000000..2c995088 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ak_alaska.xml @@ -0,0 +1,95 @@ + + + + + US AK Alaska SUTA Wage Base + us_ak_suta_wage_base + + + + + 39900.00 + + + + + 41500.00 + + + + + + + + US AK Alaska SUTA Rate + us_ak_suta_rate + + + + + 1.780 + + + + + 1.590 + + + + + + + US AK Alaska SUTA Rate EE + us_ak_suta_ee_rate + + + + + 0.500 + + + + + 0.500 + + + + + + + + US Alaska - Department of Labor and Workforce Development (ADLWD) - Unemployment Tax + + + + + + + + + + ER: US AK Alaska State Unemployment + ER_US_AK_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ak_suta_wage_base', rate='us_ak_suta_rate', state_code='AK') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ak_suta_wage_base', rate='us_ak_suta_rate', state_code='AK') + + + + + + + + + EE: US AK Alaska State Unemployment (UC-2) + EE_US_AK_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ak_suta_wage_base', rate='us_ak_suta_ee_rate', state_code='AK') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ak_suta_wage_base', rate='us_ak_suta_ee_rate', state_code='AK') + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/al_alabama.xml b/l10n_us_hr_payroll/data/state/al_alabama.xml new file mode 100644 index 00000000..b1a8cfe1 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/al_alabama.xml @@ -0,0 +1,191 @@ + + + + + US AL Alabama SUTA Wage Base + us_al_suta_wage_base + + + + + 8000.0 + + + + + 8000.0 + + + + + + + + US AL Alabama SUTA Rate + us_al_suta_rate + + + + + 2.7 + + + + + 2.7 + + + + + + + US AL Alabama SIT Tax Rate + us_al_sit_tax_rate + + + + + { + '0': [(500, 2),( 3000, 4),('inf', 5)], + 'M': [( 1000, 2),( 6000, 4),('inf', 5)], + } + + + + + { + '0' : [(500, 2),(2500, 4),('inf', 5)], + 'M': [(1000, 2),(5000, 4),('inf', 5)], + } + + + + + + + US AL Alabama Dependent Rate + us_al_sit_dependent_rate + + + + + [ + ( 1000, 20000), + ( 500, 100000), + ( 300, 'inf'), + ] + + + + + [ + ( 1000, 20000), + ( 500, 100000), + ( 300, 'inf'), + ] + + + + + + + US AL Alabama Standard Deduction Rate + us_al_sit_standard_deduction_rate + + + + + { + '0': ((23499.0, 2500.0), (33000.0, 2500.0, 25.0, 500.0), ('inf', 2000.0)), + 'S': ((23499.0, 2500.0), (33000.0, 2500.0, 25.0, 500.0), ('inf', 2000.0)), + 'MS': ((10749.0, 3750.0), (15500.0, 3750.0, 88.0, 250.0), ('inf', 2000.0)), + 'M': ((23499.0, 7500.0), (33000.0, 7500.0, 175.0, 500.0), ('inf', 4000.0)), + 'H': ((23499.0, 4700.0), (33000.0, 7500.0, 175.0, 500.0), ('inf', 4000.0)), + } + + + + + { + '0': ((23499.0, 2500.0), (33000.0, 2500.0, 25.0, 500.0), ('inf', 2000.0)), + 'S': ((23499.0, 2500.0), (33000.0, 2500.0, 25.0, 500.0), ('inf', 2000.0)), + 'MS': ((10749.0, 3750.0), (15500.0, 3750.0, 88.0, 250.0), ('inf', 2000.0)), + 'M': ((23499.0, 7500.0), (33000.0, 7500.0, 175.0, 500.0), ('inf', 4000.0)), + 'H': ((23499.0, 4700.0), (33000.0, 7500.0, 175.0, 500.0), ('inf', 4000.0)), + } + + + + + + + US AL Alabama Personal Exemption Rate + us_al_sit_personal_exemption_rate + + + + + { + '0' : 0, + 'S' : 1500, + 'MS': 1500, + 'M' : 3000, + 'H' : 3000, + } + + + + + { + '0' : 0, + 'S' : 1500, + 'MS': 1500, + 'M' : 3000, + 'H' : 3000, + } + + + + + + + + US Alabama - Department of Economic Security (IDES) - Unemployment Tax + + + + US Alabama - Department of Revenue (IDOR) - Income Tax + + + + + + + + + + ER: US AL Alabama State Unemployment + ER_US_AL_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_al_suta_wage_base', rate='us_al_suta_rate', state_code='AL') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_al_suta_wage_base', rate='us_al_suta_rate', state_code='AL') + + + + + + + + + EE: US AL Alabama State Income Tax Withholding + EE_US_AL_SIT + python + result, _ = al_alabama_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = al_alabama_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ar_arkansas.xml b/l10n_us_hr_payroll/data/state/ar_arkansas.xml new file mode 100644 index 00000000..a59e57aa --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ar_arkansas.xml @@ -0,0 +1,143 @@ + + + + + US AR Arkansas SUTA Wage Base + us_ar_suta_wage_base + + + + + 10000.0 + + + + + 8000.0 + + + + + + + + US AR Arkansas SUTA Rate + us_ar_suta_rate + + + + + 3.2 + + + + + 2.9 + + + + + + + US AR Arkansas SIT Tax Rate + us_ar_sit_tax_rate + + + + + [ + ( 4599, 0.0, 0.00), + ( 9099, 2.0, 91.98), + ( 13699, 3.0, 182.97), + ( 22599, 3.4, 237.77), + ( 37899, 5.0, 421.46), + ( 80800, 5.9, 762.55), + ( 81800, 6.6, 1243.40), + ( 82800, 6.6, 1143.40), + ( 84100, 6.6, 1043.40), + ( 85200, 6.6, 943.40), + ( 86200, 6.6, 843.40), + ( 'inf', 6.6, 803.40), + ] + + + + + [ + ( 4599, 0.0, 0.00), + ( 9099, 2.0, 91.98), + ( 13699, 3.0, 182.97), + ( 22599, 3.4, 237.77), + ( 37899, 5.0, 421.46), + ( 80800, 5.9, 762.55), + ( 81800, 6.6, 1243.40), + ( 82800, 6.6, 1143.40), + ( 84100, 6.6, 1043.40), + ( 85200, 6.6, 943.40), + ( 86200, 6.6, 843.40), + ( 'inf', 6.6, 803.40), + ] + + + + + + + US AR Arkansas Allowances Rate + us_ar_sit_standard_deduction_rate + + + + + 2200.0 + + + + + 2200.0 + + + + + + + + US Arkansas - Department of Workforce Solutions - Unemployment Tax + + + + US Arkansas - Department of Financial Administration - Income Tax + + + + + + + + + + ER: US AR Arkansas State Unemployment + ER_US_AR_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ar_suta_wage_base', rate='us_ar_suta_rate', state_code='AR') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ar_suta_wage_base', rate='us_ar_suta_rate', state_code='AR') + + + + + + + + + EE: US AR Arkansas State Income Tax Withholding + EE_US_AR_SIT + python + result, _ = ar_arkansas_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ar_arkansas_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/az_arizona.xml b/l10n_us_hr_payroll/data/state/az_arizona.xml new file mode 100644 index 00000000..80b800c1 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/az_arizona.xml @@ -0,0 +1,81 @@ + + + + + US AZ Arizona SUTA Wage Base + us_az_suta_wage_base + + + + + 7000.0 + + + + + 7000.0 + + + + + + + + US AZ Arizona SUTA Rate + us_az_suta_rate + + + + + 2.0 + + + + + 2.0 + + + + + + + + US Arizona - Department of Economic Security (ADES) - Unemployment Tax + + + + US Arizona - Department of Revenue (ADOR) - Income Tax + + + + + + + + + + ER: US AZ Arizona State Unemployment + ER_US_AZ_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_az_suta_wage_base', rate='us_az_suta_rate', state_code='AZ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_az_suta_wage_base', rate='us_az_suta_rate', state_code='AZ') + + + + + + + + + EE: US AZ Arizona State Income Tax Withholding + EE_US_AZ_SIT + python + result, _ = az_arizona_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = az_arizona_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ca_california.xml b/l10n_us_hr_payroll/data/state/ca_california.xml new file mode 100644 index 00000000..e51cb821 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ca_california.xml @@ -0,0 +1,357 @@ + + + + + US CA California SUTA Wage Base + us_ca_suta_wage_base + + + + + 7000.0 + + + + + 7000.0 + + + + + + + + US CA California SUTA Rate + us_ca_suta_rate + + + + + 3.5 + + + + + 3.4 + + + + + + + + US CA California SUTA ETT Rate + us_ca_suta_ett_rate + + + + + 0.1 + + + + + 0.1 + + + + + + + + US CA California SUTA SDI Rate + us_ca_suta_sdi_rate + + + + + 1.0 + + + + + 1.0 + + + + + + + US CA California SIT Tax Rate + us_ca_sit_tax_rate + + + + + { + 'head_household': { + 'weekly': ((316, 0.011, 0.0), (750, 0.022, 3.48), (967, 0.044, 13.03), (1196, 0.066, 22.58), (1413, 0.088, 37.69), (7212, 0.1023, 56.79), (8654, 0.1133, 650.03), (14423, 0.1243, 813.41), (19231, 0.1353, 1530.50), ('inf', 0.1463, 2181.02)), + 'bi-weekly': ((632, 0.011, 0.0), (1500, 0.022, 6.95), (1934, 0.044, 26.05), (2392, 0.066, 45.15), (2826, 0.088, 75.38), (14424, 0.1023, 113.57), (17308, 0.1133, 1300.05), (28846, 0.1243, 1626.81), (38462, 0.1353, 3060.98), ('inf', 0.1463, 4362.02)), + 'semi-monthly': ((686, 0.011, 0.0), (1625, 0.022, 7.55), (2094, 0.044, 28.21), (2592, 0.066, 48.85), (3062, 0.088, 81.72), (15625, 0.1023, 123.08), (18750, 0.1133, 1408.27), (31250, 0.1243, 1762.33), (41667, 0.1353, 3316.08), ('inf', 0.1463, 4725.50)), + 'monthly': ((1372, 0.011, 0.0), (3250, 0.022, 15.09), (4188, 0.044, 56.41), (5184, 0.066, 97.68), (6124, 0.088, 163.42), (31250, 0.1023, 246.148), (37500, 0.1133, 2816.53), (62500, 0.1243, 3524.66), (83334, 0.1353, 6632.16), ('inf', 0.1463, 9451.00)), + 'quarterly': ((4114, 0.011, 0.0), (9748, 0.022, 45.25), (12566, 0.044, 169.20), (15552, 0.066, 293.19), (18369, 0.088, 490.27), (93751, 0.1023, 738.17), (112501, 0.1133, 8449.75), (187501, 0.1243, 10574.13), (250000, 0.1353, 19896.63), ('inf', 0.1463, 28352.74)), + 'semi-annual': ((8228, 0.011, 0.0), (19496, 0.022, 90.51), (25132, 0.044, 338.41), (31104, 0.066, 586.39), (36738, 0.088, 980.54), (187502, 0.1023, 1476.33), (225002, 0.1133, 16899.49), (375002, 0.1243, 21148.24), (500000, 0.1353, 39793.24), ('inf', 0.1463, 56705.47)), + 'annually': ((16457, 0.011, 0.0), (38991, 0.022, 181.03), (50264, 0.044, 676.78), (62206, 0.066, 1172.79), (73477, 0.088, 1960.96), (375002, 0.1023, 2952.81), (450003, 0.1133, 33798.82), (750003, 0.1243, 42296.43), (1000000, 0.1353, 79586.43), ('inf', 0.1463, 113411.02)), + }, + 'married': { + 'weekly': ((316, 0.011, 0.0),(750, 0.022, 3.48),(1184, 0.044, 13.03),(1642, 0.066, 32.13), (2076, 0.088, 62.36),(10606, 0.1023, 100.55),(12726, 0.1133, 973.17),(19231, 0.1243, 1213.37),(21210, 0.1353, 2021.94),('inf', 0.1463, 2289.70)), + 'bi-weekly': ((632, 0.011, 0.0), (1500, 0.022, 6.95), (2368, 0.044, 26.05), (3284, 0.066, 64.24), (4152, 0.088, 124.70), (21212, 0.1023, 201.08), (25452, 0.1133, 1946.32), (38462, 0.1243, 2426.71), (42420, 0.1353, 4043.85), ('inf', 0.1463, 4579.37)), + 'semi-monthly': ((686, 0.011, 0.0), (1624, 0.022, 7.55), (2564, 0.044, 28.19), (3560, 0.066, 69.55), (4498, 0.088, 135.29), (22978, 0.1023, 217.83), (27574, 0.1133, 2108.33), (41667, 0.1243, 2629.06), (45956, 0.1353, 4380.82), ('inf', 0.1463, 4961.12)), + 'monthly': ((1372, 0.011, 0.0), (3248, 0.022, 15.09), (5128, 0.044, 56.36), (7120, 0.066, 139.08), (8996, 0.088, 270.55), (45956, 0.1023, 435.64), (55148, 0.1133, 4216.65), (83334, 0.1243, 5258.10), (91912, 0.1353, 8761.62), ('inf', 0.1463, 9922.22)), + 'quarterly': ((4112, 0.011, 0.0), (9748, 0.022, 45.23), (15384, 0.044, 169.22), (21356, 0.066, 417.20), (26990, 0.088, 811.35), (137870, 0.1023, 1307.14), (165442, 0.1133, 12650.16), (250000, 0.1243, 15774.07), (275736, 0.1353, 26284.63), ('inf', 0.1463, 29766.71)), + 'semi-annual': ((8224, 0.011, 0.0), (19496, 0.022, 90.46), (30768, 0.044, 338.44), (42712, 0.066, 834.41), (53980, 0.088, 1622.71), (275740, 0.1023, 2614.29), (330884, 0.1133, 25300.34), (500000, 0.1243, 31548.16), (551472, 0.1353, 52569.28), ('inf', 0.1463, 59533.44)), + 'annually': ((16446, 0.011, 0.0), (38990, 0.022, 180.91), (61538, 0.044, 676.88), (85422, 0.066, 1668.99), (107960, 0.088, 3245.33), (551476, 0.1023, 5228.67), (661768, 0.1133, 50600.36), (1000000, 0.1243, 63096.44), (1102946, 0.1353, 105138.68), ('inf', 0.1463, 119067.26)), + }, + 'single': { + 'weekly': ((158, 0.011, 0.0), (375, 0.022, 1.74), (592, 0.044, 6.51), (821, 0.066, 16.06), (1038, 0.088, 31.17), (5303, 0.1023, 50.27), (6363, 0.1133, 486.58), (10605, 0.1243, 606.68), (19231, 0.1353, 1133.96), ('inf', 0.1463, 2301.06)), + 'bi-weekly': ((316, 0.011, 0.0), (750, 0.022, 3.48), (1184, 0.044, 13.03), (1642, 0.066, 32.13), (2076, 0.088, 62.36), (10606, 0.1023, 100.55), (12726, 0.1133, 973.17), (21210, 0.1243, 1213.37), (38462, 0.1353, 2267.93), ('inf', 0.1463, 4602.13)), + 'semi-monthly': ((343, 0.011, 0.0), (812, 0.022, 3.77), (1282, 0.044, 14.09), (1780, 0.066, 34.77), (2249, 0.088, 67.64), (11489, 0.1023, 108.91), (13787, 0.1133, 1054.16), (22978, 0.1243, 1314.52), (41667, 0.1353, 2456.96),('inf', 0.1463, 4985.58)), + 'monthly': ((686, 0.011, 0.0), (1624, 0.022, 7.55), (2564, 0.044, 28.19), (3560, 0.066, 69.55), (4498, 0.088, 135.29), (22978, 0.1023, 217.83), (27574, 0.1133, 2108.33), (45956, 0.1243, 2629.06), (83334, 0.1353, 4913.94), ('inf', 0.1463, 9971.18)), + 'quarterly': ((2056, 0.011, 0.0), (4874, 0.022, 22.62), (7692, 0.044, 84.62), (10678, 0.066, 208.61), (13495, 0.088, 405.69), (68935, 0.1023, 653.59), (82721, 0.1133, 6325.10), (137868, 0.1243, 7887.05), (250000, 0.1353, 14741.82), ('inf', 0.1463, 29913.28)), + 'semi-annual': ((4112, 0.011, 0.0), (9748, 0.022, 45.23), (15384, 0.044, 169.22), (21356, 0.066, 417.20), (26990, 0.088, 811.35), (137870, 0.1023, 1307.14), (165442, 0.1133, 12650.16), (275736, 0.1243, 15774.07), (500000, 0.1353, 29483.61), ('inf', 0.1463, 59826.53)), + 'annually': ((8223, 0.011, 0.0), (19495, 0.022, 90.45), (30769, 0.044, 338.43), (42711, 0.066, 834.49), (53980, 0.088, 1622.66), (275738, 0.1023, 2614.33), (330884, 0.1133, 25300.17), (551473, 0.1243, 31548.21), (1000000, 0.1353, 58967.42), ('inf', 0.1463, 119653.12)), + }, + } + + + + + { + 'head_household': { + 'weekly': ((339, 0.011, 0.0), (803, 0.022, 3.73), (1035, 0.044, 13.93), (1281, 0.066, 24.15), (1514, 0.088, 40.39), (7725, 0.1023, 60.89), (9270, 0.1133, 696.28), (15450, 0.1243, 871.33), (19231, 0.1353, 1639.50), ('inf', 0.1463, 2151.07)), + 'bi-weekly': ((678, 0.011, 0.0), (1606, 0.022, 7.46), (2070, 0.044, 27.88), (2562, 0.066, 48.30), (3028, 0.088, 80.77), (15450, 0.1023, 121.78), (18540, 0.1133, 1392.55), (30900, 0.1243, 1742.65), (38462, 0.1353, 3279.00), ('inf', 0.1463, 4302.14)), + 'semi-monthly': ((735, 0.011, 0.0), (1740, 0.022, 8.09), (2243, 0.044, 30.20), (2777, 0.066, 52.33), (3280, 0.088, 87.57), (16738, 0.1023, 131.83), (20085, 0.1133, 1508.58), (33475, 0.1243, 1887.80), (41667, 0.1353, 3552.18), ('inf', 0.1463, 4660.56)), + 'monthly': ((1470, 0.011, 0.0), (3480, 0.022, 16.17), (4486, 0.044, 60.39), (5554, 0.066, 104.65), (6560, 0.088, 175.14), (33476, 0.1023, 263.67), (40170, 0.1133, 3017.18), (66950, 0.1243, 3775.61), (83334, 0.1353, 7104.36), ('inf', 0.1463, 9321.12)), + 'quarterly': ((4407, 0.011, 0.0), (10442, 0.022, 48.48), (13461, 0.044, 181.25), (16659, 0.066, 314.09), (19678, 0.088, 525.16), (100426, 0.1023, 790.83), (120512, 0.1133, 9051.35), (200853, 0.1243, 11327.09), (250000, 0.1353, 21313.48), ('inf', 0.1463, 27963.07)), + 'semi-annual': ((8814, 0.011, 0.0), (20884, 0.022, 96.95), (26922, 0.044, 362.49), (33318, 0.066, 628.16), (39356, 0.088, 1050.30), (200852, 0.1023, 1581.64), (241024, 0.1133, 18102.68), (401706, 0.1243, 22654.17), (500000, 0.1353, 42626.94), ('inf', 0.1463, 55926.12)), + 'annually': ((17629, 0.011, 0.0), (41768, 0.022, 193.92), (53843, 0.044, 724.98), (66636, 0.066, 1256.28), (78710, 0.088, 2100.62), (401705, 0.1023, 3163.13), (482047, 0.1133, 36205.52), (803410, 0.1243, 45308.27), (1000000, 0.1353, 85253.69), ('inf', 0.1463, 111852.32)), + }, + 'married': { + 'weekly': ((338, 0.011, 0.0),(804, 0.022, 3.72),(1268, 0.044, 13.97),(1760, 0.066, 34.39), (2224, 0.088, 66.86),(11360, 0.1023, 107.69),(13632, 0.1133, 1042.30),(19231, 0.1243, 1299.72),(22721, 0.1353, 1995.68),('inf', 0.1463, 2467.88)), + 'bi-weekly': ((676, 0.011, 0.0), (1608, 0.022, 7.44), (2536, 0.044, 27.94), (3520, 0.066, 68.77), (4448, 0.088, 124.70), (21212, 0.1023, 201.08), (25452, 0.1133, 1946.32), (38462, 0.1243, 2426.71), (42420, 0.1353, 4043.85), ('inf', 0.1463, 4579.37)), + 'semi-monthly': ((734, 0.011, 0.0), (1740, 0.022, 8.07), (2746, 0.044, 30.20), (3812, 0.066, 74.46), (4818, 0.088, 144.82), (24614, 0.1023, 233.35), (29538, 0.1133, 2258.48), (41667, 0.1243, 2816.37), (49229, 0.1353, 4324.00), ('inf', 0.1463, 5347.14)), + 'monthly': ((1468, 0.011, 0.0), (3480, 0.022, 16.15), (5492, 0.044, 60.41), (7624, 0.066, 148.94), (9636, 0.088, 2889.65), (49228, 0.1023, 466.71), (59076, 0.1133, 4516.97), (83334, 0.1243, 5632.75), (98458, 0.1353, 8648.02), ('inf', 0.1463, 10694.30)), + 'quarterly': ((4404, 0.011, 0.0), (10442, 0.022, 48.44), (16480, 0.044, 181.28), (22876, 0.066, 446.95), (28912, 0.088, 869.09), (147686, 0.1023, 1400.26), (177222, 0.1133, 13550.84), (250000, 0.1243, 16897.27), (295371, 0.1353, 25943.58), ('inf', 0.1463, 32082.28)), + 'semi-annual': ((8808, 0.011, 0.0), (20884, 0.022, 96.89), (32960, 0.044, 362.56), (45752, 0.066, 893.90), (57824, 0.088, 1738.17), (295372, 0.1023, 2800.51), (354444, 0.1133, 27101.67), (500000, 0.1243, 33794.53), (590742, 0.1353, 51887.14), ('inf', 0.1463, 64164.53)), + 'annually': ((17618, 0.011, 0.0), (41766, 0.022, 193.80), (65920, 0.044, 725.06), (91506, 0.066, 1787.84), (115648, 0.088, 3476.52), (590746, 0.1023, 5601.02), (708890, 0.1133, 54203.55), (1000000, 0.1243, 67589.27), (1181484, 0.1353, 103774.24), ('inf', 0.1463, 128329.03)), + }, + 'single': { + 'weekly': ((169, 0.011, 0.0), (402, 0.022, 1.86), (634, 0.044, 6.99), (880, 0.066, 17.20), (1112, 0.088, 33.44), (5680, 0.1023, 53.86), (6816, 0.1133, 521.17), (11360, 0.1243, 649.88), (19231, 0.1353, 1214.70), ('inf', 0.1463, 2279.65)), + 'bi-weekly': ((338, 0.011, 0.0), (804, 0.022, 3.72), (1268, 0.044, 13.97), (1760, 0.066, 34.39), (2224, 0.088, 66.86), (11360, 0.1023, 107.69), (13632, 0.1133, 1042.30), (22720, 0.1243, 1299.72), (38462, 0.1353, 2429.36), ('inf', 0.1463, 4559.25)), + 'semi-monthly': ((367, 0.011, 0.0), (870, 0.022, 4.04), (1373, 0.044, 15.11), (1906, 0.066, 37.24), (2409, 0.088, 72.42), (12307, 0.1023, 116.68), (14769, 0.1133, 1129.25), (24614, 0.1243, 1408.19), (41667, 0.1353, 2631.92),('inf', 0.1463, 4939.19)), + 'monthly': ((734, 0.011, 0.0), (1740, 0.022, 8.07), (2746, 0.044, 30.20), (3812, 0.066, 74.46), (4818, 0.088, 144.82), (24614, 0.1023, 233.35), (29538, 0.1133, 2258.48), (49228, 0.1243, 2816.37), (83334, 0.1353, 5263.84), ('inf', 0.1463, 9878.38)), + 'quarterly': ((2202, 0.011, 0.0), (5221, 0.022, 24.22), (8240, 0.044, 90.64), (11438, 0.066, 223.48), (14456, 0.088, 434.55), (73843, 0.1023, 700.13), (88611, 0.1133, 6775.42), (147686, 0.1243, 8448.63), (250000, 0.1353, 15791.65), ('inf', 0.1463, 29634.73)), + 'semi-annual': ((4404, 0.011, 0.0), (10442, 0.022, 48.44), (16480, 0.044, 181.28), (22876, 0.066, 446.95), (28912, 0.088, 869.09), (147686, 0.1023, 1400.26), (177222, 0.1133, 13550.84), (295372, 0.1243, 16897.27), (500000, 0.1353, 31583.32), ('inf', 0.1463, 59269.49)), + 'annually': ((8809, 0.011, 0.0), (20883, 0.022, 96.90), (32960, 0.044, 362.53), (45753, 0.066, 893.92), (57824, 0.088, 1738.26), (295373, 0.1023, 2800.51), (354445, 0.1133, 27101.77), (590742, 0.1243, 33794.63), (1000000, 0.1353, 63166.35), ('inf', 0.1463, 118538.96)), + }, + } + + + + + + + US CA California Low Income Exemption Rate + us_ca_sit_income_exemption_rate + + + + + { + 'weekly': ( 280, 280, 561, 561), + 'bi-weekly': ( 561, 561, 1121, 1121), + 'semi-monthly': ( 607, 607, 1214, 1214), + 'monthly': ( 1214, 1214, 2429, 2429), + 'quarterly': ( 3643, 3643, 7287, 7287), + 'semi-annual': ( 7287, 7287, 14573, 14573), + 'annually': (14573, 14573, 29146, 29146), + } + + + + + { + 'weekly': ( 289, 289, 579, 579), + 'bi-weekly': ( 579, 579, 1157, 1157), + 'semi-monthly': ( 627, 627, 1253, 1253), + 'monthly': ( 1254, 1254, 2507, 2507), + 'quarterly': ( 3761, 3761, 7521, 7521), + 'semi-annual': ( 7521, 7521, 15042, 15042), + 'annually': (15042, 15042, 30083, 30083), + } + + + + + + + US CA California Estimated Deduction Rate + us_ca_sit_estimated_deduction_rate + + + + + { + 'weekly': ( 19, 38, 58, 77, 96, 115, 135, 154, 173, 192), + 'bi-weekly': ( 38, 77, 115, 154, 192, 231, 269, 308, 346, 385), + 'semi-monthly': ( 42, 83, 125, 167, 208, 250, 292, 333, 375, 417), + 'monthly': ( 83, 167, 250, 333, 417, 500, 583, 667, 750, 833), + 'quarterly': ( 250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250, 2500), + 'semi-annual': ( 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000), + 'annually': (1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000), + } + + + + + { + 'weekly': ( 19, 38, 58, 77, 96, 115, 135, 154, 173, 192), + 'bi-weekly': ( 38, 77, 115, 154, 192, 231, 269, 308, 346, 385), + 'semi-monthly': ( 42, 83, 125, 167, 208, 250, 292, 333, 375, 417), + 'monthly': ( 83, 167, 250, 333, 417, 500, 583, 667, 750, 833), + 'quarterly': ( 250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250, 2500), + 'semi-annual': ( 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000), + 'annually': (1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000), + } + + + + + + + US CA California Standard Deduction Rate + us_ca_sit_standard_deduction_rate + + + + + { + 'weekly': ( 85, 85, 169, 169), + 'bi-weekly': ( 169, 169, 339, 339), + 'semi-monthly': ( 183, 183, 367, 367), + 'monthly': ( 367, 367, 734, 734), + 'quarterly': (1100, 1100, 2201, 2201), + 'semi-annual': (2201, 2201, 4401, 4401), + 'annually': (4401, 4401, 8802, 8802), + } + + + + + { + 'weekly': ( 87, 87, 175, 175), + 'bi-weekly': ( 175, 175, 349, 349), + 'semi-monthly': ( 189, 189, 378, 378), + 'monthly': ( 378, 378, 756, 756), + 'quarterly': (1134, 1134, 2269, 2269), + 'semi-annual': (2269, 2269, 4537, 4537), + 'annually': (4537, 4537, 9074, 9074), + } + + + + + + + US CA California Exemption Allowance Rate + us_ca_sit_exemption_allowance_rate + + + + + { + 'weekly': ( 2.41, 4.82, 7.23, 9.65, 12.06, 14.47, 16.88, 19.29, 21.70, 24.12), + 'bi-weekly': ( 4.82, 9.65, 14.47, 19.29, 24.12, 28.94, 33.76, 38.58, 43.41, 48.23), + 'semi-monthly': ( 5.23, 10.45, 15.68, 20.90, 26.13, 31.35, 36.58, 41.80, 47.03, 52.25), + 'monthly': ( 10.45, 20.90, 31.35, 41.80, 52.25, 62.70, 73.15, 83.60, 94.05, 104.50), + 'quarterly': ( 31.35, 62.70, 94.05, 125.40, 156.75, 188.10, 219.45, 250.80, 282.15, 313.50), + 'semi-annual': ( 62.70, 125.40, 188.10, 250.80, 313.50, 376.20, 438.90, 501.60, 564.30, 627.00), + 'annually': (125.40, 250.80, 376.20, 501.60, 627.00, 752.40, 877.80, 1003.20, 1128.60, 1254.00), + } + + + + + { + 'weekly': ( 2.58, 5.16, 7.74, 10.32, 12.90, 15.48, 18.07, 20.65, 23.23, 25.81), + 'bi-weekly': ( 5.16, 10.32, 15.48, 20.65, 25.81, 30.97, 36.13, 41.29, 46.45, 51.62), + 'semi-monthly': ( 5.59, 11.18, 16.78, 22.37, 27.96, 33.55, 39.14, 44.73, 50.33, 55.92), + 'monthly': ( 11.18, 22.37, 33.55, 44.73, 55.92, 67.10, 78.28, 89.47, 100.65, 111.83), + 'quarterly': ( 33.55, 67.10, 100.65, 134.20, 167.75, 201.30, 234.85, 268.40, 301.95, 335.50), + 'semi-annual': ( 67.10, 134.20, 201.30, 268.40, 335.50, 402.60, 469.70, 536.80, 603.90, 671.00), + 'annually': (134.20, 268.40, 402.60, 536.80, 671.00, 805.20, 939.40, 1073.60, 1207.80, 1342.00), + } + + + + + + + + US California - Department of Taxation (CA DE88) - Unemployment Tax + + + + US California - Department of Taxation - Income Tax + + + + + + + + + + ER: US CA California State Unemployment + ER_US_CA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ca_suta_wage_base', rate='us_ca_suta_rate', state_code='CA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ca_suta_wage_base', rate='us_ca_suta_rate', state_code='CA') + + + + + + + + + ER: US CA California State Employee Training Tax + ER_US_CA_SUTA_ETT + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ca_suta_wage_base', rate='us_ca_suta_ett_rate', state_code='CA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ca_suta_wage_base', rate='us_ca_suta_ett_rate', state_code='CA') + + + + + + + + + EE: US CA California State Disability Insurance + EE_US_CA_SUTA_SDI + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ca_suta_wage_base', rate='us_ca_suta_sdi_rate', state_code='CA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ca_suta_wage_base', rate='us_ca_suta_sdi_rate', state_code='CA') + + + + + + + + + EE: US CA California State Income Tax Withholding + EE_US_CA_SIT + python + result, _ = ca_california_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ca_california_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/co_colorado.xml b/l10n_us_hr_payroll/data/state/co_colorado.xml new file mode 100644 index 00000000..a37ee1b9 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/co_colorado.xml @@ -0,0 +1,97 @@ + + + + + US CO Colorado SUTA Wage Base + us_co_suta_wage_base + + + + + 13600.0 + + + + + + + + US CO Colorado SUTA Rate + us_co_suta_rate + + + + + 1.7 + + + + + + + US CO Colorado SIT Tax Rate + us_co_sit_tax_rate + + + + + 4.63 + + + + + + + US CO Colorado SIT Exemption Rate + us_co_sit_exemption_rate + + + + + 4000 + + + + + + + + US Colorado - Department of Labor and Employment - Unemployment Tax + + + + US Colorado - Division of Revenue - Income Tax + + + + + + + + + + ER: US CO Colorado State Unemployment + ER_US_CO_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_co_suta_wage_base', rate='us_co_suta_rate', state_code='CO') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_co_suta_wage_base', rate='us_co_suta_rate', state_code='CO') + + + + + + + + + EE: US CO Colorado State Income Tax Withholding + EE_US_CO_SIT + python + result, _ = co_colorado_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = co_colorado_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ct_connecticut.xml b/l10n_us_hr_payroll/data/state/ct_connecticut.xml new file mode 100644 index 00000000..646b9373 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ct_connecticut.xml @@ -0,0 +1,1227 @@ + + + + + US CT Connecticut SUTA Wage Base + us_ct_suta_wage_base + + + + + 15000.0 + + + + + 15000.0 + + + + + + + + US CT Connecticut SUTA Rate + us_ct_suta_rate + + + + + 3.4 + + + + + 3.2 + + + + + + + US CT Connecticut SIT Initial Tax Rate + us_ct_sit_initial_tax_rate + + + + + { + 'a': [ + ( 10000, 0, 3.00), + ( 50000, 300, 5.00), + (100000, 2300, 5.50), + (200000, 5050, 6.00), + (250000, 11050, 6.50), + (500000, 14300, 6.90), + ( 'inf', 31550, 6.99), + ], + 'b': [ + ( 16000, 0, 3.00), + ( 80000, 480, 5.00), + (160000, 3680, 5.50), + (320000, 8080, 6.00), + (400000, 17680, 6.50), + (800000, 22880, 6.90), + ( 'inf', 50480, 6.99), + ], + 'c': [ + ( 20000, 0, 3.00), + ( 100000, 600, 5.00), + ( 200000, 4600, 5.50), + ( 400000, 10100, 6.00), + ( 500000, 22100, 6.50), + (1000000, 28600, 6.90), + ( 'inf', 63100, 6.99), + ], + 'd': [ + ( 10000, 0, 3.00), + ( 50000, 300, 5.00), + (100000, 2300, 5.50), + (200000, 5050, 6.00), + (250000, 11050, 6.50), + (500000, 14300, 6.90), + ( 'inf', 31550, 6.99), + ], + 'f': [ + ( 10000, 0, 3.00), + ( 50000, 300, 5.00), + (100000, 2300, 5.50), + (200000, 5050, 6.00), + (250000, 11050, 6.50), + (500000, 14300, 6.90), + ( 'inf', 31550, 6.99), + ], + } + + + + + { + 'a': [ + ( 10000, 0, 3.00), + ( 50000, 300, 5.00), + (100000, 2300, 5.50), + (200000, 5050, 6.00), + (250000, 11050, 6.50), + (500000, 14300, 6.90), + ( 'inf', 31550, 6.99), + ], + 'b': [ + ( 16000, 0, 3.00), + ( 80000, 480, 5.00), + (160000, 3680, 5.50), + (320000, 8080, 6.00), + (400000, 17680, 6.50), + (800000, 22880, 6.90), + ( 'inf', 50480, 6.99), + ], + 'c': [ + ( 20000, 0, 3.00), + ( 100000, 600, 5.00), + ( 200000, 4600, 5.50), + ( 400000, 10100, 6.00), + ( 500000, 22100, 6.50), + (1000000, 28600, 6.90), + ( 'inf', 63100, 6.99), + ], + 'd': [ + ( 10000, 0, 3.00), + ( 50000, 300, 5.00), + (100000, 2300, 5.50), + (200000, 5050, 6.00), + (250000, 11050, 6.50), + (500000, 14300, 6.90), + ( 'inf', 31550, 6.99), + ], + 'f': [ + ( 10000, 0, 3.00), + ( 50000, 300, 5.00), + (100000, 2300, 5.50), + (200000, 5050, 6.00), + (250000, 11050, 6.50), + (500000, 14300, 6.90), + ( 'inf', 31550, 6.99), + ], + } + + + + + + + US CT Connecticut Tax Rate + us_ct_sit_tax_rate + + + + + { + 'a': [ + (50250, 0), + (52750, 20), + (55250, 40), + (57750, 60), + (60250, 80), + (62750, 100), + (65250, 120), + (67750, 140), + (70250, 160), + (72750, 180), + ('inf', 200), + ], + 'b': [ + ( 78500, 0), + ( 82500, 32), + ( 86500, 64), + ( 90500, 96), + ( 94500, 128), + ( 98500, 160), + (102500, 192), + (106500, 224), + (110500, 256), + (114500, 288), + ( 'inf', 320), + ], + 'c': [ + (100500, 0), + (105500, 40), + (110500, 80), + (115500, 120), + (120500, 160), + (125500, 200), + (130500, 240), + (135500, 280), + (140500, 320), + (145500, 360), + ( 'inf', 400), + + ], + 'd': [ + (50250, 0), + (52750, 20), + (55250, 40), + (57750, 60), + (60250, 80), + (62750, 100), + (65250, 120), + (67750, 140), + (70250, 160), + (72750, 180), + ('inf', 200), + ], + 'f': [ + ( 56500, 0), + ( 61500, 20), + ( 66500, 40), + ( 71500, 60), + ( 76500, 80), + ( 81500, 100), + ( 86500, 120), + ( 91500, 140), + ( 96500, 160), + (101500, 180), + ( 'inf', 200), + ], + } + + + + + { + 'a': [ + (50250, 0), + (52750, 20), + (55250, 40), + (57750, 60), + (60250, 80), + (62750, 100), + (65250, 120), + (67750, 140), + (70250, 160), + (72750, 180), + ('inf', 200), + ], + 'b': [ + ( 78500, 0), + ( 82500, 32), + ( 86500, 64), + ( 90500, 96), + ( 94500, 128), + ( 98500, 160), + (102500, 192), + (106500, 224), + (110500, 256), + (114500, 288), + ( 'inf', 320), + ], + 'c': [ + (100500, 0), + (105500, 40), + (110500, 80), + (115500, 120), + (120500, 160), + (125500, 200), + (130500, 240), + (135500, 280), + (140500, 320), + (145500, 360), + ( 'inf', 400), + + ], + 'd': [ + (50250, 0), + (52750, 20), + (55250, 40), + (57750, 60), + (60250, 80), + (62750, 100), + (65250, 120), + (67750, 140), + (70250, 160), + (72750, 180), + ('inf', 200), + ], + 'f': [ + ( 56500, 0), + ( 61500, 20), + ( 66500, 40), + ( 71500, 60), + ( 76500, 80), + ( 81500, 100), + ( 86500, 120), + ( 91500, 140), + ( 96500, 160), + (101500, 180), + ( 'inf', 200), + ], + } + + + + + + + US CT Connecticut Decimal Rate + us_ct_sit_decimal_rate + + + + + { + 'a': [ + (15000, 0.75), + (15500, 0.70), + (16000, 0.65), + (16500, 0.60), + (17000, 0.55), + (17500, 0.50), + (18000, 0.45), + (18500, 0.40), + (20000, 0.35), + (20500, 0.30), + (21000, 0.25), + (21500, 0.20), + (25000, 0.15), + (25500, 0.14), + (26000, 0.13), + (26500, 0.12), + (27000, 0.11), + (48000, 0.10), + (48500, 0.09), + (49000, 0.08), + (49500, 0.08), + (50000, 0.06), + (50500, 0.05), + (51000, 0.03), + (51500, 0.03), + (52000, 0.02), + (52500, 0.01), + ('inf', 0.00), + ], + 'b': [ + (24000, 0.75), + (24500, 0.70), + (25000, 0.65), + (25500, 0.60), + (26000, 0.55), + (26500, 0.50), + (27000, 0.45), + (27500, 0.40), + (34000, 0.35), + (34500, 0.30), + (35000, 0.25), + (35500, 0.20), + (44000, 0.15), + (44500, 0.14), + (45000, 0.13), + (45500, 0.12), + (46000, 0.11), + (74000, 0.10), + (74500, 0.09), + (75000, 0.08), + (75500, 0.08), + (76000, 0.06), + (76500, 0.05), + (77000, 0.03), + (77500, 0.03), + (78000, 0.02), + (78500, 0.01), + ('inf', 0.00), + ], + 'c': [ + (30000, 0.75), + (30500, 0.70), + (31000, 0.65), + (31500, 0.60), + (32000, 0.55), + (32500, 0.50), + (33000, 0.45), + (33500, 0.40), + (40000, 0.35), + (40500, 0.30), + (41000, 0.25), + (41500, 0.20), + (50000, 0.15), + (50500, 0.14), + (51000, 0.13), + (51500, 0.12), + (52000, 0.11), + (96000, 0.10), + (96500, 0.09), + (97000, 0.08), + (97500, 0.08), + (98000, 0.06), + (98500, 0.05), + (99000, 0.03), + (99500, 0.03), + (100000, 0.02), + (100500, 0.01), + ('inf', 0.00), + ], + 'f': [ + (18800, 0.75), + (19300, 0.70), + (19800, 0.65), + (20300, 0.60), + (20800, 0.55), + (21300, 0.50), + (21800, 0.45), + (22300, 0.40), + (25000, 0.35), + (25500, 0.30), + (26000, 0.25), + (26500, 0.20), + (31300, 0.15), + (31800, 0.14), + (32300, 0.13), + (32800, 0.12), + (33300, 0.11), + (60000, 0.10), + (60500, 0.09), + (61000, 0.08), + (61500, 0.08), + (62000, 0.06), + (62500, 0.05), + (63000, 0.03), + (63500, 0.03), + (64000, 0.02), + (64500, 0.01), + ('inf', 0.00), + ], + } + + + + + { + 'a': [ + (15000, 0.75), + (15500, 0.70), + (16000, 0.65), + (16500, 0.60), + (17000, 0.55), + (17500, 0.50), + (18000, 0.45), + (18500, 0.40), + (20000, 0.35), + (20500, 0.30), + (21000, 0.25), + (21500, 0.20), + (25000, 0.15), + (25500, 0.14), + (26000, 0.13), + (26500, 0.12), + (27000, 0.11), + (48000, 0.10), + (48500, 0.09), + (49000, 0.08), + (49500, 0.08), + (50000, 0.06), + (50500, 0.05), + (51000, 0.03), + (51500, 0.03), + (52000, 0.02), + (52500, 0.01), + ('inf', 0.00), + ], + 'b': [ + (24000, 0.75), + (24500, 0.70), + (25000, 0.65), + (25500, 0.60), + (26000, 0.55), + (26500, 0.50), + (27000, 0.45), + (27500, 0.40), + (34000, 0.35), + (34500, 0.30), + (35000, 0.25), + (35500, 0.20), + (44000, 0.15), + (44500, 0.14), + (45000, 0.13), + (45500, 0.12), + (46000, 0.11), + (74000, 0.10), + (74500, 0.09), + (75000, 0.08), + (75500, 0.08), + (76000, 0.06), + (76500, 0.05), + (77000, 0.03), + (77500, 0.03), + (78000, 0.02), + (78500, 0.01), + ('inf', 0.00), + ], + 'c': [ + (30000, 0.75), + (30500, 0.70), + (31000, 0.65), + (31500, 0.60), + (32000, 0.55), + (32500, 0.50), + (33000, 0.45), + (33500, 0.40), + (40000, 0.35), + (40500, 0.30), + (41000, 0.25), + (41500, 0.20), + (50000, 0.15), + (50500, 0.14), + (51000, 0.13), + (51500, 0.12), + (52000, 0.11), + (96000, 0.10), + (96500, 0.09), + (97000, 0.08), + (97500, 0.08), + (98000, 0.06), + (98500, 0.05), + (99000, 0.03), + (99500, 0.03), + (100000, 0.02), + (100500, 0.01), + ('inf', 0.00), + ], + 'f': [ + (18800, 0.75), + (19300, 0.70), + (19800, 0.65), + (20300, 0.60), + (20800, 0.55), + (21300, 0.50), + (21800, 0.45), + (22300, 0.40), + (25000, 0.35), + (25500, 0.30), + (26000, 0.25), + (26500, 0.20), + (31300, 0.15), + (31800, 0.14), + (32300, 0.13), + (32800, 0.12), + (33300, 0.11), + (60000, 0.10), + (60500, 0.09), + (61000, 0.08), + (61500, 0.08), + (62000, 0.06), + (62500, 0.05), + (63000, 0.03), + (63500, 0.03), + (64000, 0.02), + (64500, 0.01), + ('inf', 0.00), + ], + } + + + + + + + US CT Connecticut Recapture Rate + us_ct_sit_recapture_rate + + + + + { + 'a': [ + (200000, 0), + (205000, 90), + (210000, 180), + (215000, 270), + (220000, 360), + (225000, 450), + (230000, 540), + (235000, 630), + (240000, 720), + (245000, 810), + (250000, 900), + (255000, 990), + (260000, 1080), + (265000, 1170), + (270000, 1260), + (275000, 1350), + (280000, 1440), + (285000, 1530), + (290000, 1620), + (295000, 1710), + (300000, 1800), + (305000, 1890), + (310000, 1980), + (315000, 2070), + (320000, 2160), + (325000, 2250), + (330000, 2340), + (335000, 2430), + (340000, 2520), + (345000, 2610), + (500000, 2700), + (505000, 2750), + (510000, 2800), + (515000, 2850), + (520000, 2900), + (525000, 2950), + (530000, 3000), + (535000, 3050), + (540000, 3100), + ( 'inf', 200), + ], + 'b': [ + (320000, 0), + (328000, 140), + (336000, 280), + (344000, 420), + (352000, 560), + (360000, 700), + (368000, 840), + (376000, 980), + (384000, 1120), + (392000, 1260), + (400000, 1400), + (408000, 1540), + (416000, 1680), + (424000, 1820), + (432000, 1960), + (440000, 2100), + (448000, 2240), + (456000, 2380), + (464000, 2520), + (472000, 2660), + (480000, 2800), + (488000, 2940), + (496000, 3080), + (504000, 3220), + (512000, 3360), + (520000, 3500), + (528000, 3640), + (536000, 3780), + (544000, 3920), + (552000, 4060), + (800000, 4200), + (808000, 4280), + (816000, 4360), + (824000, 4440), + (832000, 4520), + (840000, 4600), + (848000, 4680), + (856000, 4760), + (864000, 4840), + ( 'inf', 4920), + ], + 'c': [ + ( 400000, 0), + ( 410000, 180), + ( 420000, 360), + ( 430000, 540), + ( 440000, 720), + ( 450000, 900), + ( 460000, 1080), + ( 470000, 1260), + ( 480000, 1440), + ( 490000, 1620), + ( 500000, 1800), + ( 510000, 1980), + ( 520000, 2160), + ( 530000, 2340), + ( 540000, 2520), + ( 550000, 2700), + ( 560000, 2880), + ( 570000, 3060), + ( 580000, 3240), + ( 590000, 3420), + ( 600000, 3600), + ( 610000, 3780), + ( 620000, 3960), + ( 630000, 4140), + ( 640000, 4320), + ( 650000, 4500), + ( 660000, 4680), + ( 670000, 4860), + ( 680000, 5040), + ( 690000, 5220), + (1000000, 5400), + (1010000, 5500), + (1020000, 5600), + (1030000, 5700), + (1040000, 5800), + (1050000, 5900), + (1060000, 6000), + (1070000, 6100), + (1080000, 6200), + ( 'inf', 6300), + ], + 'd': [ + (200000, 0), + (205000, 90), + (210000, 180), + (215000, 270), + (220000, 360), + (225000, 450), + (230000, 540), + (235000, 630), + (240000, 720), + (245000, 810), + (250000, 900), + (255000, 990), + (260000, 1080), + (265000, 1170), + (270000, 1260), + (275000, 1350), + (280000, 1440), + (285000, 1530), + (290000, 1620), + (295000, 1710), + (300000, 1800), + (305000, 1890), + (310000, 1980), + (315000, 2070), + (320000, 2160), + (325000, 2250), + (330000, 2340), + (335000, 2430), + (340000, 2520), + (345000, 2610), + (500000, 2700), + (505000, 2750), + (510000, 2800), + (515000, 2850), + (520000, 2900), + (525000, 2950), + (530000, 3000), + (535000, 3050), + (540000, 3100), + ( 'inf', 200), + ], + 'f': [ + (200000, 0), + (205000, 90), + (210000, 180), + (215000, 270), + (220000, 360), + (225000, 450), + (230000, 540), + (235000, 630), + (240000, 720), + (245000, 810), + (250000, 900), + (255000, 990), + (260000, 1080), + (265000, 1170), + (270000, 1260), + (275000, 1350), + (280000, 1440), + (285000, 1530), + (290000, 1620), + (295000, 1710), + (300000, 1800), + (305000, 1890), + (310000, 1980), + (315000, 2070), + (320000, 2160), + (325000, 2250), + (330000, 2340), + (335000, 2430), + (340000, 2520), + (345000, 2610), + (500000, 2700), + (505000, 2750), + (510000, 2800), + (515000, 2850), + (520000, 2900), + (525000, 2950), + (530000, 3000), + (535000, 3050), + (540000, 3100), + ( 'inf', 200), + ], + } + + + + + { + 'a': [ + (200000, 0), + (205000, 90), + (210000, 180), + (215000, 270), + (220000, 360), + (225000, 450), + (230000, 540), + (235000, 630), + (240000, 720), + (245000, 810), + (250000, 900), + (255000, 990), + (260000, 1080), + (265000, 1170), + (270000, 1260), + (275000, 1350), + (280000, 1440), + (285000, 1530), + (290000, 1620), + (295000, 1710), + (300000, 1800), + (305000, 1890), + (310000, 1980), + (315000, 2070), + (320000, 2160), + (325000, 2250), + (330000, 2340), + (335000, 2430), + (340000, 2520), + (345000, 2610), + (500000, 2700), + (505000, 2750), + (510000, 2800), + (515000, 2850), + (520000, 2900), + (525000, 2950), + (530000, 3000), + (535000, 3050), + (540000, 3100), + ( 'inf', 200), + ], + 'b': [ + (320000, 0), + (328000, 140), + (336000, 280), + (344000, 420), + (352000, 560), + (360000, 700), + (368000, 840), + (376000, 980), + (384000, 1120), + (392000, 1260), + (400000, 1400), + (408000, 1540), + (416000, 1680), + (424000, 1820), + (432000, 1960), + (440000, 2100), + (448000, 2240), + (456000, 2380), + (464000, 2520), + (472000, 2660), + (480000, 2800), + (488000, 2940), + (496000, 3080), + (504000, 3220), + (512000, 3360), + (520000, 3500), + (528000, 3640), + (536000, 3780), + (544000, 3920), + (552000, 4060), + (800000, 4200), + (808000, 4280), + (816000, 4360), + (824000, 4440), + (832000, 4520), + (840000, 4600), + (848000, 4680), + (856000, 4760), + (864000, 4840), + ( 'inf', 4920), + ], + 'c': [ + ( 400000, 0), + ( 410000, 180), + ( 420000, 360), + ( 430000, 540), + ( 440000, 720), + ( 450000, 900), + ( 460000, 1080), + ( 470000, 1260), + ( 480000, 1440), + ( 490000, 1620), + ( 500000, 1800), + ( 510000, 1980), + ( 520000, 2160), + ( 530000, 2340), + ( 540000, 2520), + ( 550000, 2700), + ( 560000, 2880), + ( 570000, 3060), + ( 580000, 3240), + ( 590000, 3420), + ( 600000, 3600), + ( 610000, 3780), + ( 620000, 3960), + ( 630000, 4140), + ( 640000, 4320), + ( 650000, 4500), + ( 660000, 4680), + ( 670000, 4860), + ( 680000, 5040), + ( 690000, 5220), + (1000000, 5400), + (1010000, 5500), + (1020000, 5600), + (1030000, 5700), + (1040000, 5800), + (1050000, 5900), + (1060000, 6000), + (1070000, 6100), + (1080000, 6200), + ( 'inf', 6300), + ], + 'd': [ + (200000, 0), + (205000, 90), + (210000, 180), + (215000, 270), + (220000, 360), + (225000, 450), + (230000, 540), + (235000, 630), + (240000, 720), + (245000, 810), + (250000, 900), + (255000, 990), + (260000, 1080), + (265000, 1170), + (270000, 1260), + (275000, 1350), + (280000, 1440), + (285000, 1530), + (290000, 1620), + (295000, 1710), + (300000, 1800), + (305000, 1890), + (310000, 1980), + (315000, 2070), + (320000, 2160), + (325000, 2250), + (330000, 2340), + (335000, 2430), + (340000, 2520), + (345000, 2610), + (500000, 2700), + (505000, 2750), + (510000, 2800), + (515000, 2850), + (520000, 2900), + (525000, 2950), + (530000, 3000), + (535000, 3050), + (540000, 3100), + ( 'inf', 200), + ], + 'f': [ + (200000, 0), + (205000, 90), + (210000, 180), + (215000, 270), + (220000, 360), + (225000, 450), + (230000, 540), + (235000, 630), + (240000, 720), + (245000, 810), + (250000, 900), + (255000, 990), + (260000, 1080), + (265000, 1170), + (270000, 1260), + (275000, 1350), + (280000, 1440), + (285000, 1530), + (290000, 1620), + (295000, 1710), + (300000, 1800), + (305000, 1890), + (310000, 1980), + (315000, 2070), + (320000, 2160), + (325000, 2250), + (330000, 2340), + (335000, 2430), + (340000, 2520), + (345000, 2610), + (500000, 2700), + (505000, 2750), + (510000, 2800), + (515000, 2850), + (520000, 2900), + (525000, 2950), + (530000, 3000), + (535000, 3050), + (540000, 3100), + ( 'inf', 200), + ], + } + + + + + + + US CT Connecticut Personal Exemption Rate + us_ct_sit_personal_exemption_rate + + + + + { + 'a' : [ + (24000, 12000), + (25000, 11000), + (26000, 10000), + (27000, 9000), + (28000, 8000), + (29000, 7000), + (30000, 6000), + (31000, 5000), + (32000, 4000), + (33000, 3000), + (34000, 2000), + (35000, 1000), + ('inf', 0), + ], + 'b' : [ + (38000, 19000), + (39000, 18000), + (40000, 17000), + (41000, 16000), + (42000, 15000), + (43000, 14000), + (44000, 13000), + (45000, 12000), + (46000, 11000), + (47000, 10000), + (48000, 9000), + (49000, 8000), + (50000, 7000), + (51000, 6000), + (52000, 5000), + (53000, 4000), + (54000, 3000), + (55000, 2000), + (56000, 1000), + ('inf', 0), + ], + 'c': [ + (48000, 24000), + (49000, 23000), + (50000, 22000), + (51000, 21000), + (52000, 20000), + (53000, 19000), + (54000, 18000), + (55000, 17000), + (56000, 16000), + (57000, 15000), + (58000, 14000), + (59000, 13000), + (60000, 12000), + (61000, 11000), + (62000, 10000), + (63000, 9000), + (64000, 8000), + (65000, 7000), + (66000, 6000), + (67000, 5000), + (68000, 4000), + (69000, 3000), + (70000, 2000), + (71000, 1000), + ('inf', 0), + ], + 'f' : [ + (30000, 15000), + (31000, 14000), + (22000, 13000), + (33000, 12000), + (34000, 11000), + (35000, 10000), + (36000, 9000), + (37000, 8000), + (38000, 7000), + (39000, 6000), + (40000, 5000), + (41000, 4000), + (42000, 3000), + (43000, 2000), + (44000, 1000), + ('inf', 0), + ], + } + + + + + { + 'a' : [ + (24000, 12000), + (25000, 11000), + (26000, 10000), + (27000, 9000), + (28000, 8000), + (29000, 7000), + (30000, 6000), + (31000, 5000), + (32000, 4000), + (33000, 3000), + (34000, 2000), + (35000, 1000), + ('inf', 0), + ], + 'b' : [ + (38000, 19000), + (39000, 18000), + (40000, 17000), + (41000, 16000), + (42000, 15000), + (43000, 14000), + (44000, 13000), + (45000, 12000), + (46000, 11000), + (47000, 10000), + (48000, 9000), + (49000, 8000), + (50000, 7000), + (51000, 6000), + (52000, 5000), + (53000, 4000), + (54000, 3000), + (55000, 2000), + (56000, 1000), + ('inf', 0), + ], + 'c': [ + (48000, 24000), + (49000, 23000), + (50000, 22000), + (51000, 21000), + (52000, 20000), + (53000, 19000), + (54000, 18000), + (55000, 17000), + (56000, 16000), + (57000, 15000), + (58000, 14000), + (59000, 13000), + (60000, 12000), + (61000, 11000), + (62000, 10000), + (63000, 9000), + (64000, 8000), + (65000, 7000), + (66000, 6000), + (67000, 5000), + (68000, 4000), + (69000, 3000), + (70000, 2000), + (71000, 1000), + ('inf', 0), + ], + 'f' : [ + (30000, 15000), + (31000, 14000), + (22000, 13000), + (33000, 12000), + (34000, 11000), + (35000, 10000), + (36000, 9000), + (37000, 8000), + (38000, 7000), + (39000, 6000), + (40000, 5000), + (41000, 4000), + (42000, 3000), + (43000, 2000), + (44000, 1000), + ('inf', 0), + ], + } + + + + + + + + US Connecticut - Department of Labor (CDOL) - Unemployment Tax + + + + US Connecticut - Department of Revenue Services (CDRS) - Income Tax + + + + + + + + + + ER: US CT Connecticut State Unemployment + ER_US_CT_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ct_suta_wage_base', rate='us_ct_suta_rate', state_code='CT') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ct_suta_wage_base', rate='us_ct_suta_rate', state_code='CT') + + + + + + + + + EE: US CT Connecticut State Income Tax Withholding + EE_US_CT_SIT + python + result, _ = ct_connecticut_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ct_connecticut_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/de_delaware.xml b/l10n_us_hr_payroll/data/state/de_delaware.xml new file mode 100644 index 00000000..fad2abf6 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/de_delaware.xml @@ -0,0 +1,119 @@ + + + + + US DE Delaware SUTA Wage Base + us_de_suta_wage_base + + + + + 16500.0 + + + + + + + + US DE Delaware SUTA Rate + us_de_suta_rate + + + + + 1.50 + + + + + + + US DE Delaware SIT Tax Rate + us_de_sit_tax_rate + + + + + [ + ( 2000, 0.0, 0.00), + ( 5000, 0.0, 2.20), + (10000, 66.0, 3.90), + (20000, 261.0, 4.80), + (25000, 741.0, 5.20), + (60000, 1001.0, 5.55), + ('inf', 2943.0, 6.60), + + ] + + + + + + + US DE Delaware Standard Deduction Rate + us_de_sit_standard_deduction_rate + + + + + 3250 + + + + + + + US DE Delaware Personal Exemption Rate + us_de_sit_personal_exemption_rate + + + + + 110 + + + + + + + + US Delaware - Division of Unemployment Insurance - Unemployment Tax + + + + US Delaware - Division of Revenue - Income Tax + + + + + + + + + + ER: US DE Delaware State Unemployment + ER_US_DE_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_de_suta_wage_base', rate='us_de_suta_rate', state_code='DE') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_de_suta_wage_base', rate='us_de_suta_rate', state_code='DE') + + + + + + + + + EE: US DE Delaware State Income Tax Withholding + EE_US_DE_SIT + python + result, _ = de_delaware_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = de_delaware_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/fl_florida.xml b/l10n_us_hr_payroll/data/state/fl_florida.xml new file mode 100644 index 00000000..8002a2ee --- /dev/null +++ b/l10n_us_hr_payroll/data/state/fl_florida.xml @@ -0,0 +1,63 @@ + + + + + US FL Florida SUTA Wage Base + us_fl_suta_wage_base + + + + + 7000.00 + + + + + 7000.00 + + + + + + + + US FL Florida SUTA Rate + us_fl_suta_rate + + + + + 2.7 + + + + + 2.7 + + + + + + + + US Florida - Department of Revenue + + + + + + + + + + ER: US FL Florida State Unemployment (RT-6) + ER_US_FL_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_fl_suta_wage_base', rate='us_fl_suta_rate', state_code='FL') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_fl_suta_wage_base', rate='us_fl_suta_rate', state_code='FL') + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ga_georgia.xml b/l10n_us_hr_payroll/data/state/ga_georgia.xml new file mode 100644 index 00000000..72844e5e --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ga_georgia.xml @@ -0,0 +1,279 @@ + + + + + US GA Georgia SUTA Wage Base + us_ga_suta_wage_base + + + + + 9500.00 + + + + + 9500.00 + + + + + + + + US GA Georgia SUTA Rate + us_ga_suta_rate + + + + + 2.7 + + + + + 2.7 + + + + + + + US GA Georgia SIT Rate Table + us_ga_sit_rate + + + + + { + 'married filing joint, both spouses working': { + 'weekly': ((9.50, 0.00, 1.00), (29.00, .10, 2.00), (48.00, .48, 3.00), (67.50, 1.06, 4.00), (96.00, 1.83, 5.00), ('inf', 3.27, 5.75)), + 'bi-weekly': ((19.00, 0.00, 1.00), (57.50, .19, 2.00), (96.00, .96, 3.00), (135.00, 2.12, 4.00), (192.00, 3.65, 5.00), ('inf', 6.54, 5.75)), + 'semi-monthly': ((21.00, 0.00, 1.00), (62.50, .21, 2.00), (104.00, 1.04, 3.00), (146.00, 2.29, 4.00), (208.00, 3.96, 5.00), ('inf', 7.08, 5.75)), + 'monthly': ((41.50, 0.00, 1.00), (125.50, .42, 2.00), (208.00, 2.08, 3.00), (292.00, 4.58, 4.00), (417.00, 7.92, 5.00), ('inf', 14.17, 5.75)), + 'quarterly': ((125.00, 0.00, 1.00), (375.00, 1.25, 2.00), (625.00, 6.25, 3.00), (875.00, 13.75, 4.00), (1250.00, 23.75, 5.00), ('inf', 42.50, 5.75)), + 'semi-annual': ((250.00, 0.00, 1.00), (750.00, 2.50, 2.00), (1250.00, 12.50, 3.00), (1750.00, 27.50, 4.00), (2500.00, 47.50, 5.00), ('inf', 85.00, 5.75)), + 'annual': ((500.00, 0.00, 1.00), (1500.00, 5.00, 2.00), (2500.00, 25.00, 3.00), (3500.00, 55.00, 4.00), (5000.00, 95.00, 5.00), ('inf', 170.00, 5.75)), + }, + 'married filing joint, one spouse working': { + 'weekly': ((19.00, 0.00, 1.00), (57.50, .19, 2.00), (96.00, .96, 3.00), (135.00, 2.12, 4.00), (192.50, 3.65, 5.00), ('inf', 6.54, 5.75)), + 'bi-weekly': ((38.50, 0.00, 1.00), (115.00, .38, 2.00), (192.00, 1.92, 3.00), (269.00, 4.23, 4.00), (385.00, 7.31, 5.00), ('inf', 13.08, 5.75)), + 'semi-monthly': ((41.50, 0.00, 1.00), (125.00, .42, 2.00), (208.00, 2.08, 3.00), (292.00, 4.58, 4.00), (417.00, 7.92, 5.00), ('inf', 14.17, 5.75)), + 'monthly': ((83.00, 0.00, 1.00), (250.00, .83, 2.00), (417.00, 4.17, 3.00), (583.00, 9.17, 4.00), (833.00, 15.83, 5.00), ('inf', 28.33, 5.75)), + 'quarterly': ((250.00, 0.00, 1.00), (750.00, 2.50, 2.00), (1250.00, 12.50, 3.00), (1750.00, 27.50, 4.00), (2500.00, 47.50, 5.00), ('inf', 85.00, 5.75)), + 'semi-annual': ((500.00, 0.00, 1.00), (1500.00, 5.00, 2.00), (2500.00, 25.00, 3.00), (3500.00, 55.00, 4.00), (5000.00, 95.00, 5.00), ('inf', 170.00, 5.75)), + 'annual': ((1000.00, 0.00, 1.00), (3000.00, 10.00, 2.00), (5000.00, 50.00, 3.00), (7000.00, 110.00, 4.00), (10000.00, 190.00, 5.00), ('inf', 340.00, 5.75)), + }, + 'single': { + 'weekly': ((14.50, 0.00, 1.00), (43.50, .14, 2.00), (72.00, .72, 3.00), (101.00, 1.59, 4.00), (135.00, 2.74, 5.00), ('inf', 4.42, 5.75)), + 'bi-weekly': ((29.00, 0.00, 1.00), (86.50, .29, 2.00), (144.00, 1.44, 3.00), (202.00, 3.17, 4.00), (269.00, 5.48, 5.00), ('inf', 8.85, 5.75)), + 'semi-monthly': ((31.00, 0.00, 1.00), (93.50, .31, 2.00), (156.00, 1.56, 3.00), (219.00, 3.34, 4.00), (292.00, 5.94, 5.00), ('inf', 9.58, 5.75)), + 'monthly': ((62.50, 0.00, 1.00), (187.00, .62, 2.00), (312.00, 3.12, 3.00), (437.00, 6.87, 4.00), (583.00, 11.87, 5.00), ('inf', 19.17, 5.75)), + 'quarterly': ((187.50, 0.00, 1.00), (562.50, 1.88, 2.00), (937.50, 9.38, 3.00), (1312.00, 20.63, 4.00), (1750.00, 35.63, 5.00), ('inf', 57.50, 5.75)), + 'semi-annual': ((375.00, 0.00, 1.00), (1125.00, 3.75, 2.00), (1875.00, 18.75, 3.00), (2625.00, 41.25, 4.00), (3500.00, 71.25, 5.00), ('inf', 115.00, 5.75)), + 'annual': ((750.00, 0.00, 1.00), (2250.00, 7.50, 2.00), (3750.00, 37.50, 3.00), (5250.00, 82.50, 4.00), (7000.00, 142.50, 5.00), ('inf', 230.00, 5.75)), + }, + 'head of household': { + 'weekly': ((19.00, 0.00, 1.00), (57.50, .19, 2.00), (96.00, .96, 3.00), (135.00, 2.12, 4.00), (192.50, 3.65, 5.00), ('inf', 6.54, 5.75)), + 'bi-weekly': ((38.50, 0.00, 1.00), (115.00, .38, 2.00), (192.00, 1.92, 3.00), (269.00, 4.23, 4.00), (385.00, 7.31, 5.00), ('inf', 13.08, 5.75)), + 'semi-monthly': ((41.50, 0.00, 1.00), (125.00, .42, 2.00), (208.00, 2.08, 3.00), (292.00, 4.58, 4.00), (417.00, 7.92, 5.00), ('inf', 14.17, 5.75)), + 'monthly': ((83.00, 0.00, 1.00), (250.00, .83, 2.00), (417.00, 4.17, 3.00), (583.00, 9.17, 4.00), (833.00, 15.83, 5.00), ('inf', 28.33, 5.75)), + 'quarterly': ((250.00, 0.00, 1.00), (750.00, 2.50, 2.00), (1250.00, 12.50, 3.00), (1750.00, 27.50, 4.00), (2500.00, 47.50, 5.00), ('inf', 85.00, 5.75)), + 'semi-annual': ((500.00, 0.00, 1.00), (1500.00, 5.00, 2.00), (2500.00, 25.00, 3.00), (3500.00, 55.00, 4.00), (5000.00, 95.00, 5.00), ('inf', 170.00, 5.75)), + 'annual': ((1000.00, 0.00, 1.00), (3000.00, 10.00, 2.00), (5000.00, 50.00, 3.00), (7000.00, 110.00, 4.00), (10000.00, 190.00, 5.00), ('inf', 340.00, 5.75)), + }, + 'married filing separate': { + 'weekly': ((9.50, 0.00, 1.00), (29.00, .10, 2.00), (48.00, .48, 3.00), (67.50, 1.06, 4.00), (96.00, 1.83, 5.00), ('inf', 3.27, 5.75)), + 'bi-weekly': ((19.00, 0.00, 1.00), (57.50, .19, 2.00), (96.00, .96, 3.00), (135.00, 2.12, 4.00), (192.00, 3.65, 5.00), ('inf', 6.54, 5.75)), + 'semi-monthly': ((21.00, 0.00, 1.00), (62.50, .21, 2.00), (104.00, 1.04, 3.00), (146.00, 2.29, 4.00), (208.00, 3.96, 5.00), ('inf', 7.08, 5.75)), + 'monthly': ((41.50, 0.00, 1.00), (125.50, .42, 2.00), (208.00, 2.08, 3.00), (292.00, 4.58, 4.00), (417.00, 7.92, 5.00), ('inf', 14.17, 5.75)), + 'quarterly': ((125.00, 0.00, 1.00), (375.00, 1.25, 2.00), (625.00, 6.25, 3.00), (875.00, 13.75, 4.00), (1250.00, 23.75, 5.00), ('inf', 42.50, 5.75)), + 'semi-annual': ((250.00, 0.00, 1.00), (750.00, 2.50, 2.00), (1250.00, 12.50, 3.00), (1750.00, 27.50, 4.00), (2500.00, 47.50, 5.00), ('inf', 85.00, 5.75)), + 'annual': ((500.00, 0.00, 1.00), (1500.00, 5.00, 2.00), (2500.00, 25.00, 3.00), (3500.00, 55.00, 4.00), (5000.00, 95.00, 5.00), ('inf', 170.00, 5.75)), + }, + } + + + + + + + US GA Georgia SIT Personal Allowance + us_ga_sit_personal_allowance + + + + + { + 'married filing joint, both spouses working': { + 'weekly': 142.30, + 'bi-weekly': 284.62, + 'semi-monthly': 308.33, + 'monthly': 616.67, + 'quarterly': 1850.00, + 'semi-annual': 3700.00, + 'annual': 7400.00, + }, + 'married filing joint, one spouse working': { + 'weekly': 142.30, + 'bi-weekly': 284.62, + 'semi-monthly': 308.33, + 'monthly': 616.67, + 'quarterly': 1850.00, + 'semi-annual': 3700.00, + 'annual': 7400.00, + }, + 'single': { + 'weekly': 51.92, + 'bi-weekly': 103.85, + 'semi-monthly': 112.50, + 'monthly': 225.00, + 'quarterly': 675.00, + 'semi-annual': 1350.00, + 'annual': 2700.00, + }, + 'head of household': { + 'weekly': 51.92, + 'bi-weekly': 103.85, + 'semi-monthly': 112.50, + 'monthly': 225.00, + 'quarterly': 675.00, + 'semi-annual': 1350.00, + 'annual': 2700.00, + }, + 'married filing separate': { + 'weekly': 71.15, + 'bi-weekly': 142.30, + 'semi-monthly': 154.16, + 'monthly': 308.33, + 'quarterly': 925.00, + 'semi-annual': 1850.00, + 'annual': 3700.00, + }, + } + + + + + + + US GA Georgia SIT Dependent Allowance Rate + us_ga_sit_dependent_allowance_rate + + + + + { + 'weekly': 57.50, + 'bi-weekly': 115.00, + 'semi-monthly': 125.00, + 'monthly': 250.00, + 'quarterly': 750.00, + 'semi-annual': 1500.00, + 'annual': 3000.00, + } + + + + + + + US GA Georgia SIT Deduction + us_ga_sit_deduction + + + + + { + 'married filing joint, both spouses working': { + 'weekly': 115.50, + 'bi-weekly': 230.75, + 'semi-monthly': 250.00, + 'monthly': 500.00, + 'quarterly': 1500.00, + 'semi-annual': 3000.00, + 'annual': 6000.00, + }, + 'married filing joint, one spouse working': { + 'weekly': 115.50, + 'bi-weekly': 230.75, + 'semi-monthly': 250.00, + 'monthly': 500.00, + 'quarterly': 1500.00, + 'semi-annual': 3000.00, + 'annual': 6000.00, + }, + 'single': { + 'weekly': 88.50, + 'bi-weekly': 177.00, + 'semi-monthly': 191.75, + 'monthly': 383.50, + 'quarterly': 1150.00, + 'semi-annual': 2300.00, + 'annual': 4600.00, + }, + 'head of household': { + 'weekly': 88.50, + 'bi-weekly': 177.00, + 'semi-monthly': 191.75, + 'monthly': 383.50, + 'quarterly': 1150.00, + 'semi-annual': 2300.00, + 'annual': 4600.00, + }, + 'married filing separate': { + 'weekly': 57.75, + 'bi-weekly': 115.50, + 'semi-monthly': 125.00, + 'monthly': 250.00, + 'quarterly': 750.00, + 'semi-annual': 1500.00, + 'annual': 3000.00, + }, + } + + + + + + + + US Georgia - Department of Taxation - Unemployment Tax + + + + US Georgia - Department of Taxation - Income Tax + + + + + + + + + + ER: US GA Georgia State Unemployment + ER_US_GA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ga_suta_wage_base', rate='us_ga_suta_rate', state_code='GA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ga_suta_wage_base', rate='us_ga_suta_rate', state_code='GA') + + + + + + + + + EE: US GA Georgia State Income Tax Withholding + EE_US_GA_SIT + python + result, _ = ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/hi_hawaii.xml b/l10n_us_hr_payroll/data/state/hi_hawaii.xml new file mode 100644 index 00000000..862ca102 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/hi_hawaii.xml @@ -0,0 +1,125 @@ + + + + + US HI Hawaii SUTA Wage Base + us_hi_suta_wage_base + + + + + 46800.0 + + + + + 48100.0 + + + + + + + + US HI Hawaii SUTA Rate + us_hi_suta_rate + + + + + 2.40 + + + + + 2.40 + + + + + + + US HI Hawaii SIT Tax Rate + us_hi_sit_tax_rate + + + + + { + 'single': ((2400, 0.00, 1.40), (4800, 34.00, 3.20), (9600, 110.00, 5.50), (14400, 374.00, 6.40), (19200, 682.00, 6.80), (24000, 1008.00, 7.20), (36000, 1354.00, 7.60), ('inf', 2266.00, 7.90)), + 'married': ((4800, 0.00, 1.40), (9600, 67.00, 3.20), (19200, 221.00, 5.50), (28800, 749.00, 6.40), (38400, 1363.00, 6.80), (48000, 2016.00, 7.20), (72000, 2707.00, 7.60), ('inf', 4531.00, 7.90)), + 'head_of_household': ((2400, 0.00, 1.40), (4800, 34.00, 3.20), (9600, 110.00, 5.50), (14400, 374.00, 6.40), (19200, 682.00, 6.80), (24000, 1008.00, 7.20), (36000, 1354.00, 7.60), ('inf', 2266.00, 7.90)), + } + + + + + { + 'single': ((2400, 0.00, 1.40), (4800, 34.00, 3.20), (9600, 110.00, 5.50), (14400, 374.00, 6.40), (19200, 682.00, 6.80), (24000, 1008.00, 7.20), (36000, 1354.00, 7.60), ('inf', 2266.00, 7.90)), + 'married': ((4800, 0.00, 1.40), (9600, 67.00, 3.20), (19200, 221.00, 5.50), (28800, 749.00, 6.40), (38400, 1363.00, 6.80), (48000, 2016.00, 7.20), (72000, 2707.00, 7.60), ('inf', 4531.00, 7.90)), + 'head_of_household': ((2400, 0.00, 1.40), (4800, 34.00, 3.20), (9600, 110.00, 5.50), (14400, 374.00, 6.40), (19200, 682.00, 6.80), (24000, 1008.00, 7.20), (36000, 1354.00, 7.60), ('inf', 2266.00, 7.90)), + } + + + + + + + US HI Hawaii Personal Exemption Rate + us_hi_sit_personal_exemption_rate + + + + + 1144 + + + + + 1144 + + + + + + + + US Hawaii - Department of Labor and Industrial Relations - Unemployment Tax + + + + US Hawaii - Department of Taxation - Income Tax + + + + + + + + + + ER: US HI Hawaii State Unemployment + ER_US_HI_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_hi_suta_wage_base', rate='us_hi_suta_rate', state_code='HI') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_hi_suta_wage_base', rate='us_hi_suta_rate', state_code='HI') + + + + + + + + + EE: US HI Hawaii State Income Tax Withholding + EE_US_HI_SIT + python + result, _ = hi_hawaii_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = hi_hawaii_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ia_iowa.xml b/l10n_us_hr_payroll/data/state/ia_iowa.xml new file mode 100644 index 00000000..6a7d060d --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ia_iowa.xml @@ -0,0 +1,177 @@ + + + + + US IA Iowa SUTA Wage Base + us_ia_suta_wage_base + + + + + 30600.0 + + + + + 31600.0 + + + + + + + + US IA Iowa SUTA Rate + us_ia_suta_rate + + + + + 1.0 + + + + + 1.0 + + + + + + + US IA Iowa SIT Tax Rate + us_ia_sit_tax_rate + + + + + { + 'daily': [(5.13, 0.0033, 0.0), (10.25, 0.0067, 0.02), (20.50, 0.0225, 0.05), (46.13, 0.0414, 0.28), (76.89, 0.0563, 1.34), (102.52, 0.0596, 3.07), (153.78, 0.0625, 4.60), (230.68, 0.0744, 7.80), ('inf', 0.0853, 13.52)], + 'weekly': [(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), ('inf', 0.0853, 67.63)], + 'bi-weekly': [(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), ('inf', 0.0853, 135.28)], + 'semi-monthly': [(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), ('inf', 0.0853, 146.55)], + 'monthly': [(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), ('inf', 0.0853, 293.09)], + 'annual': [(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), ('inf', 0.0853, 3516.98)], + } + + + + + { + 'daily': [(5.69, 0.0033, 0.0), (11.38, 0.0067, 0.02), (22.76, 0.0225, 0.06), (51.22, 0.0414, 0.32), (85.36, 0.0563, 1.50), (113.81, 0.0596, 3.42), (170.71, 0.0625, 5.12), (256.07, 0.0744, 8.68), ('inf', 0.0853, 15.03)], + 'weekly': [(28.46, 0.0033, 0.0), (56.90, 0.0067, 0.09), (113.81, 0.0225, 0.028), (256.08, 0.0414, 1.56), (426.79, 0.0563, 7.45), (569.04, 0.0596, 17.06), (853.56, 0.0625, 25.54), (1280.35, 0.0744, 43.32), ('inf', 0.0853, 75.07)], + 'bi-weekly': [(56.92, 0.0033, 0.00), (113.81, 0.0067, 0.19), (227.62, 0.00225, 0.57), (512.15, 0.0414, 3.13), (853.58, 0.0563, 14.91), (1138.08, 0.0596, 34.13), (1707.12, 0.0625, 51.09), (2560.69, 0.0744, 86.66), ('inf', 0.0853, 150.17)], + 'semi-monthly': [(61.67, 0.0033, 0.00), (123.29, 0.0067, 0.20), (246.58, 0.0225, 0.61), (554.83, 0.0414, 3.38), (924.71, 0.0563, 16.14), (1232.92, 0.0596, 36.96), (1849.38, 0.0625, 55.33), (2774.08, 0.0744, 93.86), ('inf', 0.0853, 162.66)], + 'monthly': [(123.33, 0.0033, 0.00), (246.58, 0.0067, 0.41), (493.17, 0.0225, 1.24), (1109.67, 0.0414, 6.79), (1849.42, 0.0563, 32.31), (2465.83, 0.0596, 73.96), (3698.75, 0.0625, 110.70), (5548.17, 0.0744, 187.76), ('inf', 0.0853, 325.36)], + 'annual': [(1480.00, 0.0033, 0.00), (2959.00, 0.0067, 4.88), (5918.00, 0.0225, 14.79), (13316.00, 0.0414, 81.37), (22193.00, 0.0563, 387.65), (29590.00, 0.0596, 887.43), (44385.00, 0.0625, 1328.29), (66578.00, 0.0744, 2252.98), ('inf', 0.0853, 3904.14)], + } + + + + + + + US IA Iowa Standard Deduction Rate + us_ia_sit_standard_deduction_rate + + + + + { + '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), + } + + + + + { + 'daily': ( 7.23, 17.81), + 'weekly': ( 36.15, 89.04), + 'bi-weekly': ( 72.31, 178.08), + 'semi-monthly': ( 78.33, 192.92), + 'monthly': ( 156.67, 385.83), + 'annually': (1880.00, 4630.00), + } + + + + + + + US IA Iowa Deduction Allowance Rate + us_ia_sit_deduction_allowance_rate + + + + + { + 'daily': 0.15, + 'weekly': 0.77, + 'bi-weekly': 1.54, + 'semi-monthly': 1.67, + 'monthly': 3.33, + 'annually': 40.00, + } + + + + + { + 'daily': 0.15, + 'weekly': 0.77, + 'bi-weekly': 1.54, + 'semi-monthly': 1.67, + 'monthly': 3.33, + 'annually': 40.00, + } + + + + + + + + US Iowa - Department of Economic Security (IDES) - Unemployment Tax + + + + US Iowa - Department of Revenue (IDOR) - Income Tax + + + + + + + + + + ER: US IA Iowa State Unemployment + ER_US_IA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ia_suta_wage_base', rate='us_ia_suta_rate', state_code='IA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ia_suta_wage_base', rate='us_ia_suta_rate', state_code='IA') + + + + + + + + + EE: US IA Iowa State Income Tax Withholding + EE_US_IA_SIT + python + result, _ = ia_iowa_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ia_iowa_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/id_idaho.xml b/l10n_us_hr_payroll/data/state/id_idaho.xml new file mode 100644 index 00000000..ef908d13 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/id_idaho.xml @@ -0,0 +1,174 @@ + + + + + US ID Idaho SUTA Wage Base + us_id_suta_wage_base + + + + + 40000.0 + + + + + 41600.0 + + + + + + + + US ID Idaho SUTA Rate + us_id_suta_rate + + + + + 1.0 + + + + + 1.0 + + + + + + + US ID Idaho SIT Tax Rate + us_id_sit_tax_rate + + + + + { + 'single': { + 'weekly': ((235, 0.00, 0.00), (264, 0.00, 1.125), (294, 0.00, 3.125), (324, 1.00, 3.625), (353, 2.00, 4.625), (383, 4.00, 5.625), (457, 5.00, 6.625), ('inf', 10.00, 6.925)), + 'bi-weekly': ((469, 0.00, 0.00), (529, 0.00, 1.125), (588, 1.00, 3.125), (647, 3.00, 3.625), (706, 5.00, 4.625), (766, 7.00, 5.625), (914, 11.00, 6.625), ('inf', 21.00, 6.925)), + 'semi-monthly': ((508, 0.00, 0.00), (573, 0.00, 1.125), (637, 1.00, 3.125), (701, 3.00, 3.625), (765, 5.00, 4.625), (829, 8.00, 5.625), (990, 12.00, 6.625), ('inf', 22.00, 6.925)), + 'monthly': ((1017, 0.00, 0.00), (1145, 0.00, 1.125), (1273, 1.00, 3.125), (1402, 5.00, 3.625), (1530, 10.00, 4.625), (1659, 16.00, 5.625), (1980, 23.00, 6.625), ('inf', 45.00, 6.925)), + 'annually': ((12200, 0.00, 0.00), (13741, 0.00, 1.125), (15281, 17.00, 3.125), (16822, 65.00, 3.625), (18362, 121.00, 4.625), (19903, 192.00, 5.625), (23754, 279.00, 6.625), ('inf', 534.00, 6.925)), + }, + 'married': { + 'weekly': ((469, 0.00, 0.00), (529, 0.00, 1.125), (588, 0.00, 3.125), (647, 1.00, 3.625), (706, 2.00, 4.625), (766, 4.00, 5.625), (914, 5.00, 6.625), ('inf', 10.00, 6.925)), + 'bi-weekly': ((938, 0.00, 0.00), (1057, 0.00, 1.125), (1175, 1.00, 3.125), (1294, 5.00, 3.625), (1412, 9.00, 4.625), (1531, 15.00, 5.625), (1827, 21.00, 6.625), ('inf', 41.00, 6.925)), + 'semi-monthly': ((1017, 0.00, 0.00), (1145, 0.00, 1.125), (1273, 1.00, 3.125), (1402, 5.00, 3.625), (1530, 10.00, 4.625), (1659, 16.00, 5.625), (1980, 23.00, 6.625), ('inf', 45.00, 6.925)), + 'monthly': ((2033, 0.00, 0.00), (2290, 0.00, 1.125), (2547, 3.00, 3.125), (2804, 11.00, 3.625), (3060, 20.00, 4.625), (3317, 32.00, 5.625), (3959, 47.00, 6.625), ('inf', 89.00, 6.925)), + 'annually': ((24400, 0.00, 0.00), (27482, 0.00, 1.125), (30562, 35.00, 3.125), (33644, 131.00, 3.625), (36724, 243.00, 4.625), (39806, 385.00, 5.625), (47508, 558.00, 6.625), ('inf', 1068.00, 6.925)), + }, + 'head of household': { + 'weekly': ((235, 0.00, 0.00), (264, 0.00, 1.125), (294, 0.00, 3.125), (324, 1.00, 3.625), (353, 2.00, 4.625), (383, 4.00, 5.625), (457, 5.00, 6.625), ('inf', 10.00, 6.925)), + 'bi-weekly': ((469, 0.00, 0.00), (529, 0.00, 1.125), (588, 1.00, 3.125), (647, 3.00, 3.625), (706, 5.00, 4.625), (766, 7.00, 5.625), (914, 11.00, 6.625), ('inf', 21.00, 6.925)), + 'semi-monthly': ((508, 0.00, 0.00), (573, 0.00, 1.125), (637, 1.00, 3.125), (701, 3.00, 3.625), (765, 5.00, 4.625), (829, 8.00, 5.625), (990, 12.00, 6.625), ('inf', 22.00, 6.925)), + 'monthly': ((1017, 0.00, 0.00), (1145, 0.00, 1.125), (1273, 1.00, 3.125), (1402, 5.00, 3.625), (1530, 10.00, 4.625), (1659, 16.00, 5.625), (1980, 23.00, 6.625), ('inf', 45.00, 6.925)), + 'annually': ((12200, 0.00, 0.00), (13741, 0.00, 1.125), (15281, 17.00, 3.125), (16822, 65.00, 3.625), (18362, 121.00, 4.625), (19903, 192.00, 5.625), (23754, 279.00, 6.625), ('inf', 534.00, 6.925)), + }, + } + + + + + { + 'single': { + 'weekly': ((235, 0.00, 0.00), (264, 0.00, 1.125), (294, 0.00, 3.125), (324, 1.00, 3.625), (353, 2.00, 4.625), (383, 4.00, 5.625), (457, 5.00, 6.625), ('inf', 10.00, 6.925)), + 'bi-weekly': ((469, 0.00, 0.00), (529, 0.00, 1.125), (588, 1.00, 3.125), (647, 3.00, 3.625), (706, 5.00, 4.625), (766, 7.00, 5.625), (914, 11.00, 6.625), ('inf', 21.00, 6.925)), + 'semi-monthly': ((508, 0.00, 0.00), (573, 0.00, 1.125), (637, 1.00, 3.125), (701, 3.00, 3.625), (765, 5.00, 4.625), (829, 8.00, 5.625), (990, 12.00, 6.625), ('inf', 22.00, 6.925)), + 'monthly': ((1017, 0.00, 0.00), (1145, 0.00, 1.125), (1273, 1.00, 3.125), (1402, 5.00, 3.625), (1530, 10.00, 4.625), (1659, 16.00, 5.625), (1980, 23.00, 6.625), ('inf', 45.00, 6.925)), + 'annually': ((12200, 0.00, 0.00), (13741, 0.00, 1.125), (15281, 17.00, 3.125), (16822, 65.00, 3.625), (18362, 121.00, 4.625), (19903, 192.00, 5.625), (23754, 279.00, 6.625), ('inf', 534.00, 6.925)), + }, + 'married': { + 'weekly': ((469, 0.00, 0.00), (529, 0.00, 1.125), (588, 0.00, 3.125), (647, 1.00, 3.625), (706, 2.00, 4.625), (766, 4.00, 5.625), (914, 5.00, 6.625), ('inf', 10.00, 6.925)), + 'bi-weekly': ((938, 0.00, 0.00), (1057, 0.00, 1.125), (1175, 1.00, 3.125), (1294, 5.00, 3.625), (1412, 9.00, 4.625), (1531, 15.00, 5.625), (1827, 21.00, 6.625), ('inf', 41.00, 6.925)), + 'semi-monthly': ((1017, 0.00, 0.00), (1145, 0.00, 1.125), (1273, 1.00, 3.125), (1402, 5.00, 3.625), (1530, 10.00, 4.625), (1659, 16.00, 5.625), (1980, 23.00, 6.625), ('inf', 45.00, 6.925)), + 'monthly': ((2033, 0.00, 0.00), (2290, 0.00, 1.125), (2547, 3.00, 3.125), (2804, 11.00, 3.625), (3060, 20.00, 4.625), (3317, 32.00, 5.625), (3959, 47.00, 6.625), ('inf', 89.00, 6.925)), + 'annually': ((24400, 0.00, 0.00), (27482, 0.00, 1.125), (30562, 35.00, 3.125), (33644, 131.00, 3.625), (36724, 243.00, 4.625), (39806, 385.00, 5.625), (47508, 558.00, 6.625), ('inf', 1068.00, 6.925)), + }, + 'head of household': { + 'weekly': ((235, 0.00, 0.00), (264, 0.00, 1.125), (294, 0.00, 3.125), (324, 1.00, 3.625), (353, 2.00, 4.625), (383, 4.00, 5.625), (457, 5.00, 6.625), ('inf', 10.00, 6.925)), + 'bi-weekly': ((469, 0.00, 0.00), (529, 0.00, 1.125), (588, 1.00, 3.125), (647, 3.00, 3.625), (706, 5.00, 4.625), (766, 7.00, 5.625), (914, 11.00, 6.625), ('inf', 21.00, 6.925)), + 'semi-monthly': ((508, 0.00, 0.00), (573, 0.00, 1.125), (637, 1.00, 3.125), (701, 3.00, 3.625), (765, 5.00, 4.625), (829, 8.00, 5.625), (990, 12.00, 6.625), ('inf', 22.00, 6.925)), + 'monthly': ((1017, 0.00, 0.00), (1145, 0.00, 1.125), (1273, 1.00, 3.125), (1402, 5.00, 3.625), (1530, 10.00, 4.625), (1659, 16.00, 5.625), (1980, 23.00, 6.625), ('inf', 45.00, 6.925)), + 'annually': ((12200, 0.00, 0.00), (13741, 0.00, 1.125), (15281, 17.00, 3.125), (16822, 65.00, 3.625), (18362, 121.00, 4.625), (19903, 192.00, 5.625), (23754, 279.00, 6.625), ('inf', 534.00, 6.925)), + }, + } + + + + + + + US ID Idaho Child Tax Credit Allowance Rate + us_id_sit_ictcat_rate + + + + + { + 'weekly': 56.92, + 'bi-weekly': 113.85, + 'semi-monthly': 123.33, + 'monthly': 246.67, + 'annually': 2960.00, + } + + + + + { + 'weekly': 56.92, + 'bi-weekly': 113.85, + 'semi-monthly': 123.33, + 'monthly': 246.67, + 'annually': 2960.00, + } + + + + + + + + + US Idaho - Department of Labor (IDOL) - Unemployment Tax + + + + US Idaho - State Tax Commission (ISTC) - Income Tax + + + + + + + + + + ER: US ID Idaho State Unemployment + ER_US_ID_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_id_suta_wage_base', rate='us_id_suta_rate', state_code='ID') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_id_suta_wage_base', rate='us_id_suta_rate', state_code='ID') + + + + + + + + + EE: US ID Idaho State Income Tax Withholding + EE_US_ID_SIT + python + result, _ = id_idaho_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = id_idaho_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/il_illinois.xml b/l10n_us_hr_payroll/data/state/il_illinois.xml new file mode 100644 index 00000000..840b2a9b --- /dev/null +++ b/l10n_us_hr_payroll/data/state/il_illinois.xml @@ -0,0 +1,117 @@ + + + + + US IL Illinois SUTA Wage Base + us_il_suta_wage_base + + + + + 12960.0 + + + + + 12740.0 + + + + + + + + US IL Illinois SUTA Rate + us_il_suta_rate + + + + + 3.175 + + + + + 3.125 + + + + + + + US IL Illinois Basic Allowances Rate + us_il_sit_basic_allowances_rate + + + + + 2275.0 + + + + + 2325.0 + + + + + + + US IL Illinois Additional Allowances Rate + us_il_sit_additional_allowances_rate + + + + + 1000.0 + + + + + 1000.0 + + + + + + + + US Illinois - Department of Economic Security (IDES) - Unemployment Tax + + + + US Illinois - Department of Revenue (IDOR) - Income Tax + + + + + + + + + + ER: US IL Illinois State Unemployment + ER_US_IL_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_il_suta_wage_base', rate='us_il_suta_rate', state_code='IL') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_il_suta_wage_base', rate='us_il_suta_rate', state_code='IL') + + + + + + + + + EE: US IL Illinois State Income Tax Withholding + EE_US_IL_SIT + python + result, _ = il_illinois_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = il_illinois_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file 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/data/state/mn_minnesota.xml b/l10n_us_hr_payroll/data/state/mn_minnesota.xml new file mode 100644 index 00000000..5a5a0241 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/mn_minnesota.xml @@ -0,0 +1,143 @@ + + + + + US MN Minnesota SUTA Wage Base + us_mn_suta_wage_base + + + + + 34000.0 + + + + + 35000.0 + + + + + + + + US MN Minnesota SUTA Rate + us_mn_suta_rate + + + + + 1.11 + + + + + 1.11 + + + + + + + US MN Minnesota SIT Tax Rate + us_mn_sit_tax_rate + + + + + { + 'single': [ + ( 28920, 2400, 5.35, 0.00), + ( 89510, 28920, 7.05, 1418.82), + (166290, 89510, 7.85, 5690.42), + ( 'inf', 166290, 9.85, 11717.65), + ], + 'married': [ + ( 47820, 9050, 5.35, 0.00), + ( 163070, 47820, 7.05, 2074.20), + ( 282200, 163070, 7.85, 10199.33), + ( 'inf', 282200, 9.85, 19551.04), + ], + } + + + + + { + 'single': [ + ( 30760, 3800, 5.35, 0.00), + ( 92350, 30760, 6.80, 1442.36), + (168200, 92350, 7.85, 5630.48), + ( 'inf', 168200, 9.85, 11584.71), + ], + 'married': [ + ( 51310, 11900, 5.35, 0.00), + ( 168470, 51310, 6.80, 2108.44), + ( 285370, 168470, 7.85, 10075.32), + ( 'inf', 285370, 9.85, 19251.97), + ], + } + + + + + + + US MN Minnesota Allowances Rate + us_mn_sit_allowances_rate + + + + + 4250.0 + + + + + 4300.0 + + + + + + + + US Minnesota - Unemployment Insurance Agency - Unemployment Tax + + + + US Minnesota - Department of Treasury - Income Tax + + + + + + + + + + ER: US MN Minnesota State Unemployment + ER_US_MN_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mn_suta_wage_base', rate='us_mn_suta_rate', state_code='MN') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mn_suta_wage_base', rate='us_mn_suta_rate', state_code='MN') + + + + + + + + + EE: US MN Minnesota State Income Tax Withholding + EE_US_MN_SIT + python + result, _ = mn_minnesota_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = mn_minnesota_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/mo_missouri.xml b/l10n_us_hr_payroll/data/state/mo_missouri.xml new file mode 100644 index 00000000..230e24eb --- /dev/null +++ b/l10n_us_hr_payroll/data/state/mo_missouri.xml @@ -0,0 +1,145 @@ + + + + + US MO Missouri SUTA Wage Base + us_mo_suta_wage_base + + + + + 12000.0 + + + + + 11500.0 + + + + + + + + US MO Missouri SUTA Rate + us_mo_suta_rate + + + + + 2.376 + + + + + 2.376 + + + + + + + US MO Missouri SIT Rate Table + us_mo_sit_rate + + + + + [ + (1053.0, 1.5), + (1053.0, 2.0), + (1053.0, 2.5), + (1053.0, 3.0), + (1053.0, 3.5), + (1053.0, 4.0), + (1053.0, 4.5), + (1053.0, 5.0), + ( 'inf', 5.4), + ] + + + + + [ + (1073.0, 1.5), + (1073.0, 2.0), + (1073.0, 2.5), + (1073.0, 3.0), + (1073.0, 3.5), + (1073.0, 4.0), + (1073.0, 4.5), + (1073.0, 5.0), + ( 'inf', 5.4), + ] + + + + + + + US MO Missouri SIT Deduction + us_mo_sit_deduction + + + + + { + 'single': 12400.0, + 'married': 24800.0, + 'head_of_household': 18650.0, + } + + + + + { + 'single': 12400.0, + 'married': 24800.0, + 'head_of_household': 18650.0, + } + + + + + + + + US Missouri - Department of Taxation - Unemployment Tax + + + + US Missouri - Department of Taxation - Income Tax + + + + + + + + + + ER: US MO Missouri State Unemployment + ER_US_MO_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mo_suta_wage_base', rate='us_mo_suta_rate', state_code='MO') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mo_suta_wage_base', rate='us_mo_suta_rate', state_code='MO') + + + + + + + + + EE: US MO Missouri State Income Tax Withholding + EE_US_MO_SIT + python + result, _ = mo_missouri_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = mo_missouri_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/ms_mississippi.xml b/l10n_us_hr_payroll/data/state/ms_mississippi.xml new file mode 100644 index 00000000..97be0112 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/ms_mississippi.xml @@ -0,0 +1,125 @@ + + + + + US MS Mississippi SUTA Wage Base + us_ms_suta_wage_base + + + + + 14000.0 + + + + + 14000.0 + + + + + + + + US MS Mississippi SUTA Rate + us_ms_suta_rate + + + + + 1.2 + + + + + 1.2 + + + + + + + US MS Mississippi SIT Rate Table + us_ms_sit_rate + + + + + [ + ( 10000.00, 290.0, 0.05), + ( 5000.00, 90.0, 0.04), + ( 2000.00, 0.0, 0.03), + ] + + + + + [ + ( 10000.00, 260.0, 0.05), + ( 5000.00, 60.0, 0.04), + ( 3000.00, 0.0, 0.03), + ] + + + + + + + US MS Mississippi SIT Deduction + us_ms_sit_deduction + + + + + { + 'single': 2300.0, + 'head_of_household': 3400.0, + 'married_dual': 2300.0, + 'married': 4600.0, + } + + + + + + + + US Mississippi - Department of Employment Security (Unemployment) + + + + US Mississippi - Mississippi Department of Revenue (Income Tax) + + + + + + + + + + ER: US MS Mississippi State Unemployment + ER_US_MS_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ms_suta_wage_base', rate='us_ms_suta_rate', state_code='MS') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ms_suta_wage_base', rate='us_ms_suta_rate', state_code='MS') + + + + + + + + + EE: US MS Mississippi State Income Tax Withholding + EE_US_MS_SIT + python + result, _ = ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/mt_montana.xml b/l10n_us_hr_payroll/data/state/mt_montana.xml new file mode 100644 index 00000000..b420c4fe --- /dev/null +++ b/l10n_us_hr_payroll/data/state/mt_montana.xml @@ -0,0 +1,176 @@ + + + + + US MT Montana SUTA Wage Base + us_mt_suta_wage_base + + + + + 33000.00 + + + + + 34100.00 + + + + + + + + US MT Montana SUTA Rate (UI) + us_mt_suta_rate + + + + + 1.18 + + + + + 1.18 + + + + + + + US MT Montana SUTA Administrative Fund Tax Rate + us_mt_suta_aft_rate + + + + + 0.13 + + + + + 0.13 + + + + + + + US MT Montana SIT Rate Table + us_mt_sit_rate + + + + + { + 'weekly': [ + ( 135.00, 0.0, 1.80), + ( 288.00, 2.0, 4.40), + ( 2308.00, 9.0, 6.00), + ( 'inf', 130.0, 6.60), + ], + 'bi-weekly': [ + ( 269.00, 0.0, 1.80), + ( 577.00, 5.0, 4.40), + ( 4615.00, 18.0, 6.00), + ( 'inf', 261.0, 6.60), + ], + 'semi-monthly': [ + ( 292.00, 0.0, 1.80), + ( 625.00, 5.0, 4.40), + ( 5000.00, 20.0, 6.00), + ( 'inf', 282.0, 6.60), + ], + 'monthly': [ + ( 583.00, 0.0, 1.80), + ( 1250.00, 11.0, 4.40), + ( 10000.00, 40.0, 6.00), + ( 'inf', 565.0, 6.60), + ], + 'annually': [ + ( 7000.00, 0.0, 1.80), + ( 15000.00, 126.0, 4.40), + ( 120000.00, 478.0, 6.00), + ( 'inf', 6778.0, 6.60), + ], + } + + + + + + + US MT Montana SIT Exemption Rate Table + us_mt_sit_exemption_rate + + + + + { + 'weekly': 37.0, + 'bi-weekly': 73.0, + 'semi-monthly': 79.0, + 'monthly': 158.0, + 'annually': 1900.0, + } + + + + + + + + US Montana - Department of Labor & Industries + + + + US Montana - Department of Revenue - Income Tax + + + + + + + + + + ER: US MT Montana State Unemployment (UI-5) + ER_US_MT_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_rate', state_code='MT') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_rate', state_code='MT') + + + + + + + + + ER: US MT Montana State Unemployment Administrative Fund Tax (AFT) (UI-5) + ER_US_MT_SUTA_AFT + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_aft_rate', state_code='MT') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_aft_rate', state_code='MT') + + + + + + + + + EE: US MT Montana State Income Tax Withholding (MW-3) + EE_US_MT_SIT + python + result, _ = mt_montana_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = mt_montana_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file 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/data/state/nh_new_hampshire.xml b/l10n_us_hr_payroll/data/state/nh_new_hampshire.xml new file mode 100644 index 00000000..374ff539 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/nh_new_hampshire.xml @@ -0,0 +1,51 @@ + + + + + US NH New Hampshire SUTA Wage Base + us_nh_suta_wage_base + + + + + 14000.00 + + + + + + + + US NH New Hampshire SUTA Rate + us_nh_suta_rate + + + + + 1.2 + + + + + + + + US New Hampshire - Department of Employment Security - Unemployment Tax + + + + + + + + ER: US NH New Hampshire State Unemployment + ER_US_NH_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nh_suta_wage_base', rate='us_nh_suta_rate', state_code='NH') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nh_suta_wage_base', rate='us_nh_suta_rate', state_code='NH') + + + + + diff --git a/l10n_us_hr_payroll/data/state/nj_newjersey.xml b/l10n_us_hr_payroll/data/state/nj_newjersey.xml new file mode 100644 index 00000000..93d87265 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/nj_newjersey.xml @@ -0,0 +1,458 @@ + + + + + US NJ NewJersey SUTA Wage Base + us_nj_suta_wage_base + + + + + 34400.00 + + + + + 35300.00 + + + + + + + + + US NJ New Jersey Employer Unemployment SUTA Rate + us_nj_suta_rate + + + + + 2.6825 + + + + + 2.6825 + + + + + + + US NJ New Jersey Employee Unemployment SUTA Rate + us_nj_suta_ee_rate + + + + + 0.3825 + + + + + 0.3825 + + + + + + + + US NJ New Jersey Employer State Disability Insurance Rate + us_nj_sdi_rate + + + + + 0.5 + + + + + 0.5 + + + + + + + US NJ New Jersey Employee State Disability Insurance Rate + us_nj_sdi_ee_rate + + + + + 0.17 + + + + + 0.26 + + + + + + + + US NJ New Jersey Employer Workforce Development Rate + us_nj_wf_rate + + + + + 0.1175 + + + + + 0.1175 + + + + + + + US NJ New Jersey Employee Workforce Development Rate + us_nj_wf_ee_rate + + + + + 0.0425 + + + + + 0.0425 + + + + + + + + US NJ New Jersey Employer Family Leave Insurance Rate + us_nj_fli_rate + + + + + 0.0 + + + + + 0.0 + + + + + + + US NJ New Jersey Employee Family Leave Insurance Rate + us_nj_fli_ee_rate + + + + + 0.08 + + + + + 0.16 + + + + + + + + US NJ NewJersey SIT Rate Table + us_nj_sit_rate + + + + + { + 'A': { + 'weekly': ((385, 0.0, 1.50), (673, 5.77, 2.00), (769, 11.54, 3.90), (1442, 15.29, 6.10), (9615, 56.34, 7.00), (96154, 628.46, 9.90), ('inf', 9195.77, 11.80)), + 'bi-weekly': ((769, 0.00, 1.50), (1346, 12.00, 2.00), (1538, 23.00, 3.90), (2885, 31.00, 6.10), (19231, 113.00, 7.00), (192308, 1257.00, 9.90), ('inf',18392.00, 11.80)), + 'semi-monthly': ((833, 0.00, 1.50), (1458, 13.00, 2.00), (1667, 25.00, 3.90), (3125, 33.00, 6.10), (20833, 122.00, 7.00), (208333, 1362.00, 9.90), ('inf', 19924.00, 11.80)), + 'monthly': ((1667, 0.00, 1.50), (2917, 25.00, 2.00), (3333, 50.00, 3.90), (6250, 66.00, 6.10), (41667, 244.00, 7.00), (416667, 2723.00, 9.90), ('inf', 39848.00, 11.80)), + 'quarterly': ((5000, 0.00, 1.50), (8750, 75.00, 2.00), (10000, 150.00, 3.90), (18750, 198.75, 6.10), (125000, 732.50, 7.00), (1250000, 8170.00, 9.90), ('inf', 119545.00, 11.80)), + 'semi-annual': ((10000, 0.00, 1.50), (17500, 150.00, 2.00), (20000, 300.00, 3.90), (37500, 397.50, 6.10), (250000, 1465.00, 7.00), (2500000, 16340.00, 9.90), ('inf', 239090.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (35000, 300.00, 2.00), (40000, 600.00, 3.90), (75000, 795.00, 6.10), (500000, 2930.00, 7.00), (5000000, 32680.00, 9.90), ('inf', 478180.00, 11.80)), + }, + 'B': { + 'weekly': ((385, 0.0, 1.50), (962, 5.77, 2.00), (1346, 17.31, 2.70), (1538, 27.69, 3.9), (2885, 35.19, 6.10), (9615, 117.31, 7.00), (96154, 588.46, 9.90), ('inf', 9155.77, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1923, 12.00, 2.00), (2692, 35.00, 2.70), (3076, 55.00, 3.9), (5769, 70.00, 6.10), (19231, 235.00, 700), (192308, 1177.00, 9.90), ('inf', 18312.00, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (2083, 12.50, 2.00), (2917, 37.50, 2.70), (3333, 59.99, 3.9), (6250, 76.25, 6.10), (20833, 254.19, 7.00), (208333, 1275.00, 9.90), ('inf', 19838.00, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (4167, 25.00, 2.00), (5833, 75.00, 2.70), (6667, 120.00, 3.9), (12500, 153.00, 6.10), (41667, 508.00, 7.00), (416667, 2550.00, 9.90), ('inf', 39675.00, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (12500, 75.00, 2.00), (17500, 225.00, 2.70), (20000, 360.00, 3.9), (37500, 397.50, 6.10), (125000, 1525.00, 7.00), (1250000, 7650.00, 9.90), ('inf', 119025.00, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (25000, 150.00, 2.00), (35000, 450.00, 2.70), (40000, 720.00, 3.9), (75000, 915.00, 6.10), (250000, 3050.00, 7.00), (2500000, 15300.00, 9.90), ('inf', 238050.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (50000, 300.00, 2.00), (70000, 900.00, 2.70), (80000, 1440.00, 3.9), (150000, 1830.00, 6.10), (500000, 6100.00, 7.00), (5000000, 30600.00, 9.90), ('inf', 476100.00, 11.80)), + }, + 'C': { + 'weekly': ((385, 0.0, 1.50), (769, 5.77, 2.30), (962, 14.62, 2.80), (1154, 20.00, 3.50), (2885, 26.73, 5.60), (9615, 123.65, 6.60), (96154, 567.88, 9.90), ('inf', 9135.19, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1538, 11.54, 2.30), (1923, 29.23, 2.80), (2308, 40.00, 3.50), (5769, 53.46, 5.60), (19231, 247.31, 6.60), (192308, 1135.77, 9.90), ('inf', 18270.38, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (1667, 12.50, 2.30), (2083, 31.67, 2.80), (2500, 43.33, 3.50), (6250, 57.92, 5.60), (20833, 267.92, 6.60), (208333, 1230.42, 9.90), ('inf', 19792.92, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (3333, 25.00, 2.30), (4167, 63.33, 2.80), (5000, 86.67, 3.50), (12500, 115.83, 5.60), (41667, 535.85, 6.60), (416667, 2460.83, 9.90), ('inf', 39585.83, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (10000, 75.00, 2.30), (12500, 190.00, 2.80), (15000, 260.00, 3.50), (37500, 347.50, 5.60), (125000, 1607.50, 6.60), (1250000, 7382.50, 9.90), ('inf', 118757.50, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (20000, 150.00, 2.30), (25000, 380.00, 2.80), (30000, 520.00, 3.50), (75000, 695.00, 5.60), (250000, 3215.00, 6.60), (2500000, 14765.00, 9.90), ('inf', 237515.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (40000, 300.00, 2.30), (50000, 760.00, 2.80), (60000, 1040.00, 3.50), (150000, 1390.00, 5.60), (500000, 6430.00, 6.60), (5000000, 29530.00, 9.90), ('inf', 475030.00, 11.80)), + }, + 'D': { + 'weekly': ((385, 0.0, 1.50), (769, 5.77, 2.70), (962, 16.15, 3.40), (1154, 22.69, 4.30), (2885, 30.96, 5.60), (9615, 127.88, 6.50), (96154, 565.38, 9.90), ('inf', 9132.69, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1538, 11.54, 2.70), (1923, 32.31, 3.40), (2308, 45.38, 4.30), (5769, 61.92, 5.60), (19231, 255.77, 6.50), (192308, 1130.77, 9.90), ('inf', 18265.38, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (1667, 12.50, 2.70), (2083, 35.00, 3.40), (2500, 49.17, 4.30), (6250, 67.08, 5.60), (20833, 277.08, 6.50), (208333, 1225.00, 9.90), ('inf', 19787.50, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (3333, 25.00, 2.70), (4167, 70.00, 3.40), (5000, 98.33, 4.00), (12500, 134.17, 5.60), (41667, 554.17, 6.50), (416667, 2450.00, 9.90), ('inf', 39575.00, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (10000, 75.00, 2.07), (12500, 210.00, 3.40), (15000, 295.00, 4.30), (37500, 402.50, 5.60), (125000, 1662.50, 6.50), (1250000, 7350.00, 9.90), ('inf', 118725.00, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (20000, 150.00, 2.70), (25000, 420.00, 3.40), (30000, 590.00, 4.30), (75000, 805.00, 5.60), (250000, 3325.00, 6.50), (2500000, 14700.00, 9.90), ('inf', 237450.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (40000, 300.00, 2.70), (50000, 840.00, 3.40), (60000, 1180.00, 4.30), (150000, 1610.00, 5.60), (250000, 6650.00, 6.50), (2500000, 29400.00, 9.90), ('inf', 474900.00, 11.80)), + }, + 'E': { + 'weekly': ((385, 0.0, 1.50), (673, 5.77, 2.00), (1923, 11.54, 5.80), (9615, 84.04, 6.50), (96154, 584.04, 9.90), ('inf', 9151.35, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1346, 12.00, 2.00), (3846, 23.00, 5.80), (19231, 168.00, 6.50), (192308, 1168.00, 9.90), ('inf', 18303.00, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (1458, 13.00, 2.00), (4167, 25.00, 5.80), (20833, 182.00, 6.50), (208333, 1265.00, 9.90), ('inf', 19828.00, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (2916, 25.00, 2.00), (8333, 50.00, 5.80), (41667, 364.00, 6.50), (416667, 2531.00, 9.90), ('inf', 39656, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (8750, 75.00, 2.00), (25000, 150.00, 5.80), (125000, 1092.50, 6.50), (1250000, 7592.50, 9.90), ('inf', 118967.50, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (17500, 150.00, 2.00), (50000, 300.00, 5.80), (250000, 2185.00, 6.50), (2500000, 15185.00, 9.90), ('inf', 237935.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (35000, 300.00, 2.00), (100000, 600.00, 5.80), (500000, 4370.00, 6.50), (5000000, 30370.00, 9.90), ('inf', 475870.00, 11.80)), + }, + } + + + + + { + 'A': { + 'weekly': ((385, 0.0, 1.50), (673, 5.77, 2.00), (769, 11.54, 3.90), (1442, 15.29, 6.10), (9615, 56.34, 7.00), (96154, 628.46, 9.90), ('inf', 9195.77, 11.80)), + 'bi-weekly': ((769, 0.00, 1.50), (1346, 12.00, 2.00), (1538, 23.00, 3.90), (2885, 31.00, 6.10), (19231, 113.00, 7.00), (192308, 1257.00, 9.90), ('inf',18392.00, 11.80)), + 'semi-monthly': ((833, 0.00, 1.50), (1458, 13.00, 2.00), (1667, 25.00, 3.90), (3125, 33.00, 6.10), (20833, 122.00, 57.00), (208333, 1362.00, 9.90), ('inf', 19924.00, 11.80)), + 'monthly': ((1667, 0.00, 1.50), (2917, 25.00, 2.00), (3333, 50.00, 3.90), (6250, 66.00, 6.10), (41667, 244.00, 57.00), (416667, 2723.00, 9.90), ('inf', 39848.00, 11.80)), + 'quarterly': ((5000, 0.00, 1.50), (8750, 75.00, 2.00), (10000, 150.00, 3.90), (18750, 198.75, 6.10), (125000, 732.50, 57.00), (1250000, 8170.00, 9.90), ('inf', 119545.00, 11.80)), + 'semi-annual': ((10000, 0.00, 1.50), (17500, 150.00, 2.00), (20000, 300.00, 3.90), (37500, 397.50, 6.10), (250000, 1465.00, 57.00), (2500000, 16340.00, 9.90), ('inf', 239090.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (35000, 300.00, 2.00), (40000, 600.00, 3.90), (75000, 795.00, 6.10), (500000, 2930.00, 57.00), (5000000, 32680.00, 9.90), ('inf', 478180.00, 11.80)), + }, + 'B': { + 'weekly': ((385, 0.0, 1.50), (962, 5.77, 2.00), (1346, 17.31, 2.70), (1538, 27.69, 3.9), (2885, 35.19, 6.10), (9615, 117.31, 7.00), (96154, 588.46, 9.90), ('inf', 9155.77, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1923, 12.00, 2.00), (2692, 35.00, 2.70), (3076, 55.00, 3.9), (5769, 70.00, 6.10), (19231, 235.00, 700), (192308, 1177.00, 9.90), ('inf', 18312.00, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (2083, 12.50, 2.00), (2917, 37.50, 2.70), (3333, 59.99, 3.9), (6250, 76.25, 6.10), (20833, 254.19, 7.00), (208333, 1275.00, 9.90), ('inf', 19838.00, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (4167, 25.00, 2.00), (5833, 75.00, 2.70), (6667, 120.00, 3.9), (12500, 153.00, 6.10), (41667, 508.00, 7.00), (416667, 2550.00, 9.90), ('inf', 39675.00, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (12500, 75.00, 2.00), (17500, 225.00, 2.70), (20000, 360.00, 3.9), (37500, 397.50, 6.10), (125000, 1525.00, 7.00), (1250000, 7650.00, 9.90), ('inf', 119025.00, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (25000, 150.00, 2.00), (35000, 450.00, 2.70), (40000, 720.00, 3.9), (75000, 915.00, 6.10), (250000, 3050.00, 7.00), (2500000, 15300.00, 9.90), ('inf', 238050.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (50000, 300.00, 2.00), (70000, 900.00, 2.70), (80000, 1440.00, 3.9), (150000, 1830.00, 6.10), (500000, 6100.00, 7.00), (5000000, 30600.00, 9.90), ('inf', 476100.00, 11.80)), + }, + 'C': { + 'weekly': ((385, 0.0, 1.50), (769, 5.77, 2.30), (962, 14.62, 2.80), (1154, 20.00, 3.50), (2885, 26.73, 5.60), (9615, 123.65, 6.60), (96154, 567.88, 9.90), ('inf', 9135.19, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1538, 11.54, 2.30), (1923, 29.23, 2.80), (2308, 40.00, 3.50), (5769, 53.46, 5.60), (19231, 247.31, 6.60), (192308, 1135.77, 9.90), ('inf', 18270.38, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (1667, 12.50, 2.30), (2083, 31.67, 2.80), (2500, 43.33, 3.50), (6250, 57.92, 5.60), (20833, 267.92, 6.60), (208333, 1230.42, 9.90), ('inf', 19792.92, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (3333, 25.00, 2.30), (4167, 63.33, 2.80), (5000, 86.67, 3.50), (12500, 115.83, 5.60), (41667, 535.85, 6.60), (416667, 2460.83, 9.90), ('inf', 39585.83, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (10000, 75.00, 2.30), (12500, 190.00, 2.80), (15000, 260.00, 3.50), (37500, 347.50, 5.60), (125000, 1607.50, 6.60), (1250000, 7382.50, 9.90), ('inf', 118757.50, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (20000, 150.00, 2.30), (25000, 380.00, 2.80), (30000, 520.00, 3.50), (75000, 695.00, 5.60), (250000, 3215.00, 6.60), (2500000, 14765.00, 9.90), ('inf', 237515.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (40000, 300.00, 2.30), (50000, 760.00, 2.80), (60000, 1040.00, 3.50), (150000, 1390.00, 5.60), (500000, 6430.00, 6.60), (5000000, 29530.00, 9.90), ('inf', 475030.00, 11.80)), + }, + 'D': { + 'weekly': ((385, 0.0, 1.50), (769, 5.77, 2.70), (962, 16.15, 3.40), (1154, 22.69, 4.30), (2885, 30.96, 5.60), (9615, 127.88, 6.50), (96154, 565.38, 9.90), ('inf', 9132.69, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1538, 11.54, 2.70), (1923, 32.31, 3.40), (2308, 45.38, 4.30), (5769, 61.92, 5.60), (19231, 255.77, 6.50), (192308, 1130.77, 9.90), ('inf', 18265.38, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (1667, 12.50, 2.70), (2083, 35.00, 3.40), (2500, 49.17, 4.30), (6250, 67.08, 5.60), (20833, 277.08, 6.50), (208333, 1225.00, 9.90), ('inf', 19787.50, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (3333, 25.00, 2.70), (4167, 70.00, 3.40), (5000, 98.33, 4.00), (12500, 134.17, 5.60), (41667, 554.17, 6.50), (416667, 2450.00, 9.90), ('inf', 39575.00, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (10000, 75.00, 2.07), (12500, 210.00, 3.40), (15000, 295.00, 4.30), (37500, 402.50, 5.60), (125000, 1662.50, 6.50), (1250000, 7350.00, 9.90), ('inf', 118725.00, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (20000, 150.00, 2.70), (25000, 420.00, 3.40), (30000, 590.00, 4.30), (75000, 805.00, 5.60), (250000, 3325.00, 6.50), (2500000, 14700.00, 9.90), ('inf', 237450.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (40000, 300.00, 2.70), (50000, 840.00, 3.40), (60000, 1180.00, 4.30), (150000, 1610.00, 5.60), (250000, 6650.00, 6.50), (2500000, 29400.00, 9.90), ('inf', 474900.00, 11.80)), + }, + 'E': { + 'weekly': ((385, 0.0, 1.50), (673, 5.77, 2.00), (1923, 11.54, 5.80), (9615, 84.04, 6.50), (96154, 584.04, 9.90), ('inf', 9151.35, 11.80)), + 'bi-weekly': ((769, 0.0, 1.50), (1346, 12.00, 2.00), (3846, 23.00, 5.80), (19231, 168.00, 6.50), (192308, 1168.00, 9.90), ('inf', 18303.00, 11.80)), + 'semi-monthly': ((833, 0.0, 1.50), (1458, 13.00, 2.00), (4167, 25.00, 5.80), (20833, 182.00, 6.50), (208333, 1265.00, 9.90), ('inf', 19828.00, 11.80)), + 'monthly': ((1667, 0.0, 1.50), (2916, 25.00, 2.00), (8333, 50.00, 5.80), (41667, 364.00, 6.50), (416667, 2531.00, 9.90), ('inf', 39656, 11.80)), + 'quarterly': ((5000, 0.0, 1.50), (8750, 75.00, 2.00), (25000, 150.00, 5.80), (125000, 1092.50, 6.50), (1250000, 7592.50, 9.90), ('inf', 118967.50, 11.80)), + 'semi-annual': ((10000, 0.0, 1.50), (17500, 150.00, 2.00), (50000, 300.00, 5.80), (250000, 2185.00, 6.50), (2500000, 15185.00, 9.90), ('inf', 237935.00, 11.80)), + 'annual': ((20000, 0.0, 1.50), (35000, 300.00, 2.00), (100000, 600.00, 5.80), (500000, 4370.00, 6.50), (5000000, 30370.00, 9.90), ('inf', 475870.00, 11.80)), + }, + } + + + + + + + US NJ NewJersey SIT Allowance Rate + us_nj_sit_allowance_rate + + + + + { + 'weekly': 19.20, + 'bi-weekly': 38.40, + 'semi-monthly': 41.60, + 'monthly': 83.30, + 'quarterly': 250.00, + 'semi-annual': 500.00, + 'annual': 1000.00, + 'daily or miscellaneous': 2.70, + } + + + + + { + 'weekly': 19.20, + 'bi-weekly': 38.40, + 'semi-monthly': 41.60, + 'monthly': 83.30, + 'quarterly': 250.00, + 'semi-annual': 500.00, + 'annual': 1000.00, + 'daily or miscellaneous': 2.70, + } + + + + + + + + US New Jersey - Division of Taxation - Unemployment Tax + + + + US New Jersey - Division of Taxation - Income Tax + + + + + + + + + ER: US NJ New Jersey State Unemployment + ER_US_NJ_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_suta_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_suta_rate', state_code='NJ') + + + + + + + + + EE: US NJ New Jersey State Unemployment + EE_US_NJ_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_suta_ee_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_suta_ee_rate', state_code='NJ') + + + + + + + + + + ER: US NJ New Jersey State Disability Insurance + ER_US_NJ_SDI + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_sdi_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_sdi_rate', state_code='NJ') + + + + + + + + + EE: US NJ New Jersey State Disability Insurance + EE_US_NJ_SDI + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_sdi_ee_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_sdi_ee_rate', state_code='NJ') + + + + + + + + + + ER: US NJ New Jersey Workforce Development + ER_US_NJ_WF + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_wf_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_wf_rate', state_code='NJ') + + + + + + + + + EE: US NJ New Jersey Workforce Development + EE_US_NJ_WF + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_wf_ee_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_wf_ee_rate', state_code='NJ') + + + + + + + + + + ER: US NJ New Jersey Family Leave Insurance + ER_US_NJ_FLI + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_fli_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_fli_rate', state_code='NJ') + + + + + + + + + EE: US NJ New Jersey Family Leave Insurance + EE_US_NJ_FLI + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_fli_ee_rate', state_code='NJ') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nj_suta_wage_base', rate='us_nj_fli_ee_rate', state_code='NJ') + + + + + + + + + EE: US NJ New Jersey State Income Tax Withholding + EE_US_NJ_SIT + python + result, _ = nj_newjersey_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = nj_newjersey_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/nm_new_mexico.xml b/l10n_us_hr_payroll/data/state/nm_new_mexico.xml new file mode 100644 index 00000000..a823c608 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/nm_new_mexico.xml @@ -0,0 +1,113 @@ + + + + + US NM New Mexico SUTA Wage Base + us_nm_suta_wage_base + + + + + 25800.0 + + + + + + + + US NM New Mexico SUTA Rate + us_nm_suta_rate + + + + + 1.0 + + + + + + + US NM New Mexico SIT Tax Rate + us_nm_sit_tax_rate + + + + + { + 'single': { + 'weekly': ((119, 0.00, 0.0), (225, 0.00, 1.7), (331, 1.80, 3.2), (427, 5.18, 4.7), (619, 9.70, 4.9), (927, 19.13, 4.9), (1369, 34.20, 4.9), ('inf', 55.88, 4.9)), + 'bi-weekly': ((238, 0.00, 0.0), (450, 0.00, 1.7), (662, 3.60, 3.2), (854, 10.37, 4.7), (1238, 19.40, 4.9), (1854, 38.25, 4.9), (2738, 68.40, 4.9), ('inf', 111.75, 4.9)), + 'semi-monthly': ((258, 0.00, 0.0), (488, 0.00, 1.7), (717, 3.90, 3.2), (925, 11.23, 4.7), (1342, 21.02, 4.9), (2008, 41.44, 4.9), (2967, 74.10, 4.9), ('inf', 121.06, 4.9)), + 'monthly': ((517, 0.00, 0.0), (975, 0.00, 1.7), (1433, 7.79, 3.2), (1850, 22.46, 4.7), (2683, 42.04, 4.9), (4017, 82.88, 4.9), (5933, 148.21, 4.9), ('inf', 242.13, 4.9)), + 'quarterly': ((1550, 0.00, 0.0), (2925, 0.00, 1.7), (4300, 23.38, 3.2), (5550, 67.38, 4.7), (8050, 126.13, 4.9), (12050, 248.63, 4.9), (17800, 444.63, 4.9), ('inf', 726.38, 4.9)), + 'semi-annual': ((3100, 0.00, 0.0), (5850, 0.00, 1.7), (8600, 46.75, 3.2), (11100, 134.75, 4.7), (16100, 252.25, 4.9), (24100, 497.25, 4.9), (35600, 889.25, 4.9), ('inf', 1452.75, 4.9)), + 'annually': ((6200, 0.00, 0.0), (11700, 0.00, 1.7), (17200, 93.50, 3.2), (22200, 269.50, 4.7), (32200, 504.50, 4.9), (48200, 994.50, 4.9), (71200, 1778.50, 4.9), ('inf', 2905.50, 4.9)), + }, + 'married': { + 'weekly': ((238, 0.00, 0.0), (392, 0.00, 1.7), (546, 2.62, 3.2), (700, 7.54, 4.7), (1008, 14.77, 4.9), (1469, 29.85, 4.9), (2162, 52.46, 4.9), ('inf', 86.38, 4.9)), + 'bi-weekly': ((477, 0.00, 0.0), (785, 0.00, 1.7), (1092, 5.23, 3.2), (1400, 15.08, 4.7), (2015, 29.54, 4.9), (2938, 59.69, 4.9), (4323, 104.92, 4.9), ('inf', 172.77, 4.9)), + 'semi-monthly': ((517, 0.00, 0.0), (850, 0.00, 1.7), (1183, 5.67, 3.2), (1517, 16.33, 4.7), (2183, 32.00, 4.9), (3183, 64.67, 4.9), (4683, 113.67, 4.9), ('inf', 187.17, 4.9)), + 'monthly': ((1033, 0.00, 0.0), (1700, 0.00, 1.7), (2367, 11.33, 3.2), (3033, 32.67, 4.7), (4367, 64.00, 4.9), (6367, 129.33, 4.9), (9367, 227.33, 4.9), ('inf', 374.33, 4.9)), + 'quarterly': ((3100, 0.00, 0.0), (5100, 0.00, 1.7), (7100, 34.00, 3.2), (9100, 98.00, 4.7), (13100, 192.00, 4.9), (19100, 388.00, 4.9), (28100, 682.00, 4.9), ('inf', 1123.00, 4.9)), + 'semi-annual': ((6200, 0.00, 0.0), (10200, 0.00, 1.7), (14200, 68.00, 3.2), (18200, 196.00, 4.7), (26200, 384.00, 4.9), (38200, 776.00, 4.9), (56200, 1364.00, 4.9), ('inf', 2246.00, 4.9)), + 'annually': ((12400, 0.00, 0.0), (20400, 0.00, 1.7), (28400, 136.00, 3.2), (36400, 392.00, 4.7), (52400, 768.00, 4.9), (76400, 1552.00, 4.9), (112400, 2728.00, 4.9), ('inf', 4492.00, 4.9)), + }, + 'married_as_single': { + 'weekly': ((179, 0.00, 0.0), (333, 0.00, 1.7), (487, 2.62, 3.2), (641, 7.54, 4.7), (949, 14.77, 4.9), (1410, 29.85, 4.9), (2102, 52.46, 4.9), ('inf', 86.38, 4.9)), + 'bi-weekly': ((359, 0.00, 0.0), (666, 0.00, 1.7), (974, 5.23, 3.2), (1282, 15.08, 4.7), (1897, 29.54, 4.9), (2820, 59.69, 4.9), (4205, 104.92, 4.9), ('inf', 172.77, 4.9)), + 'semi-monthly': ((389, 0.00, 0.0), (722, 0.00, 1.7), (1055, 5.67, 3.2), (1389, 16.33, 4.7), (2055, 32.00, 4.9), (3055, 64.67, 4.9), (4555, 113.67, 4.9), ('inf', 187.17, 4.9)), + 'monthly': ((777, 0.00, 0.0), (1444, 0.00, 1.7), (2110, 11.33, 3.2), (2777, 32.67, 4.7), (4110, 64.00, 4.9), (6110, 129.33, 4.9), (9110, 227.33, 4.9), ('inf', 374.33, 4.9)), + 'quarterly': ((2331, 0.00, 0.0), (4331, 0.00, 1.7), (6331, 34.00, 3.2), (8331, 98.00, 4.7), (12331, 192.00, 4.9), (18331, 388.00, 4.9), (27331, 682.00, 4.9), ('inf', 1123.00, 4.9)), + 'semi-annual': ((4663, 0.00, 0.0), (8663, 0.00, 1.7), (12663, 68.00, 3.2), (16663, 196.00, 4.7), (24663, 384.00, 4.9), (36663, 776.00, 4.9), (54663, 1364.00, 4.9), ('inf', 2246.00, 4.9)), + 'annually': ((9325, 0.00, 0.0), (17325, 0.00, 1.7), (25325, 136.00, 3.2), (33325, 392.00, 4.7), (49325, 768.00, 4.9), (73325, 1552.00, 4.9), (109325, 2728.00, 4.9), ('inf', 4492.00, 4.9)), + } + } + + + + + + + + + US New Mexico - Department of Workforce Solutions - Unemployment Tax + + + + US New Mexico - Department of Taxation and Revenue - Income Tax + + + + + + + + + + ER: US NM New Mexico State Unemployment + ER_US_NM_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nm_suta_wage_base', rate='us_nm_suta_rate', state_code='NM') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nm_suta_wage_base', rate='us_nm_suta_rate', state_code='NM') + + + + + + + + + EE: US NM New Mexico State Income Tax Withholding + EE_US_NM_SIT + python + result, _ = nm_new_mexico_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = nm_new_mexico_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/oh_ohio.xml b/l10n_us_hr_payroll/data/state/oh_ohio.xml new file mode 100644 index 00000000..e6db8eb8 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/oh_ohio.xml @@ -0,0 +1,157 @@ + + + + + US OH Ohio SUTA Wage Base + us_oh_suta_wage_base + + + + + 9500.00 + + + + + 9000.00 + + + + + + + + US OH Ohio SUTA Rate + us_oh_suta_rate + + + + + 2.7 + + + + + 2.7 + + + + + + + US OH Ohio SIT Rate Table + us_oh_sit_rate + + + + + + + [ + ( 5000.00, 0.0, 0.005), + ( 10000.00, 25.0, 0.010), + ( 15000.00, 75.0, 0.020), + ( 20000.00, 175.0, 0.025), + ( 40000.00, 300.0, 0.030), + ( 80000.00, 900.0, 0.035), + ( 100000.00, 2300.0, 0.040), + ( 'inf', 3100.0, 0.050), + ] + + + + + + + [ + ( 5000.00, 0.0, 0.005), + ( 10000.00, 25.0, 0.010), + ( 15000.00, 75.0, 0.020), + ( 20000.00, 175.0, 0.025), + ( 40000.00, 300.0, 0.030), + ( 80000.00, 900.0, 0.035), + ( 100000.00, 2300.0, 0.040), + ( 'inf', 3100.0, 0.050), + ] + + + + + + + US OH Ohio SIT Exemption Rate + us_oh_sit_exemption_rate + + + + + 650.0 + + + + + 650.0 + + + + + + + US OH Ohio SIT Multiplier Value + us_oh_sit_multiplier + + + + + 1.075 + + + + + 1.032 + + + + + + + + US Ohio - OBG - Unemployment + + + + US Ohio - OBG - Income Withholding + + + + + + + + + + ER: US OH Ohio State Unemployment (JFS-20125) + ER_US_OH_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_oh_suta_wage_base', rate='us_oh_suta_rate', state_code='OH') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_oh_suta_wage_base', rate='us_oh_suta_rate', state_code='OH') + + + + + + + + + EE: US OH Ohio State Income Tax Withholding (IT 501) + EE_US_OH_SIT + python + result, _ = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml b/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml new file mode 100644 index 00000000..f46a92b4 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml @@ -0,0 +1,131 @@ + + + + + US PA Pennsylvania SUTA Wage Base (ER) + us_pa_suta_wage_base + + + + + 10000.00 + + + + + 10000.00 + + + + + + + + US PA Pennsylvania SUTA Rate + us_pa_suta_rate + + + + + 3.6890 + + + + + 3.6890 + + + + + + + US PA Pennsylvania SUTA Employee Rate + us_pa_suta_ee_rate + + + + + 0.06 + + + + + 0.06 + + + + + + + US PA Pennsylvania SIT Rate + us_pa_sit_rate + + + + + 3.07 + + + + + 3.07 + + + + + + + + US Pennsylvania - Department of Revenue - Unemployment Tax + + + + US Pennsylvania - Department of Revenue - Income Tax + + + + + + + + + + ER: US PA Pennsylvania State Unemployment (UC-2) + ER_US_PA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_pa_suta_wage_base', rate='us_pa_suta_rate', state_code='PA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_pa_suta_wage_base', rate='us_pa_suta_rate', state_code='PA') + + + + + + + + + EE: US PA Pennsylvania State Unemployment (UC-2) + EE_US_PA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, rate='us_pa_suta_ee_rate', state_code='PA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, rate='us_pa_suta_ee_rate', state_code='PA') + + + + + + + + + EE: US PA Pennsylvania State Income Tax Withholding (PA-501) + EE_US_PA_SIT + python + result, _ = general_state_income_withholding(payslip, categories, worked_days, inputs, rate='us_pa_sit_rate', state_code='PA') + code + result, result_rate = general_state_income_withholding(payslip, categories, worked_days, inputs, rate='us_pa_sit_rate', state_code='PA') + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/tx_texas.xml b/l10n_us_hr_payroll/data/state/tx_texas.xml new file mode 100644 index 00000000..5d9c5772 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/tx_texas.xml @@ -0,0 +1,127 @@ + + + + + US TX Texas SUTA Wage Base + us_tx_suta_wage_base + + + + + 9000.0 + + + + + 9000.0 + + + + + + + + US TX Texas SUTA Rate + us_tx_suta_rate + + + + + 2.7 + + + + + 2.7 + + + + + + + US TX Texas Obligation Assessment Rate + us_tx_suta_oa_rate + + + + + 0.0 + + + + + 0.0 + + + + + + + US TX Texas Employment & Training Investment Assessment Rate + us_tx_suta_etia_rate + + + + + 0.1 + + + + + 0.1 + + + + + + + + US Texas - Workforce Commission (Unemployment) + + + + + + + + + + ER: US TX Texas State Unemployment (C-3) + ER_US_TX_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_rate', state_code='TX') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_rate', state_code='TX') + + + + + + + + + ER: US TX Texas Obligation Assessment (C-3) + ER_US_TX_SUTA_OA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_oa_rate', state_code='TX') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_oa_rate', state_code='TX') + + + + + + + + + ER: US TX Texas Employment & Training Investment Assessment (C-3) + ER_US_TX_SUTA_ETIA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_etia_rate', state_code='TX') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_etia_rate', state_code='TX') + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/va_virginia.xml b/l10n_us_hr_payroll/data/state/va_virginia.xml new file mode 100644 index 00000000..a07daae7 --- /dev/null +++ b/l10n_us_hr_payroll/data/state/va_virginia.xml @@ -0,0 +1,138 @@ + + + + + US VA Virginia SUTA Wage Base + us_va_suta_wage_base + + + + + 8000.0 + + + + + 8000.0 + + + + + + + + US VA Virginia SUTA Rate + us_va_suta_rate + + + + + 2.51 + + + + + 2.51 + + + + + + + US VA Virginia SIT Rate Table + us_va_sit_rate + + + + + [ + ( 0.00, 0.0, 2.00), + ( 3000.00, 60.0, 3.00), + ( 5000.00, 120.0, 5.00), + ( 17000.00, 720.0, 5.75), + ] + + + + + + + US VA Virginia SIT Exemption Rate Table + us_va_sit_exemption_rate + + + + + 930.0 + + + + + + + US VA Virginia SIT Other Exemption Rate Table + us_va_sit_other_exemption_rate + + + + + 800.0 + + + + + + + US VA Virginia SIT Deduction + us_va_sit_deduction + + + + + 4500.0 + + + + + + + + US Virginia - Department of Taxation - Unemployment Tax + + + + US Virginia - Department of Taxation - Income Tax + + + + + + + + + + ER: US VA Virginia State Unemployment + ER_US_VA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_va_suta_wage_base', rate='us_va_suta_rate', state_code='VA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_va_suta_wage_base', rate='us_va_suta_rate', state_code='VA') + + + + + + + + + EE: US VA Virginia State Income Tax Withholding + EE_US_VA_SIT + python + result, _ = va_virginia_state_income_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = va_virginia_state_income_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/wa_washington.xml b/l10n_us_hr_payroll/data/state/wa_washington.xml new file mode 100644 index 00000000..3307689c --- /dev/null +++ b/l10n_us_hr_payroll/data/state/wa_washington.xml @@ -0,0 +1,200 @@ + + + + + US WA Washington SUTA Wage Base + us_wa_suta_wage_base + + + + + 49800.0 + + + + + 52700.00 + + + + + + + US WA Washington FML Wage Base + us_wa_fml_wage_base + + + + + 132900.00 + + + + + 137700.00 + + + + + + + + US WA Washington SUTA Rate + us_wa_suta_rate + + + + + 1.18 + + + + + 1.0 + + + + + + + US WA Washington FML Rate (Total) + us_wa_fml_rate + + + + + 0.4 + + + + + 0.4 + + + + + + + US WA Washington FML Rate (Employee) + us_wa_fml_rate_ee + + + + + 66.33 + + + + + 66.33 + + + + + + + US WA Washington FML Rate (Employer) + us_wa_fml_rate_er + + + + + 33.67 + + + + + 33.67 + + + + + + + + US Washington - Employment Security Department (Unemployment) + + + + US Washington - Department of Labor & Industries + + + + US Washington - Employment Security Department (PFML) + + + + + + + + + + ER: US WA Washington State Unemployment (5208A/B) + ER_US_WA_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_wa_suta_wage_base', rate='us_wa_suta_rate', state_code='WA') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_wa_suta_wage_base', rate='us_wa_suta_rate', state_code='WA') + + + + + + + + + ER: US WA Washington State Family Medical Leave + ER_US_WA_FML + python + result, _ = wa_washington_fml_er(payslip, categories, worked_days, inputs) + code + result, result_rate = wa_washington_fml_er(payslip, categories, worked_days, inputs) + + + + + + + + + EE: US WA Washington State Family Medical Leave + EE_US_WA_FML + python + result, _ = wa_washington_fml_ee(payslip, categories, worked_days, inputs) + code + result, result_rate = wa_washington_fml_ee(payslip, categories, worked_days, inputs) + + + + + + + + + + ER: US WA Washington State LNI + ER_US_WA_LNI + python + result = is_us_state(payslip, 'WA') and payslip.contract_id.us_payroll_config_value('workers_comp_ee_code') and worked_days.WORK100 and worked_days.WORK100.number_of_hours and payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_er_code')) + code + result, result_rate = worked_days.WORK100.number_of_hours, -payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_er_code')) + + + + + + + + + EE: US WA Washington State LNI + EE_US_WA_LNI + python + result = is_us_state(payslip, 'WA') and payslip.contract_id.us_payroll_config_value('workers_comp_ee_code') and worked_days.WORK100 and worked_days.WORK100.number_of_hours and payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_ee_code')) + code + result, result_rate = worked_days.WORK100.number_of_hours, -payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_ee_code')) + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py new file mode 100644 index 00000000..8611b49a --- /dev/null +++ b/l10n_us_hr_payroll/models/__init__.py @@ -0,0 +1,7 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import browsable_object +from . import hr_contract +from . import hr_payslip +from . import res_config_settings +from . import us_payroll_config diff --git a/l10n_us_hr_payroll/models/browsable_object.py b/l10n_us_hr_payroll/models/browsable_object.py new file mode 100644 index 00000000..17f29c3f --- /dev/null +++ b/l10n_us_hr_payroll/models/browsable_object.py @@ -0,0 +1,122 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields +from odoo.addons.hr_payroll.models import browsable_object + + +class BrowsableObject(object): + def __init__(self, employee_id, dict, env): + self.employee_id = employee_id + self.dict = dict + self.env = env + # Customization to allow changing the behavior of the discrete browsable objects. + # you can think of this as 'compiling' the query based on the configuration. + sum_field = env['ir.config_parameter'].sudo().get_param('hr_payroll.payslip.sum_behavior', 'date_from') + if sum_field == 'date' and 'date' not in env['hr.payslip']: + # missing attribute, closest by definition + sum_field = 'date_to' + if not sum_field: + sum_field = 'date_from' + self._compile_browsable_query(sum_field) + + def __getattr__(self, attr): + return attr in self.dict and self.dict.__getitem__(attr) or 0.0 + + def _compile_browsable_query(self, sum_field): + pass + + +class InputLine(BrowsableObject): + """a class that will be used into the python code, mainly for usability purposes""" + def _compile_browsable_query(self, sum_field): + self.__browsable_query = """ + SELECT sum(amount) as sum + FROM hr_payslip as hp, hr_payslip_input as pi + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field) + + def sum(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + self.env.cr.execute(self.__browsable_query, (self.employee_id, from_date, to_date, code)) + return self.env.cr.fetchone()[0] or 0.0 + + +class WorkedDays(BrowsableObject): + """a class that will be used into the python code, mainly for usability purposes""" + def _compile_browsable_query(self, sum_field): + self.__browsable_query = """ + SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours + FROM hr_payslip as hp, hr_payslip_worked_days as pi + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""".format(sum_field=sum_field) + + def _sum(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + self.env.cr.execute(self.__browsable_query, (self.employee_id, from_date, to_date, code)) + return self.env.cr.fetchone() + + def sum(self, code, from_date, to_date=None): + res = self._sum(code, from_date, to_date) + return res and res[0] or 0.0 + + def sum_hours(self, code, from_date, to_date=None): + res = self._sum(code, from_date, to_date) + return res and res[1] or 0.0 + + +class Payslips(BrowsableObject): + """a class that will be used into the python code, mainly for usability purposes""" + def _compile_browsable_query(self, sum_field): + # Note that the core odoo has this as `hp.credit_note = False` but what if it is NULL? + # reverse of the desired behavior. + self.__browsable_query_rule = """ + SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end) + FROM hr_payslip as hp, hr_payslip_line as pl + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""".format(sum_field=sum_field) + self.__browsable_query_category = """ + SELECT sum(case when hp.credit_note is not True then (pl.total) else (-pl.total) end) + FROM hr_payslip as hp, hr_payslip_line as pl, hr_salary_rule_category as rc + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.{sum_field} >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id + AND rc.id = pl.category_id AND rc.code = %s""".format(sum_field=sum_field) + + def sum(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + self.env.cr.execute(self.__browsable_query_rule, (self.employee_id, from_date, to_date, code)) + res = self.env.cr.fetchone() + return res and res[0] or 0.0 + + def rule_parameter(self, code): + return self.env['hr.rule.parameter']._get_parameter_from_code(code, self.dict.date_to) + + def sum_category(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + + self.env['hr.payslip'].flush(['credit_note', 'employee_id', 'state', 'date_from', 'date_to']) + self.env['hr.payslip.line'].flush(['total', 'slip_id', 'category_id']) + self.env['hr.salary.rule.category'].flush(['code']) + + self.env.cr.execute(self.__browsable_query_category, (self.employee_id, from_date, to_date, code)) + res = self.env.cr.fetchone() + return res and res[0] or 0.0 + + @property + def paid_amount(self): + return self.dict._get_paid_amount() + + +# Patch over Core +browsable_object.BrowsableObject.__init__ = BrowsableObject.__init__ +browsable_object.BrowsableObject._compile_browsable_query = BrowsableObject._compile_browsable_query +browsable_object.InputLine._compile_browsable_query = InputLine._compile_browsable_query +browsable_object.InputLine.sum = InputLine.sum +browsable_object.WorkedDays._compile_browsable_query = WorkedDays._compile_browsable_query +browsable_object.WorkedDays.sum = WorkedDays.sum +browsable_object.Payslips._compile_browsable_query = Payslips._compile_browsable_query +browsable_object.Payslips.sum = Payslips.sum +browsable_object.Payslips.sum_category = Payslips.sum_category diff --git a/l10n_us_hr_payroll/models/federal/__init__.py b/l10n_us_hr_payroll/models/federal/__init__.py new file mode 100644 index 00000000..0358305d --- /dev/null +++ b/l10n_us_hr_payroll/models/federal/__init__.py @@ -0,0 +1 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. diff --git a/l10n_us_hr_payroll/models/federal/fed_940.py b/l10n_us_hr_payroll/models/federal/fed_940.py new file mode 100644 index 00000000..bbd4be17 --- /dev/null +++ b/l10n_us_hr_payroll/models/federal/fed_940.py @@ -0,0 +1,79 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + + +def futa_wage(payslip, categories): + """ + Returns FUTA eligible wage for current Payslip (no wage_base, just by categories) + WAGE = GROSS - ALW_FUTA_EXEMPT + DED_FUTA_EXEMPT + :return: wage + """ + wage = categories.GROSS + + wage -= categories.ALW_FUTA_EXEMPT + \ + categories.ALW_FIT_FUTA_EXEMPT + \ + categories.ALW_FIT_FICA_FUTA_EXEMPT + \ + categories.ALW_FICA_FUTA_EXEMPT + + wage += categories.DED_FUTA_EXEMPT + \ + categories.DED_FIT_FUTA_EXEMPT + \ + categories.DED_FIT_FICA_FUTA_EXEMPT + \ + categories.DED_FICA_FUTA_EXEMPT + + return wage + + +def futa_wage_ytd(payslip, categories): + """ + Returns Year to Date FUTA eligible wages + WAGE = GROSS - ALW_FUTA_EXEMPT + DED_FUTA_EXEMPT + :return: wage + """ + year = payslip.dict.get_year() + ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage -= payslip.sum_category('ALW_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage += payslip.sum_category('DED_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage += payslip.contract_id.external_wages + return ytd_wage + + +def er_us_940_futa(payslip, categories, worked_days, inputs): + """ + Returns FUTA eligible wage and rate. + :return: result, result_rate (wage, percent) + """ + + # Determine Rate. + if payslip.contract_id.futa_type == payslip.contract_id.FUTA_TYPE_EXEMPT: + # Exit early + return 0.0, 0.0 + elif payslip.contract_id.futa_type == payslip.contract_id.FUTA_TYPE_BASIC: + result_rate = -payslip.rule_parameter('fed_940_futa_rate_basic') + else: + result_rate = -payslip.rule_parameter('fed_940_futa_rate_normal') + + # Determine Wage + wage = futa_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + ytd_wage = futa_wage_ytd(payslip, categories) + wage_base = payslip.rule_parameter('fed_940_futa_wage_base') + remaining = wage_base - ytd_wage + + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + return result, result_rate diff --git a/l10n_us_hr_payroll/models/federal/fed_941.py b/l10n_us_hr_payroll/models/federal/fed_941.py new file mode 100644 index 00000000..c154bca7 --- /dev/null +++ b/l10n_us_hr_payroll/models/federal/fed_941.py @@ -0,0 +1,319 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +# import logging +# _logger = logging.getLogger(__name__) + + +def fica_wage(payslip, categories): + """ + Returns FICA eligible wage for current Payslip (no wage_base, just by categories) + WAGE = GROSS - ALW_FICA_EXEMPT + DED_FICA_EXEMPT + :return: wage + """ + wage = categories.GROSS + + wage -= categories.ALW_FICA_EXEMPT + \ + categories.ALW_FIT_FICA_EXEMPT + \ + categories.ALW_FIT_FICA_FUTA_EXEMPT + \ + categories.ALW_FICA_FUTA_EXEMPT + + wage += categories.DED_FICA_EXEMPT + \ + categories.DED_FIT_FICA_EXEMPT + \ + categories.DED_FIT_FICA_FUTA_EXEMPT + \ + categories.DED_FICA_FUTA_EXEMPT + + return wage + + +def fica_wage_ytd(payslip, categories): + """ + Returns Year to Date FICA eligible wages + WAGE = GROSS - ALW_FICA_EXEMPT + DED_FICA_EXEMPT + :return: wage + """ + year = payslip.dict.get_year() + ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage -= payslip.sum_category('ALW_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage += payslip.sum_category('DED_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage += payslip.contract_id.external_wages + return ytd_wage + + +def ee_us_941_fica_ss(payslip, categories, worked_days, inputs): + """ + Returns FICA Social Security eligible wage and rate. + :return: result, result_rate (wage, percent) + """ + exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt') + if exempt: + return 0.0, 0.0 + + # Determine Rate. + result_rate = -payslip.rule_parameter('fed_941_fica_ss_rate') + + # Determine Wage + wage = fica_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + ytd_wage = fica_wage_ytd(payslip, categories) + wage_base = payslip.rule_parameter('fed_941_fica_ss_wage_base') + remaining = wage_base - ytd_wage + + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + return result, result_rate + + +er_us_941_fica_ss = ee_us_941_fica_ss + + +def ee_us_941_fica_m(payslip, categories, worked_days, inputs): + """ + Returns FICA Medicare eligible wage and rate. + :return: result, result_rate (wage, percent) + """ + exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt') + if exempt: + return 0.0, 0.0 + + # Determine Rate. + result_rate = -payslip.rule_parameter('fed_941_fica_m_rate') + + # Determine Wage + wage = fica_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + ytd_wage = fica_wage_ytd(payslip, categories) + wage_base = float(payslip.rule_parameter('fed_941_fica_m_wage_base')) # inf + remaining = wage_base - ytd_wage + + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + return result, result_rate + + +er_us_941_fica_m = ee_us_941_fica_m + + +def ee_us_941_fica_m_add(payslip, categories, worked_days, inputs): + """ + Returns FICA Medicare Additional eligible wage and rate. + :return: result, result_rate (wage, percent) + """ + exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt') + if exempt: + return 0.0, 0.0 + + # Determine Rate. + result_rate = -payslip.rule_parameter('fed_941_fica_m_add_rate') + + # Determine Wage + wage = fica_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + ytd_wage = fica_wage_ytd(payslip, categories) + wage_start = payslip.rule_parameter('fed_941_fica_m_add_wage_start') + existing_wage = ytd_wage - wage_start + + if existing_wage >= 0.0: + result = wage + elif wage + existing_wage > 0.0: + result = wage + existing_wage + else: + result = 0.0 + + return result, result_rate + + +def fit_wage(payslip, categories): + """ + Returns FIT eligible wage for current Payslip (no wage_base, just by categories) + WAGE = GROSS - ALW_FIT_EXEMPT + DED_FIT_EXEMPT + :return: wage + """ + wage = categories.GROSS + + wage -= categories.ALW_FIT_EXEMPT + \ + categories.ALW_FIT_FICA_EXEMPT + \ + categories.ALW_FIT_FICA_FUTA_EXEMPT + \ + categories.ALW_FIT_FUTA_EXEMPT + + wage += categories.DED_FIT_EXEMPT + \ + categories.DED_FIT_FICA_EXEMPT + \ + categories.DED_FIT_FICA_FUTA_EXEMPT + \ + categories.DED_FIT_FUTA_EXEMPT + + return wage + + +def fit_wage_ytd(payslip, categories): + """ + Returns Year to Date FIT eligible wages + WAGE = GROSS - ALW_FIT_EXEMPT + DED_FIT_EXEMPT + :return: wage + """ + year = payslip.dict.get_year() + ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage -= payslip.sum_category('ALW_FIT_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('ALW_FIT_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage += payslip.sum_category('DED_FIT_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \ + payslip.sum_category('DED_FIT_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + + ytd_wage += payslip.contract_id.external_wages + return ytd_wage + + +# Federal Income Tax +def ee_us_941_fit(payslip, categories, worked_days, inputs): + """ + Returns Wage and rate that is computed given the amount to withhold. + :return: result, result_rate (wage, percent) + """ + filing_status = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status') + if not filing_status: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + wage = fit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + #_logger.warn('initial gross wage: ' + str(wage)) + year = payslip.dict.get_year() + if year >= 2020: + # Large changes in Federal Income Tax in 2020 and the W4 + # We will assume that your W4 is the 2020 version + # Steps are from IRS Publication 15-T + # + # Step 1 + working_wage = wage + is_nra = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_is_nonresident_alien') + if is_nra: + nra_table = payslip.rule_parameter('fed_941_fit_nra_additional') + working_wage += nra_table.get(schedule_pay, 0.0) + #_logger.warn(' is_nrm after wage: ' + str(working_wage)) + + pay_periods = payslip.dict.get_pay_periods_in_year() + wage_annual = pay_periods * working_wage + #_logger.warn('annual wage: ' + str(wage_annual)) + wage_annual += payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_other_income') + #_logger.warn(' after other income: ' + str(wage_annual)) + + deductions = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_deductions') + #_logger.warn('deductions from W4: ' + str(deductions)) + + higher_rate_type = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_multiple_jobs_higher') + if not higher_rate_type: + deductions += 12900.0 if filing_status == 'married' else 8600.0 + #_logger.warn(' deductions after standard deduction: ' + str(deductions)) + + adjusted_wage_annual = wage_annual - deductions + if adjusted_wage_annual < 0.0: + adjusted_wage_annual = 0.0 + #_logger.warn('adusted annual wage: ' + str(adjusted_wage_annual)) + + # Step 2 + if filing_status == 'single': + tax_tables = payslip.rule_parameter('fed_941_fit_table_single') + elif filing_status == 'married': + tax_tables = payslip.rule_parameter('fed_941_fit_table_married') + else: + # married_as_single for historic reasons + tax_tables = payslip.rule_parameter('fed_941_fit_table_hh') + + if higher_rate_type: + tax_table = tax_tables['higher'] + else: + tax_table = tax_tables['standard'] + + selected_row = None + for row in tax_table: + if row[0] <= adjusted_wage_annual: + selected_row = row + else: + # First row where wage is higher than adjusted_wage_annual + break + + wage_threshold, base_withholding_amount, marginal_rate = selected_row + #_logger.warn(' selected row: ' + str(selected_row)) + working_wage = adjusted_wage_annual - wage_threshold + tentative_withholding_amount = (working_wage * marginal_rate) + base_withholding_amount + tentative_withholding_amount = tentative_withholding_amount / pay_periods + #_logger.warn('tenative withholding amount: ' + str(tentative_withholding_amount)) + + # Step 3 + dependent_credit = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_dependent_credit') + dependent_credit = dependent_credit / pay_periods + #_logger.warn('dependent credit (per period): ' + str(dependent_credit)) + tentative_withholding_amount -= dependent_credit + if tentative_withholding_amount < 0.0: + tentative_withholding_amount = 0.0 + + # Step 4 + withholding_amount = tentative_withholding_amount + payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_additional_withholding') + #_logger.warn('final withholding amount: ' + str(withholding_amount)) + # Ideally we would set the 'taxable wage' as the result and compute the percentage tax. + # This is off by 1 penny across our tests, but I feel like it is worth it for the added reporting. + # - Jared Kipe 2019 during Odoo 13.0 rewrite. + # + # return -withholding_amount, 100.0 + return wage, -(withholding_amount / wage * 100.0) + else: + working_wage = wage + is_nra = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_is_nonresident_alien') + if is_nra: + nra_table = payslip.rule_parameter('fed_941_fit_nra_additional') + working_wage += nra_table[schedule_pay] + + allowance_table = payslip.rule_parameter('fed_941_fit_allowance') + allowances = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_allowances') + working_wage -= allowance_table[schedule_pay] * allowances + tax = 0.0 + last_limit = 0.0 + if filing_status == 'married': + tax_table = payslip.rule_parameter('fed_941_fit_table_married') + else: + tax_table = payslip.rule_parameter('fed_941_fit_table_single') + for row in tax_table[schedule_pay]: + limit, base, percent = row + limit = float(limit) # 'inf' + if working_wage <= limit: + tax = base + ((working_wage - last_limit) * (percent / 100.0)) + break + last_limit = limit + + tax += payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_additional_withholding') + # Ideally we would set the 'taxable wage' as the result and compute the percentage tax. + # This is off by 1 penny across our tests, but I feel like it is worth it for the added reporting. + # - Jared Kipe 2019 during Odoo 13.0 rewrite. + # + # return -tax, 100.0 + return wage, -(tax / wage * 100.0) diff --git a/l10n_us_hr_payroll/models/hr_contract.py b/l10n_us_hr_payroll/models/hr_contract.py new file mode 100644 index 00000000..46bc42b7 --- /dev/null +++ b/l10n_us_hr_payroll/models/hr_contract.py @@ -0,0 +1,28 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models +from .us_payroll_config import FUTA_TYPE_NORMAL, \ + FUTA_TYPE_BASIC, \ + FUTA_TYPE_EXEMPT + + +class HrPayrollStructure(models.Model): + _inherit = 'hr.payroll.structure' + schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')]) + + +class USHRContract(models.Model): + _inherit = 'hr.contract' + + FUTA_TYPE_NORMAL = FUTA_TYPE_NORMAL + FUTA_TYPE_BASIC = FUTA_TYPE_BASIC + FUTA_TYPE_EXEMPT = FUTA_TYPE_EXEMPT + + us_payroll_config_id = fields.Many2one('hr.contract.us_payroll_config', 'Payroll Forms') + external_wages = fields.Float(string='External Existing Wages') + + # Simplified fields for easier rules, state code will exempt based on contract's futa_type + futa_type = fields.Selection(related='us_payroll_config_id.fed_940_type') + + def us_payroll_config_value(self, name): + return self.us_payroll_config_id[name] diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py new file mode 100644 index 00000000..888a9ebc --- /dev/null +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -0,0 +1,102 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + +from .federal.fed_940 import er_us_940_futa +from .federal.fed_941 import ee_us_941_fica_ss, \ + ee_us_941_fica_m, \ + ee_us_941_fica_m_add,\ + er_us_941_fica_ss, \ + er_us_941_fica_m, \ + ee_us_941_fit +from .state.general import general_state_unemployment, \ + general_state_income_withholding, \ + is_us_state +from .state.al_alabama import al_alabama_state_income_withholding +from .state.ar_arkansas import ar_arkansas_state_income_withholding +from .state.az_arizona import az_arizona_state_income_withholding +from .state.ca_california import ca_california_state_income_withholding +from .state.co_colorado import co_colorado_state_income_withholding +from .state.ct_connecticut import ct_connecticut_state_income_withholding +from .state.de_delaware import de_delaware_state_income_withholding +from .state.ga_georgia import ga_georgia_state_income_withholding +from .state.hi_hawaii import hi_hawaii_state_income_withholding +from .state.ia_iowa import ia_iowa_state_income_withholding +from .state.id_idaho import id_idaho_state_income_withholding +from .state.il_illinois import il_illinois_state_income_withholding +from .state.mi_michigan import mi_michigan_state_income_withholding +from .state.mn_minnesota import mn_minnesota_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.nm_new_mexico import nm_new_mexico_state_income_withholding +from .state.oh_ohio import oh_ohio_state_income_withholding +from .state.va_virginia import va_virginia_state_income_withholding +from .state.wa_washington import wa_washington_fml_er, \ + wa_washington_fml_ee + + +class HRPayslip(models.Model): + _inherit = 'hr.payslip' + + # From IRS Publication 15-T or logically (annually, bi-monthly) + PAY_PERIODS_IN_YEAR = { + 'annually': 1, + 'semi-annually': 2, + 'quarterly': 4, + 'bi-monthly': 6, + 'monthly': 12, + 'semi-monthly': 24, + 'bi-weekly': 26, + 'weekly': 52, + 'daily': 260, + } + + def _get_base_local_dict(self): + res = super()._get_base_local_dict() + res.update({ + 'er_us_940_futa': er_us_940_futa, + 'ee_us_941_fica_ss': ee_us_941_fica_ss, + 'ee_us_941_fica_m': ee_us_941_fica_m, + 'ee_us_941_fica_m_add': ee_us_941_fica_m_add, + 'er_us_941_fica_ss': er_us_941_fica_ss, + 'er_us_941_fica_m': er_us_941_fica_m, + 'ee_us_941_fit': ee_us_941_fit, + 'general_state_unemployment': general_state_unemployment, + 'general_state_income_withholding': general_state_income_withholding, + 'is_us_state': is_us_state, + 'al_alabama_state_income_withholding': al_alabama_state_income_withholding, + 'ar_arkansas_state_income_withholding': ar_arkansas_state_income_withholding, + 'az_arizona_state_income_withholding': az_arizona_state_income_withholding, + 'ca_california_state_income_withholding': ca_california_state_income_withholding, + 'co_colorado_state_income_withholding': co_colorado_state_income_withholding, + 'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding, + 'de_delaware_state_income_withholding': de_delaware_state_income_withholding, + 'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding, + 'hi_hawaii_state_income_withholding': hi_hawaii_state_income_withholding, + 'ia_iowa_state_income_withholding': ia_iowa_state_income_withholding, + 'id_idaho_state_income_withholding': id_idaho_state_income_withholding, + 'il_illinois_state_income_withholding': il_illinois_state_income_withholding, + 'mi_michigan_state_income_withholding': mi_michigan_state_income_withholding, + 'mn_minnesota_state_income_withholding': mn_minnesota_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, + 'nc_northcarolina_state_income_withholding': nc_northcarolina_state_income_withholding, + 'nj_newjersey_state_income_withholding': nj_newjersey_state_income_withholding, + 'nm_new_mexico_state_income_withholding': nm_new_mexico_state_income_withholding, + 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, + 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, + 'wa_washington_fml_er': wa_washington_fml_er, + 'wa_washington_fml_ee': wa_washington_fml_ee, + }) + return res + + def get_year(self): + # Helper method to get the year (normalized between Odoo Versions) + return self.date_to.year + + def get_pay_periods_in_year(self): + return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0) diff --git a/l10n_us_hr_payroll/models/res_config_settings.py b/l10n_us_hr_payroll/models/res_config_settings.py new file mode 100644 index 00000000..05af9430 --- /dev/null +++ b/l10n_us_hr_payroll/models/res_config_settings.py @@ -0,0 +1,24 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + payslip_sum_type = fields.Selection([ + ('date_from', 'Date From'), + ('date_to', 'Date To'), + ('date', 'Accounting Date'), + ], 'Payslip Sum Behavior', help="Behavior for what payslips are considered " + "during rule execution. Stock Odoo behavior " + "would not consider a payslip starting on 2019-12-30 " + "ending on 2020-01-07 when summing a 2020 payslip category.\n\n" + "Accounting Date requires Payroll Accounting and will " + "fall back to date_to as the 'closest behavior'.", + config_parameter='hr_payroll.payslip.sum_behavior') + + def set_values(self): + super(ResConfigSettings, self).set_values() + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', + self.payslip_sum_type or 'date_from') diff --git a/l10n_us_hr_payroll/models/state/__init__.py b/l10n_us_hr_payroll/models/state/__init__.py new file mode 100644 index 00000000..0358305d --- /dev/null +++ b/l10n_us_hr_payroll/models/state/__init__.py @@ -0,0 +1 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. diff --git a/l10n_us_hr_payroll/models/state/al_alabama.py b/l10n_us_hr_payroll/models/state/al_alabama.py new file mode 100644 index 00000000..15740c91 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/al_alabama.py @@ -0,0 +1,77 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def al_alabama_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'AL' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + personal_exempt = payslip.contract_id.us_payroll_config_value('state_income_tax_exempt') + if personal_exempt: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + tax_table = payslip.rule_parameter('us_al_sit_tax_rate') + exemptions = payslip.contract_id.us_payroll_config_value('al_a4_sit_exemptions') + dependent_rate = payslip.rule_parameter('us_al_sit_dependent_rate') + standard_deduction = payslip.rule_parameter('us_al_sit_standard_deduction_rate').get(exemptions, 0.0) + personal_exemption = payslip.rule_parameter('us_al_sit_personal_exemption_rate').get(exemptions, 0.0) + dependent = payslip.contract_id.us_payroll_config_value('al_a4_sit_dependents') + fed_withholding = categories.EE_US_941_FIT + + annual_wage = wage * pay_periods + standard_deduction_amt = 0.0 + personal_exemption_amt = 0.0 + dependent_amt = 0.0 + withholding = 0.0 + + if standard_deduction: + row = standard_deduction + last_amt = 0.0 + for data in row: + if annual_wage < float(data[0]): + if len(data) > 3: + increment_count = (- (wage - last_amt) // data[3]) + standard_deduction_amt = data[1] - (increment_count * data[2]) + else: + standard_deduction_amt = data[1] + else: + last_amt = data[0] + after_deduction = annual_wage - standard_deduction_amt + after_fed_withholding = (fed_withholding * pay_periods) + after_deduction + if not personal_exempt: + personal_exemption_amt = personal_exemption + after_personal_exemption = after_fed_withholding - personal_exemption_amt + for row in dependent_rate: + if annual_wage < float(row[1]): + dependent_amt = row[0] * dependent + break + + taxable_amount = after_personal_exemption - dependent_amt + last = 0.0 + tax_table = tax_table['M'] if exemptions == 'M' else tax_table['0'] + for row in tax_table: + if taxable_amount < float(row[0]): + withholding = withholding + ((taxable_amount - last) * (row[1] / 100)) + break + withholding = withholding + ((row[0] - last) * (row[1] / 100)) + last = row[0] + + if withholding < 0.0: + withholding = 0.0 + withholding /= pay_periods + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/ar_arkansas.py b/l10n_us_hr_payroll/models/state/ar_arkansas.py new file mode 100644 index 00000000..e22c41b3 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/ar_arkansas.py @@ -0,0 +1,47 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def ar_arkansas_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'AR' + 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 = sit_wage(payslip, categories) + if wage == 0.0: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + sit_tax_rate = payslip.rule_parameter('us_ar_sit_tax_rate') + standard_deduction = payslip.rule_parameter('us_ar_sit_standard_deduction_rate') + allowances = payslip.contract_id.us_payroll_config_value('ar_ar4ec_sit_allowances') + + allowances_amt = allowances * 26.0 + taxable_income = (wage * pay_periods) - standard_deduction + if taxable_income < 87001.0: + taxable_income = (taxable_income // 50) * 50.0 + 50.0 + + withholding = 0.0 + for row in sit_tax_rate: + cap, rate, adjust_amount = row + cap = float(cap) + if cap > taxable_income: + withholding = (((rate / 100.0) * taxable_income) - adjust_amount) - allowances_amt + break + + # In case withholding or taxable_income is negative + withholding = max(withholding, 0.0) + withholding = round(withholding / pay_periods) + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/az_arizona.py b/l10n_us_hr_payroll/models/state/az_arizona.py new file mode 100644 index 00000000..90c44898 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/az_arizona.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, sit_wage + + +def az_arizona_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 = 'AZ' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + withholding_percent = payslip.contract_id.us_payroll_config_value('az_a4_sit_withholding_percentage') + + if withholding_percent <= 0.0: + return 0.0, 0.0 + + wh_percentage = withholding_percent / 100.0 + withholding = wage * wh_percentage + + if withholding < 0.0: + withholding = 0.0 + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/ca_california.py b/l10n_us_hr_payroll/models/state/ca_california.py new file mode 100644 index 00000000..62382402 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/ca_california.py @@ -0,0 +1,98 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + +MAX_ALLOWANCES = 10 + + +def ca_california_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + + state_code = 'CA' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('ca_de4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + sit_allowances = payslip.contract_id.us_payroll_config_value('ca_de4_sit_allowances') + additional_allowances = payslip.contract_id.us_payroll_config_value('ca_de4_sit_additional_allowances') + low_income_exemption = payslip.rule_parameter('us_ca_sit_income_exemption_rate')[schedule_pay] + estimated_deduction = payslip.rule_parameter('us_ca_sit_estimated_deduction_rate')[schedule_pay] + tax_table = payslip.rule_parameter('us_ca_sit_tax_rate')[filing_status].get(schedule_pay) + standard_deduction = payslip.rule_parameter('us_ca_sit_standard_deduction_rate')[schedule_pay] + exemption_allowances = payslip.rule_parameter('us_ca_sit_exemption_allowance_rate')[schedule_pay] + + low_income = False + if filing_status == 'head_household': + _, _, _, income = low_income_exemption + if wage <= income: + low_income = True + elif filing_status == 'married': + if sit_allowances >= 2: + _, _, income, _ = low_income_exemption + if wage <= income: + low_income = True + else: + _, income, _, _ = low_income_exemption + if wage <= income: + low_income = True + else: + income, _, _, _ = low_income_exemption + if wage <= income: + low_income = True + + withholding = 0.0 + taxable_wage = wage + if not low_income: + allowance_index = max(additional_allowances - 1, 0) + if additional_allowances > MAX_ALLOWANCES: + deduction = (estimated_deduction[0] * additional_allowances) + taxable_wage -= deduction + elif additional_allowances > 0: + deduction = estimated_deduction[allowance_index] + taxable_wage -= deduction + + if filing_status == 'head_household': + _, _, _, deduction = standard_deduction + taxable_wage -= deduction + elif filing_status == 'married': + if sit_allowances >= 2: + _, _, deduction, _ = standard_deduction + taxable_wage -= deduction + else: + _, deduction, _, _ = standard_deduction + taxable_wage -= deduction + else: + deduction, _, _, _ = standard_deduction + taxable_wage -= deduction + + over = 0.0 + for row in tax_table: + if taxable_wage <= row[0]: + withholding = ((taxable_wage - over) * row[1]) + row[2] + break + over = row[0] + + allowance_index = sit_allowances - 1 + if sit_allowances > MAX_ALLOWANCES: + deduction = exemption_allowances[0] * sit_allowances + withholding -= deduction + elif sit_allowances > 0: + deduction = exemption_allowances[allowance_index] + withholding -= deduction + + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/co_colorado.py b/l10n_us_hr_payroll/models/state/co_colorado.py new file mode 100644 index 00000000..f0c7b436 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/co_colorado.py @@ -0,0 +1,45 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def co_colorado_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'CO' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status') + if not filing_status: + return 0.0, 0.0 + + state_exempt = payslip.contract_id.us_payroll_config_value('state_income_tax_exempt') + if state_exempt: + return 0.0, 0.0 + + 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_co_sit_exemption_rate') + tax_rate = payslip.rule_parameter('us_co_sit_tax_rate') + + taxable_income = wage * pay_periods + if filing_status == 'married': + taxable_income -= exemption_rate * 2 + else: + taxable_income -= exemption_rate + + withholding = taxable_income * (tax_rate / 100) + + withholding = max(withholding, 0.0) + withholding = withholding / pay_periods + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/ct_connecticut.py b/l10n_us_hr_payroll/models/state/ct_connecticut.py new file mode 100644 index 00000000..344dc9c8 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/ct_connecticut.py @@ -0,0 +1,76 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def ct_connecticut_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'CT' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + withholding_code = payslip.contract_id.us_payroll_config_value('ct_w4na_sit_code') + exemption_table = payslip.rule_parameter('us_ct_sit_personal_exemption_rate').get(withholding_code, [('inf', 0.0)]) + initial_tax_tbl = payslip.rule_parameter('us_ct_sit_initial_tax_rate').get(withholding_code, [('inf', 0.0, 0.0)]) + tax_table = payslip.rule_parameter('us_ct_sit_tax_rate').get(withholding_code, [('inf', 0.0)]) + recapture_table = payslip.rule_parameter('us_ct_sit_recapture_rate').get(withholding_code, [('inf', 0.0)]) + decimal_table = payslip.rule_parameter('us_ct_sit_decimal_rate').get(withholding_code, [('inf', 0.0)]) + + annual_wages = wage * pay_periods + personal_exemption = 0.0 + for bracket in exemption_table: + if annual_wages <= float(bracket[0]): + personal_exemption = bracket[1] + break + + withholding = 0.0 + taxable_income = annual_wages - personal_exemption + if taxable_income < 0.0: + taxable_income = 0.0 + + if taxable_income: + initial_tax = 0.0 + last = 0.0 + for bracket in initial_tax_tbl: + if taxable_income <= float(bracket[0]): + initial_tax = bracket[1] + ((bracket[2] / 100.0) * (taxable_income - last)) + break + last = bracket[0] + + tax_add_back = 0.0 + for bracket in tax_table: + if annual_wages <= float(bracket[0]): + tax_add_back = bracket[1] + break + + recapture_amount = 0.0 + for bracket in recapture_table: + if annual_wages <= float(bracket[0]): + recapture_amount = bracket[1] + break + + withholding = initial_tax + tax_add_back + recapture_amount + decimal_amount = 1.0 + for bracket in decimal_table: + if annual_wages <= float(bracket[0]): + decimal_amount= bracket[1] + break + + withholding = withholding * (1.00 - decimal_amount) + if withholding < 0.0: + withholding = 0.0 + withholding /= pay_periods + + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/de_delaware.py b/l10n_us_hr_payroll/models/state/de_delaware.py new file mode 100644 index 00000000..b2588e5d --- /dev/null +++ b/l10n_us_hr_payroll/models/state/de_delaware.py @@ -0,0 +1,49 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def de_delaware_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'DE' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('de_w4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + tax_table = payslip.rule_parameter('us_de_sit_tax_rate') + personal_exemption = payslip.rule_parameter('us_de_sit_personal_exemption_rate') + allowances = payslip.contract_id.us_payroll_config_value('de_w4_sit_dependent') + standard_deduction = payslip.rule_parameter('us_de_sit_standard_deduction_rate') + + taxable_income = wage * pay_periods + if filing_status == 'single': + taxable_income -= standard_deduction + else: + taxable_income -= standard_deduction * 2 + + withholding = 0.0 + last = 0.0 + for row in tax_table: + if taxable_income <= float(row[0]): + withholding = (row[1] + ((row[2] / 100.0) * (taxable_income - last)) - (allowances * personal_exemption)) + break + last = row[0] + + withholding = max(withholding, 0.0) + withholding = withholding / pay_periods + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/ga_georgia.py b/l10n_us_hr_payroll/models/state/ga_georgia.py new file mode 100644 index 00000000..66503e35 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/ga_georgia.py @@ -0,0 +1,51 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'GA' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + ga_filing_status = payslip.contract_id.us_payroll_config_value('ga_g4_sit_filing_status') + if not ga_filing_status or ga_filing_status == 'exempt': + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + dependent_allowances = payslip.contract_id.us_payroll_config_value('ga_g4_sit_dependent_allowances') + additional_allowances = payslip.contract_id.us_payroll_config_value('ga_g4_sit_additional_allowances') + dependent_allowance_rate = payslip.rule_parameter('us_ga_sit_dependent_allowance_rate').get(schedule_pay) + personal_allowance = payslip.rule_parameter('us_ga_sit_personal_allowance').get(ga_filing_status, {}).get(schedule_pay) + deduction = payslip.rule_parameter('us_ga_sit_deduction').get(ga_filing_status, {}).get(schedule_pay) + withholding_rate = payslip.rule_parameter('us_ga_sit_rate').get(ga_filing_status, {}).get(schedule_pay) + if not all((dependent_allowance_rate, personal_allowance, deduction, withholding_rate)): + return 0.0, 0.0 + + after_standard_deduction = wage - deduction + allowances = dependent_allowances + additional_allowances + working_wages = after_standard_deduction - (personal_allowance + (allowances * dependent_allowance_rate)) + + withholding = 0.0 + if working_wages > 0.0: + prior_row_base = 0.0 + for row in withholding_rate: + wage_base, base, rate = row + wage_base = float(wage_base) + if working_wages < wage_base: + withholding = base + ((working_wages - prior_row_base) * rate / 100.0) + break + prior_row_base = wage_base + + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py new file mode 100644 index 00000000..44d2aeb5 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/general.py @@ -0,0 +1,132 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. +from odoo.exceptions import UserError +from ..federal.fed_940 import futa_wage, futa_wage_ytd +from ..federal.fed_941 import fit_wage, fit_wage_ytd + +# import logging +# _logger = logging.getLogger(__name__) + +suta_wage = futa_wage +suta_wage_ytd = futa_wage_ytd +sit_wage = fit_wage +sit_wage_ytd = fit_wage_ytd + + +def _state_applies(payslip, state_code): + return state_code == payslip.contract_id.us_payroll_config_value('state_code') + + +# Export for eval context +is_us_state = _state_applies + + +def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, rate=None): + """ + Function parameters: + wage_base, wage_start, rate can either be strings (rule_parameters) or floats + :return: result, result_rate(wage, percent) + """ + + # Resolve parameters. On exception, return (probably missing a year, would rather not have exception) + if wage_base and isinstance(wage_base, str): + try: + wage_base = payslip.rule_parameter(wage_base) + except (KeyError, UserError): + return 0.0, 0.0 + + if wage_start and isinstance(wage_start, str): + try: + wage_start = payslip.rule_parameter(wage_start) + except (KeyError, UserError): + return 0.0, 0.0 + + if rate and isinstance(rate, str): + try: + rate = payslip.rule_parameter(rate) + except (KeyError, UserError): + return 0.0, 0.0 + + if not rate: + return 0.0, 0.0 + else: + # Rate assumed positive percentage! + rate = -rate + + if wage_base: + remaining = wage_base - ytd_wage + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + # _logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) + return result, rate + if wage_start: + if ytd_wage >= wage_start: + # _logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate + if ytd_wage + wage <= wage_start: + # _logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) + return 0.0, 0.0 + # _logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) + return (wage - (wage_start - ytd_wage)), rate + + # If the wage doesn't have a start or a base + # _logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate + + +def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): + """ + Returns SUTA eligible wage and rate. + WAGE = GROSS + DED_FUTA_EXEMPT + + The contract's `futa_type` determines if SUTA should be collected. + + :return: result, result_rate(wage, percent) + """ + + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Eligible. + if payslip.contract_id.futa_type in (payslip.contract_id.FUTA_TYPE_EXEMPT, payslip.contract_id.FUTA_TYPE_BASIC): + return 0.0, 0.0 + + # Determine Wage + wage = suta_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + ytd_wage = suta_wage_ytd(payslip, categories) + + wage = categories.GROSS + categories.DED_FUTA_EXEMPT + return _general_rate(payslip, wage, ytd_wage, wage_base=wage_base, wage_start=wage_start, rate=rate) + + +def general_state_income_withholding(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): + """ + Returns SIT eligible wage and rate. + WAGE = GROSS + DED_FIT_EXEMPT + + :return: result, result_rate (wage, percent) + """ + 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 + ytd_wage = sit_wage_ytd(payslip, categories) + + wage = sit_wage(payslip, categories) + result, result_rate = _general_rate(payslip, wage, ytd_wage, wage_base=wage_base, wage_start=wage_start, rate=rate) + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + if additional: + tax = result * (result_rate / 100.0) + tax -= additional # assumed result_rate is negative and that the 'additional' should increase it. + return result, ((tax / result) * 100.0) + return result, result_rate diff --git a/l10n_us_hr_payroll/models/state/hi_hawaii.py b/l10n_us_hr_payroll/models/state/hi_hawaii.py new file mode 100644 index 00000000..42c51e3e --- /dev/null +++ b/l10n_us_hr_payroll/models/state/hi_hawaii.py @@ -0,0 +1,43 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def hi_hawaii_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'HI' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('hi_hw4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + allowances = payslip.contract_id.us_payroll_config_value('hi_hw4_sit_allowances') + tax_table = payslip.rule_parameter('us_hi_sit_tax_rate')[filing_status] + personal_exemption = payslip.rule_parameter('us_hi_sit_personal_exemption_rate') + + taxable_income = (wage * pay_periods) - (personal_exemption * allowances) + withholding = 0.0 + last = 0.0 + for row in tax_table: + if taxable_income <= float(row[0]): + withholding = row[1] + ((row[2] / 100.0) * (taxable_income - last)) + break + last = row[0] + + withholding = max(withholding, 0.0) + withholding = withholding / pay_periods + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/ia_iowa.py b/l10n_us_hr_payroll/models/state/ia_iowa.py new file mode 100644 index 00000000..d12adc64 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/ia_iowa.py @@ -0,0 +1,44 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def ia_iowa_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'IA' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + fed_withholding = categories.EE_US_941_FIT + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + allowances = payslip.contract_id.us_payroll_config_value('ia_w4_sit_allowances') + standard_deduction = payslip.rule_parameter('us_ia_sit_standard_deduction_rate')[schedule_pay] + tax_table = payslip.rule_parameter('us_ia_sit_tax_rate')[schedule_pay] + deduction_per_allowance = payslip.rule_parameter('us_ia_sit_deduction_allowance_rate')[schedule_pay] + + t1 = wage + fed_withholding + t2 = t1 - standard_deduction[0] if allowances < 2 else standard_deduction[1] + t3 = 0.0 + last = 0.0 + for row in tax_table: + cap, rate, flat_fee = row + if float(cap) > float(t2): + taxed_amount = t2 - last + t3 = flat_fee + (rate * taxed_amount) + break + last = cap + withholding = t3 - (deduction_per_allowance * allowances) + + withholding = max(withholding, 0.0) + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/id_idaho.py b/l10n_us_hr_payroll/models/state/id_idaho.py new file mode 100644 index 00000000..5bf503da --- /dev/null +++ b/l10n_us_hr_payroll/models/state/id_idaho.py @@ -0,0 +1,41 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def id_idaho_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'ID' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('id_w4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + allowances = payslip.contract_id.us_payroll_config_value('id_w4_sit_allowances') + ictcat_table = payslip.rule_parameter('us_id_sit_ictcat_rate')[schedule_pay] + tax_table = payslip.rule_parameter('us_id_sit_tax_rate')[filing_status].get(schedule_pay) + + taxable_income = wage - (ictcat_table * allowances) + withholding = 0.0 + last = 0.0 + for row in tax_table: + if taxable_income <= float(row[0]): + withholding = row[1] + ((row[2] / 100.0) * (taxable_income - last)) + break + last = row[0] + + withholding = max(withholding, 0.0) + withholding = round(withholding) + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/il_illinois.py b/l10n_us_hr_payroll/models/state/il_illinois.py new file mode 100644 index 00000000..6c8919c4 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/il_illinois.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, sit_wage + + +def il_illinois_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 = 'IL' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + basic_allowances_rate = payslip.rule_parameter('us_il_sit_basic_allowances_rate') + additional_allowances_rate = payslip.rule_parameter('us_il_sit_additional_allowances_rate') + basic_allowances = payslip.contract_id.us_payroll_config_value('il_w4_sit_basic_allowances') + additional_allowances = payslip.contract_id.us_payroll_config_value('il_w4_sit_additional_allowances') + + rate = 4.95 / 100.0 + withholding = rate * (wage - (((basic_allowances * basic_allowances_rate) + (additional_allowances * + additional_allowances_rate)) / pay_periods)) + if withholding < 0.0: + withholding = 0.0 + withholding += additional + return wage, -((withholding / wage) * 100.0) 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..f9656529 --- /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, sit_wage + + +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 = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + 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') + + 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/state/mn_minnesota.py b/l10n_us_hr_payroll/models/state/mn_minnesota.py new file mode 100644 index 00000000..c626bc3b --- /dev/null +++ b/l10n_us_hr_payroll/models/state/mn_minnesota.py @@ -0,0 +1,44 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def mn_minnesota_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 = 'MN' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('mn_w4mn_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + sit_tax_rate = payslip.rule_parameter('us_mn_sit_tax_rate')[filing_status] + allowances_rate = payslip.rule_parameter('us_mn_sit_allowances_rate') + allowances = payslip.contract_id.us_payroll_config_value('mn_w4mn_sit_allowances') + + taxable_income = (wage * pay_periods) - (allowances * allowances_rate) + withholding = 0.0 + for row in sit_tax_rate: + cap, subtract_amt, rate, flat_fee = row + cap = float(cap) + if cap > taxable_income: + withholding = ((rate / 100.00) * (taxable_income - subtract_amt)) + flat_fee + break + withholding = round(withholding / pay_periods) + if withholding < 0.0: + withholding = 0.0 + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/mo_missouri.py b/l10n_us_hr_payroll/models/state/mo_missouri.py new file mode 100644 index 00000000..47e56639 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/mo_missouri.py @@ -0,0 +1,53 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def mo_missouri_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 = 'MO' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('mo_mow4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + reduced_withholding = payslip.contract_id.us_payroll_config_value('mo_mow4_sit_withholding') + if reduced_withholding: + return wage, -((reduced_withholding / wage) * 100.0) + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + sit_table = payslip.rule_parameter('us_mo_sit_rate') + deduction = payslip.rule_parameter('us_mo_sit_deduction')[filing_status] + + gross_taxable_income = wage * pay_periods + gross_taxable_income -= deduction + + remaining_taxable_income = gross_taxable_income + withholding = 0.0 + for amt, rate in sit_table: + amt = float(amt) + rate = rate / 100.0 + if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0: + withholding += rate * amt + else: + withholding += rate * remaining_taxable_income + break + remaining_taxable_income = remaining_taxable_income - amt + + withholding /= pay_periods + withholding += additional + withholding = round(withholding) + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/ms_mississippi.py b/l10n_us_hr_payroll/models/state/ms_mississippi.py new file mode 100644 index 00000000..10f30ee2 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/ms_mississippi.py @@ -0,0 +1,46 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def ms_mississippi_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 = 'MS' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('ms_89_350_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + exemptions = payslip.contract_id.us_payroll_config_value('ms_89_350_sit_exemption_value') + standard_deduction = payslip.rule_parameter('us_ms_sit_deduction').get(filing_status) + withholding_rate = payslip.rule_parameter('us_ms_sit_rate') + + wage_annual = wage * pay_periods + taxable_income = wage_annual - (exemptions + standard_deduction) + if taxable_income <= 0.01: + return wage, 0.0 + + withholding = 0.0 + for row in withholding_rate: + wage_base, base, rate = row + if taxable_income >= wage_base: + withholding = base + ((taxable_income - wage_base) * rate) + break + withholding /= pay_periods + withholding = round(withholding) + withholding += round(additional) + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py new file mode 100644 index 00000000..6e33261a --- /dev/null +++ b/l10n_us_hr_payroll/models/state/mt_montana.py @@ -0,0 +1,45 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def mt_montana_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 = 'MT' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + if payslip.contract_id.us_payroll_config_value('mt_mw4_sit_exempt'): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + exemptions = payslip.contract_id.us_payroll_config_value('mt_mw4_sit_exemptions') + exemption_rate = payslip.rule_parameter('us_mt_sit_exemption_rate').get(schedule_pay) + withholding_rate = payslip.rule_parameter('us_mt_sit_rate').get(schedule_pay) + if not exemption_rate or not withholding_rate: + return 0.0, 0.0 + + adjusted_wage = wage - (exemption_rate * (exemptions or 0)) + withholding = 0.0 + if adjusted_wage > 0.0: + prior_wage_cap = 0.0 + for row in withholding_rate: + wage_cap, base, rate = row + wage_cap = float(wage_cap) # e.g. 'inf' + if adjusted_wage < wage_cap: + withholding = round(base + ((rate / 100.0) * (adjusted_wage - prior_wage_cap))) + break + prior_wage_cap = wage_cap + withholding += additional + return wage, -((withholding / wage) * 100.0) 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..056d1fe8 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/nc_northcarolina.py @@ -0,0 +1,38 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +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 = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + 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'] + + 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/state/nj_newjersey.py b/l10n_us_hr_payroll/models/state/nj_newjersey.py new file mode 100644 index 00000000..f0a805b9 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/nj_newjersey.py @@ -0,0 +1,52 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def nj_newjersey_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 = 'NJ' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('nj_njw4_sit_filing_status') + if not filing_status: + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + allowances = payslip.contract_id.us_payroll_config_value('nj_njw4_sit_allowances') + sit_rate_table_key = payslip.contract_id.us_payroll_config_value('nj_njw4_sit_rate_table') + if not sit_rate_table_key and filing_status in ('single', 'married_joint'): + sit_rate_table_key = 'A' + elif not sit_rate_table_key: + sit_rate_table_key = 'B' + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + sit_table = payslip.rule_parameter('us_nj_sit_rate')[sit_rate_table_key].get(schedule_pay) + allowance_value = payslip.rule_parameter('us_nj_sit_allowance_rate')[schedule_pay] + if not allowances: + return 0.0, 0.0 + + gross_taxable_income = wage - (allowance_value * allowances) + withholding = 0.0 + prior_wage_base = 0.0 + for row in sit_table: + wage_base, base_amt, rate = row + wage_base = float(wage_base) + rate = rate / 100.0 + if gross_taxable_income <= wage_base: + withholding = base_amt + ((gross_taxable_income - prior_wage_base) * rate) + break + prior_wage_base = wage_base + + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/nm_new_mexico.py b/l10n_us_hr_payroll/models/state/nm_new_mexico.py new file mode 100644 index 00000000..48bf1ae1 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/nm_new_mexico.py @@ -0,0 +1,40 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def nm_new_mexico_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + + :return: result, result_rate (wage, percent) + """ + state_code = 'NM' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Wage + wage = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + filing_status = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status') + if not filing_status: + return 0.0, 0.0 + + schedule_pay = payslip.contract_id.schedule_pay + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + tax_table = payslip.rule_parameter('us_nm_sit_tax_rate')[filing_status].get(schedule_pay) + + taxable_income = wage + withholding = 0.0 + last = 0.0 + for row in tax_table: + if taxable_income <= float(row[0]): + withholding = row[1] + ((row[2] / 100.0) * (taxable_income - last)) + break + last = row[0] + + withholding = max(withholding, 0.0) + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py new file mode 100644 index 00000000..5a7c3869 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/oh_ohio.py @@ -0,0 +1,47 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def oh_ohio_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 = 'OH' + 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 = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + exemptions = payslip.contract_id.us_payroll_config_value('oh_it4_sit_exemptions') + exemption_rate = payslip.rule_parameter('us_oh_sit_exemption_rate') + withholding_rate = payslip.rule_parameter('us_oh_sit_rate') + multiplier_rate = payslip.rule_parameter('us_oh_sit_multiplier') + + taxable_wage = (wage * pay_periods) - (exemption_rate * (exemptions or 0)) + withholding = 0.0 + if taxable_wage > 0.0: + prior_wage_cap = 0.0 + for row in withholding_rate: + wage_cap, base, rate = row + wage_cap = float(wage_cap) # e.g. 'inf' + if taxable_wage < wage_cap: + withholding = base + (rate * (taxable_wage - prior_wage_cap)) + break + prior_wage_cap = wage_cap + # Normalize to pay periods + withholding /= pay_periods + withholding *= multiplier_rate + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/va_virginia.py b/l10n_us_hr_payroll/models/state/va_virginia.py new file mode 100644 index 00000000..a09f80a0 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/va_virginia.py @@ -0,0 +1,44 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, sit_wage + + +def va_virginia_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 = 'VA' + 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 = sit_wage(payslip, categories) + if not wage: + return 0.0, 0.0 + + pay_periods = payslip.dict.get_pay_periods_in_year() + additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') + personal_exemptions = payslip.contract_id.us_payroll_config_value('va_va4_sit_exemptions') + other_exemptions = payslip.contract_id.us_payroll_config_value('va_va4_sit_other_exemptions') + personal_exemption_rate = payslip.rule_parameter('us_va_sit_exemption_rate') + other_exemption_rate = payslip.rule_parameter('us_va_sit_other_exemption_rate') + deduction = payslip.rule_parameter('us_va_sit_deduction') + withholding_rate = payslip.rule_parameter('us_va_sit_rate') + + taxable_wage = (wage * pay_periods) - (deduction + (personal_exemptions * personal_exemption_rate) + (other_exemptions * other_exemption_rate)) + withholding = 0.0 + if taxable_wage > 0.0: + for row in withholding_rate: + if taxable_wage > row[0]: + selected_row = row + wage_min, base, rate = selected_row + withholding = base + ((taxable_wage - wage_min) * rate / 100.0) + withholding /= pay_periods + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/state/wa_washington.py b/l10n_us_hr_payroll/models/state/wa_washington.py new file mode 100644 index 00000000..4294b5f5 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/wa_washington.py @@ -0,0 +1,27 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .general import _state_applies, _general_rate + + +def _wa_washington_fml(payslip, categories, worked_days, inputs, inner_rate=None): + if not inner_rate: + return 0.0, 0.0 + + if not _state_applies(payslip, 'WA'): + return 0.0, 0.0 + + wage = categories.GROSS + year = payslip.dict.get_year() + ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year + 1) + '-01-01') + ytd_wage += payslip.contract_id.external_wages + rate = payslip.rule_parameter('us_wa_fml_rate') + rate *= payslip.rule_parameter(inner_rate) / 100.0 + return _general_rate(payslip, wage, ytd_wage, wage_base='us_wa_fml_wage_base', rate=rate) + + +def wa_washington_fml_er(payslip, categories, worked_days, inputs): + return _wa_washington_fml(payslip, categories, worked_days, inputs, inner_rate='us_wa_fml_rate_er') + + +def wa_washington_fml_ee(payslip, categories, worked_days, inputs): + return _wa_washington_fml(payslip, categories, worked_days, inputs, inner_rate='us_wa_fml_rate_ee') diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py new file mode 100644 index 00000000..b1aa0699 --- /dev/null +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -0,0 +1,199 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + +FUTA_TYPE_EXEMPT = 'exempt' +FUTA_TYPE_BASIC = 'basic' +FUTA_TYPE_NORMAL = 'normal' + + +class HRContractUSPayrollConfig(models.Model): + _name = 'hr.contract.us_payroll_config' + _description = 'Contract US Payroll Forms' + + name = fields.Char(string="Description") + employee_id = fields.Many2one('hr.employee', string="Employee", required=True) + state_id = fields.Many2one('res.country.state', string="Applied State") + state_code = fields.Char(related='state_id.code') + state_income_tax_exempt = fields.Boolean(string='State Income Tax Exempt') + state_income_tax_additional_withholding = fields.Float(string='State Income Tax Additional Withholding') + workers_comp_ee_code = fields.Char(string='Workers\' Comp Code (Employee Withholding)', + help='Code for a Rule Parameter, used by some states or your own rules.') + workers_comp_er_code = fields.Char(string='Workers\' Comp Code (Employer Withholding)', + help='Code for a Rule Parameter, used by some states or your own rules.') + + fed_940_type = fields.Selection([ + (FUTA_TYPE_EXEMPT, 'Exempt (0%)'), + (FUTA_TYPE_NORMAL, 'Normal Net Rate (0.6%)'), + (FUTA_TYPE_BASIC, 'Basic Rate (6%)'), + ], string="Federal Unemployment Tax Type (FUTA)", default='normal') + + fed_941_fica_exempt = fields.Boolean(string='FICA Exempt', help="Exempt from Social Security and " + "Medicare e.g. F1 Student Visa") + + fed_941_fit_w4_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single or Married filing separately'), + ('married', 'Married filing jointly'), + ('married_as_single', 'Head of Household'), + ], string='Federal W4 Filing Status [1(c)]', default='single') + fed_941_fit_w4_allowances = fields.Integer(string='Federal W4 Allowances (before 2020)') + fed_941_fit_w4_is_nonresident_alien = fields.Boolean(string='Federal W4 Is Nonresident Alien') + fed_941_fit_w4_multiple_jobs_higher = fields.Boolean(string='Federal W4 Multiple Jobs Higher [2(c)]', + help='Form W4 (2020+) 2(c) Checkbox. ' + 'Uses Higher Withholding tables.') + fed_941_fit_w4_dependent_credit = fields.Float(string='Federal W4 Dependent Credit [3]', + help='Form W4 (2020+) Line 3') + fed_941_fit_w4_other_income = fields.Float(string='Federal W4 Other Income [4(a)]', + help='Form W4 (2020+) 4(a)') + fed_941_fit_w4_deductions = fields.Float(string='Federal W4 Deductions [4(b)]', + help='Form W4 (2020+) 4(b)') + fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]', + help='Form W4 (2020+) 4(c)') + + al_a4_sit_exemptions = fields.Selection([ + ('0', '0'), + ('S', 'S'), + ('MS', 'MS'), + ('M', 'M'), + ('H', 'H'), + ], string='Alabama A4 Withholding Exemptions', help='A4 1. 2. 3.') + al_a4_sit_dependents = fields.Integer(string='Alabama A4 Dependents', help='A4 4.') + + ar_ar4ec_sit_allowances = fields.Integer(string='Arkansas AR4EC allowances', help='AR4EC 3.') + + az_a4_sit_withholding_percentage = fields.Float( + string='Arizona A-4 Withholding Percentage', + help='A-4 1. (0.8 or 1.3 or 1.8 or 2.7 or 3.6 or 4.2 or 5.1 or 0 for exempt.') + + ca_de4_sit_allowances = fields.Integer(string='California W-4 Allowances', + help='CA W-4 3.') + ca_de4_sit_additional_allowances = fields.Integer(string='California W-4 Additional Allowances', + help='CA W-4 4(c).') + ca_de4_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single or Married filing separately'), + ('married', 'Married filing jointly'), + ('head_household', 'Head of Household') + ], string='California W-4 Filing Status', help='CA W-4 1(c).') + + ct_w4na_sit_code = fields.Selection([ + ('a', 'A'), + ('b', 'B'), + ('c', 'C'), + ('d', 'D'), + ('f', 'F'), + ], string='Connecticut CT-W4 Withholding Code', help='CT-W4 1.') + + de_w4_sit_filing_status = fields.Selection([ + ('single', 'Single or Married filing separately'), + ('married', 'Married filing jointly'), + ], string='Delaware W-4 Marital Status', help='DE W-4 3.') + de_w4_sit_dependent = fields.Integer(string='Delaware W-4 Dependents', help='DE W-4 4.') + + ga_g4_sit_filing_status = fields.Selection([ + ('exempt', 'Exempt'), + ('single', 'Single'), + ('married filing joint, both spouses working', 'Married Filing Joint, both spouses working'), + ('married filing joint, one spouse working', 'Married Filing Joint, one spouse working'), + ('married filing separate', 'Married Filing Separate'), + ('head of household', 'Head of Household'), + ], string='Georgia G-4 Filing Status', help='G-4 3.') + ga_g4_sit_dependent_allowances = fields.Integer(string='Georgia G-4 Dependent Allowances', + help='G-4 4.') + ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances', + help='G-4 5.') + + hi_hw4_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single'), + ('married', 'Married'), + ('head_of_household', 'Head of Household'), + ], string='Hawaii HW-4 Marital Status', help='HI HW-4 3.') + hi_hw4_sit_allowances = fields.Integer(string='Hawaii HW-4 Allowances', help='HI HW-4 4.') + + ia_w4_sit_allowances = fields.Integer(string='Iowa W-4 allowances', help='IA W-4 6.') + + id_w4_sit_filing_status = fields.Selection([ + ('single', 'Single'), + ('married', 'Married'), + ('head of household', 'Head of Household'), + ], string='Idaho ID W-4 Withholding Status', help='ID W-4 A.B.C.') + id_w4_sit_allowances = fields.Integer(string='Idaho ID W-4 Allowances', help='ID W-4 1.') + + il_w4_sit_basic_allowances = fields.Integer(string='Illinois IL-W-4 Number of Basic Allowances', help='IL-W-4 Step 1.') + il_w4_sit_additional_allowances = fields.Integer(string='Illinois IL-W-4 Number of Additional Allowances', help='IL-W-4 Step 2.') + + mi_w4_sit_exemptions = fields.Integer(string='Michigan MI W-4 Exemptions', help='MI-W4 6.') + + mn_w4mn_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single'), + ('married', 'Married'), + ], string='Minnesota W-4MN Marital Status', help='W-4MN') + mn_w4mn_sit_allowances = fields.Integer(string='Minnesota Allowances', help='W-4MN 1.') + + mo_mow4_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single or Married Spouse Works or Married Filing Separate'), + ('married', 'Married (Spouse does not work)'), + ('head_of_household', 'Head of Household'), + ], string='Missouri W-4 Filing Status', help='MO W-4 1.') + mo_mow4_sit_withholding = fields.Integer(string='Missouri MO W-4 Reduced Withholding', help='MO W-4 3.') + + ms_89_350_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single'), + ('married', 'Married (spouse NOT employed)'), + ('married_dual', 'Married (spouse IS employed)'), + ('head_of_household', 'Head of Household'), + ], string='Mississippi 89-350 Filing Status', help='89-350 1. 2. 3. 8.') + ms_89_350_sit_exemption_value = fields.Float(string='Mississippi 89-350 Exemption Total', + help='89-350 Box 6 (including filing status amounts)') + + mt_mw4_sit_exemptions = fields.Integer(string='Montana MW-4 Exemptions', + help='MW-4 Box G') + # Don't use the main state_income_tax_exempt because of special meaning and reporting + # Use additional withholding but name it on the form 'MW-4 Box H' + mt_mw4_sit_exempt = fields.Selection([ + ('', 'Not Exempt'), + ('tribe', 'Registered Tribe'), + ('reserve', 'Reserve or National Guard'), + ('north_dakota', 'North Dakota'), + ('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'), + ('married_separate', 'Married/Civil Union partner Separate'), + ('married_joint', 'Married/Civil Union Couple Joint'), + ('widower', 'Widower/Surviving Civil Union Partner'), + ('head_household', 'Head of Household') + ], string='New Jersey NJ-W4 Filing Status', help='NJ-W4 2.') + nj_njw4_sit_allowances = fields.Integer(string='New Jersey NJ-W4 Allowances', help='NJ-W4 4.') + nj_njw4_sit_rate_table = fields.Selection([ + ('A', 'A'), + ('B', 'B'), + ('C', 'C'), + ('D', 'D'), + ('E', 'E') + ], string='New Jersey Wage Chart Letter', help='NJ-W4. 3.') + + # Ohio will use generic SIT exempt and additional fields + oh_it4_sit_exemptions = fields.Integer(string='Ohio IT-4 Exemptions', + help='Line 4') + + va_va4_sit_exemptions = fields.Integer(string='Virginia VA-4(P) Personal Exemptions', + help='VA-4(P) 1(a)') + va_va4_sit_other_exemptions = fields.Integer(string='Virginia VA-4(P) Age & Blindness Exemptions', + help='VA-4(P) 1(b)') diff --git a/l10n_us_hr_payroll/security/ir.model.access.csv b/l10n_us_hr_payroll/security/ir.model.access.csv new file mode 100644 index 00000000..67a8fa2a --- /dev/null +++ b/l10n_us_hr_payroll/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_contract_us_payroll_config,hr.contract.us_payroll_config,model_hr_contract_us_payroll_config,hr_payroll.group_hr_payroll_manager,1,1,1,1 diff --git a/l10n_us_hr_payroll/static/description/icon.png b/l10n_us_hr_payroll/static/description/icon.png new file mode 100644 index 00000000..2a58a381 Binary files /dev/null and b/l10n_us_hr_payroll/static/description/icon.png differ diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py new file mode 100755 index 00000000..d2b95c24 --- /dev/null +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -0,0 +1,88 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import common + +from . import test_special + +from . import test_us_payslip_2019 +from . import test_us_payslip_2020 + +from . import test_us_ak_alaska_payslip_2019 +from . import test_us_ak_alaska_payslip_2020 + +from . import test_us_al_alabama_payslip_2019 +from . import test_us_al_alabama_payslip_2020 + +from . import test_us_ar_arkansas_payslip_2019 +from . import test_us_ar_arkansas_payslip_2020 + +from . import test_us_az_arizona_payslip_2019 +from . import test_us_az_arizona_payslip_2020 + +from . import test_us_ca_california_payslip_2019 +from . import test_us_ca_california_payslip_2020 + +from . import test_us_co_colorado_payslip_2020 + +from . import test_us_ct_connecticut_payslip_2019 +from . import test_us_ct_connecticut_payslip_2020 + +from . import test_us_de_delaware_payslip_2020 + +from . import test_us_fl_florida_payslip_2019 +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_hi_hawaii_payslip_2019 +from . import test_us_hi_hawaii_payslip_2020 + +from . import test_us_ia_iowa_payslip_2019 +from . import test_us_ia_iowa_payslip_2020 + +from . import test_us_id_idaho_payslip_2019 +from . import test_us_id_idaho_payslip_2020 + +from . import test_us_il_illinois_payslip_2019 +from . import test_us_il_illinois_payslip_2020 + +from . import test_us_mi_michigan_payslip_2019 +from . import test_us_mi_michigan_payslip_2020 + +from . import test_us_mn_minnesota_payslip_2019 +from . import test_us_mn_minnesota_payslip_2020 + +from . import test_us_mo_missouri_payslip_2019 +from . import test_us_mo_missouri_payslip_2020 + +from . import test_us_ms_mississippi_payslip_2019 +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_nh_new_hampshire_payslip_2020 + +from . import test_us_nj_newjersey_payslip_2019 +from . import test_us_nj_newjersey_payslip_2020 + +from . import test_us_nm_new_mexico_payslip_2020 + +from . import test_us_oh_ohio_payslip_2019 +from . import test_us_oh_ohio_payslip_2020 + +from . import test_us_pa_pennsylvania_payslip_2019 +from . import test_us_pa_pennsylvania_payslip_2020 + +from . import test_us_tx_texas_payslip_2019 +from . import test_us_tx_texas_payslip_2020 + +from . import test_us_va_virginia_payslip_2019 +from . import test_us_va_virginia_payslip_2020 + +from . import test_us_wa_washington_payslip_2019 +from . import test_us_wa_washington_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py new file mode 100755 index 00000000..df6491a6 --- /dev/null +++ b/l10n_us_hr_payroll/tests/common.py @@ -0,0 +1,236 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from logging import getLogger +from sys import float_info as sys_float_info +from collections import defaultdict +from datetime import timedelta + +from odoo.tests import common +from odoo.tools.float_utils import float_round as odoo_float_round +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +def process_payslip(payslip): + try: + payslip.action_payslip_done() + except AttributeError: + # v9 + payslip.process_sheet() + + +class TestUsPayslip(common.TransactionCase): + debug = False + _logger = getLogger(__name__) + + def setUp(self): + super(TestUsPayslip, self).setUp() + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to') + + float_info = sys_float_info + + def float_round(self, value, digits): + return odoo_float_round(value, digits) + + _payroll_digits = -1 + + @property + def payroll_digits(self): + if self._payroll_digits == -1: + self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll') + return self._payroll_digits + + def _log(self, message): + if self.debug: + self._logger.warn(message) + + def _createEmployee(self): + return self.env['hr.employee'].create({ + 'birthday': '1985-03-14', + 'country_id': self.ref('base.us'), + 'department_id': self.ref('hr.dep_rd'), + 'gender': 'male', + 'name': 'Jared' + }) + + def _createContract(self, employee, **kwargs): + if not 'schedule_pay' in kwargs: + kwargs['schedule_pay'] = 'monthly' + schedule_pay = kwargs['schedule_pay'] + config_model = self.env['hr.contract.us_payroll_config'] + contract_model = self.env['hr.contract'] + config_values = { + 'name': 'Test Config Values', + 'employee_id': employee.id, + } + contract_values = { + 'name': 'Test Contract', + 'employee_id': employee.id, + } + + # Backwards compatability with 'futa_type' + if 'futa_type' in kwargs: + kwargs['fed_940_type'] = kwargs['futa_type'] + + for key, val in kwargs.items(): + # Assume any Odoo object is in a Many2one + if hasattr(val, 'id'): + val = val.id + found = False + if hasattr(contract_model, key): + contract_values[key] = val + found = True + if hasattr(config_model, key): + config_values[key] = val + found = True + if not found: + self._logger.warn('cannot locate attribute names "%s" on contract or payroll config' % (key, )) + + # US Payroll Config Defaults Should be set on the Model + config = config_model.create(config_values) + contract_values['us_payroll_config_id'] = config.id + + # Some Basic Defaults + if not contract_values.get('state'): + contract_values['state'] = 'open' # Running + if not contract_values.get('structure_type_id'): + contract_values['structure_type_id'] = self.ref('l10n_us_hr_payroll.structure_type_employee') + if not contract_values.get('date_start'): + contract_values['date_start'] = '2016-01-01' + if not contract_values.get('date_end'): + contract_values['date_end'] = '2030-12-31' + if not contract_values.get('resource_calendar_id'): + contract_values['resource_calendar_id'] = self.ref('resource.resource_calendar_std') + + # Compatibility with earlier Odoo versions + if not contract_values.get('journal_id') and hasattr(contract_model, 'journal_id'): + try: + contract_values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id + except KeyError: + # Accounting not installed + pass + + contract = contract_model.create(contract_values) + + # Compatibility with Odoo 13 + contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay + return contract + + def _createPayslip(self, employee, date_from, date_to): + slip = self.env['hr.payslip'].create({ + 'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to), + 'employee_id': employee.id, + 'date_from': date_from, + 'date_to': date_to + }) + slip._onchange_employee() + return slip + + def _getCategories(self, payslip): + categories = defaultdict(float) + for line in payslip.line_ids: + self._log(' line code: ' + str(line.code) + + ' category code: ' + line.category_id.code + + ' total: ' + str(line.total) + + ' rate: ' + str(line.rate) + + ' amount: ' + str(line.amount)) + category_id = line.category_id + category_code = line.category_id.code + while category_code: + categories[category_code] += line.total + category_id = category_id.parent_id + category_code = category_id.code + return categories + + def _getRules(self, payslip): + rules = defaultdict(float) + for line in payslip.line_ids: + rules[line.code] += line.total + return rules + + def assertPayrollEqual(self, first, second): + self.assertAlmostEqual(first, second, self.payroll_digits) + + def assertPayrollAlmostEqual(self, first, second): + self.assertAlmostEqual(first, second, self.payroll_digits-1) + + def get_us_state(self, code, cache={}): + country_key = 'US_COUNTRY' + if code in cache: + return cache[code] + if country_key not in cache: + cache[country_key] = self.env.ref('base.us') + us_country = cache[country_key] + us_state = self.env['res.country.state'].search([ + ('country_id', '=', us_country.id), + ('code', '=', code), + ], limit=1) + cache[code] = us_state + return us_state + + def _test_suta(self, category, state_code, rate, date, wage_base=None, relaxed=False, **extra_contract): + if relaxed: + _assert = self.assertPayrollAlmostEqual + else: + _assert = self.assertPayrollEqual + if wage_base: + # Slightly larger than 1/2 the wage_base + wage = round(wage_base / 2.0) + 100.0 + self.assertTrue((2 * wage) > wage_base, 'Granularity of wage_base too low.') + else: + wage = 1000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state(state_code), + **extra_contract) + + rate = -rate / 100.0 # Assumed passed as percent positive + + # Tests + payslip = self._createPayslip(employee, date, date + timedelta(days=30)) + + # Test exemptions + contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_EXEMPT + payslip.compute_sheet() + cats = self._getCategories(payslip) + _assert(cats.get(category, 0.0), 0.0) + + contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_BASIC + payslip.compute_sheet() + cats = self._getCategories(payslip) + _assert(cats.get(category, 0.0), 0.0) + + # Test Normal + contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_NORMAL + payslip.compute_sheet() + cats = self._getCategories(payslip) + _assert(cats.get(category, 0.0), wage * rate) + process_payslip(payslip) + + # Second Payslip + payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60)) + payslip.compute_sheet() + cats = self._getCategories(payslip) + + if wage_base: + remaining_unemp_wages = wage_base - wage + self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative + _assert(cats.get(category, 0.0), remaining_unemp_wages * rate) + + # As if they were paid once already, so the first "two payslips" would remove all of the tax obligation + # 1 wage - Payslip (confirmed) + # 1 wage - external_wages + # 1 wage - current Payslip + contract.external_wages = wage + payslip.compute_sheet() + cats = self._getCategories(payslip) + _assert(cats.get(category, 0.0), 0.0) + else: + _assert(cats.get(category, 0.0), wage * rate) + + def _test_er_suta(self, state_code, rate, date, wage_base=None, relaxed=False, **extra_contract): + self._test_suta('ER_US_SUTA', state_code, rate, date, wage_base=wage_base, relaxed=relaxed, **extra_contract) + + def _test_ee_suta(self, state_code, rate, date, wage_base=None, relaxed=False, **extra_contract): + self._test_suta('EE_US_SUTA', state_code, rate, date, wage_base=wage_base, relaxed=relaxed, **extra_contract) diff --git a/l10n_us_hr_payroll/tests/test_special.py b/l10n_us_hr_payroll/tests/test_special.py new file mode 100644 index 00000000..430af0ea --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_special.py @@ -0,0 +1,65 @@ +from .common import TestUsPayslip, process_payslip + + +class TestSpecial(TestUsPayslip): + def test_semi_monthly(self): + salary = 80000.0 + employee = self._createEmployee() + # so the schedule_pay is now on the Structure... + contract = self._createContract(employee, wage=salary, schedule_pay='semi-monthly') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14') + payslip.compute_sheet() + + def test_payslip_sum_behavior(self): + us_structure = self.env.ref('l10n_us_hr_payroll.hr_payroll_structure') + rule_category_comp = self.env.ref('hr_payroll.COMP') + test_rule_category = self.env['hr.salary.rule.category'].create({ + 'name': 'Test Sum Behavior', + 'code': 'test_sum_behavior', + 'parent_id': rule_category_comp.id, + }) + test_rule = self.env['hr.salary.rule'].create({ + 'sequence': 450, + 'struct_id': us_structure.id, + 'category_id': test_rule_category.id, + 'name': 'Test Sum Behavior', + 'code': 'test_sum_behavior', + 'condition_select': 'python', + 'condition_python': 'result = 1', + 'amount_select': 'code', + 'amount_python_compute': ''' +ytd_category = payslip.sum_category('test_sum_behavior', '2020-01-01', '2021-01-01') +ytd_rule = payslip.sum('test_sum_behavior', '2020-01-01', '2021-01-01') +result = 0.0 +if ytd_category != ytd_rule: + # error + result = -1.0 +elif ytd_rule == 0.0: + # first payslip in period + result = 1.0 +''' + }) + salary = 80000.0 + employee = self._createEmployee() + contract = self._createContract(employee, wage=salary, schedule_pay='bi-weekly') + payslip = self._createPayslip(employee, '2019-12-30', '2020-01-12') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertEqual(cats['test_sum_behavior'], 1.0) + process_payslip(payslip) + + # Basic date_from behavior. + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_from') + # The the date_from on the last payslip will not be found + payslip = self._createPayslip(employee, '2020-01-13', '2020-01-27') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertEqual(cats['test_sum_behavior'], 1.0) + + # date_to behavior. + self.env['ir.config_parameter'].set_param('hr_payroll.payslip.sum_behavior', 'date_to') + # The date_to on the last payslip is found + payslip = self._createPayslip(employee, '2020-01-13', '2020-01-27') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertEqual(cats['test_sum_behavior'], 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2019.py new file mode 100644 index 00000000..3eb62184 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2019.py @@ -0,0 +1,61 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsAKPayslip(TestUsPayslip): + # TAXES AND RATES + AK_UNEMP_MAX_WAGE = 39900.00 + AK_UNEMP = -(1.780 / 100.0) + AK_UNEMP_EE = -(0.5 / 100.0) + + def test_taxes_monthly_over_max(self): + salary = 50000.00 + schedule_pay = 'monthly' + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AK'), + state_income_tax_additional_withholding=0.0, + schedule_pay=schedule_pay) + + self._log('2019 Alaska 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'], self.AK_UNEMP_MAX_WAGE * self.AK_UNEMP) + self.assertPayrollEqual(cats['EE_US_SUTA'], self.AK_UNEMP_MAX_WAGE * self.AK_UNEMP_EE) + + process_payslip(payslip) + + remaining_ak_unemp_wages = 0.00 # We already reached the maximum wage for unemployment insurance. + + self._log('2019 Alaska 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_ak_unemp_wages * self.AK_UNEMP) # 0 + + def test_taxes_weekly_under_max(self): + salary = 5000.00 + schedule_pay = 'weekly' + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AK'), + state_income_tax_additional_withholding=0.0, + schedule_pay=schedule_pay) + + self._log('2019 Alaska 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.AK_UNEMP) + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * self.AK_UNEMP_EE) + + process_payslip(payslip) diff --git a/l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2020.py new file mode 100644 index 00000000..868a8dff --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2020.py @@ -0,0 +1,15 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsAKPayslip(TestUsPayslip): + # TAXES AND RATES + AK_UNEMP_MAX_WAGE = 41500.00 + AK_UNEMP = 1.590 + AK_UNEMP_EE = 0.5 + + def test_2020_taxes(self): + self._test_er_suta('AK', self.AK_UNEMP, date(2020, 1, 1), wage_base=self.AK_UNEMP_MAX_WAGE) + self._test_ee_suta('AK', self.AK_UNEMP_EE, date(2020, 1, 1), wage_base=self.AK_UNEMP_MAX_WAGE) diff --git a/l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2019.py new file mode 100644 index 00000000..61290314 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2019.py @@ -0,0 +1,264 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsALPayslip(TestUsPayslip): + # TAXES AND RATES + AL_UNEMP_MAX_WAGE = 8000.00 + AL_UNEMP = -2.70 / 100.0 + + def test_taxes_weekly(self): + salary = 10000.00 + schedule_pay = 'weekly' + dependents = 1 + filing_status = 'S' + # see https://revenue.alabama.gov/wp-content/uploads/2019/01/whbooklet_0119.pdf for reference + # Hand Calculated Amount to Test + # Step 1 -> 10000.00 for wages per period , 52.0 for weekly -> 10000 * 52 -> 520000.0 + # Step 2A -> standard deduction for highest wage bracket -> 2000. Subtract from yearly income + # 520000 - 2000 = 518000.0 + # Step 2B -> Subtract Federal Income Tax in yearly form -> Our Fed withholding is -2999.66 * 52 = -155982.32 + # -> 518000.0 - 155982.32 = 362017.68 + # Step 2C -> Subtract the personal exemption -> 1500 for single filing_status + # -> 362017.68 - 1500 = 360517.68 + # Step 2D -> Since income is so high, only 300$ per dependent -> 300$. Subtract + # -> 360517.68 - 300 = 360217.68 + # + # Step 5 (after adding previous lines) -> Compute marginal taxes. + # (500 * (2.00 / 100)) + (2500 * (4.00 / 100)) + ((360217.68 - 500 - 2500) * (5.00 / 100)) -> 17970.884000000002 + # Convert back to pay period + # wh = round(17970.884000000002, 2) -> 17970.88 / 52.0 -> 345.59 + wh = -345.59 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions=filing_status, + state_income_tax_additional_withholding=0.0, + state_income_tax_exempt=False, + al_a4_sit_dependents=dependents, + schedule_pay=schedule_pay) + + self._log('2019 Alabama 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_941_FIT'], -2999.66) # Hand Calculated. + self.assertPayrollEqual(cats['ER_US_SUTA'], self.AL_UNEMP_MAX_WAGE * self.AL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + remaining_AL_UNEMP_wages = 0.00 # We already reached the maximum wage for unemployment insurance. + + self._log('2019 Alabama 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_AL_UNEMP_wages * self.AL_UNEMP) # 0 + + def test_taxes_married_jointly(self): + salary = 10000.00 + schedule_pay = 'weekly' + dependents = 1 + filing_status = 'M' + + # see https://revenue.alabama.gov/wp-content/uploads/2019/01/whbooklet_0119.pdf for reference + # Hand Calculated Amount to Test + # Step 1 -> 10000.00 for wages per period , 52.0 for weekly -> 10000 * 52 -> 520000.0 + # Step 2A -> standard deduction for highest wage bracket -> 4000. Subtract from yearly income + # 520000 - 4000 = 516000.0 + # Step 2B -> Subtract Federal Income Tax in yearly form -> Our Fed withholding is -2999.66 * 52 = -155982.32 + # -> 516000.0 - 155982.32 = 360017.68 + # Step 2C -> Subtract the personal exemption -> 3000 for married filing jointly. + # -> 360017.68 - 3000 = 357017.68 + # Step 2D -> Since income is so high, only 300$ per dependent -> 300$. Subtract + # -> 357017.68 - 300 = 356717.68 + # + # Step 5 (after adding previous lines) -> Compute marginal taxes. + # (1000 * (2.00 / 100)) + (5000 * (4.00 / 100)) + ((356717.68 - 1000 - 50000) * (5.00 / 100)) + # -> 17755.884000000002 + # Convert back to pay period + # wh = round(17755.884000000002, 2) -> 15505.88 / 52.0 -> 341.45923076923077 + wh = -341.46 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions=filing_status, + state_income_tax_additional_withholding=0.0, + state_income_tax_exempt=False, + al_a4_sit_dependents=dependents, + schedule_pay=schedule_pay) + + self._log('2019 Alabama 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_941_FIT'], -2999.66) # Hand Calculated. + self.assertPayrollEqual(cats['ER_US_SUTA'], self.AL_UNEMP_MAX_WAGE * self.AL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + + def test_taxes_semimonthly_filing_seperate(self): + salary = 20000.00 + schedule_pay = 'monthly' + filing_status = 'MS' + dependents = 2 + + # see https://revenue.alabama.gov/wp-content/uploads/2019/01/whbooklet_0119.pdf for reference + # Hand Calculated Amount to Test + # Step 1 -> 10000.00 for wages per period , 12.0 for monthly -> 20000 * 12 -> 240000.00 + # Step 2A -> standard deduction for highest wage bracket -> 2000. Subtract from yearly income + # 240000.00 - 2000 = 238000.00 + # Step 2B -> Subtract Federal Income Tax in yearly form -> Our Fed withholding is -4821.99 * 12 = -57863.88 + # -> 238000.00 - 57863.88 = 180136.12 + # Step 2C -> Subtract the personal exemption -> 1500 for married filing separately + # -> 180136.12 - 1500 = 178636.12 + # Step 2D -> Since income is so high, only 300$ per dependent -> 600. Subtract + # -> 178636.12 - 600 = 178036.12 + # + # Step 5 (after adding previous lines) -> Compute marginal taxes. + # (500 * (2.00 / 100)) + (2500 * (4.00 / 100)) + ((178036.12 - 500 - 2500) * (5.00 / 100)) -> 8861.806 + # Convert back to pay period + # wh = 8861.806 / 12.0 rounded -> 738.48 + wh = -738.48 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions=filing_status, + state_income_tax_additional_withholding=0.0, + state_income_tax_exempt=False, + al_a4_sit_dependents=dependents, + schedule_pay=schedule_pay) + + self._log('2019 Alabama tax first payslip monthly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_941_FIT'], -4822.00) # Hand Calculated. + self.assertPayrollEqual(cats['ER_US_SUTA'], self.AL_UNEMP_MAX_WAGE * self.AL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + def test_tax_exempt(self): + salary = 5500.00 + wh = 0 + schedule_pay = 'weekly' + dependents = 2 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions='0', + state_income_tax_additional_withholding=0.0, + state_income_tax_exempt=True, + al_a4_sit_dependents=dependents, + schedule_pay=schedule_pay) + + self._log('2019 Alabama tax first payslip exempt:') + 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.AL_UNEMP) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), wh) + + def test_additional_withholding(self): + salary = 5500.0 + schedule_pay = 'weekly' + additional_wh = 40.0 + dependents = 2 + # filing status default is single + + # see https://revenue.alabama.gov/wp-content/uploads/2019/01/whbooklet_0119.pdf for reference + # Hand Calculated Amount to Test + # Step 1 -> 5500.00 for wages per period , 52.0 for monthly -> 5500 * 52.0 -> 286000.0 + # Step 2A -> standard deduction for highest wage bracket -> 2000. Subtract from yearly income + # 286000.0 - 2000 = 284000.0 + # Step 2B -> Subtract Federal Income Tax in yearly form -> Our Fed withholding is -1422.4 * 52.0 = -73964.8 + # -> 284000.0 - 73964.8 = 210035.2 + # Step 2C -> Subtract the personal exemption -> 1500 for single + # -> 210035.2 - 1500 = 208535.2 + # Step 2D -> Since income is so high, only 300$ per dependent -> 600. Subtract + # -> 208535.2 - 600 = 207935.2 + # + # Step 5 (after adding previous lines) -> Compute marginal taxes. + # (500 * (2.00 / 100)) + (2500 * (4.00 / 100)) + ((207935.2 - 500 - 2500) * (5.00 / 100)) -> 10356.76 + # Convert back to pay period + # wh = 10356.76 / 52.0 rounded -> 199.17 + wh = -199.17 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions='S', + state_income_tax_additional_withholding=40.0, + state_income_tax_exempt=False, + al_a4_sit_dependents=dependents, + schedule_pay=schedule_pay) + + self._log('2019 Alabama 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['EE_US_941_FIT'], -1422.4) # Hand Calculated. + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.AL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh - additional_wh) + + def test_personal_exemption(self): + salary = 5500.0 + schedule_pay = 'weekly' + # filing status default is single + + # see https://revenue.alabama.gov/wp-content/uploads/2019/01/whbooklet_0119.pdf for reference + # Hand Calculated Amount to Test + # Step 1 -> 5500.00 for wages per period , 52.0 for monthly -> 5500 * 52.0 -> 286000.0 + # Step 2A -> standard deduction for highest wage bracket -> 2000. Subtract from yearly income + # 286000.0 - 2000 = 284000.0 + # Step 2B -> Subtract Federal Income Tax in yearly form -> Our Fed withholding is -1422.4 * 52.0 = -73964.8 + # -> 284000.0 - 73964.8 = 210035.2 + # Step 2C -> Subtract the personal exemption -> 0 for personal exemptioon + # -> 210035.2 - 0 = 210035.2 + # Step 2D -> Subtract per dependent. No dependents so 0 + # -> 210035.2 - 0 = 210035.2 + # + # Step 5 (after adding previous lines) -> Compute marginal taxes. + # (500 * (2.00 / 100)) + (2500 * (4.00 / 100)) + ((210035.2 - 500 - 2500) * (5.00 / 100)) -> 10461.76 + # Convert back to pay period + # wh = 10461.76 / 52.0 rounded -> 201.19 + wh = -199.74 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions='S', + state_income_tax_additional_withholding=0.0, + state_income_tax_exempt=False, + al_a4_sit_dependents=0.0, + schedule_pay=schedule_pay) + + self._log('2019 Alabama 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['EE_US_941_FIT'], -1422.4) # Hand Calculated. + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.AL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) diff --git a/l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2020.py new file mode 100644 index 00000000..055c95cb --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_al_alabama_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 TestUsALPayslip(TestUsPayslip): + # Taxes and Rates + AL_UNEMP_MAX_WAGE = 8000.00 + AL_UNEMP = 2.70 + + def _test_sit(self, wage, exempt, exemptions, additional_withholding, dependent, schedule_pay, date_start, expected_withholding): + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('AL'), + al_a4_sit_exemptions=exempt, + state_income_tax_exempt=exemptions, + state_income_tax_additional_withholding=additional_withholding, + al_a4_sit_dependents=dependent, + 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('AL', self.AL_UNEMP, date(2020, 1, 1), wage_base=self.AL_UNEMP_MAX_WAGE) + self._test_sit(10000.0, 'S', False, 0.0, 1.0, 'weekly', date(2020, 1, 1), 349.08) + self._test_sit(850.0, 'M', False, 0.0, 2.0, 'weekly', date(2020, 1, 1), 29.98) + self._test_sit(5000.0, 'H', False, 0.0, 2.0, 'bi-weekly', date(2020, 1, 1), 191.15) + self._test_sit(20000.0, 'MS', False, 2.0, 0, 'monthly', date(2020, 1, 1), 757.6) + self._test_sit(5500.0, '0', True, 2.0, 150, 'weekly', date(2020, 1, 1), 0) diff --git a/l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2019.py new file mode 100644 index 00000000..73b0f59c --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2019.py @@ -0,0 +1,72 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsARPayslip(TestUsPayslip): + # https://www.dfa.arkansas.gov/images/uploads/incomeTaxOffice/whformula.pdf Calculation based on this file. + AR_UNEMP_MAX_WAGE = 10000.00 + AR_UNEMP = -3.2 / 100.0 + AR_INC_TAX = -0.0535 + + def test_taxes_monthly(self): + salary = 2127.0 + schedule_pay = 'monthly' + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AR'), + state_income_tax_additional_withholding=0.0, + ar_ar4ec_sit_allowances=2.0, + state_income_tax_exempt=False, + 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['ER_US_SUTA'], salary * 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._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['ER_US_SUTA'], remaining_AR_UNEMP_wages * self.AR_UNEMP) + + def test_additional_withholding(self): + salary = 5000.0 + schedule_pay = 'monthly' + pay_periods = 12 + allowances = 2 + # TODO: comment on how it was calculated + test_ar_amt = 2598.60 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AR'), + state_income_tax_additional_withholding=100.0, + ar_ar4ec_sit_allowances=2.0, + state_income_tax_exempt=False, + 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['ER_US_SUTA'], salary * self.AR_UNEMP) + # TODO: change to hand the test_ar_amt already be divided by pay periods + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], -round(test_ar_amt / pay_periods) - 100) + + process_payslip(payslip) diff --git a/l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2020.py new file mode 100644 index 00000000..bf630b6c --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ar_arkansas_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 TestUsARPayslip(TestUsPayslip): + # Taxes and Rates + AR_UNEMP_MAX_WAGE = 8000.0 + AR_UNEMP = 2.9 + + def _test_sit(self, wage, exemptions, allowances, additional_withholding, schedule_pay, date_start, expected_withholding): + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('AR'), + state_income_tax_exempt=exemptions, + state_income_tax_additional_withholding=additional_withholding, + ar_ar4ec_sit_allowances=allowances, + 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('AR', self.AR_UNEMP, date(2020, 1, 1), wage_base=self.AR_UNEMP_MAX_WAGE) + self._test_sit(5000.0, True, 0.0, 0, 'monthly', date(2020, 1, 1), 0.0) + self._test_sit(5000.0, False, 0.0, 0, 'monthly', date(2020, 1, 1), 221.0) + self._test_sit(5000.0, False, 0.0, 150, 'monthly', date(2020, 1, 1), 371.0) + self._test_sit(5000.0, False, 2.0, 0, 'monthly', date(2020, 1, 1), 217) + self._test_sit(5000.0, False, 2.0, 150, 'monthly', date(2020, 1, 1), 367) diff --git a/l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2019.py new file mode 100644 index 00000000..b97063b6 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2019.py @@ -0,0 +1,72 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsAZPayslip(TestUsPayslip): + + # TAXES AND RATES + AZ_UNEMP_MAX_WAGE = 7000.00 + AZ_UNEMP = -(2.00 / 100.0) + + def test_taxes_with_additional_wh(self): + salary = 15000.00 + schedule_pay = 'weekly' + withholding_percentage = 5.1 + percent_wh = (5.10 / 100) # 5.1% + additional_wh = 12.50 + + wh_to_test = -((percent_wh * salary) + additional_wh) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AZ'), + state_income_tax_additional_withholding=12.50, + az_a4_sit_withholding_percentage=withholding_percentage, + schedule_pay=schedule_pay) + + self._log('2019 Arizona 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'], self.AZ_UNEMP_MAX_WAGE * self.AZ_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_test) + + process_payslip(payslip) + + remaining_AZ_UNEMP_wages = 0.0 # We already reached max unemployment wages. + + self._log('2019 Arizona 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_AZ_UNEMP_wages * self.AZ_UNEMP) + + def test_taxes_monthly(self): + salary = 1000.00 + schedule_pay = 'monthly' + withholding_percentage = 2.7 + percent_wh = (2.70 / 100) # 2.7% + additional_wh = 0.0 + wh_to_test = -((percent_wh * salary) + additional_wh) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('AZ'), + state_income_tax_additional_withholding=0.0, + az_a4_sit_withholding_percentage=withholding_percentage, + schedule_pay=schedule_pay) + + self._log('2019 Arizona 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.AZ_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_test) + + process_payslip(payslip) diff --git a/l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2020.py new file mode 100644 index 00000000..d1d14d80 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2020.py @@ -0,0 +1,33 @@ +# 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 TestUsAZPayslip(TestUsPayslip): + # Taxes and Rates + AZ_UNEMP_MAX_WAGE = 7000.0 + AZ_UNEMP = 2.0 + + def _test_sit(self, wage, additional_withholding, withholding_percent, schedule_pay, date_start, expected_withholding): + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('AZ'), + state_income_tax_additional_withholding=additional_withholding, + az_a4_sit_withholding_percentage=withholding_percent, + 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('AZ', self.AZ_UNEMP, date(2020, 1, 1), wage_base=self.AZ_UNEMP_MAX_WAGE) + self._test_sit(1000.0, 0.0, 2.70, 'monthly', date(2020, 1, 1), 27.0) + self._test_sit(1000.0, 10.0, 2.70, 'monthly', date(2020, 1, 1), 37.0) + self._test_sit(15000.0, 0.0, 3.60, 'weekly', date(2020, 1, 1), 540.0) + self._test_sit(8000.0, 0.0, 4.20, 'semi-monthly', date(2020, 1, 1), 336.0) diff --git a/l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2019.py new file mode 100644 index 00000000..b9331fe3 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2019.py @@ -0,0 +1,245 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsCAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + CA_MAX_WAGE = 7000 + CA_UIT = -3.5 / 100.0 + CA_ETT = -0.1 / 100.0 + CA_SDI = -1.0 / 100.0 + + # Examples from https://www.edd.ca.gov/pdf_pub_ctr/20methb.pdf + def test_example_a(self): + salary = 210 + schedule_pay = 'weekly' + allowances = 1 + additional_allowances = 0 + + wh = 0.00 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status='single', + state_income_tax_additional_withholding=0.0, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_allowances, + schedule_pay=schedule_pay) + + self._log('2019 California 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.CA_UIT + self.CA_ETT)) + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * self.CA_SDI) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + def test_example_b(self): + salary = 1250 + schedule_pay = 'bi-weekly' + allowances = 2 + additional_allowances = 1 + + # Example B + subject_to_withholding = salary - 38 + taxable_income = subject_to_withholding - 339 + computed_tax = (taxable_income - 632) * 0.022 + 6.95 # 6.95 Marginal Amount + wh = computed_tax - 9.65 # two exemption allowances + wh = -wh + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_allowances, + schedule_pay=schedule_pay) + + self._log('2019 California 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.CA_UIT + self.CA_ETT)) + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * self.CA_SDI) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + + def test_example_c(self): + salary = 4100 + schedule_pay = 'monthly' + allowances = 5 + additional_allowances = 0.0 + + wh = -9.3 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_allowances, + schedule_pay=schedule_pay) + + self._log('2019 California 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.CA_UIT + self.CA_ETT)) + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * self.CA_SDI) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_MAX_WAGE - salary if (self.CA_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 California 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'], round((remaining_ca_uit_wages * (self.CA_UIT + self.CA_ETT)), 2)) + + def test_example_d(self): + salary = 800 + schedule_pay = 'weekly' + allowances = 3 + additional_allowances = 0 + + wh = -3.18 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status='head_household', + state_income_tax_additional_withholding=0.0, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_allowances, + schedule_pay=schedule_pay) + + self._log('2019 California 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.CA_UIT + self.CA_ETT)) + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * self.CA_SDI) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_MAX_WAGE - salary if (self.CA_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 California 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'], round((remaining_ca_uit_wages * (self.CA_UIT + self.CA_ETT)), 2)) + + def test_example_e(self): + salary = 1800 + schedule_pay = 'semi-monthly' + allowances = 4 + additional_allowances = 0 + + wh = -3.08 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_allowances, + schedule_pay=schedule_pay) + + self._log('2019 California 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.CA_UIT + self.CA_ETT)) + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * self.CA_SDI) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_MAX_WAGE - salary if (self.CA_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 California 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'], round((remaining_ca_uit_wages * (self.CA_UIT + self.CA_ETT)), 2)) + + def test_example_f(self): + salary = 45000 + schedule_pay = 'annually' + allowances = 4 + additional_allowances = 0 + + wh = -113.85 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_allowances, + schedule_pay=schedule_pay) + + self._log('2019 California 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'], self.CA_MAX_WAGE * (self.CA_UIT + self.CA_ETT)) + self.assertPayrollEqual(cats['EE_US_SUTA'], self.CA_MAX_WAGE * self.CA_SDI) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) \ No newline at end of file diff --git a/l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2020.py new file mode 100755 index 00000000..c6c58547 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2020.py @@ -0,0 +1,42 @@ +# 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 TestUsCAPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + CA_UNEMP_MAX_WAGE = 7000.0 # Note that this is used for SDI and FLI as well + CA_UIT = 3.4 + CA_ETT = 0.1 + CA_SDI = 1.0 + + def _test_sit(self, wage, filing_status, allowances, additional_allowances, additional_withholding, schedule_pay, date_start, expected_withholding): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('CA'), + ca_de4_sit_filing_status=filing_status, + ca_de4_sit_allowances=allowances, + ca_de4_sit_additional_allowances=additional_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.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected_withholding if filing_status else 0.0) + + def test_2020_taxes_example1(self): + combined_er_rate = self.CA_UIT + self.CA_ETT + self._test_er_suta('CA', combined_er_rate, date(2020, 1, 1), wage_base=self.CA_UNEMP_MAX_WAGE) + self._test_ee_suta('CA', self.CA_SDI, date(2020, 1, 1), wage_base=self.CA_UNEMP_MAX_WAGE, relaxed=True) + # these expected values come from https://www.edd.ca.gov/pdf_pub_ctr/20methb.pdf + self._test_sit(210.0, 'single', 1, 0, 0, 'weekly', date(2020, 1, 1), 0.00) + self._test_sit(1250.0, 'married', 2, 1, 0, 'bi-weekly', date(2020, 1, 1), 1.23) + self._test_sit(4100.0, 'married', 5, 0, 0, 'monthly', date(2020, 1, 1), 1.5) + self._test_sit(800.0, 'head_household', 3, 0, 0, 'weekly', date(2020, 1, 1), 2.28) + self._test_sit(1800.0, 'married', 4, 0, 0, 'semi-monthly', date(2020, 1, 1), 0.84) + self._test_sit(45000.0, 'married', 4, 0, 0, 'annually', date(2020, 1, 1), 59.78) + self._test_sit(45000.0, 'married', 4, 0, 20.0, 'annually', date(2020, 1, 1), 79.78) diff --git a/l10n_us_hr_payroll/tests/test_us_co_colorado_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_co_colorado_payslip_2020.py new file mode 100755 index 00000000..6e24cbb0 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_co_colorado_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 TestUsCOPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + CO_UNEMP_MAX_WAGE = 13600.0 + CO_UNEMP = 1.7 + + def _test_sit(self, wage, filing_status, additional_withholding, schedule_pay, date_start, expected_withholding, state_income_tax_exempt=False): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('CO'), + fed_941_fit_w4_filing_status=filing_status, + state_income_tax_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + 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('CO', self.CO_UNEMP, date(2020, 1, 1), wage_base=self.CO_UNEMP_MAX_WAGE) + self._test_sit(5000.0, 'married', 0.0, 'semi-monthly', date(2020, 1, 1), 216.07) + self._test_sit(800.0, 'single', 0.0, 'weekly', date(2020, 1, 1), 33.48) + self._test_sit(20000.0, 'married', 0.0, 'quarterly', date(2020, 1, 1), 833.4) + self._test_sit(20000.0, 'married', 10.0, 'quarterly', date(2020, 1, 1), 843.4) + self._test_sit(20000.0, 'married', 0.0, 'quarterly', date(2020, 1, 1), 0.0, True) diff --git a/l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2019.py new file mode 100644 index 00000000..ab423131 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2019.py @@ -0,0 +1,121 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsCTPayslip(TestUsPayslip): + # TAXES AND RATES + CT_UNEMP_MAX_WAGE = 15000.00 + CT_UNEMP = -(3.40 / 100.0) + + def test_taxes_weekly_with_additional_wh(self): + + # Tax tables can be found here: + # https://portal.ct.gov/-/media/DRS/Publications/pubsip/2019/IP-2019(1).pdf?la=en + # Step 1 - Wages per period -> 10000.00 + salary = 10000.00 + # Step 2 and 3 - Annual wages -> 10000.00 * 52.0 -> 520000.0 + schedule_pay = 'weekly' + # Step 4 Employee Withholding Code -> A + wh_code = 'a' + # Step 5 - Use annual wages and withholding code with table for exemption amount. + # exemption_amt = 0 since highest bracket. + # Step 6 - Subtract 5 from 3 for taxable income. + # taxable income = 520000.00 since we do not have an exemption. + # Step 7 - Determine initial amount from table + # initial = 31550 + ((6.99 / 100) * (520000.00 - 500000.00)) + # 32948.0 + # Step 8 - Determine the tax rate phase out add back from table. + # phase_out = 200 + # Step 9 - Determine the recapture amount from table. + # Close to top, but not top. -> 2900 + # Step 10 - Add Step 7, 8, 9 + # 32948.0 + 200 + 2900.00 - > 36048.0 + # Step 11 - Determine decimal amount from personal tax credits. + # We get no tax credit. + # Step 12 - Multiple Step 10 by 1.00 - Step 11 + # 36048.0 * 1.00 = 36048.0 + # Step 13 - Divide by the number of pay periods. + # 36048.0 / 52.0 = 693.23 + # Step 14 & 15 & 16- Add / Subtract the additional or under withholding amount. Then Add this to the amount + # for withholding per period. + additional_wh = 12.50 + # 693.23 + 12.50 -> + wh = -705.73 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CT'), + ct_w4na_sit_code=wh_code, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self._log('2019 Connecticut 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.CT_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + remaining_CT_UNEMP_wages = 5000.00 # We already reached the maximum wage for unemployment insurance. + self._log('2019 Connecticut 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_CT_UNEMP_wages * self.CT_UNEMP) + + def test_taxes_weekly_with_different_code(self): + + # Tax tables can be found here: + # https://portal.ct.gov/-/media/DRS/Publications/pubsip/2019/IP-2019(1).pdf?la=en + # Step 1 - Wages per period -> 15000.00 + salary = 15000.00 + # Step 2 and 3 - Annual wages -> 15000.00 * 12.0 -> 180000.0 + schedule_pay = 'monthly' + # Step 4 Employee Withholding Code -> B + wh_code = 'b' + # Step 5 - Use annual wages and withholding code with table for exemption amount. + # exemption_amt = 0 since highest bracket. + # Step 6 - Subtract 5 from 3 for taxable income. + # taxable income = 180000.0 since we do not have an exemption. + # Step 7 - Determine initial amount from table + # initial = 8080 + ((6.00 / 100) * (180000.0 - 160000)) + # 9280.0 + # Step 8 - Determine the tax rate phase out add back from table. + # phase_out = 320 + # Step 9 - Determine the recapture amount from table. + # Bottom -> 0 + # Step 10 - Add Step 7, 8, 9 + # 9280.0 + 320 + 0 - > 9600.0 + # Step 11 - Determine decimal amount from personal tax credits. + # We get no tax credit. + # Step 12 - Multiple Step 10 by 1.00 - Step 11 + # 9600.0 * 1.00 = 9600.0 + # Step 13 - Divide by the number of pay periods. + # 9600.0 / 12.0 = 800.0 + # Step 14 & 15 & 16- Add / Subtract the additional or under withholding amount. Then Add this to the amount + # for withholding per period. + additional_wh = 15.00 + # 800.0 + 15.00 -> + wh = -815.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('CT'), + ct_w4na_sit_code=wh_code, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self._log('2019 Connecticut 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'], self.CT_UNEMP_MAX_WAGE * self.CT_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) diff --git a/l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2020.py new file mode 100644 index 00000000..a5db79a6 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2020.py @@ -0,0 +1,34 @@ +# 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 TestUsCTPayslip(TestUsPayslip): + # Taxes and Rates + CT_UNEMP_MAX_WAGE = 15000.0 + CT_UNEMP = 3.2 + + def _test_sit(self, wage, withholding_code, additional_withholding, schedule_pay, date_start, expected_withholding): + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('CT'), + ct_w4na_sit_code=withholding_code, + 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('CT', self.CT_UNEMP, date(2020, 1, 1), wage_base=self.CT_UNEMP_MAX_WAGE) + self._test_sit(10000.0, 'a', 0.0, 'weekly', date(2020, 1, 1), 693.23) + self._test_sit(12000.0, 'b', 15.0, 'bi-weekly', date(2020, 1, 1), 688.85) + self._test_sit(5000.0, 'f', 15.0, 'monthly', date(2020, 1, 1), 230.25) + self._test_sit(15000.0, 'c', 0.0, 'monthly', date(2020, 1, 1), 783.33) + self._test_sit(18000.0, 'b', 0.0, 'weekly', date(2020, 1, 1), 1254.35) diff --git a/l10n_us_hr_payroll/tests/test_us_de_delaware_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_de_delaware_payslip_2020.py new file mode 100755 index 00000000..ed285368 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_de_delaware_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 TestUsDEPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + DE_UNEMP_MAX_WAGE = 16500.0 + DE_UNEMP = 1.50 + # Calculation based on section 17. https://revenue.delaware.gov/employers-guide-withholding-regulations-employers-duties/ + + def _test_sit(self, wage, filing_status, additional_withholding, dependents, schedule_pay, date_start, expected_withholding): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('DE'), + de_w4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_withholding, + de_w4_sit_dependent=dependents, + 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('DE', self.DE_UNEMP, date(2020, 1, 1), wage_base=self.DE_UNEMP_MAX_WAGE) + self._test_sit(480.77, 'single', 0.0, 1.0, 'weekly', date(2020, 1, 1), 13.88) + self._test_sit(5000.0, 'single', 0.0, 2.0, 'monthly', date(2020, 1, 1), 211.93) + self._test_sit(5000.0, 'single', 10.0, 1.0, 'monthly', date(2020, 1, 1), 231.1) + self._test_sit(20000.0, 'married', 0.0, 3.0, 'quarterly', date(2020, 1, 1), 876.0) diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py new file mode 100755 index 00000000..419be377 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py @@ -0,0 +1,84 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsFlPayslip(TestUsPayslip): + ### + # 2019 Taxes and Rates + ### + FL_UNEMP_MAX_WAGE = 7000.0 + FL_UNEMP = -2.7 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('FL')) + + self._log('2019 Florida 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.FL_UNEMP) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_fl_unemp_wages = self.FL_UNEMP_MAX_WAGE - salary if (self.FL_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Florida 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_fl_unemp_wages * self.FL_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + state_id=self.get_us_state('FL')) + + self._log('2019 Forida_external 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'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC, + state_id=self.get_us_state('FL')) + + self._log('2019 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py new file mode 100755 index 00000000..5952eb1f --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py @@ -0,0 +1,16 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsFlPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + FL_UNEMP_MAX_WAGE = 7000.0 + FL_UNEMP = 2.7 + + def test_2020_taxes(self): + # Only has state unemployment + self._test_er_suta('FL', self.FL_UNEMP, date(2020, 1, 1), wage_base=self.FL_UNEMP_MAX_WAGE) diff --git a/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py new file mode 100755 index 00000000..b407a079 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py @@ -0,0 +1,135 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsGAPayslip(TestUsPayslip): + + # TAXES AND RATES + GA_UNEMP_MAX_WAGE = 9500.00 + GA_UNEMP = -(2.70 / 100.0) + + def test_taxes_weekly_single_with_additional_wh(self): + salary = 15000.00 + schedule_pay = 'weekly' + allowances = 1 + filing_status = 'single' + additional_wh = 12.50 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for single weekly is 88.50 + # step1 = 15000.00 - 88.50 = 14911.5 + # Step 2 - Subtract personal allowance from step1. Allowance for single weekly is 51.92 + # step2 = step1 - 51.92 = 14859.58 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 57.50 + # step3 = 14859.58 - 57.50 = 14802.08 + # Step 4 -Determine wh amount from tables + # step4 = 4.42 + ((5.75 / 100.00) * (14802.08 - 135.00)) + # Add additional_wh + # wh = 847.7771 + 12.50 = 860.2771 + wh = -860.28 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('GA'), + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 Georgia 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'], self.GA_UNEMP_MAX_WAGE * self.GA_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + remaining_GA_UNEMP_wages = 0.0 # We already reached max unemployment wages. + + self._log('2019 Georgia 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_GA_UNEMP_wages * self.GA_UNEMP) + + + def test_taxes_monthly_head_of_household(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'head of household' + additional_wh = 15.00 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for head of household monthly is 383.50 + # step1 = 25000.00 - 383.50 = 24616.5 + # Step 2 - Subtract personal allowance from step1. Allowance for head of household monthly is 225.00 + # step2 = 24616.5 - 225.00 = 24391.5 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 250.00 + # step3 = 24391.5 - (2 * 250.00) = 23891.5 + # Step 4 - Determine wh amount from tables + # step4 = 28.33 + ((5.75 / 100.00) * (23891.5 - 833.00)) = 1354.19375 + # Add additional_wh + # wh = 1354.19375 + 15.00 = 1369.19375 + wh = -1369.19 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('GA'), + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self.assertEqual(contract.schedule_pay, 'monthly') + + self._log('2019 Georgia 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'], self.GA_UNEMP_MAX_WAGE * self.GA_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + remaining_GA_UNEMP_wages = 0.0 # We already reached max unemployment wages. + + self._log('2019 Georgia 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_GA_UNEMP_wages * self.GA_UNEMP) + + def test_taxes_exempt(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'exempt' + additional_wh = 15.00 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('GA'), + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self._log('2019 Georgia tax first payslip exempt:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('EE_US_SIT', 0), 0) diff --git a/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py new file mode 100755 index 00000000..6debc2ca --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py @@ -0,0 +1,148 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsGAPayslip(TestUsPayslip): + + # TAXES AND RATES + GA_UNEMP_MAX_WAGE = 9500.00 + GA_UNEMP = 2.70 + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + ga_g4_sit_dependent_allowances=0, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=None, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + ga_g4_sit_dependent_allowances=ga_g4_sit_dependent_allowances, + ga_g4_sit_additional_allowances=ga_g4_sit_additional_allowances, + ga_g4_sit_filing_status=ga_g4_sit_filing_status, + state_id=self.get_us_state('GA'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected) + return payslip + + def test_taxes_weekly_single_with_additional_wh(self): + self._test_er_suta('GA', self.GA_UNEMP, date(2020, 1, 1), wage_base=self.GA_UNEMP_MAX_WAGE) + salary = 15000.00 + schedule_pay = 'weekly' + allowances = 1 + filing_status = 'single' + additional_wh = 12.50 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for single weekly is 88.50 + # step1 = 15000.00 - 88.50 = 14911.5 + # Step 2 - Subtract personal allowance from step1. Allowance for single weekly is 51.92 + # step2 = step1 - 51.92 = 14859.58 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 57.50 + # step3 = 14859.58 - 57.50 = 14802.08 + # Step 4 -Determine wh amount from tables + # step4 = 4.42 + ((5.75 / 100.00) * (14802.08 - 135.00)) + # Add additional_wh + # wh = 847.7771 + 12.50 = 860.2771 + wh = 860.28 + + self._run_test_sit(wage=salary, + schedule_pay=schedule_pay, + state_income_tax_additional_withholding=additional_wh, + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + expected=wh, + ) + + + def test_taxes_monthly_head_of_household(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'head of household' + additional_wh = 15.00 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for head of household monthly is 383.50 + # step1 = 25000.00 - 383.50 = 24616.5 + # Step 2 - Subtract personal allowance from step1. Allowance for head of household monthly is 225.00 + # step2 = 24616.5 - 225.00 = 24391.5 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 250.00 + # step3 = 24391.5 - (2 * 250.00) = 23891.5 + # Step 4 - Determine wh amount from tables + # step4 = 28.33 + ((5.75 / 100.00) * (23891.5 - 833.00)) = 1354.19375 + # Add additional_wh + # wh = 1354.19375 + 15.00 = 1369.19375 + wh = 1369.19 + + self._run_test_sit(wage=salary, + schedule_pay=schedule_pay, + state_income_tax_additional_withholding=additional_wh, + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + expected=wh, + ) + + # additional from external calculator + self._run_test_sit(wage=425.0, + schedule_pay='weekly', + state_income_tax_additional_withholding=0.0, + ga_g4_sit_dependent_allowances=1, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status='married filing separate', + expected=11.45, + ) + + self._run_test_sit(wage=3000.0, + schedule_pay='quarterly', + state_income_tax_additional_withholding=0.0, + ga_g4_sit_dependent_allowances=1, + ga_g4_sit_additional_allowances=1, + ga_g4_sit_filing_status='single', + expected=0.0, + ) + + # TODO 'married filing joint, both spouses working' returns lower than calculator + # TODO 'married filing joint, one spouse working' returns lower than calculator + + def test_taxes_exempt(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'exempt' + additional_wh = 15.00 + + self._run_test_sit(wage=salary, + schedule_pay=schedule_pay, + state_income_tax_additional_withholding=additional_wh, + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + expected=0.0, + ) diff --git a/l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2019.py new file mode 100644 index 00000000..13f1f2b5 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2019.py @@ -0,0 +1,93 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsHIPayslip(TestUsPayslip): + + # TAXES AND RATES + HI_UNEMP_MAX_WAGE = 46800.00 + HI_UNEMP = -(2.40 / 100.0) + + def test_taxes_single_weekly(self): + salary = 375.00 + schedule_pay = 'weekly' + filing_status = 'single' + allowances = 3 + wh_to_check = -15.3 + # Taxable income = (wage * payperiod ) - (allownaces * personal_exemption) + # taxable_income = (375 * 52) - (3 * 1144) = 16068 + # Last = row[0] = 692 + # withholding = row[1] + ((row[2] / 100.0) * (taxable_income - last)) + # withholding = 682 + ((6.80 / 100.0 ) * (16068 - 14400)) = 795.42 + # wh_to_check = 795.42/52 = 15.3 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('HI'), + hi_hw4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=0.0, + hi_hw4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + self._log('2019 Hawaii tax first payslip single:') + 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.HI_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_check) + + process_payslip(payslip) + + remaining_id_unemp_wages = self.HI_UNEMP_MAX_WAGE - salary if (self.HI_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Hawaii tax second payslip single:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_id_unemp_wages * self.HI_UNEMP) + + def test_taxes_married_monthly(self): + salary = 5000.00 + schedule_pay = 'monthly' + filing_status = 'married' + allowances = 2 + wh_to_check = -287.1 + # Taxable income = (wage * payperiod ) - (allownaces * personal_exemption) + # taxable_income = (5000 * 12) - (2 * 1144) = 57712 + # Last = row[0] = 48000 + # withholding = row[1] + ((row[2] / 100.0) * (taxable_income - last)) + # withholding = 2707 + ((7.70 / 100.0 ) * (57712 - 48000)) = 3445.112 + # wh_to_check = 3445.112/52 = 287.092 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('HI'), + hi_hw4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=0.0, + hi_hw4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + self._log('2019 Hawaii 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.HI_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_check) + + process_payslip(payslip) + + remaining_id_unemp_wages = self.HI_UNEMP_MAX_WAGE - salary if (self.HI_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Hawaii 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_id_unemp_wages * self.HI_UNEMP) + diff --git a/l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2020.py new file mode 100755 index 00000000..9684c52d --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_hi_hawaii_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 TestUsHIPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + HI_UNEMP_MAX_WAGE = 48100.00 + HI_UNEMP = 2.4 + + def _test_sit(self, wage, filing_status, additional_withholding, allowances, schedule_pay, date_start, expected_withholding): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('HI'), + hi_hw4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_withholding, + hi_hw4_sit_allowances=allowances, + 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('HI', self.HI_UNEMP, date(2020, 1, 1), wage_base=self.HI_UNEMP_MAX_WAGE) + self._test_sit(375.0, 'single', 0.0, 3.0, 'weekly', date(2020, 1, 1), 15.3) + self._test_sit(5000.0, 'married', 0.0, 2.0, 'monthly', date(2020, 1, 1), 287.1) + self._test_sit(5000.0, 'married', 10.0, 2.0, 'monthly', date(2020, 1, 1), 297.1) + self._test_sit(50000.0, 'head_of_household', 0.0, 3.0, 'weekly', date(2020, 1, 1), 3933.65) diff --git a/l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2019.py new file mode 100644 index 00000000..cb3bccfd --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2019.py @@ -0,0 +1,152 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +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, + wage=wages, + state_id=self.get_us_state('IA'), + state_income_tax_additional_withholding=additional_wh, + ia_w4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + 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_941_FIT'] + self.assertPayrollAlmostEqual(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.assertPayrollAlmostEqual(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.assertPayrollAlmostEqual(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.assertPayrollAlmostEqual(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.assertPayrollAlmostEqual(t5_to_test, -1637.61) + + self.assertPayrollEqual(cats['ER_US_SUTA'], wages * self.IA_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], t5_to_test) + + + # 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['ER_US_SUTA'], 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, + wage=wages, + state_id=self.get_us_state('IA'), + state_income_tax_additional_withholding=additional_wh, + ia_w4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + 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_941_FIT'] + # 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['ER_US_SUTA'], wages * self.IA_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], 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 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wages, + state_id=self.get_us_state('IA'), + state_income_tax_additional_withholding=additional_wh, + ia_w4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + 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_941_FIT'] + # 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['ER_US_SUTA'], wages * self.IA_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], t5_to_test) + + process_payslip(payslip) \ No newline at end of file diff --git a/l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2020.py new file mode 100755 index 00000000..d5d66b16 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2020.py @@ -0,0 +1,33 @@ +# 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 TestUsIAPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + IA_UNEMP_MAX_WAGE = 31600.00 + IA_UNEMP = 1.0 + + def _test_sit(self, wage, additional_withholding, allowances, schedule_pay, date_start, expected_withholding): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('IA'), + state_income_tax_additional_withholding=additional_withholding, + ia_w4_sit_allowances=allowances, + 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('IA', self.IA_UNEMP, date(2020, 1, 1), wage_base=self.IA_UNEMP_MAX_WAGE) + self._test_sit(3000.0, 0.0, 1.0, 'bi-weekly', date(2020, 1, 1), 146.68) + self._test_sit(3000.0, 10.0, 1.0, 'bi-weekly', date(2020, 1, 1), 156.68) + self._test_sit(30000.0, 0.0, 1.0, 'weekly', date(2020, 1, 1), 1640.04) diff --git a/l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2019.py new file mode 100644 index 00000000..8e3576d6 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2019.py @@ -0,0 +1,85 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsIDPayslip(TestUsPayslip): + + # TAXES AND RATES + ID_UNEMP_MAX_WAGE = 40000.00 + ID_UNEMP = -(1.00 / 100.0) + + def test_taxes_single_biweekly(self): + salary = 1212.00 + schedule_pay = 'bi-weekly' + filing_status = 'single' + allowances = 4 + # SEE https://tax.idaho.gov/i-1026.cfm?seg=compute for example calculations + wh_to_check = -10.00 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('ID'), + id_w4_sit_filing_status=filing_status, + id_w4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + self._log('2019 Idaho tax first payslip single:') + 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.ID_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_check) + + process_payslip(payslip) + + remaining_id_unemp_wages = self.ID_UNEMP_MAX_WAGE - salary if (self.ID_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Idaho tax second payslip single:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_id_unemp_wages * self.ID_UNEMP) + + def test_taxes_married_monthly(self): + salary = 5000.00 + schedule_pay = 'monthly' + filing_status = 'married' + allowances = 2 + + # ICTCAT says monthly allowances are 246.67 + # we have 2 so 246.67 * 2 = 493.34 + # 5000.00 - 493.34 = 4506.66 + # Wh is 89$ plus 6.925% over 3959,00 + # 126.92545499999999 - > 127.0 + wh_to_check = -127.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('ID'), + id_w4_sit_filing_status=filing_status, + id_w4_sit_allowances=allowances, + schedule_pay=schedule_pay) + + self._log('2019 Idaho 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.ID_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_check) + + process_payslip(payslip) + + remaining_id_unemp_wages = self.ID_UNEMP_MAX_WAGE - salary if (self.ID_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Idaho 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_id_unemp_wages * self.ID_UNEMP) diff --git a/l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2020.py new file mode 100755 index 00000000..bf687080 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_id_idaho_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 TestUsIDPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + ID_UNEMP_MAX_WAGE = 41600.00 + ID_UNEMP = 1.0 + + def _test_sit(self, wage, filing_status, allowances, schedule_pay, date_start, expected_withholding): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('ID'), + id_w4_sit_filing_status=filing_status, + id_w4_sit_allowances=allowances, + 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('ID', self.ID_UNEMP, date(2020, 1, 1), wage_base=self.ID_UNEMP_MAX_WAGE) + self._test_sit(1212.0, 'single', 4.0, 'bi-weekly', date(2020, 1, 1), 10.0) + self._test_sit(10000.0, 'married', 1.0, 'annually', date(2020, 1, 1), 0.0) + self._test_sit(52000.0, 'married', 4.0, 'monthly', date(2020, 1, 1), 3348.02) + self._test_sit(5000.0, 'head of household', 0.0, 'semi-monthly', date(2020, 1, 1), 300.0) + self._test_sit(5900.0, 'single', 5.0, 'weekly', date(2020, 1, 1), 367.0) diff --git a/l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2019.py new file mode 100644 index 00000000..ba633607 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2019.py @@ -0,0 +1,71 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsILPayslip(TestUsPayslip): + # TAXES AND RATES + IL_UNEMP_MAX_WAGE = 12960.00 + IL_UNEMP = -(3.175 / 100.0) + + def test_taxes_monthly(self): + salary = 15000.00 + schedule_pay = 'monthly' + basic_allowances = 1 + additional_allowances = 1 + flat_rate = (4.95 / 100) + wh_to_test = -(flat_rate * (salary - ((basic_allowances * 2275 + additional_allowances * 1000) / 12.0))) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('IL'), + state_income_tax_additional_withholding=0.0, + il_w4_sit_basic_allowances=1.0, + il_w4_sit_additional_allowances=1.0, + schedule_pay='monthly') + + self._log('2019 Illinois 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'], self.IL_UNEMP_MAX_WAGE * self.IL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_test) + + process_payslip(payslip) + + remaining_IL_UNEMP_wages = 0.0 # We already reached max unemployment wages. + + self._log('2019 Illinois 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_IL_UNEMP_wages * self.IL_UNEMP) + + def test_taxes_with_additional_wh(self): + salary = 15000.00 + schedule_pay = 'monthly' + basic_allowances = 1 + additional_allowances = 1 + additional_wh = 15.0 + flat_rate = (4.95 / 100) + wh_to_test = -(flat_rate * (salary - ((basic_allowances * 2275 + additional_allowances * 1000) / 12.0)) + additional_wh) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('IL'), + state_income_tax_additional_withholding=15.0, + il_w4_sit_basic_allowances=1.0, + il_w4_sit_additional_allowances=1.0, + schedule_pay='monthly') + + self._log('2019 Illinois 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'], self.IL_UNEMP_MAX_WAGE * self.IL_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh_to_test) diff --git a/l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2020.py new file mode 100644 index 00000000..ead932e4 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_il_illinois_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 TestUsILPayslip(TestUsPayslip): + # Taxes and Rates + IL_UNEMP_MAX_WAGE = 12740.0 + IL_UNEMP = 3.125 + + def _test_sit(self, wage, additional_withholding, basic_allowances, additional_allowances, schedule_pay, date_start, expected_withholding): + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('IL'), + state_income_tax_additional_withholding=additional_withholding, + il_w4_sit_basic_allowances=basic_allowances, + il_w4_sit_additional_allowances=additional_allowances, + 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('IL', self.IL_UNEMP, date(2020, 1, 1), wage_base=self.IL_UNEMP_MAX_WAGE) + self._test_sit(800.0, 0.0, 2, 2, 'weekly', date(2020, 1, 1), 33.27) + self._test_sit(800.0, 10.0, 2, 2, 'weekly', date(2020, 1, 1), 43.27) + self._test_sit(2500.0, 0.0, 1, 1, 'monthly', date(2020, 1, 1), 110.04) + self._test_sit(2500.0, 0.0, 0, 0, 'monthly', date(2020, 1, 1), 123.75) + self._test_sit(3000.0, 15.0, 0, 0, 'quarterly', date(2020, 1, 1), 163.50) + 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/tests/test_us_mn_minnesota_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mn_minnesota_payslip_2019.py new file mode 100755 index 00000000..2a64b57d --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mn_minnesota_payslip_2019.py @@ -0,0 +1,159 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsMNPayslip(TestUsPayslip): + # TAXES AND RATES + MN_UNEMP_MAX_WAGE = 34000.0 + MN_UNEMP = -1.11 / 100.0 + + def test_taxes_weekly(self): + salary = 30000.0 + # 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, + wage=salary, + state_id=self.get_us_state('MN'), + mn_w4mn_sit_filing_status='single', + state_income_tax_additional_withholding=0.0, + mn_w4mn_sit_allowances=1.0, + 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['ER_US_SUTA'], salary * self.MN_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) # Test numbers are off by 1 penny + + 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['ER_US_SUTA'], remaining_MN_UNEMP_wages * self.MN_UNEMP) + + def test_taxes_married(self): + salary = 5000.00 + + # 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, + wage=salary, + state_id=self.get_us_state('MN'), + mn_w4mn_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + mn_w4mn_sit_allowances=1.0, + schedule_pay='weekly') + + 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['ER_US_SUTA'], salary * self.MN_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) + + def test_taxes_semimonthly(self): + salary = 6500.00 + # 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, + wage=salary, + state_id=self.get_us_state('MN'), + mn_w4mn_sit_filing_status='single', + state_income_tax_additional_withholding=0.0, + mn_w4mn_sit_allowances=1.0, + 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['ER_US_SUTA'], salary * self.MN_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) + + def test_tax_exempt(self): + salary = 5500.00 + wh = 0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MN'), + mn_w4mn_sit_filing_status='', + state_income_tax_additional_withholding=0.0, + mn_w4mn_sit_allowances=2.0, + 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['ER_US_SUTA'], salary * self.MN_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) + + def test_additional_withholding(self): + salary = 5500.0 + # 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 = -476.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MN'), + mn_w4mn_sit_filing_status='single', + state_income_tax_additional_withholding=40.0, + mn_w4mn_sit_allowances=2.0, + 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['ER_US_SUTA'], salary * self.MN_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) diff --git a/l10n_us_hr_payroll/tests/test_us_mn_minnesota_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mn_minnesota_payslip_2020.py new file mode 100755 index 00000000..c91fa2a8 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mn_minnesota_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 TestUsMNPayslip(TestUsPayslip): + # TAXES AND RATES + MN_UNEMP_MAX_WAGE = 35000.0 + MN_UNEMP = 1.11 + + 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('MN'), + mn_w4mn_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_withholding, + mn_w4mn_sit_allowances=allowances, + 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('MN', self.MN_UNEMP, date(2020, 1, 1), wage_base=self.MN_UNEMP_MAX_WAGE) + self._test_sit(5000.0, 'single', 1.0, 0.0, 'weekly', date(2020, 1, 1), 389.0) + self._test_sit(30000.0, 'single', 1.0, 0.0, 'weekly', date(2020, 1, 1), 2850.99) + self._test_sit(5000.0, 'married', 1.0, 0.0, 'weekly', date(2020, 1, 1), 325.0) + self._test_sit(6500.0, 'single', 1.0, 0.0, 'semi-monthly', date(2020, 1, 1), 429.0) + self._test_sit(5500.0, '', 2.0, 0.0, 'weekly', date(2020, 1, 1), 0.0) + self._test_sit(5500.0, 'single', 2.0, 40.0, 'weekly', date(2020, 1, 1), 470.0) diff --git a/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py new file mode 100755 index 00000000..27a0ad93 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py @@ -0,0 +1,188 @@ + +from datetime import date +from .common import TestUsPayslip + + +class TestUsMoPayslip(TestUsPayslip): + # Calculations from http://dor.mo.gov/forms/4282_2019.pdf + SALARY = 12000.0 + MO_UNEMP = -2.376 / 100.0 + + TAX = [ + (1053.0, 1.5), + (1053.0, 2.0), + (1053.0, 2.5), + (1053.0, 3.0), + (1053.0, 3.5), + (1053.0, 4.0), + (1053.0, 4.5), + (1053.0, 5.0), + (999999999.0, 5.4), + ] + + def test_2019_taxes_single(self): + # Payroll Period Monthly + salary = self.SALARY + pp = 12.0 + gross_salary = salary * pp + spouse_employed = False + + # Single + standard_deduction = 12400.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MO'), + mo_mow4_sit_filing_status='single', + state_income_tax_additional_withholding=0.0, + schedule_pay='monthly') + + self._log('2019 Missouri tax single 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.MO_UNEMP) + + mo_taxable_income = gross_salary - standard_deduction + self._log('%s = %s - %s -' % (mo_taxable_income, gross_salary, standard_deduction)) + + remaining_taxable_income = mo_taxable_income + tax = 0.0 + for amt, rate in self.TAX: + amt = float(amt) + rate = rate / 100.0 + self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income)) + if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0: + tax += rate * amt + else: + tax += rate * remaining_taxable_income + break + remaining_taxable_income = remaining_taxable_income - amt + + tax = -tax + self._log('Computed annual tax: ' + str(tax)) + + tax = tax / pp + tax = round(tax) + self._log('Computed period tax: ' + str(tax)) + self.assertPayrollEqual(cats['EE_US_SIT'], tax) + + def test_2019_spouse_not_employed(self): + # Payroll Period Semi-monthly + salary = self.SALARY + pp = 24.0 + gross_salary = salary * pp + + # Married + standard_deduction = 24800.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MO'), + mo_mow4_sit_filing_status='married', + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2019 Missouri tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + mo_taxable_income = gross_salary - standard_deduction + self._log(mo_taxable_income) + + remaining_taxable_income = mo_taxable_income + tax = 0.0 + for amt, rate in self.TAX: + amt = float(amt) + rate = rate / 100.0 + self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income)) + if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0: + tax += rate * amt + else: + tax += rate * remaining_taxable_income + break + remaining_taxable_income = remaining_taxable_income - amt + + tax = -tax + self._log('Computed annual tax: ' + str(tax)) + + tax = tax / pp + tax = round(tax) + self._log('Computed period tax: ' + str(tax)) + self.assertPayrollEqual(cats['EE_US_SIT'], tax) + + def test_2019_head_of_household(self): + # Payroll Period Weekly + salary = self.SALARY + + # Payroll Period Weekly + salary = self.SALARY + pp = 52.0 + gross_salary = salary * pp + + # Single HoH + standard_deduction = 18650.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MO'), + mo_mow4_sit_filing_status='head_of_household', + state_income_tax_additional_withholding=0.0, + schedule_pay='weekly') + + self._log('2019 Missouri tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + mo_taxable_income = gross_salary - standard_deduction + self._log(mo_taxable_income) + + remaining_taxable_income = mo_taxable_income + tax = 0.0 + for amt, rate in self.TAX: + amt = float(amt) + rate = rate / 100.0 + self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income)) + if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0: + tax += rate * amt + else: + tax += rate * remaining_taxable_income + break + remaining_taxable_income = remaining_taxable_income - amt + tax = -tax + self._log('Computed annual tax: ' + str(tax)) + + tax = tax / pp + tax = round(tax) + self._log('Computed period tax: ' + str(tax)) + self.assertPayrollEqual(cats['EE_US_SIT'], tax) + + def test_2019_underflow(self): + # Payroll Period Weekly + salary = 200.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MO')) + + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SIT'], 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py new file mode 100755 index 00000000..164b0f0f --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py @@ -0,0 +1,105 @@ + +from datetime import date +from .common import TestUsPayslip + + +class TestUsMoPayslip(TestUsPayslip): + # Calculations from http://dor.mo.gov/forms/4282_2020.pdf + MO_UNEMP_MAX_WAGE = 11500.0 + MO_UNEMP = 2.376 + + TAX = [ + (1073.0, 1.5), + (1073.0, 2.0), + (1073.0, 2.5), + (1073.0, 3.0), + (1073.0, 3.5), + (1073.0, 4.0), + (1073.0, 4.5), + (1073.0, 5.0), + ( 'inf', 5.4), + ] + STD_DED = { + '': 0.0, # Exempt + 'single': 12400.0, + 'married': 24800.0, + 'head_of_household': 18650.0, + } + + def _test_sit(self, filing_status, schedule_pay): + wage = 5000.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('MO'), + mo_mow4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=0.0, + schedule_pay=schedule_pay) + + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + pp = payslip.get_pay_periods_in_year() + gross_salary = wage * pp + standard_deduction = self.STD_DED[filing_status] + + mo_taxable_income = gross_salary - standard_deduction + self._log('%s = %s - %s -' % (mo_taxable_income, gross_salary, standard_deduction)) + + remaining_taxable_income = mo_taxable_income + tax = 0.0 + for amt, rate in self.TAX: + amt = float(amt) + rate = rate / 100.0 + self._log(str(amt) + ' : ' + str(rate) + ' : ' + str(remaining_taxable_income)) + if (remaining_taxable_income - amt) > 0.0 or (remaining_taxable_income - amt) == 0.0: + tax += rate * amt + else: + tax += rate * remaining_taxable_income + break + remaining_taxable_income = remaining_taxable_income - amt + + tax = -tax + self._log('Computed annual tax: ' + str(tax)) + + tax = tax / pp + tax = round(tax) + self._log('Computed period tax: ' + str(tax)) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), tax if filing_status else 0.0) + + contract.us_payroll_config_id.state_income_tax_additional_withholding = 100.0 + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), (tax - 100.0) if filing_status else 0.0) + + contract.us_payroll_config_id.mo_mow4_sit_withholding = 200.0 + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), -200.0 if filing_status else 0.0) + + def test_2020_taxes_single(self): + self._test_er_suta('MO', self.MO_UNEMP, date(2020, 1, 1), wage_base=self.MO_UNEMP_MAX_WAGE) + self._test_sit('single', 'weekly') + + def test_2020_spouse_not_employed(self): + self._test_sit('married', 'semi-monthly') + + def test_2020_head_of_household(self): + self._test_sit('head_of_household', 'monthly') + + def test_2020_underflow(self): + # Payroll Period Weekly + salary = 200.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MO')) + + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SIT'], 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py new file mode 100755 index 00000000..e7ce35d0 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py @@ -0,0 +1,94 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip + + +class TestUsMsPayslip(TestUsPayslip): + # Calculations from https://www.dor.ms.gov/Documents/Computer%20Payroll%20Accounting%201-2-19.pdf + MS_UNEMP = -1.2 / 100.0 + + def test_2019_taxes_one(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='head_of_household', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2019 Mississippi tax single 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.MS_UNEMP) + + STDED = 3400.0 # Head of Household + AGP = salary * 24 # Semi-Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 15600.0) + TAX = ((TI - 10000) * 0.05) + 290 # Over 10,000 + self.assertPayrollEqual(TAX, 570.0) + + ms_withhold = round(TAX / 24) # Semi-Monthly + self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold) + + def test_2019_taxes_one_exempt(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2019 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2019_taxes_additional(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + additional = 40.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='head_of_household', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=additional, + schedule_pay='semi-monthly') + + self._log('2019 Mississippi tax single 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.MS_UNEMP) + + STDED = 3400.0 # Head of Household + AGP = salary * 24 # Semi-Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 15600.0) + TAX = ((TI - 10000) * 0.05) + 290 # Over 10,000 + self.assertPayrollEqual(TAX, 570.0) + + ms_withhold = round(TAX / 24) # Semi-Monthly + self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold + -additional) diff --git a/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py new file mode 100755 index 00000000..5942d7ad --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py @@ -0,0 +1,120 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsMsPayslip(TestUsPayslip): + # Calculations from https://www.dor.ms.gov/Documents/Computer%20Payroll%20Accounting%201-2-19.pdf + MS_UNEMP = 1.2 + MS_UNEMP_MAX_WAGE = 14000.0 + + def test_2020_taxes_one(self): + self._test_er_suta('MS', self.MS_UNEMP, date(2020, 1, 1), wage_base=self.MS_UNEMP_MAX_WAGE) + + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='head_of_household', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2020 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + STDED = 3400.0 # Head of Household + AGP = salary * 24 # Semi-Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 15600.0) + TAX = ((TI - 10000) * 0.05) + 260 # Over 10,000 + self.assertPayrollEqual(TAX, 540.0) + + ms_withhold = round(TAX / 24) # Semi-Monthly + self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold) + + def test_2020_taxes_one_exempt(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2020 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2020_taxes_additional(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + additional = 40.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='single', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=additional, + schedule_pay='monthly') + + self._log('2020 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + STDED = 2300.0 # Single + AGP = salary * 12 # Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 1700.0) + TAX = ((TI - 3000) * 0.03) + self.assertPayrollEqual(TAX, -39.0) + + ms_withhold = round(TAX / 12) # Monthly + self.assertTrue(ms_withhold <= 0.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -40.0) # only additional + + # Test with higher wage + salary = 1700.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='single', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=additional, + schedule_pay='monthly') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + STDED = 2300.0 # Single + AGP = salary * 12 # Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 7100.0) + TAX = ((TI - 5000) * 0.04) + 60.0 + self.assertPayrollEqual(TAX, 144.0) + + ms_withhold = round(TAX / 12) # Monthly + self.assertPayrollEqual(ms_withhold, 12.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -(ms_withhold + additional)) diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py new file mode 100755 index 00000000..ff6e2daf --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py @@ -0,0 +1,139 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsMtPayslip(TestUsPayslip): + # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705 + MT_UNEMP = -1.18 / 100.0 + MT_UNEMP_AFT = -0.13 / 100.0 + + def test_2019_taxes_one(self): + # Payroll Period Semi-Monthly example + salary = 550 + mt_mw4_exemptions = 5 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='semi-monthly') + + self._log('2019 Montana tax single 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.MT_UNEMP + self.MT_UNEMP_AFT)) # New non-combined... + + mt_taxable_income = salary - (79.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 155.0) + self.assertPayrollEqual(mt_withhold, 3.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_two(self): + # Payroll Period Bi-Weekly example + salary = 2950 + mt_mw4_exemptions = 2 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='bi-weekly') + + self._log('2019 Montana tax single 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'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2)) + + # Note!! + # The example calculation uses A = 16 but the actual table describes this as A = 18 + mt_taxable_income = salary - (73.0 * mt_mw4_exemptions) + mt_withhold = round(18 + (0.06 * (mt_taxable_income - 577))) + self.assertPayrollEqual(mt_taxable_income, 2804.0) + self.assertPayrollEqual(mt_withhold, 152.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_three(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='weekly') + + self._log('2019 Montana tax single 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'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2)) + + mt_taxable_income = salary - (37.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 98.0) + self.assertPayrollEqual(mt_withhold, 2.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_three_exempt(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + mt_mw4_sit_exempt='reserve', + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2019_taxes_three_additional(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + mt_mw4_additional_withholding = 20.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + state_income_tax_additional_withholding=mt_mw4_additional_withholding, + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + cats = self._getCategories(payslip) + + mt_taxable_income = salary - (37.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 98.0) + self.assertPayrollEqual(mt_withhold, 2.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold + -mt_mw4_additional_withholding) diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py new file mode 100755 index 00000000..ec861a0d --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py @@ -0,0 +1,17 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsMtPayslip(TestUsPayslip): + # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705 + MT_UNEMP_WAGE_MAX = 34100.0 + MT_UNEMP = 1.18 + MT_UNEMP_AFT = 0.13 + + def test_2020_taxes_one(self): + combined_rate = self.MT_UNEMP + self.MT_UNEMP_AFT # Combined for test as they both go to the same category and have the same cap + self._test_er_suta('MT', combined_rate, date(2020, 1, 1), wage_base=self.MT_UNEMP_WAGE_MAX) + + # TODO Montana Incometax rates for 2020 when released 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/tests/test_us_nh_new_hampshire_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_nh_new_hampshire_payslip_2020.py new file mode 100644 index 00000000..1d85e700 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_nh_new_hampshire_payslip_2020.py @@ -0,0 +1,13 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsNHPayslip(TestUsPayslip): + # TAXES AND RATES + NH_UNEMP_MAX_WAGE = 14000.00 + NH_UNEMP = 1.2 + + def test_2020_taxes(self): + self._test_er_suta('NH', self.NH_UNEMP, date(2020, 1, 1), wage_base=self.NH_UNEMP_MAX_WAGE) diff --git a/l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2019.py new file mode 100755 index 00000000..c28849b5 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2019.py @@ -0,0 +1,128 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsNJPayslip(TestUsPayslip): + ### + # 2019 Taxes and Rates + ### + NJ_UNEMP_MAX_WAGE = 34400.0 # Note that this is used for SDI and FLI as well + + ER_NJ_UNEMP = -2.6825 / 100.0 + EE_NJ_UNEMP = -0.3825 / 100.0 + + ER_NJ_SDI = -0.5 / 100.0 + EE_NJ_SDI = -0.17 / 100.0 + + ER_NJ_WF = -0.1175 / 100.0 + EE_NJ_WF = -0.0425 / 100.0 + + ER_NJ_FLI = 0.0 + EE_NJ_FLI = -0.08 / 100.0 + + # Examples found on page 24 of http://www.state.nj.us/treasury/taxation/pdf/current/njwt.pdf + def test_2019_taxes_example1(self): + salary = 300 + + # Tax Percentage Method for Single, taxable under $385 + wh = -4.21 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NJ'), + nj_njw4_sit_filing_status='single', + nj_njw4_sit_allowances=1, + state_income_tax_additional_withholding=0.0, + nj_njw4_sit_rate_table='A', + schedule_pay='weekly') + + self._log('2019 New Jersey tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SUTA'], salary * (self.EE_NJ_UNEMP + self.EE_NJ_SDI + self.EE_NJ_WF + self.EE_NJ_FLI)) + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * (self.ER_NJ_UNEMP + self.ER_NJ_SDI + self.ER_NJ_WF + self.ER_NJ_FLI)) + self.assertTrue(all((cats['EE_US_SUTA'], cats['ER_US_SUTA']))) + # self.assertPayrollEqual(cats['EE_US_NJ_SDI_SIT'], cats['EE_US_NJ_SDI_SIT'] * self.EE_NJ_SDI) + # self.assertPayrollEqual(cats['ER_US_NJ_SDI_SUTA'], cats['ER_US_NJ_SDI_SUTA'] * self.ER_NJ_SDI) + # self.assertPayrollEqual(cats['EE_US_NJ_FLI_SIT'], cats['EE_US_NJ_FLI_SIT'] * self.EE_NJ_FLI) + # self.assertPayrollEqual(cats['EE_US_NJ_WF_SIT'], cats['EE_US_NJ_WF_SIT'] * self.EE_NJ_WF) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + # # Make a new payslip, this one will have maximums + # + remaining_nj_unemp_wages = self.NJ_UNEMP_MAX_WAGE - salary if (self.NJ_UNEMP_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2019 New Jersey tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + # self.assertPayrollEqual(cats['WAGE_US_NJ_UNEMP'], remaining_nj_unemp_wages) + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_nj_unemp_wages * (self.ER_NJ_UNEMP + self.ER_NJ_SDI + self.ER_NJ_WF + self.ER_NJ_FLI)) + self.assertPayrollEqual(cats['EE_US_SUTA'], remaining_nj_unemp_wages * (self.EE_NJ_UNEMP + self.EE_NJ_SDI + self.EE_NJ_WF + self.EE_NJ_FLI)) + + def test_2019_taxes_example2(self): + salary = 1400.00 + + # Tax Percentage Method for Single, taxable pay over $962, under $1346 + wh = -27.58 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NJ'), + nj_njw4_sit_filing_status='married_separate', + nj_njw4_sit_allowances=3, + state_income_tax_additional_withholding=0.0, + #nj_njw4_sit_rate_table='B', + schedule_pay='weekly') + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 New Jersey tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + + def test_2019_taxes_to_the_limits(self): + salary = 30000.00 + + # Tax Percentage Method for Single, taxable pay over $18750, under 125000 + wh = -1467.51 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('NJ'), + nj_njw4_sit_filing_status='married_joint', + nj_njw4_sit_allowances=3, + state_income_tax_additional_withholding=0.0, + # nj_njw4_sit_rate_table='B', + schedule_pay='quarterly') + + self.assertEqual(contract.schedule_pay, 'quarterly') + + self._log('2019 New Jersey tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-03-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SIT'], wh) diff --git a/l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2020.py new file mode 100755 index 00000000..79e0b861 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2020.py @@ -0,0 +1,48 @@ +# 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 TestUsNJPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + NJ_UNEMP_MAX_WAGE = 35300.0 # Note that this is used for SDI and FLI as well + + ER_NJ_UNEMP = 2.6825 + EE_NJ_UNEMP = 0.3825 + + ER_NJ_SDI = 0.5 + EE_NJ_SDI = 0.26 + + ER_NJ_WF = 0.1175 + EE_NJ_WF = 0.0425 + + ER_NJ_FLI = 0.0 + EE_NJ_FLI = 0.16 + + def _test_sit(self, wage, filing_status, allowances, schedule_pay, date_start, expected_withholding, rate_table=False): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('NJ'), + nj_njw4_sit_filing_status=filing_status, + nj_njw4_sit_allowances=allowances, + state_income_tax_additional_withholding=0.0, + nj_njw4_sit_rate_table=rate_table, + schedule_pay=schedule_pay) + payslip = self._createPayslip(employee, date_start, date_start + timedelta(days=7)) + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected_withholding if filing_status else 0.0) + + def test_2020_taxes_example1(self): + combined_er_rate = self.ER_NJ_UNEMP + self.ER_NJ_FLI + self.ER_NJ_SDI + self.ER_NJ_WF + self._test_er_suta('NJ', combined_er_rate, date(2020, 1, 1), wage_base=self.NJ_UNEMP_MAX_WAGE) + combined_ee_rate = self.EE_NJ_UNEMP + self.EE_NJ_FLI + self.EE_NJ_SDI + self.EE_NJ_WF + self._test_ee_suta('NJ', combined_ee_rate, date(2020, 1, 1), wage_base=self.NJ_UNEMP_MAX_WAGE, relaxed=True) + # these expected values come from https://www.state.nj.us/treasury/taxation/pdf/current/njwt.pdf + self._test_sit(300.0, 'single', 1, 'weekly', date(2020, 1, 1), 4.21) + self._test_sit(375.0, 'married_separate', 3, 'weekly', date(2020, 1, 1), 4.76) + self._test_sit(1400.0, 'head_household', 3, 'weekly', date(2020, 1, 1), 27.60) diff --git a/l10n_us_hr_payroll/tests/test_us_nm_new_mexico_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_nm_new_mexico_payslip_2020.py new file mode 100755 index 00000000..0ab6c321 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_nm_new_mexico_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 TestUsNMPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + NM_UNEMP_MAX_WAGE = 25800.0 + NM_UNEMP = 1.0 + # Calculation based on section 17. https://s3.amazonaws.com/realFile34821a95-73ca-43e7-b06d-fad20f5183fd/a9bf1098-533b-4a3d-806a-4bf6336af6e4?response-content-disposition=filename%3D%22FYI-104+-+New+Mexico+Withholding+Tax+-+Effective+January+1%2C+2020.pdf%22&response-content-type=application%2Fpdf&AWSAccessKeyId=AKIAJBI25DHBYGD7I7TA&Signature=feu%2F1oJvU6BciRfKcoR0iNxoVZE%3D&Expires=1585159702 + + def _test_sit(self, wage, filing_status, additional_withholding, schedule_pay, date_start, expected_withholding): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state('NM'), + fed_941_fit_w4_filing_status=filing_status, + 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('NM', self.NM_UNEMP, date(2020, 1, 1), wage_base=self.NM_UNEMP_MAX_WAGE) + self._test_sit(1000.0, 'married', 0.0, 'weekly', date(2020, 1, 1), 29.47) + self._test_sit(1000.0, 'married', 10.0, 'weekly', date(2020, 1, 1), 39.47) + self._test_sit(25000.0, 'single', 0.0, 'bi-weekly', date(2020, 1, 1), 1202.60) + self._test_sit(25000.0, 'married_as_single', 0.0, 'monthly', date(2020, 1, 1), 1152.95) diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py new file mode 100755 index 00000000..d1f65f05 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py @@ -0,0 +1,96 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsOhPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + OH_UNEMP_MAX_WAGE = 9500.0 + OH_UNEMP = -2.7 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + # For formula here + # http://www.tax.ohio.gov/Portals/0/employer_withholding/August2015Rates/WTH_OptionalComputerFormula_073015.pdf + tw = salary * 12 # = 60000 + wd = ((tw - 40000) * 0.035 + 900) / 12 * 1.075 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + ) + + self._log('2019 Ohio 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.OH_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], -wd) # Off by 0.6 cents so it rounds off by a penny + #self.assertPayrollEqual(cats['EE_US_SIT'], -wd) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_oh_unemp_wages = self.OH_UNEMP_MAX_WAGE - salary if (self.OH_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Ohio 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_oh_unemp_wages * self.OH_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + external_wages=external_wages, + ) + + self._log('2019 Ohio_external 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'], (self.OH_UNEMP_MAX_WAGE - external_wages) * self.OH_UNEMP) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2019 Ohio exempt tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + # FUTA_TYPE_BASIC + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), salary * 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py new file mode 100755 index 00000000..9026da92 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py @@ -0,0 +1,108 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsOhPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + OH_UNEMP_MAX_WAGE = 9000.0 + OH_UNEMP = 2.7 + + def test_2020_taxes(self): + self._test_er_suta('OH', self.OH_UNEMP, date(2020, 1, 1), wage_base=self.OH_UNEMP_MAX_WAGE) + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=0, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + oh_it4_sit_exemptions=oh_it4_sit_exemptions, + state_id=self.get_us_state('OH'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected) + return payslip + + def test_2020_sit_1(self): + wage = 400.0 + exemptions = 1 + additional = 10.0 + pay_periods = 12.0 + annual_adjusted_wage = (wage * pay_periods) - (650.0 * exemptions) + self.assertPayrollEqual(4150.0, annual_adjusted_wage) + WD = ((annual_adjusted_wage * 0.005) / pay_periods) * 1.032 + self.assertPayrollEqual(WD, 1.7845) + expected = WD + additional + self._run_test_sit(wage=wage, + schedule_pay='monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=additional, + oh_it4_sit_exemptions=exemptions, + expected=expected, + ) + + # the above agrees with online calculator to the penny 0.01 + # below expected coming from calculator to 0.10 + # + # semi-monthly + self._run_test_sit(wage=1200, + schedule_pay='semi-monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=20.0, + oh_it4_sit_exemptions=2, + expected=42.58, + ) + + # bi-weekly + self._run_test_sit(wage=3000, + schedule_pay='bi-weekly', + state_income_tax_exempt=False, + #state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=0, + expected=88.51, + ) + # weekly + self._run_test_sit(wage=355, + schedule_pay='weekly', + state_income_tax_exempt=False, + # state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=1, + expected=4.87, + ) + + # Exempt! + self._run_test_sit(wage=355, + schedule_pay='weekly', + state_income_tax_exempt=True, + # state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=1, + expected=0.0, + ) diff --git a/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py new file mode 100755 index 00000000..ce7e4fb4 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py @@ -0,0 +1,33 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsPAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + PA_UNEMP_MAX_WAGE = 10000.0 + ER_PA_UNEMP = -3.6890 / 100.0 + EE_PA_UNEMP = -0.06 / 100.0 + PA_INC_WITHHOLD = 3.07 + + def test_2019_taxes(self): + salary = 4166.67 + wh = -127.92 + + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('PA')) + + self._log('2019 Pennsylvania tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SUTA'], cats['GROSS'] * self.EE_PA_UNEMP) + self.assertPayrollEqual(cats['ER_US_SUTA'], cats['GROSS'] * self.ER_PA_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) diff --git a/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py new file mode 100755 index 00000000..3dd3fd27 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py @@ -0,0 +1,43 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsPAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + PA_UNEMP_MAX_WAGE = 10000.0 + ER_PA_UNEMP = 3.6890 + EE_PA_UNEMP = 0.06 + PA_INC_WITHHOLD = 3.07 + + def test_2020_taxes(self): + self._test_er_suta('PA', self.ER_PA_UNEMP, date(2020, 1, 1), wage_base=self.PA_UNEMP_MAX_WAGE) + self._test_ee_suta('PA', self.EE_PA_UNEMP, date(2020, 1, 1)) + + salary = 4166.67 + wh = -127.92 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('PA')) + + self._log('2019 Pennsylvania tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + # Test Additional + contract.us_payroll_config_id.state_income_tax_additional_withholding = 100.0 + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_SIT'], wh - 100.0) + + # Test Exempt + contract.us_payroll_config_id.state_income_tax_exempt = True + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py new file mode 100644 index 00000000..db6004e9 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py @@ -0,0 +1,338 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + +from sys import float_info + + +class TestUsPayslip2019(TestUsPayslip): + # FUTA Constants + FUTA_RATE_NORMAL = 0.6 + FUTA_RATE_BASIC = 6.0 + FUTA_RATE_EXEMPT = 0.0 + + # Wage caps + FICA_SS_MAX_WAGE = 132900.0 + FICA_M_MAX_WAGE = float_info.max + FICA_M_ADD_START_WAGE = 200000.0 + FUTA_MAX_WAGE = 7000.0 + + # Rates + FICA_SS = 6.2 / -100.0 + FICA_M = 1.45 / -100.0 + FUTA = FUTA_RATE_NORMAL / -100.0 + FICA_M_ADD = 0.9 / -100.0 + + ### + # 2019 Taxes and Rates + ### + + def test_2019_taxes(self): + self.debug = False + # salary is high so that second payslip runs over max + # social security salary + salary = 80000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, wage=salary) + self._log(contract.read()) + + self._log('2018 tax last slip') + payslip = self._createPayslip(employee, '2018-12-01', '2018-12-31') + payslip.compute_sheet() + self._log(payslip.read()) + process_payslip(payslip) + + # Ensure amounts are there, they shouldn't be added in the next year... + cats = self._getCategories(payslip) + self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA) + + self._log('2019 tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + # Employee + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0) + # Employer + self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS']) + self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M']) + self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA) + + process_payslip(payslip) + + # Make a new payslip, this one will have reached Medicare Additional (employee only) + remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary + remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary + + self._log('2019 tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0) + + process_payslip(payslip) + + # Make a new payslip, this one will have reached Medicare Additional (employee only) + self._log('2019 tax third payslip:') + payslip = self._createPayslip(employee, '2019-03-01', '2019-03-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k + + process_payslip(payslip) + + # Make a new payslip, this one will have all salary as Medicare Additional + + self._log('2019 tax fourth payslip:') + payslip = self._createPayslip(employee, '2019-04-01', '2019-04-30') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD) + + process_payslip(payslip) + + def test_2019_fed_income_withholding_single(self): + self.debug = False + + salary = 6000.00 + schedule_pay = 'monthly' + w4_allowances = 3 + w4_allowance_amt = 350.00 * w4_allowances + adjusted_salary = salary - w4_allowance_amt # should be 4962.60, but would work over a wide value for the rate + ### + # Single MONTHLY form Publication 15 + expected_withholding = self.float_round(-(378.52 + ((adjusted_salary - 3606) * 0.22)), self.payroll_digits) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + schedule_pay=schedule_pay, + fed_941_fit_w4_filing_status='single', + fed_941_fit_w4_allowances=w4_allowances + ) + + self._log('2019 fed income single payslip: adjusted_salary: ' + str(adjusted_salary)) + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding) + + def test_2019_fed_income_withholding_married_as_single(self): + salary = 500.00 + schedule_pay = 'weekly' + w4_allowances = 1 + w4_allowance_amt = 80.80 * w4_allowances + adjusted_salary = salary - w4_allowance_amt # should be 420.50, but would work over a wide value for the rate + ### + expected_withholding = self.float_round(-(18.70 + ((adjusted_salary - 260) * 0.12)), self.payroll_digits) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + schedule_pay=schedule_pay, + fed_941_fit_w4_filing_status='married_as_single', + fed_941_fit_w4_allowances=w4_allowances, + ) + + self._log('2019 fed income married_as_single payslip: adjusted_salary: ' + str(adjusted_salary)) + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding) + + def test_2019_fed_income_withholding_married(self): + salary = 14000.00 + schedule_pay = 'bi-weekly' + w4_allowances = 2 + w4_allowance_amt = 161.50 * w4_allowances + adjusted_salary = salary - w4_allowance_amt # should be 13680.80, but would work over a wide value for the rate + ### + # Single MONTHLY form Publication 15 + expected_withholding = self.float_round(-(2519.06 + ((adjusted_salary - 12817) * 0.32)), self.payroll_digits) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + schedule_pay=schedule_pay, + fed_941_fit_w4_filing_status='married', + fed_941_fit_w4_allowances=w4_allowances + ) + + self._log('2019 fed income married payslip: adjusted_salary: ' + str(adjusted_salary)) + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # This is off by 1 penny given our new reporting of adjusted wage * computed percentage + #self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding) + self.assertTrue(abs(cats['EE_US_941_FIT'] - expected_withholding) < 0.011) + + def test_2019_taxes_with_external(self): + # social security salary + salary = self.FICA_M_ADD_START_WAGE + external_wages = 6000.0 + + employee = self._createEmployee() + + self._createContract(employee, wage=salary, external_wages=external_wages) + + self._log('2019 tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD) + self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS']) + self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M']) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA) + + def test_2019_taxes_with_full_futa(self): + futa_rate = self.FUTA_RATE_BASIC / -100.0 + # social security salary + salary = self.FICA_M_ADD_START_WAGE + + employee = self._createEmployee() + + self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2019 tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD) + self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS']) + self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M']) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate) + + def test_2019_taxes_with_futa_exempt(self): + futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption + + # social security salary + salary = self.FICA_M_ADD_START_WAGE + employee = self._createEmployee() + self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT) + self._log('2019 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_940_FUTA'], 0.0) + + def test_2019_fed_income_withholding_nonresident_alien(self): + salary = 3500.00 + schedule_pay = 'quarterly' + w4_allowances = 1 + w4_allowance_amt = 1050.0 * w4_allowances + nra_adjustment = 2000.0 # for quarterly + adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4450 + + ### + # Single QUARTERLY form Publication 15 + expected_withholding = self.float_round(-(242.50 + ((adjusted_salary - 3375) * 0.12)), self.payroll_digits) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + schedule_pay=schedule_pay, + fed_941_fit_w4_allowances=w4_allowances, + fed_941_fit_w4_is_nonresident_alien=True, + ) + + self._log('2019 fed income single payslip nonresident alien: adjusted_salary: ' + str(adjusted_salary)) + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding) + + def test_2019_fed_income_additional_withholding(self): + salary = 50000.00 + schedule_pay = 'annually' + w4_additional_withholding = 5000.0 + w4_allowances = 2 + w4_allowance_amt = 4200.0 * w4_allowances + adjusted_salary = salary - w4_allowance_amt # 41700 + + ### + # Single ANNUAL form Publication 15 + expected_withholding = \ + self.float_round(-((1940.00 + ((adjusted_salary - 31200) * 0.12)) + w4_additional_withholding), + self.payroll_digits) + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + schedule_pay=schedule_pay, + fed_941_fit_w4_filing_status='married', + fed_941_fit_w4_allowances=w4_allowances, + fed_941_fit_w4_additional_withholding=w4_additional_withholding, + ) + + self._log('2019 fed income married payslip additional withholding: adjusted_salary: ' + str(adjusted_salary)) + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding) + + def test_2019_taxes_with_w4_exempt(self): + salary = 6000.0 + schedule_pay = 'bi-weekly' + w4_allowances = 0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + schedule_pay=schedule_pay, + fed_941_fit_w4_allowances=w4_allowances, + fed_941_fit_w4_filing_status='', + ) + + self._log('2019 tax w4 exempt payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FIT'], 0.0) + + def test_2019_taxes_with_fica_exempt(self): + salary = 6000.0 + schedule_pay = 'bi-weekly' + employee = self._createEmployee() + contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay) + contract.us_payroll_config_id.fed_941_fica_exempt = True + + self._log('2019 tax w4 exempt payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0) + self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_payslip_2020.py new file mode 100644 index 00000000..c10a230b --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_payslip_2020.py @@ -0,0 +1,302 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + +from sys import float_info + + +class TestUsPayslip2020(TestUsPayslip): + # FUTA Constants + FUTA_RATE_NORMAL = 0.6 + FUTA_RATE_BASIC = 6.0 + FUTA_RATE_EXEMPT = 0.0 + + # Wage caps + FICA_SS_MAX_WAGE = 137700.0 + FICA_M_MAX_WAGE = float_info.max + FICA_M_ADD_START_WAGE = 200000.0 + FUTA_MAX_WAGE = 7000.0 + + # Rates + FICA_SS = 6.2 / -100.0 + FICA_M = 1.45 / -100.0 + FUTA = FUTA_RATE_NORMAL / -100.0 + FICA_M_ADD = 0.9 / -100.0 + + ### + # 2020 Taxes and Rates + ### + + def test_2020_taxes(self): + self.debug = False + # salary is high so that second payslip runs over max + # social security salary + salary = 80000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, wage=salary) + self._log(contract.read()) + + self._log('2019 tax last slip') + payslip = self._createPayslip(employee, '2019-12-01', '2019-12-31') + payslip.compute_sheet() + self._log(payslip.read()) + process_payslip(payslip) + + # Ensure amounts are there, they shouldn't be added in the next year... + cats = self._getCategories(payslip) + self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA) + + self._log('2020 tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + # Employee + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0) + # Employer + self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS']) + self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M']) + self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA) + + process_payslip(payslip) + + # Make a new payslip, this one will have reached Medicare Additional (employee only) + remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary + remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary + + self._log('2020 tax second payslip:') + payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0) + + process_payslip(payslip) + + # Make a new payslip, this one will have reached Medicare Additional (employee only) + self._log('2020 tax third payslip:') + payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k + + process_payslip(payslip) + + # Make a new payslip, this one will have all salary as Medicare Additional + + self._log('2020 tax fourth payslip:') + payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD) + + process_payslip(payslip) + + def test_2020_taxes_with_external(self): + # social security salary + salary = self.FICA_M_ADD_START_WAGE + external_wages = 6000.0 + + employee = self._createEmployee() + + self._createContract(employee, wage=salary, external_wages=external_wages) + + self._log('2020 tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD) + self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS']) + self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M']) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA) + + def test_2020_taxes_with_full_futa(self): + futa_rate = self.FUTA_RATE_BASIC / -100.0 + # social security salary + salary = self.FICA_M_ADD_START_WAGE + + employee = self._createEmployee() + + self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2020 tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS) + self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M) + self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD) + self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS']) + self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M']) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate) + + def test_2020_taxes_with_futa_exempt(self): + futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption + + # social security salary + salary = self.FICA_M_ADD_START_WAGE + employee = self._createEmployee() + self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT) + self._log('2020 tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0) + + def test_2020_taxes_with_fica_exempt(self): + salary = 6000.0 + schedule_pay = 'bi-weekly' + employee = self._createEmployee() + contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay) + contract.us_payroll_config_id.fed_941_fica_exempt = True + + self._log('2020 tax w4 exempt payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0) + self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0) + + """ + For Federal Income Tax Withholding, we are utilizing the calculations from the new IRS Excel calculator. + Given that you CAN round, we will round to compare even though we will calculate as close to the penny as possible + with the wage * computed_percent method. + """ + + def _run_test_fit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + expected_standard=0.0, + expected_higher=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_standard) + + contract.us_payroll_config_id.fed_941_fit_w4_multiple_jobs_higher = True + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_higher) + return payslip + + def test_2020_fed_income_withholding_single(self): + _ = self._run_test_fit( + wage=6000.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=100.0, + other_income=200.0, + deductions=300.0, + additional_withholding=400.0, + is_nonresident_alien=False, + expected_standard=1132.0, + expected_higher=1459.0, + ) + + def test_2020_fed_income_withholding_married_as_single(self): + # This is "Head of Household" though the field name is the same for historical reasons. + _ = self._run_test_fit( + wage=500.0, + schedule_pay='weekly', + filing_status='married_as_single', + dependent_credit=20.0, + other_income=30.0, + deductions=40.0, + additional_withholding=10.0, + is_nonresident_alien=False, + expected_standard=24.0, + expected_higher=45.0, + ) + + def test_2020_fed_income_withholding_married(self): + _ = self._run_test_fit( + wage=14000.00, + schedule_pay='bi-weekly', + filing_status='married', + dependent_credit=2500.0, + other_income=1200.0, + deductions=1000.0, + additional_withholding=0.0, + is_nonresident_alien=False, + expected_standard=2621.0, + expected_higher=3702.0, + ) + + def test_2020_fed_income_withholding_nonresident_alien(self): + # Monthly NRA additional wage is 1033.30 + # Wage input on IRS Form entered as (3500+1033.30)=4533.30, not 3500.0 + _ = self._run_test_fit( + wage=3500.00, + schedule_pay='monthly', + filing_status='married', + dependent_credit=340.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=True, + expected_standard=235.0, + expected_higher=391.0, + ) + + def test_2020_taxes_with_w4_exempt(self): + _ = self._run_test_fit( + wage=3500.00, + schedule_pay='monthly', + filing_status='', # Exempt + dependent_credit=340.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=True, + expected_standard=0.0, + expected_higher=0.0, + ) diff --git a/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py new file mode 100755 index 00000000..15e657ae --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py @@ -0,0 +1,100 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + +class TestUsTXPayslip(TestUsPayslip): + ### + # 2019 Taxes and Rates + ### + TX_UNEMP_MAX_WAGE = 9000.0 + TX_UNEMP = -2.7 / 100.0 + TX_OA = 0.0 + TX_ETIA = -0.1 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('TX'), + ) + + self._log('2019 Texas tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['ER_US_TX_SUTA'], salary * self.TX_UNEMP) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_OA'], salary * self.TX_OA) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_ETIA'], salary * self.TX_ETIA) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_tx_unemp_wages = self.TX_UNEMP_MAX_WAGE - salary if (self.TX_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Texas tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['ER_US_TX_SUTA'], remaining_tx_unemp_wages * self.TX_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('TX'), + external_wages=external_wages, + ) + + self._log('2019 Texas_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + expected_wage = self.TX_UNEMP_MAX_WAGE - external_wages + self.assertPayrollEqual(rules['ER_US_TX_SUTA'], expected_wage * self.TX_UNEMP) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_OA'], expected_wage * self.TX_OA) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_ETIA'], expected_wage * self.TX_ETIA) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('TX'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2019 Texas_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules.get('ER_US_TX_SUTA', 0.0), 0.0) + self.assertPayrollEqual(rules.get('ER_US_TX_SUTA_OA', 0.0), 0.0) + self.assertPayrollEqual(rules.get('ER_US_TX_SUTA_ETIA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py new file mode 100755 index 00000000..8dba312c --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py @@ -0,0 +1,17 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + +class TestUsTXPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + TX_UNEMP_MAX_WAGE = 9000.0 + TX_UNEMP = 2.7 + TX_OA = 0.0 + TX_ETIA = 0.1 + + def test_2020_taxes(self): + combined_rate = self.TX_UNEMP + self.TX_OA + self.TX_ETIA + self._test_er_suta('TX', combined_rate, date(2020, 1, 1), wage_base=self.TX_UNEMP_MAX_WAGE) diff --git a/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py new file mode 100644 index 00000000..b8f14393 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py @@ -0,0 +1,133 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsVaPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + VA_UNEMP_MAX_WAGE = 8000.0 + VA_UNEMP = 2.51 + VA_SIT_DEDUCTION = 4500.0 + VA_SIT_EXEMPTION = 930.0 + VA_SIT_OTHER_EXEMPTION = 800.0 + + def test_2019_taxes(self): + salary = 5000.0 + + # For formula from https://www.tax.virginia.gov/withholding-calculator + """ + Key + G = Gross Pay for Pay Period P = Pay periods per year + A = Annualized gross pay E1 = Personal and Dependent Exemptions + T = Annualized taxable income E2 = Age 65 and Over & Blind Exemptions + WH = Tax to be withheld for pay period W = Annualized tax to be withheld + G x P - [$3000+ (E1 x 930) + (E2 x 800)] = T + Calculate W as follows: + If T is: W is: + Not over $3,000 2% of T + Over But Not Over Then + $3,000 $5,000 $60 + (3% of excess over $3,000) + $5,000 $17,000 $120 + (5% of excess over $5,000) + $17,000 $720 + (5.75% of excess over $17,000) + W / P = WH + """ + e1 = 2 + e2 = 0 + t = salary * 12 - (self.VA_SIT_DEDUCTION + (e1 * self.VA_SIT_EXEMPTION) + (e2 * self.VA_SIT_OTHER_EXEMPTION)) + + if t <= 3000: + w = 0.02 * t + elif t <= 5000: + w = 60 + (0.03 * (t - 3000)) + elif t <= 17000: + w = 120 + (0.05 * (t - 5000)) + else: + w = 720 + (0.0575 * (t - 17000)) + + wh = w / 12 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('VA'), + va_va4_sit_exemptions=e1, + va_va4_sit_other_exemptions=e2 + ) + + # tax rates + va_unemp = self.VA_UNEMP / -100.0 + + self._log('2019 Virginia 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 * va_unemp) + self.assertPayrollEqual(cats['EE_US_SIT'], -wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_va_unemp_wages = self.VA_UNEMP_MAX_WAGE - salary if (self.VA_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Virginia 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_va_unemp_wages * va_unemp) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('VA'), + external_wages=external_wages, + ) + + # tax rates + va_unemp = self.VA_UNEMP / -100.0 + + self._log('2019 Virginia_external 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'], (self.VA_UNEMP_MAX_WAGE - external_wages) * va_unemp) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('VA'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + # tax rates + self._log('2019 Virginia exempt 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'], 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py new file mode 100644 index 00000000..012e4845 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py @@ -0,0 +1,116 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsVaPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + VA_UNEMP_MAX_WAGE = 8000.0 + VA_UNEMP = 2.51 + VA_SIT_DEDUCTION = 4500.0 + VA_SIT_EXEMPTION = 930.0 + VA_SIT_OTHER_EXEMPTION = 800.0 + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=0, + va_va4_sit_other_exemptions=0, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + va_va4_sit_exemptions=va_va4_sit_exemptions, + va_va4_sit_other_exemptions=va_va4_sit_other_exemptions, + state_id=self.get_us_state('VA'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected) + return payslip + + def test_2020_taxes(self): + self._test_er_suta('VA', self.VA_UNEMP, date(2020, 1, 1), wage_base=self.VA_UNEMP_MAX_WAGE) + + salary = 5000.0 + + # For formula from https://www.tax.virginia.gov/withholding-calculator + e1 = 2 + e2 = 0 + t = salary * 12 - (self.VA_SIT_DEDUCTION + (e1 * self.VA_SIT_EXEMPTION) + (e2 * self.VA_SIT_OTHER_EXEMPTION)) + + if t <= 3000: + w = 0.02 * t + elif t <= 5000: + w = 60 + (0.03 * (t - 3000)) + elif t <= 17000: + w = 120 + (0.05 * (t - 5000)) + else: + w = 720 + (0.0575 * (t - 17000)) + + wh = w / 12 + + self._run_test_sit(wage=salary, + schedule_pay='monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=e1, + va_va4_sit_other_exemptions=e2, + expected=wh,) + self.assertPayrollEqual(wh, 235.57) # To test against calculator + + # Below expected comes from the calculator linked above + self._run_test_sit(wage=450.0, + schedule_pay='weekly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=3, + va_va4_sit_other_exemptions=1, + expected=12.22,) + self._run_test_sit(wage=2500.0, + schedule_pay='bi-weekly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=1, + va_va4_sit_other_exemptions=0, + expected=121.84,) + self._run_test_sit(wage=10000.0, + schedule_pay='semi-monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=100.0, + va_va4_sit_exemptions=0, + va_va4_sit_other_exemptions=1, + expected=651.57,) + + # Test exempt + self._run_test_sit(wage=2400.0, + schedule_pay='monthly', + state_income_tax_exempt=True, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=1, + va_va4_sit_other_exemptions=1, + expected=0.0,) diff --git a/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py new file mode 100755 index 00000000..b67f69c6 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py @@ -0,0 +1,92 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsWAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + WA_UNEMP_MAX_WAGE = 49800.0 + WA_UNEMP_RATE = 1.18 + WA_FML_RATE = 0.4 + WA_FML_RATE_EE = 66.33 + WA_FML_RATE_ER = 33.67 + + def setUp(self): + super(TestUsWAPayslip, self).setUp() + # self.lni = self.env['hr.contract.lni.wa'].create({ + # 'name': '5302 Computer Consulting', + # 'rate': 0.1261, + # 'rate_emp_withhold': 0.05575, + # }) + self.test_ee_lni = 0.05575 # per 100 hours + self.test_er_lni = 0.1261 # per 100 hours + self.parameter_lni_ee = self.env['hr.rule.parameter'].create({ + 'name': 'Test LNI EE', + 'code': 'test_lni_ee', + 'parameter_version_ids': [(0, 0, { + 'date_from': date(2019, 1, 1), + 'parameter_value': str(self.test_ee_lni * 100), + })], + }) + self.parameter_lni_er = self.env['hr.rule.parameter'].create({ + 'name': 'Test LNI ER', + 'code': 'test_lni_er', + 'parameter_version_ids': [(0, 0, { + 'date_from': date(2019, 1, 1), + 'parameter_value': str(self.test_er_lni * 100), + })], + }) + + def test_2019_taxes(self): + salary = 25000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('WA'), + workers_comp_ee_code=self.parameter_lni_ee.code, + workers_comp_er_code=self.parameter_lni_er.code, + ) + self._log(str(contract.resource_calendar_id) + ' ' + contract.resource_calendar_id.name) + + + # tax rates + wa_unemp = self.WA_UNEMP_RATE / -100.0 + + self._log('2019 Washington tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + hours_in_period = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100').number_of_hours + self.assertEqual(hours_in_period, 184) # only asserted to test algorithm + payslip.compute_sheet() + + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * wa_unemp) + self.assertPayrollEqual(rules['EE_US_WA_LNI'], -(self.test_ee_lni * hours_in_period)) + self.assertPayrollEqual(rules['ER_US_WA_LNI'], -(self.test_er_lni * hours_in_period)) + # Both of these are known to be within 1 penny + self.assertPayrollAlmostEqual(rules['EE_US_WA_FML'], -(salary * (self.WA_FML_RATE / 100.0) * (self.WA_FML_RATE_EE / 100.0))) + self.assertPayrollAlmostEqual(rules['ER_US_WA_FML'], -(salary * (self.WA_FML_RATE / 100.0) * (self.WA_FML_RATE_ER / 100.0))) + + # FML + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_wa_unemp_wages = self.WA_UNEMP_MAX_WAGE - salary if (self.WA_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Washington 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_wa_unemp_wages * wa_unemp) diff --git a/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2020.py new file mode 100755 index 00000000..509e19b1 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2020.py @@ -0,0 +1,90 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsWAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + WA_UNEMP_MAX_WAGE = 52700.00 + WA_UNEMP_RATE = 1.0 + WA_FML_MAX_WAGE = 137700.00 + WA_FML_RATE = 0.4 + WA_FML_RATE_EE = 66.33 + WA_FML_RATE_ER = 33.67 + + def setUp(self): + super(TestUsWAPayslip, self).setUp() + # self.lni = self.env['hr.contract.lni.wa'].create({ + # 'name': '5302 Computer Consulting', + # 'rate': 0.1261, + # 'rate_emp_withhold': 0.05575, + # }) + self.test_ee_lni = 0.05575 # per 100 hours + self.test_er_lni = 0.1261 # per 100 hours + self.parameter_lni_ee = self.env['hr.rule.parameter'].create({ + 'name': 'Test LNI EE', + 'code': 'test_lni_ee', + 'parameter_version_ids': [(0, 0, { + 'date_from': date(2020, 1, 1), + 'parameter_value': str(self.test_ee_lni * 100), + })], + }) + self.parameter_lni_er = self.env['hr.rule.parameter'].create({ + 'name': 'Test LNI ER', + 'code': 'test_lni_er', + 'parameter_version_ids': [(0, 0, { + 'date_from': date(2020, 1, 1), + 'parameter_value': str(self.test_er_lni * 100), + })], + }) + + def test_2020_taxes(self): + self._test_er_suta('WA', self.WA_UNEMP_RATE, date(2020, 1, 1), wage_base=self.WA_UNEMP_MAX_WAGE) + + salary = (self.WA_FML_MAX_WAGE / 2.0) + 1000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('WA'), + workers_comp_ee_code=self.parameter_lni_ee.code, + workers_comp_er_code=self.parameter_lni_er.code, + ) + self._log(str(contract.resource_calendar_id) + ' ' + contract.resource_calendar_id.name) + + + # Non SUTA + self._log('2020 Washington tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + hours_in_period = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'WORK100').number_of_hours + self.assertEqual(hours_in_period, 184) # only asserted to test algorithm + payslip.compute_sheet() + + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['EE_US_WA_LNI'], -(self.test_ee_lni * hours_in_period)) + self.assertPayrollEqual(rules['ER_US_WA_LNI'], -(self.test_er_lni * hours_in_period)) + # Both of these are known to be within 1 penny + self.assertPayrollAlmostEqual(rules['EE_US_WA_FML'], -(salary * (self.WA_FML_RATE / 100.0) * (self.WA_FML_RATE_EE / 100.0))) + self.assertPayrollAlmostEqual(rules['ER_US_WA_FML'], -(salary * (self.WA_FML_RATE / 100.0) * (self.WA_FML_RATE_ER / 100.0))) + process_payslip(payslip) + + # Second payslip + remaining_wage = self.WA_FML_MAX_WAGE - salary + payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31') + payslip.compute_sheet() + rules = self._getRules(payslip) + self.assertPayrollAlmostEqual(rules['EE_US_WA_FML'], -(remaining_wage * (self.WA_FML_RATE / 100.0) * (self.WA_FML_RATE_EE / 100.0))) + self.assertPayrollAlmostEqual(rules['ER_US_WA_FML'], -(remaining_wage * (self.WA_FML_RATE / 100.0) * (self.WA_FML_RATE_ER / 100.0))) + process_payslip(payslip) + + # Third payslip + payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30') + payslip.compute_sheet() + rules = self._getRules(payslip) + self.assertPayrollAlmostEqual(rules['EE_US_WA_FML'], 0.0) + self.assertPayrollAlmostEqual(rules['ER_US_WA_FML'], 0.0) diff --git a/l10n_us_hr_payroll/views/hr_contract_views.xml b/l10n_us_hr_payroll/views/hr_contract_views.xml new file mode 100644 index 00000000..001b19cd --- /dev/null +++ b/l10n_us_hr_payroll/views/hr_contract_views.xml @@ -0,0 +1,19 @@ + + + + + hr.contract.form.inherit + hr.contract + + + + + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/views/res_config_settings_views.xml b/l10n_us_hr_payroll/views/res_config_settings_views.xml new file mode 100644 index 00000000..3c69b42f --- /dev/null +++ b/l10n_us_hr_payroll/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + + res.config.settings.view.form.inherit + res.config.settings + + + +
+
+
+ Payslip Sum Behavior +
+ Customize the behavior of what payslips are eligible when summing over date ranges in rules. + Generally, "Date To" or "Accounting Date" would be preferred in the United States and anywhere + else where the ending date on the payslip is used to calculate wage bases. +
+
+
+
+
+
+
+
+
+
+
+ +
diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml new file mode 100644 index 00000000..a91f80e4 --- /dev/null +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -0,0 +1,228 @@ + + + + hr.contract.us_payroll_config.tree + hr.contract.us_payroll_config + + + + + + + + + + + + + hr.contract.us_payroll_config.form + hr.contract.us_payroll_config + +
+ + + + + + + +

Form 940 - Federal Unemployment

+ +

Form 941 / W4 - Federal Income Tax

+ + + + + + + + + +

State Information and Extra

+ + + +
+ + +

Form A4 - State Income Tax

+ + + + +
+ +

Form AR4EC - State Income Tax

+ + + +
+ +

Form A-4 - State Income Tax

+ + +
+ +

Form W-4 - State Income Tax

+ + + + +
+ +

Form W-4 - State Income Tax

+ + +
+ +

Form DE W-4 - State Income Tax

+ + + +
+ +

Form CT-W4 - State Income Tax

+ + +
+ +

No additional fields.

+
+ +

Form G-4 - State Income Tax

+ + + + + +
+ +

Form HI HW-4 - State Income Tax

+ + + +
+ +

Form IA W-4 - State Income Tax

+ + + +
+ +

Form ID W-4 - State Income Tax

+ + +
+ +

Form IL-W-4 - State Income Tax

+ + + +
+ +

Form MI-W4 - State Income Tax

+ + + +
+ +

Form W-4MN - State Income Tax

+ + + +
+ +

Form MO W-4 - State Income Tax

+ + + +
+ +

Form 89-350 - State Income Tax

+ + + +
+ +

Form MT-4 - State Income Tax

+ + + +
+ +

Form NC-4 - State Income Tax

+ + + +
+ +

No additional fields.

+
+ +

Form NJ-W4 - State Income Tax

+ + + + +
+ +

Form NM W-4 - State Income Tax

+ +
+ +

Form IT-4 - State Income Tax

+ + + +
+ + + + + +

No additional fields.

+
+ +

Form VA-4/VA-4P - State Income Tax

+ + + + +
+ +

No additional fields.

+

Ensure that your Employee and Employer workers' comp code fields are filled in for WA LNI withholding.

+
+
+
+
+
+
+ + + hr.contract.us_payroll_config.search + hr.contract.us_payroll_config + + + + + + + + + + + Employee Payroll Forms + hr.contract.us_payroll_config + tree,form + +

+ No Forms +

+
+
+ + +
diff --git a/l10n_us_hr_payroll_401k/__init__.py b/l10n_us_hr_payroll_401k/__init__.py new file mode 100644 index 00000000..09434554 --- /dev/null +++ b/l10n_us_hr_payroll_401k/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models diff --git a/l10n_us_hr_payroll_401k/__manifest__.py b/l10n_us_hr_payroll_401k/__manifest__.py new file mode 100644 index 00000000..7e77a6bb --- /dev/null +++ b/l10n_us_hr_payroll_401k/__manifest__.py @@ -0,0 +1,24 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'USA - 401K Payroll', + 'author': 'Hibou Corp. ', + 'version': '13.0.1.0.0', + 'category': 'Payroll', + 'depends': [ + 'l10n_us_hr_payroll', + ], + 'description': """ +* Adds fields to HR Contract for amount or percentage to withhold for retirement savings. +* Adds rules to withhold and have a company match. + """, + + 'data': [ + 'data/payroll.xml', + 'views/contract_views.xml', + ], + 'demo': [ + ], + 'auto_install': False, + 'license': 'OPL-1', +} diff --git a/l10n_us_hr_payroll_401k/data/payroll.xml b/l10n_us_hr_payroll_401k/data/payroll.xml new file mode 100644 index 00000000..78a3771e --- /dev/null +++ b/l10n_us_hr_payroll_401k/data/payroll.xml @@ -0,0 +1,119 @@ + + + + + IRA Provider + 1 + + + + + Employee 401K Contribution Limit + ee_401k_contribution_limit + + + + 19500.0 + + + + + + Employee 401K Catch-up + ee_401k_catchup + + + + 6500.0 + + + + + + Employer 401K Contribution Limit + er_401k_contribution_limit + + + + 37500.0 + + + + + + Employer 401K Match (%) + er_401k_match_percent + + + + + 0.0 + + + + + + + + EE: 401K Traditional + EE_IRA + + + + EE: 401K Roth + EE_IRA_ROTH + + + + + ER: 401K Contribution + ER_IRA + + + + + + + + + EE: 401K + EE_IRA + python + result = ee_401k(contract.ira_amount, contract.ira_rate, payslip, categories, worked_days, inputs) + code + result = ee_401k(contract.ira_amount, contract.ira_rate, payslip, categories, worked_days, inputs) + + + + + + + + + EE: 401K Roth + EE_IRA_ROTH + python + result = ee_401k(contract.ira_roth_amount, contract.ira_roth_rate, payslip, categories, worked_days, inputs) + code + result = ee_401k(contract.ira_roth_amount, contract.ira_roth_rate, payslip, categories, worked_days, inputs) + + + + + + + + + ER: 401K Match + ER_IRA_MATCH + python + result = er_401k_match(categories.BASIC, payslip, categories, worked_days, inputs) + code + result = er_401k_match(categories.BASIC, payslip, categories, worked_days, inputs) + + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll_401k/migrations/13.0.0.0.1/pre-migration.py b/l10n_us_hr_payroll_401k/migrations/13.0.0.0.1/pre-migration.py new file mode 100644 index 00000000..0d6b8ea7 --- /dev/null +++ b/l10n_us_hr_payroll_401k/migrations/13.0.0.0.1/pre-migration.py @@ -0,0 +1,22 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +import odoo + + +def migrate(cr, version): + """ + Salary Rules can be archived by Odoo S.A. during migration. + This leaves them archived after the migration, and even un-archiving them + is not enough because they will then be pointed to a "migrated" structure. + """ + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + xml_refs = env['ir.model.data'].search([ + ('module', '=', 'l10n_us_hr_payroll_401k'), + ('model', '=', 'hr.salary.rule'), + ]) + # I don't know why Odoo makes these non-updatable... + xml_refs.write({'noupdate': False}) + + rule_ids = xml_refs.mapped('res_id') + rules = env['hr.salary.rule'].browse(rule_ids) + rules.write({'active': True}) diff --git a/l10n_us_hr_payroll_401k/models/__init__.py b/l10n_us_hr_payroll_401k/models/__init__.py new file mode 100644 index 00000000..9b8578e7 --- /dev/null +++ b/l10n_us_hr_payroll_401k/models/__init__.py @@ -0,0 +1,4 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import contract +from . import payslip diff --git a/l10n_us_hr_payroll_401k/models/contract.py b/l10n_us_hr_payroll_401k/models/contract.py new file mode 100644 index 00000000..5ce008a9 --- /dev/null +++ b/l10n_us_hr_payroll_401k/models/contract.py @@ -0,0 +1,21 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class HRContract(models.Model): + _inherit = 'hr.contract' + + ira_amount = fields.Float(string="401K Contribution Amount", + help="Pre-Tax (traditional) Contribution Amount") + ira_rate = fields.Float(string="401K Contribution (%)", + help="Pre-Tax (traditional) Contribution Percentage") + ira_roth_amount = fields.Float(string="Roth 401K Contribution Amount", + help="Post-Tax Contribution Amount") + ira_roth_rate = fields.Float(string="Roth 401K Contribution (%)", + help="Post-Tax Contribution Percentage") + + def company_401k_match_percent(self, payslip): + # payslip is payslip rule's current payslip browse object + # Override if you have employee, payslip, or contract differences. + return payslip.rule_parameter('er_401k_match_percent') diff --git a/l10n_us_hr_payroll_401k/models/payslip.py b/l10n_us_hr_payroll_401k/models/payslip.py new file mode 100644 index 00000000..5725ad3a --- /dev/null +++ b/l10n_us_hr_payroll_401k/models/payslip.py @@ -0,0 +1,83 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from odoo import fields, models + + +def ee_401k(amount, rate, payslip, categories, worked_days, inputs): + MAX = payslip.rule_parameter('ee_401k_contribution_limit') + if payslip.dict.ira_period_age() >= 50: + MAX += payslip.rule_parameter('ee_401k_catchup') + wages = categories.BASIC + year = payslip.date_to.year + next_year = str(year + 1) + from_ = str(year) + '-01-01' + to = next_year + '-01-01' + ytd = payslip.sum_category('EE_IRA', from_, to) + ytd += payslip.sum_category('EE_IRA_ROTH', from_, to) + remaining = MAX + ytd + if remaining <= 0.0: + result = 0 + else: + result = -amount + result -= (wages * rate) / 100.0 + if remaining + result <= 0.0: + result = -remaining + return result + + +def er_401k_match(wages, payslip, categories, worked_days, inputs): + MAX = payslip.rule_parameter('er_401k_contribution_limit') + employee_contrib = -(categories.EE_IRA + categories.EE_IRA_ROTH) + + year = payslip.date_to.year + next_year = str(year + 1) + from_ = str(year) + '-01-01' + to = next_year + '-01-01' + ytd = payslip.sum_category('ER_IRA', from_, to) + + rate = payslip.contract_id.company_401k_match_percent(payslip) + wages_match = (wages * rate) / 100.0 + if employee_contrib <= wages_match: + result = employee_contrib + else: + result = wages_match + remaining = MAX - ytd + if remaining <= 0.0: + result = 0 + else: + if remaining - result < 0.0: + result = remaining + return result + + +class HRPayslip(models.Model): + _inherit = 'hr.payslip' + + def _age_on_date(self, birthday, cutoff): + if isinstance(cutoff, str): + try: + cutoff = fields.Date.from_string(cutoff) + except: + cutoff = None + if cutoff is None: + # Dec. 31st in calendar year + cutoff = date(self.date_to.year, 12, 31) + if not birthday: + return -1 + years = cutoff.year - birthday.year + if birthday.month > cutoff.month or (birthday.month == cutoff.month and birthday.day > cutoff.day): + years -= 1 + return years + + def ira_period_age(self, cutoff=None): + birthday = self.employee_id.birthday + return self._age_on_date(birthday, cutoff) + + def _get_base_local_dict(self): + res = super()._get_base_local_dict() + res.update({ + 'ee_401k': ee_401k, + 'er_401k_match': er_401k_match, + }) + return res diff --git a/l10n_us_hr_payroll_401k/tests/__init__.py b/l10n_us_hr_payroll_401k/tests/__init__.py new file mode 100644 index 00000000..cf880a90 --- /dev/null +++ b/l10n_us_hr_payroll_401k/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import test_payroll diff --git a/l10n_us_hr_payroll_401k/tests/test_payroll.py b/l10n_us_hr_payroll_401k/tests/test_payroll.py new file mode 100644 index 00000000..87e5fca4 --- /dev/null +++ b/l10n_us_hr_payroll_401k/tests/test_payroll.py @@ -0,0 +1,132 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields +from odoo.addons.l10n_us_hr_payroll.tests import common +from datetime import timedelta + + +class TestUsPayslip(common.TestUsPayslip): + EE_LIMIT = 19500.0 + EE_LIMIT_CATCHUP = 6500.0 + ER_LIMIT = 37500.0 + + def setUp(self): + super().setUp() + self.schedule_pay_salary = 'bi-weekly' + self.payslip_date_start = fields.Date.from_string('2020-01-01') + self.payslip_date_end = self.payslip_date_start + timedelta(days=14) + self.er_match_parameter = self.env.ref('l10n_us_hr_payroll_401k.rule_parameter_er_401k_match_percent_2020') + self.er_match_parameter.parameter_value = '4.0' # 4% match up to salary + + def test_01_payslip_traditional(self): + wage = 2000.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + ira_rate=5.0, + schedule_pay=self.schedule_pay_salary) + payslip = self._createPayslip(employee, self.payslip_date_start, self.payslip_date_end) + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -100.0) + + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, 80.0) # 4% of wage up to their contribution + + contract.ira_rate = 0.0 + contract.ira_amount = 25.0 + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -25.0) + + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, 25.0) # 4% of wage up to their contribution + + def test_02_payslip_roth(self): + wage = 2000.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + ira_roth_rate=5.0, + schedule_pay=self.schedule_pay_salary) + payslip = self._createPayslip(employee, self.payslip_date_start, self.payslip_date_end) + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA_ROTH') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -100.0) + + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, 80.0) # 4% of wage up to their contribution + + contract.ira_roth_rate = 0.0 + contract.ira_roth_amount = 25.0 + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA_ROTH') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -25.0) + + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, 25.0) # 4% of wage up to their contribution + + def test_10_payslip_limits(self): + self.er_match_parameter.parameter_value = '20.0' # 20% match up to salary + wage = 80000.0 + rate = 20.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + ira_rate=rate, + schedule_pay=self.schedule_pay_salary) + + # Payslip 1 - 16k + payslip = self._createPayslip(employee, self.payslip_date_start, self.payslip_date_end) + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -(wage * rate / 100.0)) + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, -ira_line.amount) + common.process_payslip(payslip) + + # Payslip 2 - 3.5k + payslip = self._createPayslip(employee, self.payslip_date_start + timedelta(days=14), + self.payslip_date_end + timedelta(days=14)) + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -(self.EE_LIMIT-(wage * rate / 100.0))) + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, -ira_line.amount) + common.process_payslip(payslip) + + # Payslip 3 - 0 (over limit) + payslip = self._createPayslip(employee, self.payslip_date_start + timedelta(days=28), + self.payslip_date_end + timedelta(days=28)) + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA') + self.assertFalse(ira_line) + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertFalse(er_ira_line) + + # Payslip 3 - Catch-up + employee.birthday = '1960-01-01' + payslip.compute_sheet() + ira_line = payslip.line_ids.filtered(lambda l: l.code == 'EE_IRA') + self.assertTrue(ira_line) + self.assertPayrollEqual(ira_line.amount, -self.EE_LIMIT_CATCHUP) + er_ira_line = payslip.line_ids.filtered(lambda l: l.code == 'ER_IRA_MATCH') + self.assertTrue(er_ira_line) + self.assertPayrollEqual(er_ira_line.amount, -ira_line.amount) + common.process_payslip(payslip) + + # Note that the company limit is higher than what is possible by 'match' + # because even with 100% (or more) you would never be able to out-pace + # the employee's own contributions. diff --git a/l10n_us_hr_payroll_401k/views/contract_views.xml b/l10n_us_hr_payroll_401k/views/contract_views.xml new file mode 100644 index 00000000..16c20352 --- /dev/null +++ b/l10n_us_hr_payroll_401k/views/contract_views.xml @@ -0,0 +1,20 @@ + + + + + hr.contract.form.inherit + hr.contract + + + + + + + + + + + + + + \ No newline at end of file