From 8e2a8f484b783b8724c89916b39391a0a582f97e Mon Sep 17 00:00:00 2001 From: Kristen Marie Kulha Date: Thu, 17 May 2018 13:08:54 -0700 Subject: [PATCH] Initial commit of `l10n_us_ca_hr_payroll` for 11.0. --- l10n_us_ca_hr_payroll/__init__.py | 1 + l10n_us_ca_hr_payroll/__manifest__.py | 31 + l10n_us_ca_hr_payroll/data/base.xml | 88 +++ l10n_us_ca_hr_payroll/data/final.xml | 23 + l10n_us_ca_hr_payroll/data/rules_2018.xml | 576 ++++++++++++++++++ l10n_us_ca_hr_payroll/hr_payroll.py | 56 ++ l10n_us_ca_hr_payroll/hr_payroll_view.xml | 34 ++ l10n_us_ca_hr_payroll/tests/__init__.py | 1 + .../tests/test_us_ca_payslip_2018.py | 461 ++++++++++++++ 9 files changed, 1271 insertions(+) create mode 100755 l10n_us_ca_hr_payroll/__init__.py create mode 100755 l10n_us_ca_hr_payroll/__manifest__.py create mode 100755 l10n_us_ca_hr_payroll/data/base.xml create mode 100755 l10n_us_ca_hr_payroll/data/final.xml create mode 100755 l10n_us_ca_hr_payroll/data/rules_2018.xml create mode 100755 l10n_us_ca_hr_payroll/hr_payroll.py create mode 100755 l10n_us_ca_hr_payroll/hr_payroll_view.xml create mode 100755 l10n_us_ca_hr_payroll/tests/__init__.py create mode 100755 l10n_us_ca_hr_payroll/tests/test_us_ca_payslip_2018.py diff --git a/l10n_us_ca_hr_payroll/__init__.py b/l10n_us_ca_hr_payroll/__init__.py new file mode 100755 index 00000000..1027e233 --- /dev/null +++ b/l10n_us_ca_hr_payroll/__init__.py @@ -0,0 +1 @@ +from . import hr_payroll \ No newline at end of file diff --git a/l10n_us_ca_hr_payroll/__manifest__.py b/l10n_us_ca_hr_payroll/__manifest__.py new file mode 100755 index 00000000..1d2ae749 --- /dev/null +++ b/l10n_us_ca_hr_payroll/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': 'USA - California - Payroll', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Localization', + 'depends': ['l10n_us_hr_payroll'], + 'version': '11.0.2018.0.0', + 'description': """ +USA::California Payroll Rules. +============================== + +* Contribution register and partner for California Department of Taxation - Unemployment Insurance Taz +* Contribution register and partner for California Department of Taxation - Income Tax Withholding +* Contribution register and partner for Califronia Department of Taxation - Employee Training Tax +* Contribution register and partner for Califronia Department of Taxation - State Disability Insurance +* Contract level California Exemptions +* Contract level California State Disability Insurance +* Company level California Unemployment Insurance Tax +* Company level California Employee Training Taz + """, + + 'auto_install': False, + 'website': 'https://hibou.io/', + 'data': [ + 'hr_payroll_view.xml', + 'data/base.xml', + 'data/rules_2018.xml', + 'data/final.xml', + ], + 'installable': True +} diff --git a/l10n_us_ca_hr_payroll/data/base.xml b/l10n_us_ca_hr_payroll/data/base.xml new file mode 100755 index 00000000..b00d6277 --- /dev/null +++ b/l10n_us_ca_hr_payroll/data/base.xml @@ -0,0 +1,88 @@ + + + + + + California Department of Taxation - Unemployment Insurance Tax + 1 + + + + California Department of Taxation - Income Tax Withholding + 1 + + + + California Department of Taxation - Employment Training Tax + 1 + + + + California Department of Taxation - State Disability Insurance + 1 + + + + + California Unemployment Insurance Tax + California Department of Taxation - Unemployment Insurance Tax + + + + California Income Tax Withholding + California Department of Taxation - Income Tax Withholding + + + + Employment Training Tax + California Department of Taxation - Employment Training Tax + + + + State Disability Insurance + California Department of Taxation - State Disability Insurance + + + + + + + California Unemployment Insurance Tax - Wages + CA_UIT_WAGES + + + + California Unemployment Insurance Tax + CA_UIT + + + + + California Employee Training Tax - Wages + CA_ETT_WAGES + + + + California Employee Training Tax + CA_ETT + + + + + California State Disability Insurance - Wages + CA_SDI_WAGES + + + + California State Disability Insurance + CA_SDI + + + + + California Income Withholding + CA_WITHHOLD + + + + \ No newline at end of file diff --git a/l10n_us_ca_hr_payroll/data/final.xml b/l10n_us_ca_hr_payroll/data/final.xml new file mode 100755 index 00000000..2aca7646 --- /dev/null +++ b/l10n_us_ca_hr_payroll/data/final.xml @@ -0,0 +1,23 @@ + + + + + + + US_CA_EMP + USA California Employee + + + + + + + diff --git a/l10n_us_ca_hr_payroll/data/rules_2018.xml b/l10n_us_ca_hr_payroll/data/rules_2018.xml new file mode 100755 index 00000000..dd8c7d6e --- /dev/null +++ b/l10n_us_ca_hr_payroll/data/rules_2018.xml @@ -0,0 +1,576 @@ + + + + + + + + + + California Unemployment Insurance Tax - Wages (2018) + CA_UIT_WAGES_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +### +ytd = payslip.sum('CA_UIT_WAGES_2018', '2018-01-01', '2019-01-01') +ytd += contract.external_wages +remaining = 7000.0 - ytd +if remaining <= 0.0: + result = 0 +elif remaining < categories.GROSS: + result = remaining +else: + result = categories.GROSS + + + + + + + + California Unemployment Insurance Tax(2018) + CA_UIT_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +result_rate = -contract.ca_uit_rate(2018) +result = categories.CA_UIT_WAGES + +# result_rate of 0 implies 100% due to bug +if result_rate == 0.0: + result = 0.0 + + + + + + + + + + California Employment Training Tax - Wages (2018) + CA_ETT_WAGES_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +### +ytd = payslip.sum('CA_ETT_WAGES_2018', '2018-01-01', '2019-01-01') +ytd += contract.external_wages +remaining = 7000.0 - ytd +if remaining <= 0.0: + result = 0 +elif remaining < categories.GROSS: + result = remaining +else: + result = categories.GROSS + + + + + + + + California Employee Training Tax(2018) + CA_ETT_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +result_rate = -contract.ca_ett_rate(2018) +result = categories.CA_ETT_WAGES + +# result_rate of 0 implies 100% due to bug +if result_rate == 0.0: + result = 0.0 + + + + + + + + + + California State Disability Insurance Tax - Wages (2018) + CA_SDI_WAGES_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +### +ytd = payslip.sum('CA_SDI_WAGES_2018', '2018-01-01', '2019-01-01') +ytd += contract.external_wages +remaining = 114967.0 - ytd +if remaining <= 0.0: + result = 0 +elif remaining < categories.GROSS: + result = remaining +else: + result = categories.GROSS + + + + + + + + California State Disability Insurance(2018) + CA_SDI_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +result_rate = -contract.ca_sdi_rate(2018) +result = categories.CA_SDI_WAGES + +# result_rate of 0 implies 100% due to bug +if result_rate == 0.0: + result = 0.0 + + + + + + + + + + California Income Withholding + CA_INC_WITHHOLD_2018 + python + result = (payslip.date_to[:4] == '2018') + code + +wages = categories.GROSS +allowances = contract.ca_de4_allowances +additional_allowances = contract.ca_additional_allowances +schedule_pay = contract.schedule_pay +filing_status = contract.ca_de4_filing_status +low_income = False + +# Tables are found in http://www.edd.ca.gov/pdf_pub_ctr/18methb.pdf +# First check low income exemption table (Step 1) +low_income_exemption_table = { +'weekly': (270, 270, 540, 540), +'bi-weekly': (540, 540, 1081, 1081), +'semi-monthly': (585, 585, 1171, 1171), +'monthly': (1171, 1171, 2341, 2341), +'quarterly': (3512, 3512, 7024, 7024), +'semi-annual': (7024, 7024, 14048, 14048), +'annually': (14048, 14048, 28095, 28095), +} + +if filing_status == 'head_household': + _, _, _, income = low_income_exemption_table[schedule_pay] + if wages <= income: + result = 0 + low_income = True +elif filing_status == 'married': + if allowances >= 2: + _, _, income, _ = low_income_exemption_table[schedule_pay] + if wages <= income: + result = 0 + low_income = True + else: + _, income, _, _ = low_income_exemption_table[schedule_pay] + if wages <= income: + result = 0 + low_income = True +else: + income, _, _, _ = low_income_exemption_table[schedule_pay] + if wages <= income: + result = 0 + low_income = True + +if not low_income: + # Estimated deduction table (Step 2) + estimated_deduction_table = { + '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), + } + + allowance_index = additional_allowances - 1 + if additional_allowances > 10: + deduction = (estimated_deduction_table[schedule_pay][0]) * additional_allowances + wages -= deduction + elif additional_allowances > 0: + deduction = estimated_deduction_table[schedule_pay][allowance_index] + wages -= deduction + + # Standard deduction table (Step 3) + standard_deduction_table = { + 'weekly': (81, 81, 163, 163), + 'bi-weekly': (163, 163, 326, 326), + 'semi-monthly': (177, 177, 353, 353), + 'monthly': (353, 353, 706, 706), + 'quarterly': (1059, 1059, 2118, 2118), + 'semi-annual': (2118, 2118, 4236, 4236), + 'annually': (4236, 4236, 8472, 8472), + } + + if filing_status == 'head_household': + _, _, _, deduction = standard_deduction_table[schedule_pay] + wages -= deduction + elif filing_status == 'married': + if allowances >= 2: + _, _, deduction, _ = standard_deduction_table[schedule_pay] + wages -= deduction + else: + _, deduction, _, _ = standard_deduction_table[schedule_pay] + wages -= deduction + else: + deduction, _, _, _ = standard_deduction_table[schedule_pay] + wages -= deduction + + # Tax Rate Tables (Step 4) + #### WEEKLY #### + if schedule_pay == 'weekly': + if filing_status == 'head_household' and wages > 0: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 2181.02), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 2289.70), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 2301.06), + ] + + #### BI-WEEKLY #### + elif schedule_pay == 'bi-weekly': + if filing_status == 'head_household': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 4362.02), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 4579.37), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 4602.13), + ] + + #### SEMI-MONTHLY #### + elif schedule_pay == 'semi-monthly': + if filing_status == 'head_household': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 4725.50), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 4961.12), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 4985.58), + ] + + #### MONTHLY #### + elif schedule_pay == 'monthly': + if filing_status == 'head_household': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 9451.00), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 9922.22), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 9971.18), + ] + + #### QUARTERLY #### + elif schedule_pay == 'quarterly': + if filing_status == 'head_household': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 28352.74), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 29766.71), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 29913.28), + ] + + #### SEMI-ANNUAL #### + elif schedule_pay == 'semi-annual': + if filing_status == 'head_household': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 56705.47), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 59533.44), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 59826.53), + ] + + #### ANNUAL #### + elif schedule_pay == 'annually': + if filing_status == 'head_household': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 113411.02), + ] + + elif filing_status == 'married': + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 119067.26), + ] + + else: + tax_rate_table = [ + (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), + (float('inf'), 0.1463, 119653.12), + ] + + over = 0.0 + tax = 0.0 + for row in tax_rate_table: + if wages <= row[0]: + tax = ((wages - over) * row[1]) + row[2] + break + over = row[0] + + # Exemption allowance table (Step 5) + exemption_allowance_table = { + '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), + } + + allowance_index = allowances - 1 + if allowances > 10: + deduction = (exemption_allowance_table[schedule_pay][0]) * allowances + tax -= deduction + elif allowances > 0: + deduction = exemption_allowance_table[schedule_pay][allowance_index] + tax -= deduction + + result = -tax + + + + + + diff --git a/l10n_us_ca_hr_payroll/hr_payroll.py b/l10n_us_ca_hr_payroll/hr_payroll.py new file mode 100755 index 00000000..b1a3eebf --- /dev/null +++ b/l10n_us_ca_hr_payroll/hr_payroll.py @@ -0,0 +1,56 @@ +from odoo import models, fields, api + + +class USCAHrContract(models.Model): + _inherit = 'hr.contract' + + ca_de4_allowances = fields.Integer(string="California CA-4 Allowances", + default=0, + help="Estimated Deductions claimed on DE-4") + ca_additional_allowances = fields.Integer(string="Additional Allowances", default=0) + ca_de4_filing_status = fields.Selection([ + ('exempt', 'Exempt'), + ('single', 'Single'), + ('married', 'Married'), + ('head_household', 'Head of Household') + ], string='CA Filing Status', default='single') + + @api.multi + def ca_uit_rate(self, year): + self.ensure_one() + if self.futa_type == self.FUTA_TYPE_BASIC: + return 0.0 + + if hasattr(self.employee_id.company_id, 'ca_uit_rate_' + str(year)): + return self.employee_id.company_id['ca_uit_rate_' + str(year)] + + raise NotImplemented('Year (' + str(year) + ') Not implemented for US California.') + + def ca_ett_rate(self, year): + self.ensure_one() + if self.futa_type == self.FUTA_TYPE_BASIC: + return 0.0 + + if hasattr(self.employee_id.company_id, 'ca_ett_rate_' + str(year)): + return self.employee_id.company_id['ca_ett_rate_' + str(year)] + + raise NotImplemented('Year (' + str(year) + ') Not implemented for US California.') + + def ca_sdi_rate(self, year): + self.ensure_one() + if self.futa_type == self.FUTA_TYPE_BASIC: + return 0.0 + + if hasattr(self.employee_id.company_id, 'ca_sdi_rate_' + str(year)): + return self.employee_id.company_id['ca_sdi_rate_' + str(year)] + + raise NotImplemented('Year (' + str(year) + ') Not implemented for US California.') + + +class CACompany(models.Model): + _inherit = 'res.company' + + # UIT can be calculated using http://www.edd.ca.gov/pdf_pub_ctr/de44.pdf ETT is default. + ca_uit_rate_2018 = fields.Float(string="California Unemployment Insurance Tax Rate 2018", default=2.6) + ca_ett_rate_2018 = fields.Float(string="California Employment Training Tax Rate 2018", default=0.1) + ca_sdi_rate_2018 = fields.Float(string="California State Disability Insurance Rate 2018", default=1.0) diff --git a/l10n_us_ca_hr_payroll/hr_payroll_view.xml b/l10n_us_ca_hr_payroll/hr_payroll_view.xml new file mode 100755 index 00000000..87ef4b6b --- /dev/null +++ b/l10n_us_ca_hr_payroll/hr_payroll_view.xml @@ -0,0 +1,34 @@ + + + + + res.company.form + res.company + 64 + + + + + + + + + + + hr.contract.form.inherit + hr.contract + 147 + + + + + + + + + + + + + + diff --git a/l10n_us_ca_hr_payroll/tests/__init__.py b/l10n_us_ca_hr_payroll/tests/__init__.py new file mode 100755 index 00000000..8011621b --- /dev/null +++ b/l10n_us_ca_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_us_ca_payslip_2018 \ No newline at end of file diff --git a/l10n_us_ca_hr_payroll/tests/test_us_ca_payslip_2018.py b/l10n_us_ca_hr_payroll/tests/test_us_ca_payslip_2018.py new file mode 100755 index 00000000..e58892a8 --- /dev/null +++ b/l10n_us_ca_hr_payroll/tests/test_us_ca_payslip_2018.py @@ -0,0 +1,461 @@ +from odoo.addons.l10n_us_hr_payroll.tests.test_us_payslip import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.l10n_us_hr_payroll import USHrContract + + +class TestUsCAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + CA_UIT_MAX_WAGE = 7000 + CA_UIT_MAX_WAGE = 7000 + CA_SDI_MAX_WAGE = 114967 + + # Examples from http://www.edd.ca.gov/pdf_pub_ctr/18methb.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, + salary, + struct_id=self.ref( + 'l10n_us_ca_hr_payroll.hr_payroll_salary_structure_us_ca_employee'), + schedule_pay=schedule_pay) + contract.ca_c4_exemptions = allowances + contract.ca_additional_allowances = additional_allowances + contract.ca_de4_filing_status = 'single' + + self.assertEqual(contract.schedule_pay, 'weekly') + + # tax rates + ca_uit = contract.ca_uit_rate(2018) / -100.0 + ca_ett = contract.ca_ett_rate(2018) / -100.0 + ca_sdi = contract.ca_sdi_rate(2018) / -100.0 + + self._log('2017 California tax last payslip:') + payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31') + payslip.compute_sheet() + process_payslip(payslip) + + self._log('2018 California tax first payslip:') + payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_UIT'], cats['CA_UIT_WAGES'] * ca_uit) + self.assertPayrollEqual(cats['CA_ETT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_ETT'], cats['CA_ETT_WAGES'] * ca_ett) + self.assertPayrollEqual(cats['CA_SDI_WAGES'], salary) + self.assertPayrollEqual(cats['CA_SDI'], cats['CA_SDI_WAGES'] * ca_sdi) + self.assertPayrollEqual(cats['CA_WITHHOLD'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_UIT_MAX_WAGE - salary if (self.CA_UIT_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2018 California tax second payslip:') + payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], remaining_ca_uit_wages) + self.assertPayrollEqual(cats['CA_UIT'], remaining_ca_uit_wages * ca_uit) + + def test_example_b(self): + salary = 1250 + schedule_pay = 'bi-weekly' + allowances = 2 + additional_allowances = 1 + + wh = -2.89 + # tax rates + ca_uit = 2.6 + ca_ett = 0.1 + ca_sdi = 1.0 + + employee = self._createEmployee() + employee.company_id.ca_uit_rate_2018 = ca_uit + employee.company_id.ca_ett_rate_2018 = ca_ett + employee.company_id.ca_sdi_rate_2018 = ca_sdi + + contract = self._createContract(employee, + salary, + struct_id=self.ref( + 'l10n_us_ca_hr_payroll.hr_payroll_salary_structure_us_ca_employee'), + schedule_pay=schedule_pay) + contract.ca_de4_allowances = allowances + contract.ca_additional_allowances = additional_allowances + contract.ca_de4_filing_status = 'married' + + self.assertEqual(contract.schedule_pay, 'bi-weekly') + self.assertEqual(contract.ca_de4_filing_status, 'married') + self.assertEqual(contract.ca_de4_allowances, 2) + self.assertEqual(contract.ca_additional_allowances, 1) + + self._log('2017 California tax last payslip:') + payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31') + payslip.compute_sheet() + process_payslip(payslip) + + self._log('2018 California tax first payslip:') + payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_UIT'], round((cats['CA_UIT_WAGES'] * ca_uit)/-100, 2)) + self.assertPayrollEqual(cats['CA_ETT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_ETT'], round((cats['CA_ETT_WAGES'] * ca_ett)/-100, 2)) + self.assertPayrollEqual(cats['CA_SDI_WAGES'], salary) + self.assertPayrollEqual(cats['CA_SDI'], round((cats['CA_SDI_WAGES'] * ca_sdi)/-100, 2)) + self.assertPayrollEqual(cats['CA_WITHHOLD'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_UIT_MAX_WAGE - salary if (self.CA_UIT_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2018 California tax second payslip:') + payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], remaining_ca_uit_wages) + self.assertPayrollEqual(cats['CA_UIT'], round((remaining_ca_uit_wages * ca_uit)/-100, 2)) + + def test_example_c(self): + salary = 3800 + schedule_pay = 'monthly' + allowances = 5 + additional_allowances = 0.72 + + wh = -0.72 + # tax rates + ca_uit = 2.6 + ca_ett = 0.1 + ca_sdi = 1.0 + + employee = self._createEmployee() + employee.company_id.ca_uit_rate_2018 = ca_uit + employee.company_id.ca_ett_rate_2018 = ca_ett + employee.company_id.ca_sdi_rate_2018 = ca_sdi + + contract = self._createContract(employee, + salary, + struct_id=self.ref( + 'l10n_us_ca_hr_payroll.hr_payroll_salary_structure_us_ca_employee'), + schedule_pay=schedule_pay) + contract.ca_de4_allowances = allowances + contract.ca_additional_allowances = additional_allowances + contract.ca_de4_filing_status = 'married' + + self.assertEqual(contract.schedule_pay, 'monthly') + self.assertEqual(contract.ca_de4_filing_status, 'married') + self.assertEqual(contract.ca_de4_allowances, 5) + self.assertEqual(contract.ca_additional_allowances, 0) + + self._log('2017 California tax last payslip:') + payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31') + payslip.compute_sheet() + process_payslip(payslip) + + self._log('2018 California tax first payslip:') + payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_UIT'], round((cats['CA_UIT_WAGES'] * ca_uit)/-100, 2)) + self.assertPayrollEqual(cats['CA_ETT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_ETT'], round((cats['CA_ETT_WAGES'] * ca_ett)/-100, 2)) + self.assertPayrollEqual(cats['CA_SDI_WAGES'], salary) + self.assertPayrollEqual(cats['CA_SDI'], round((cats['CA_SDI_WAGES'] * ca_sdi)/-100, 2)) + self.assertPayrollEqual(cats['CA_WITHHOLD'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_UIT_MAX_WAGE - salary if (self.CA_UIT_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2018 California tax second payslip:') + payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], remaining_ca_uit_wages) + self.assertPayrollEqual(cats['CA_UIT'], round((remaining_ca_uit_wages * ca_uit)/-100, 2)) + + def test_example_d(self): + salary = 800 + schedule_pay = 'weekly' + allowances = 3 + additional_allowances = 0 + + wh = -3.31 + # tax rates + ca_uit = 2.6 + ca_ett = 0.1 + ca_sdi = 1.0 + + employee = self._createEmployee() + employee.company_id.ca_uit_rate_2018 = ca_uit + employee.company_id.ca_ett_rate_2018 = ca_ett + employee.company_id.ca_sdi_rate_2018 = ca_sdi + + contract = self._createContract(employee, + salary, + struct_id=self.ref( + 'l10n_us_ca_hr_payroll.hr_payroll_salary_structure_us_ca_employee'), + schedule_pay=schedule_pay) + contract.ca_de4_allowances = allowances + contract.ca_additional_allowances = additional_allowances + contract.ca_de4_filing_status = 'head_household' + + self.assertEqual(contract.schedule_pay, 'weekly') + self.assertEqual(contract.ca_de4_filing_status, 'head_household') + self.assertEqual(contract.ca_de4_allowances, 3) + self.assertEqual(contract.ca_additional_allowances, 0) + + self._log('2017 California tax last payslip:') + payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31') + payslip.compute_sheet() + process_payslip(payslip) + + self._log('2018 California tax first payslip:') + payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_UIT'], round((cats['CA_UIT_WAGES'] * ca_uit)/-100, 2)) + self.assertPayrollEqual(cats['CA_ETT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_ETT'], round((cats['CA_ETT_WAGES'] * ca_ett)/-100, 2)) + self.assertPayrollEqual(cats['CA_SDI_WAGES'], salary) + self.assertPayrollEqual(cats['CA_SDI'], round((cats['CA_SDI_WAGES'] * ca_sdi)/-100, 2)) + self.assertPayrollEqual(cats['CA_WITHHOLD'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_UIT_MAX_WAGE - salary if (self.CA_UIT_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2018 California tax second payslip:') + payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], remaining_ca_uit_wages) + self.assertPayrollEqual(cats['CA_UIT'], round((remaining_ca_uit_wages * ca_uit)/-100, 2)) + + def test_example_e(self): + salary = 1800 + schedule_pay = 'semi-monthly' + allowances = 4 + additional_allowances = 0 + + wh = -3.39 + # tax rates + ca_uit = 2.6 + ca_ett = 0.1 + ca_sdi = 1.0 + + employee = self._createEmployee() + employee.company_id.ca_uit_rate_2018 = ca_uit + employee.company_id.ca_ett_rate_2018 = ca_ett + employee.company_id.ca_sdi_rate_2018 = ca_sdi + + contract = self._createContract(employee, + salary, + struct_id=self.ref( + 'l10n_us_ca_hr_payroll.hr_payroll_salary_structure_us_ca_employee'), + schedule_pay=schedule_pay) + contract.ca_de4_allowances = allowances + contract.ca_additional_allowances = additional_allowances + contract.ca_de4_filing_status = 'married' + + self.assertEqual(contract.schedule_pay, 'semi-monthly') + self.assertEqual(contract.ca_de4_filing_status, 'married') + self.assertEqual(contract.ca_de4_allowances, 4) + self.assertEqual(contract.ca_additional_allowances, 0) + + self._log('2017 California tax last payslip:') + payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31') + payslip.compute_sheet() + process_payslip(payslip) + + self._log('2018 California tax first payslip:') + payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_UIT'], round((cats['CA_UIT_WAGES'] * ca_uit)/-100, 2)) + self.assertPayrollEqual(cats['CA_ETT_WAGES'], salary) + self.assertPayrollEqual(cats['CA_ETT'], round((cats['CA_ETT_WAGES'] * ca_ett)/-100, 2)) + self.assertPayrollEqual(cats['CA_SDI_WAGES'], salary) + self.assertPayrollEqual(cats['CA_SDI'], round((cats['CA_SDI_WAGES'] * ca_sdi)/-100, 2)) + self.assertPayrollEqual(cats['CA_WITHHOLD'], wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_ca_uit_wages = self.CA_UIT_MAX_WAGE - salary if (self.CA_UIT_MAX_WAGE - 2 * salary < salary) \ + else salary + + self._log('2018 California tax second payslip:') + payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT_WAGES'], remaining_ca_uit_wages) + self.assertPayrollEqual(cats['CA_UIT'], round((remaining_ca_uit_wages * ca_uit)/-100, 2)) + + def test_example_f(self): + salary = 45000 + schedule_pay = 'annually' + allowances = 4 + additional_allowances = 0 + + wh = -121.11 + # tax rates + ca_uit = 2.6 + ca_ett = 0.1 + ca_sdi = 1.0 + + employee = self._createEmployee() + employee.company_id.ca_uit_rate_2018 = ca_uit + employee.company_id.ca_ett_rate_2018 = ca_ett + employee.company_id.ca_sdi_rate_2018 = ca_sdi + + contract = self._createContract(employee, + salary, + struct_id=self.ref( + 'l10n_us_ca_hr_payroll.hr_payroll_salary_structure_us_ca_employee'), + schedule_pay=schedule_pay) + contract.ca_de4_allowances = allowances + contract.ca_additional_allowances = additional_allowances + contract.ca_de4_filing_status = 'married' + + self.assertEqual(contract.schedule_pay, 'annually') + self.assertEqual(contract.ca_de4_filing_status, 'married') + self.assertEqual(contract.ca_de4_allowances, 4) + self.assertEqual(contract.ca_additional_allowances, 0) + + self._log('2017 California tax last payslip:') + payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31') + payslip.compute_sheet() + process_payslip(payslip) + + self._log('2018 California tax first payslip:') + payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['CA_UIT'], round((cats['CA_UIT_WAGES'] * ca_uit)/-100, 2)) + self.assertPayrollEqual(cats['CA_ETT'], round((cats['CA_ETT_WAGES'] * ca_ett)/-100, 2)) + self.assertPayrollEqual(cats['CA_SDI'], round((cats['CA_SDI_WAGES'] * ca_sdi)/-100, 2)) + self.assertPayrollEqual(cats['CA_WITHHOLD'], wh) + + process_payslip(payslip) + + def test_estimated_deduction_table(self): + salary = 600 + allowances = 5 + schedule_pay = 'bi-weekly' + expected_deduction = 192 + deduction = 0 + taxable_pay = 0 + estimated_deduction_table = { + '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), + 'annual': (1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000), + } + + allowance_index = allowances - 1 + if allowances > 10: + deduction = (estimated_deduction_table[schedule_pay][0]) * allowances + taxable_pay = salary - deduction + elif allowances > 0: + deduction = estimated_deduction_table[schedule_pay][allowance_index] + taxable_pay = salary - deduction + + self.assertEqual(expected_deduction, deduction) + self.assertTrue(taxable_pay < salary) + self.assertEqual(taxable_pay, salary - deduction) + + def test_standard_deduction_table(self): + salary = 3000 + schedule_pay = 'monthly' + filing_status = 'head_household' + expected_deduction = 706 + deduction = 0 + taxable_pay = 0 + standard_deduction_table = { + 'weekly': (81, 81, 163, 163), + 'bi-weekly': (163, 163, 326, 326), + 'semi-monthly': (177, 177, 353, 353), + 'monthly': (353, 352, 706, 706), + 'quarterly': (1059, 1059, 2188, 2188), + 'semi-annual': (2118, 2118, 4236, 4236), + 'annual': (4236, 4236, 8471, 8472), + } + + if filing_status == 'head_household': + _, _, _, deduction = standard_deduction_table[schedule_pay] + taxable_pay = salary - deduction + elif filing_status == 'married': + if allowances >= 2: + _, _, deduction, _ = standard_deduction_table[schedule_pay] + taxable_pay = salary - deduction + else: + _, deduction, _, _ = standard_deduction_table[schedule_pay] + taxable_pay = salary - deduction + else: + deduction, _, _, _ = standard_deduction_table[schedule_pay] + taxable_pay = salary - deduction + + self.assertEqual(expected_deduction, deduction) + self.assertTrue(taxable_pay < salary) + self.assertEqual(taxable_pay, salary - deduction)