From acbea5611dd02bafdba6e7469b86bfd5b0386302 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Sun, 5 Jan 2020 14:07:26 -0800
Subject: [PATCH 01/43] NEW `l10n_us_hr_payroll` Initiall commit for Odoo 13
(rewrite) and 2020 Federal Rates (including new W4)
---
l10n_us_hr_payroll/__init__.py | 3 +
l10n_us_hr_payroll/__manifest__.py | 35 ++
l10n_us_hr_payroll/data/base.xml | 20 +
.../data/federal/fed_940_futa_parameters.xml | 38 ++
.../data/federal/fed_940_futa_rules.xml | 34 ++
.../data/federal/fed_941_fica_parameters.xml | 88 +++
.../data/federal/fed_941_fica_rules.xml | 100 ++++
.../data/federal/fed_941_fit_parameters.xml | 504 ++++++++++++++++++
.../data/federal/fed_941_fit_rules.xml | 29 +
l10n_us_hr_payroll/data/integration_rules.xml | 29 +
l10n_us_hr_payroll/models/__init__.py | 5 +
l10n_us_hr_payroll/models/federal/__init__.py | 1 +
l10n_us_hr_payroll/models/federal/fed_940.py | 37 ++
l10n_us_hr_payroll/models/federal/fed_941.py | 239 +++++++++
l10n_us_hr_payroll/models/hr_contract.py | 28 +
l10n_us_hr_payroll/models/hr_payslip.py | 48 ++
.../models/us_payroll_config.py | 45 ++
.../security/ir.model.access.csv | 2 +
.../static/description/icon.png | Bin 0 -> 8776 bytes
l10n_us_hr_payroll/tests/__init__.py | 5 +
l10n_us_hr_payroll/tests/common.py | 150 ++++++
.../tests/test_us_payslip_2019.py | 338 ++++++++++++
.../tests/test_us_payslip_2020.py | 302 +++++++++++
.../views/hr_contract_views.xml | 19 +
.../views/us_payroll_config_views.xml | 75 +++
25 files changed, 2174 insertions(+)
create mode 100644 l10n_us_hr_payroll/__init__.py
create mode 100644 l10n_us_hr_payroll/__manifest__.py
create mode 100644 l10n_us_hr_payroll/data/base.xml
create mode 100644 l10n_us_hr_payroll/data/federal/fed_940_futa_parameters.xml
create mode 100644 l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml
create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fica_parameters.xml
create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml
create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fit_parameters.xml
create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml
create mode 100644 l10n_us_hr_payroll/data/integration_rules.xml
create mode 100644 l10n_us_hr_payroll/models/__init__.py
create mode 100644 l10n_us_hr_payroll/models/federal/__init__.py
create mode 100644 l10n_us_hr_payroll/models/federal/fed_940.py
create mode 100644 l10n_us_hr_payroll/models/federal/fed_941.py
create mode 100644 l10n_us_hr_payroll/models/hr_contract.py
create mode 100644 l10n_us_hr_payroll/models/hr_payslip.py
create mode 100644 l10n_us_hr_payroll/models/us_payroll_config.py
create mode 100644 l10n_us_hr_payroll/security/ir.model.access.csv
create mode 100644 l10n_us_hr_payroll/static/description/icon.png
create mode 100755 l10n_us_hr_payroll/tests/__init__.py
create mode 100755 l10n_us_hr_payroll/tests/common.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_payslip_2020.py
create mode 100644 l10n_us_hr_payroll/views/hr_contract_views.xml
create mode 100644 l10n_us_hr_payroll/views/us_payroll_config_views.xml
diff --git a/l10n_us_hr_payroll/__init__.py b/l10n_us_hr_payroll/__init__.py
new file mode 100644
index 00000000..09434554
--- /dev/null
+++ b/l10n_us_hr_payroll/__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/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
new file mode 100644
index 00000000..a748aae6
--- /dev/null
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -0,0 +1,35 @@
+# 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',
+ 'views/hr_contract_views.xml',
+ 'views/us_payroll_config_views.xml',
+ ],
+ 'demo': [
+ ],
+ 'auto_install': False,
+ '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..6838f283
--- /dev/null
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ USA Employee
+
+
+
+
+
+ USA Employee Standard
+
+
+
+
+
+
+
\ 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..5b315100
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ US Federal 940 - EFTPS
+
+
+
+ ER: Federal 940 FUTA
+ ER_US_940_FUTA
+
+
+
+
+
+ WAGE: Federal 940 FUTA Exempt
+ WAGE_US_940_FUTA_EXEMPT
+
+
+
+
+
+
+ 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..324958a4
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml
@@ -0,0 +1,100 @@
+
+
+
+
+ US Federal 941 - EFTPS
+
+
+
+ EE: Federal 941 FICA
+ EE_US_941_FICA
+
+
+
+
+ ER: Federal 941 FICA
+ ER_US_941_FICA
+
+
+
+
+
+ WAGE: Federal 941 FICA Exempt
+ WAGE_US_941_FICA_EXEMPT
+
+
+
+
+
+
+
+
+ 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..a7751adf
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml
@@ -0,0 +1,29 @@
+
+
+
+
+ WAGE: Federal 941 Income Tax Exempt
+ WAGE_US_941_FIT_EXEMPT
+
+
+
+ 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/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py
new file mode 100644
index 00000000..c6d607ff
--- /dev/null
+++ b/l10n_us_hr_payroll/models/__init__.py
@@ -0,0 +1,5 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import hr_contract
+from . import hr_payslip
+from . import us_payroll_config
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..1cf042c7
--- /dev/null
+++ b/l10n_us_hr_payroll/models/federal/fed_940.py
@@ -0,0 +1,37 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+def er_us_940_futa(payslip, categories, worked_days, inputs):
+ """
+ Returns FUTA eligible wage and rate.
+ WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT
+ :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
+ 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('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage_base = payslip.rule_parameter('fed_940_futa_wage_base')
+ remaining = wage_base - ytd_wage
+
+ wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT
+
+ 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..e6288f88
--- /dev/null
+++ b/l10n_us_hr_payroll/models/federal/fed_941.py
@@ -0,0 +1,239 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+# import logging
+# _logger = logging.getLogger(__name__)
+
+
+def ee_us_941_fica_ss(payslip, categories, worked_days, inputs):
+ """
+ Returns FICA Social Security eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_FICA_EXEMPT
+ :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
+ 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('WAGE_US_941_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage_base = payslip.rule_parameter('fed_941_fica_ss_wage_base')
+ remaining = wage_base - ytd_wage
+
+ wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
+
+ 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.
+ WAGE = GROSS - WAGE_US_941_FICA_EXEMPT
+ :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
+ 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('WAGE_US_941_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage_base = float(payslip.rule_parameter('fed_941_fica_m_wage_base')) # inf
+ remaining = wage_base - ytd_wage
+
+ wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
+
+ 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.
+ Note that this wage is not capped like the above rules.
+ WAGE = GROSS - WAGE_FICA_EXEMPT
+ :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
+ 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('WAGE_US_941_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage_start = payslip.rule_parameter('fed_941_fica_m_add_wage_start')
+ existing_wage = ytd_wage - wage_start
+
+ wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
+
+ 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
+
+
+# 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.
+ WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ :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 = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ #_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..92a1e0fd
--- /dev/null
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -0,0 +1,48 @@
+# 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
+
+
+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,
+ })
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
new file mode 100644
index 00000000..ed0e6d73
--- /dev/null
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -0,0 +1,45 @@
+# 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")
+
+ 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)')
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 0000000000000000000000000000000000000000..2a58a38132bd0b9a86324caa86d0cc6fc145fd4c
GIT binary patch
literal 8776
zcmai4XH-+$wx)Nb3DQeYst|e!z1K(=klqOd2qg3>M3AafrFRgJ04g9IK|p%%(gXyg
z_bR-2&bjy8JMMk&{n*J~Ykuq7-<)&JxyBx2N9jCPy+cGtgn@x^M_o-x5B;z6`y#+Y
z|HhLaR$yS@yn`4RBaFdXAX|4=K5IL78+$%qR~XukfgvU93$wO$u}84j*gHbpqyc-4
ztpFB?oixBm1k4YHDcUb6YyB9(l06^PW{vt;}9R6l^ga73|IuO9$7N7thKk#4S_P!7YW#fO!v4y~_`J7?E
z-_!qrwe~{0{g>{4WBw-^jrb4t|7Xd+ib2QtzoUJvVgI294F-e%Z@8=Lzo~&El)cf>
z`fncm;{k93KbSpG&mQjX}~IcVEaSi@8E&dU9BDMwd~y-5l+$oaej1ie+T|=
zlM%%3KP|t>pkBXIX6Ob%KVI5LYx7{p^3y`?uNTznRf-1)<3y4t`432z#_%egO$y0Rdiq5d#5n
z5Wg@;n4gy)eehcy9oSCAndK0u0urL4yn=iJ7#N%h
zUI{}yO1k&DW$_Z2)D`2pF|1hn8?wAGmzB
zYMh_-%9AejE`s%`T$=@=lEG#
zoW>{R(K#H!mO$7$d&NuT-E=xv)0=ZO_49sG1Qu0IyJB>4M!Ri0WqGl~Q7WuFF$zeO
zL*TRJDZfl_sx5vEh0iW)XPg)dlc3nG-5ZdU58v4#42><~wFgj~t!LGiqp$kG+Un5z
z?j0<>oUBc;LJDtKf=GH9zG#Osndq{&o^gaE+V>dRO@1x-pp;I+KA(?LJ1P8G4(r?CkME}LzMVl)(7*9^I8pyQyq29DMkz%{&-;YjrC;hes
z^Vc1xn1j_~W(V-1x{80ru=ljdvJt29;|iYMLF
zUG(p(;&?Sv-%ZTVKAr!0vU3&SQ*p5(Q`5jZL*WbA%nTeV9jn}{xSTKB`(-#$8GKUC
zWBjY_wkxS$k=!vjrzIHWLR@*vy+$RUW4_YYLw=db%|v`?rZ613NQC1Rky?|~e^0$I
zMomr^r!T-%_ul88QsMY5F@Xh!qLPBBf)&ED;yZGjcKqXuU5GL{Zqq~Da-zN~(|xI)
zo=xFw4stow0Llumb^PN838{fK{^o)@`wz@3;;s+&n!@WAtEgFV)YONEFTOU49UYu4
zs&P8?y9^E(ksfd`w}#tG^WKPXZSKV%)oU;GfHUxdO7$p+=mM{Qi5({RxU=WFusl>C
zaBB?~3AvW^86Kg?s6?)-?>A`*LI~8>Gy<>k?$hK9=0b?7osbHpz1lf4$UK)fsfwEL
zWQ~c{wgfJ2kr<^ixkuCr8fwKcfm@%m7$hWAzYY^bV%_5rVOj{r=<|k|y`1#Ley*1?
zkKe8C5M2njpG~(#G_AN@0xt7)m-ND{POyU~w&*;!ei%0G4aQxc)YS@kLh(Y^nk%lC
zyWLiL)>V9+u7V3WqLA2uKW|Ji&M#&6a?MbAZ;8&ny^o(mnGRUqaGr?ZH`#jwlI@lpyW;1iW<522u4|0WE-$c}@3V;=x>tXx~7MXi=Ra43H
z(7feOOaufpbl5=^3?NY4)+ZL6?Ddhw39WkPWq&4AvRZdRkwAOz!V_V`&dltUbH~=O
z9TVU0qvP_Qf8rMwa|!jru^=B4V~NyZKv(~3Ev7P4&z`s(bCaGHi_*G8!E
zSR}{8D;0JDl92bCJ5U>YOZuTb`i>K+!ehb~w_h0e@mfj!sr-l(o0-B7Prvtlh}^6x
ze|<``@rh~1j27u$9C*Gk5DPAn5EmqDi?-kSJ(N0Mm5GlX&@EMKUuN-ft0#G|9dvPT
z#*7eY)sZf<8#L#y()~kRAREJK=BLGO$dt(SBEFC3VJUVp=DlY&8#ws>1S?CtRM?f2
zVH2ck=K3ZhQySd*3Y<4@L}jw_N?DO)VTjr!NW@Pf$;lUJh+Q@uolGWY<}>Y8vQWr^M+clXt+oB-1B`GK#R2KXXU
z>e>O6IU!4Y+2@@Ei~jq5dOcFhPA?W4_VC)}vu-cNUcz8@6nWggGwiv@NPjUp71I7S
zfr~6`LUI0jbFU?dS*kXYS*G@Ny;63B@teiQ5aN`wPNSBCHlfaggi#lyQVXESBd9eK>JM;lO{@%Y#4qS}#N60}ER)Lk^T-C=5XjDWU~Kv+
zwaYV|FAex+ukPKfLAa?X^9UB|?HS!0z2CIngv_PA(ZEhLpMJCq921^t)qG3~SMwvIbKyv->5+tVh#sfYYL_IP!Owh0r)L*rKE;
zZ2zmUBx$CSMii>_o7a*3MtW~8BfbDHeyCT6v{}d+hHT*VLy9y2#?ArW0{kjv8w5*i
z2IgP|DqimLzP6R*dwG5R%3TO6V~ThEaM`WpcG@Bgu6`pW^0V;+^1U!g(_<$!f(^Q@
zh=8xFJktSYa`~J@*v%ElaLgOmt6=CHCLv>m03ZF&9|;hU%=DpASVWM
z!-P>W9aKvoZDjm_4(AjBE<$gHBD`OzulQAuA4|mCU&Ys06iDj8Eu({mrLa1%o1rp~y3CU2J%awa!
zg@gu3|3MPh=+sO`rs7(@SMsVT#hwbv=wW)aV+7BRZ1k382hFYm-
zwfq_VE8;X!Rc8Tq0jQNcJEC#b<6o?fbaKu@|L50KB?}9_Xzh@
zTQBY^$Y%j>e`#}3a}yXYFU(CIZS76B<^0-Vi)G5d#4LZ}eQ<%l`yG9RtbOJf!`7gjT|xJJxDmDdyCR=g%EbCOr?-C1$z7~4fB&H>BH8ZHo@urv
z$9ARX0%f0mSJIoA^{q>CdCLW>`OA@N38p|#=F$d2#d)@0OG#HW5&~O|4bMC$R!g%A
z#-WkvknrO^W}2*ghgtMVNykW9sn>~am?*Iyy#X!0#d`$edx=Yk?B{;CEDs4hZVtVC
z6^7T|TnAf+Jswi9$fx7%(th|Fy04ocypqRfh`WG6zy%x`xdJ?MD0#1G9;r!ZGvvyA
z<#Q*HkxZ7@>|hV4<*rk79?*-C5AtHu{bs3&ZUdL3Kc4!VRp=6wqC!XJrNJwOgMsw1
z$*AKFxi5YUA$q!18FW=oGe$J1uzE=HcVB#>@mMgK_Ld$^;oj7^(|z;;kK}2)ybUI{
zGJzSF0OS2WFApiwsU%;$LEyk+O<7kC9)(5$@W!
zyt0oGiXV~TocBxfex>6yWIr%3O?+fj-7IwF5K%TtHav0>63T+8JalPYaq@rj}g0Gvg>gX(@gU)(Ps*|D0=qm&P7%851t;Os}!FZCFC~?
zLc;o~QX9Bqa(mz;!y2ZUZK{j#kyrS+C_0wolAQc&=G)j92);(lV(>2>)LYmuywygx
zAjO6s^mwfNyA5e_+{8o8Upgt`9&AIcb-`L(v_ia@Mo)&1$ZEr3W|r7U*~j@VYCB%B
z;d+-2(JKI{A)f#)q781nvB9615oC{uX>j-wUzbC`caCn=xYXBTV!l!q$0hRQ4>}C9
zJB(16D0Si}FLeUM=PSg67(cm!eXIH3-v41r9v_p)lUI1OVLSE~ZlY8g_G(F4@l%$E
zrCR`{1-w5*JrvuH>iMqmYCgZK(%P3&zuMmD_T;=PKIth%@EZT!Y?dm=dpoO-@|oBO
zmT2g*f4K9+~&V_q8k?0;t
zkD^ezP?!8~vuZW0v5dlG6vyf6RXXh3>c=U~##5H@bECeZQQ1lvTDdz?vC=Y%BDwl;
z%2n8xO_@3^TEZ!8iClw8T;X^>iYE*fYspJmDWUj#>y5D1gCNdqN5f-wMIJ^FUTAQ4
z+(t*=udhcd`btdK5|;<17qpe*vtPE$WCVkDlAmV4PCCsED@~Nf{JwvnO%J)D=$%8g
zyF_RAuml}`yMH&y6Z)P$lB?zm%*$1tX&4;i#I3#6V*-lL1Q>9#Ia7ajJS%V@N?r^Q
zI+YXiG$C~mNB+8Q5z8o;LS9zOP2ESL$S=QjKX1q<70KeboaY#M37(igezoJ`xScN2
zkJ>GnrBN<(R3-OK*Wjhj8>i?fGx@@vRQ2A`f;a_AezZhbj0Nhk&oZEAbL=B`T)j7!
zAwb`*^j`&LGdG{>c3n%xE}
zg1)Xybs<-$)eQO{Yc%O9rnX39aU{pX3(w~Ud9f@0)AHfO&g
zS=ddj!{n(K1u?F!;j`qDwPXo#fj+ZIOAk&|cl0`G^>(Lj@}=F1cb)F_KI61v^H`gs
z7^3d>$aK(*v3Fq;iQ$H$kXLg0C>?T?EIk#^rrM+H6uTi{=pxt%jpg#4h4#WI`7qFhgqebk+{YKirM;Fp5&UE^c4f)REn#Q6sEf*M0jGD#`!31HWrV1_XX@u=a8x75W435G34ZJW7)Yo=%52Go5wiY}BmIX2u
zBg4@(I$3Go==&Ucfk!jCg~&w6(fEe&-qX5RaNkpOUeX4rgB*F5p4PUeP8iTBUt=Ui=zh$t{xuYcE=<|JLY;%`8mt~73>wnle8d*3!H7jq!=
z*%48>#fu98&GD=pg*HS>zv`{Wl?f)n!?C=lVu2#cX?1x;7(O>Is^lvAr1(>oNn6ht
z^y@n#sy4;%xlCe7w6GWjJnvy!gV2lMWp-F!Ov+&bLfP5mjhBuL&F)vo&nT`QEo(Q=rTnNnyB=y+N
z-UYLU16$YWP!;}P4uTuLBk;I5+NrGFDuC^^
zmzZrM(@GS{j2&~I~>HL!=h6jlgFsg4X5I@
zE-UW*97T%+7%xz-H&8K}Ne*M4m&=TKiwkBgZD?VAkSXrs`0?W5pi}PsJGP`JfVhYH
zO@2=8p4ZU?@~%fu6<+QsRrtJi0%@;raP=P0I6P3Rc7A^~jl)4no9gog5`WGyIo2si
z*~3t(Kse{|O6~*uxv|-LxzOE#hV~tStS77<&lKu=QEV$(4+M|vIw8^Ln)O81kZ=)FLK1*N-p3fcjS(;p00eRw@c=%IA?JmaM8C(h3(8JUOyjbJbkQ
zmonAD!p?QjFAbhfGL=ssnQ9bQA?Rqn1r$11wzPX~^|6<_7`V3*F+>gvU6H5IMQ@F^
zIwt?o&|4}euH=9<8k(6x-!t2zMm!qGcy9Y
zOEXtQRrB0d{YS_q8c~W}wm8|6FF+acZ`kidJ#xwZ7RN}W2dt^ecIk!`JL1|I5y;XIWv)0qq^Lf*gV?-wb(`62~f=D-CM;_5_}KI?*?_y
zLhpI|nC5S&NKghJz6FE8kpHwP&Yf;BePq|8l2&Y5qW;BwfIPRUs8z)_Yt^X@>
z?F&6r{tRDSJ%xc*Qv|=85%DREd6Fvp(|~{l51!&rx|#u(>eoIL(*ek@2V6FrSQ_<2
zsn}%^_esAiEBGGZZBt3V$#}9{E0$BEoa;dBq+i+u$|!9z!O0?}vh(wGI
z&nyZnOWpLb(V8qC9hPNW%^FYyK|#35!8JcxgPq!tpbXT8Dm%=SPxj0$J%6cIbh%bE
zD`?NF-6eKOM5Ssd$BSHPLWw>@K1*W|OOsl-07G3$g3;eUhi3ul+HRJowHtVVY{eQ^
zq7Fi|Qx{7Ut}gE5o3SJ>xSBFrwR(GY3EzF$n%5qa!aW9uP;4?3;kI;55nD@aa1;--
z|Db5|-K(u=eTUU*EiP>+6SMN9gH-xR6My$jDwy3UQ+BX{Y*UN8&--l}^>g_4;%t0}
z7N7$dFsjgVk6@cl-uRRT_Wd>YHQtKwGi^-2omJsy_f>(Ps#ZQd7?ogm)GK`d+;3_O
zB0Sl8nODZ0F#O$>cql0|T|Gfu{O;O9Pq(r@XjRCW!d}lYP)s7BxIbH)KR+)O9$FR(xyk$-{gU7~l>A>ir
z>4W1he6KvIrzaX+w-WJ4_&$2KVy>px#qU!#UUR-Xk~5^T86k^6k>AA?`1qljt)VzC
zYsH__ij0Vkra?tfSok(5Vk)ePQ9<$){pulQB^x|sqSrsw;4?Sb(YA>0XJJFkhmBwT
zy4V$`Hg=rtb1TE+YuORziW*UnnRSLKv#Abmv)c@k29*#W`ab_zlgY8_uR_0^hgCTU
zc6|x0?w$+4j%zfVE_~NqZui;>G>SPDwRcs(S&N`ygA^V9&?lrWJciC%L-w4>)WExD
zQ*0SPf!F<)-IkbO{ixrsj%2vy?C+I=KVe%xHC$3{Ydv+FYgpAyjUm{cuNA?*b~N*8
z3_eaKk@-Mcgjhx&w%%&?hxat794JgOaYs%K#{g*qNrO~9KQv=@v+5E%i4r2STq|+Nlv!RvvqFv_~7S`(a8j=nSoZ=qJm&4AVpd`pY!{p
zFsY$4@tmy%p1f5}WcD<8|3XnUSz(jM_b?A#7nJuGKGxw?&P4Y#%Cd!$MZX`6iIB0b
zF!i%s%igkekI=Vf%8m?%M(?ix`!~ATTY1ZV$bR?l4ybcA7tC37Sq>}s$gy`53&8fT
zB#B(G^jr7Z6RCU}lyd)0b1mpb@D;^F{dYJyvY&*`w{P}J>%X@y`;Ik}-VQy?fmjk(
zYUC#C!5oGRD>&}R63OvAgoEKkb(3aikB5@Bq_u08#4(XN!v<#`p(eMIw-Q8LpFIqb
zuweszf`pNK_C`AUYN`{#qN9rm*wDf4+N_%`Og#b*U2=cwXL=gAIh@$C&a4DD)DH@>
z5pqY-YjP+1J*d&rdasGiRbGn+G)ZFbLQHB!Oh!rH8vP_`z=C>s7AIsyPCAu=sJCCp
z(r-az$!0%-o&g>wIZP$hvrPQ#dAA5hl4&yM5Ai4fey-w%?uXs`Zum9HaO~~%c4tb;
z4$$quVe~a^Jf$umPbt?1EOPTh8pt^^tYE7RN{WKrlngC8y?lJ^|NPJ&Nb&Pgt(c|C
zfWI(e?2;=GOFPMFVhn;+eqw-Erl+xG!*XC|dPQ8L=eY~~tS0B*8U2GDey7W7@x>fj
zk9yx-+)4|0n~ca5m#gWsVF2@Mp^UpOS-ga{pKb$ERDBl6n|2QwEm|0a&zN^e=RXD?
zG7OoVH^orI5ZqJn)zhF1>95{jJcsNMd#YgHk<(AlFWP7pqe}MeFPEt<$2X_Uqo3l)
z+B7hRycqZG+i|zQy#(
X4vqDeFsl6h0f@TtV
+
+
+
+ hr.contract.form.inherit
+ hr.contract
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..dfe0e301
--- /dev/null
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -0,0 +1,75 @@
+
+
+
+ hr.contract.us_payroll_config.tree
+ hr.contract.us_payroll_config
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.contract.us_payroll_config.form
+ hr.contract.us_payroll_config
+
+
+
+ Form 941 / W4 - Federal Income Tax
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.contract.us_payroll_config.search
+ hr.contract.us_payroll_config
+
+
+
+
+
+
+
+
+
+
+ Employee Payroll Forms
+ hr.contract.us_payroll_config
+ tree,form
+
+
+ No Forms
+
+
+
+
+
+
From e377ea224a9235dd0b65e1857598eec490dea818 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Tue, 7 Jan 2020 07:52:41 -0800
Subject: [PATCH 02/43] IMP `l10n_us_hr_payroll` Add Generic SUTA Category and
method, add FL Florida (unemployment, no income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/base.xml | 12 +++
l10n_us_hr_payroll/data/state/fl_florida.xml | 63 ++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/__init__.py | 1 +
l10n_us_hr_payroll/models/state/general.py | 85 +++++++++++++++++++
.../models/us_payroll_config.py | 1 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
l10n_us_hr_payroll/tests/common.py | 18 ++++
.../tests/test_us_fl_florida_payslip_2019.py | 82 ++++++++++++++++++
.../tests/test_us_fl_florida_payslip_2020.py | 82 ++++++++++++++++++
.../views/us_payroll_config_views.xml | 4 +
12 files changed, 354 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/fl_florida.xml
create mode 100644 l10n_us_hr_payroll/models/state/__init__.py
create mode 100644 l10n_us_hr_payroll/models/state/general.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index a748aae6..01fb7563 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -25,6 +25,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fica_rules.xml',
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
+ 'data/state/fl_florida.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
],
diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml
index 6838f283..a614f313 100644
--- a/l10n_us_hr_payroll/data/base.xml
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -17,4 +17,16 @@
]"/>
+
+
+ EE: State Unemployment SUTA
+ EE_US_SUTA
+
+
+
+ ER: State Unemployment SUTA
+ ER_US_SUTA
+
+
+
\ 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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 92a1e0fd..2865ee56 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -9,6 +9,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
er_us_941_fica_ss, \
er_us_941_fica_m, \
ee_us_941_fit
+from .state.general import general_state_unemployment
class HRPayslip(models.Model):
@@ -37,6 +38,7 @@ class HRPayslip(models.Model):
'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,
})
return res
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/general.py b/l10n_us_hr_payroll/models/state/general.py
new file mode 100644
index 00000000..f576510d
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -0,0 +1,85 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+from odoo.exceptions import UserError
+
+# import logging
+# _logger = logging.getLogger(__name__)
+
+
+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 - WAGE_US_940_FUTA_EXEMPT
+
+ The contract's `futa_type` determines if SUTA should be collected.
+
+ Function parameters:
+ wage_base, wage_start, rate can either be strings (rule_parameters) or floats
+
+ :return: result, result_rate (wage, percent)
+ """
+
+ if state_code != payslip.contract_id.us_payroll_config_value('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
+
+ # 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
+
+ # Determine 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('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT
+ #_logger.warn('ytd_wage: ' + str(ytd_wage) + ' wage: ' + str(wage))
+
+ 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
diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index ed0e6d73..d87ebcac 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -14,6 +14,7 @@ class HRContractUSPayrollConfig(models.Model):
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')
fed_940_type = fields.Selection([
(FUTA_TYPE_EXEMPT, 'Exempt (0%)'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 23702419..10545ad0 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -3,3 +3,6 @@
from . import common
from . import test_us_payslip_2019
from . import test_us_payslip_2020
+
+from . import test_us_fl_florida_payslip_2019
+from . import test_us_fl_florida_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py
index fc84b0fe..57dd216f 100755
--- a/l10n_us_hr_payroll/tests/common.py
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -61,6 +61,10 @@ class TestUsPayslip(common.TransactionCase):
'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'):
@@ -148,3 +152,17 @@ class TestUsPayslip(common.TransactionCase):
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14')
payslip.compute_sheet()
+
+ 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
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..981e9ce0
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py
@@ -0,0 +1,82 @@
+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..b38e77d6
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py
@@ -0,0 +1,82 @@
+from .common import TestUsPayslip, process_payslip
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+
+class TestUsFlPayslip(TestUsPayslip):
+ ###
+ # 2020 Taxes and Rates
+ ###
+ FL_UNEMP_MAX_WAGE = 7000.0
+ FL_UNEMP = -2.7 / 100.0
+
+ def test_2020_taxes(self):
+ salary = 5000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('FL'))
+
+ self._log('2020 Florida 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_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('2020 Florida tax second payslip:')
+ payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_fl_unemp_wages * self.FL_UNEMP)
+
+ def test_2020_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('2020 Forida_external 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_SUTA'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP)
+
+ def test_2020_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('2020 Forida_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index dfe0e301..e7ae0338 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -40,6 +40,10 @@
+
+
+ No additional fields.
+
From cb2d1cd9d453f57f37b07caea8530d4f7a20c216 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Tue, 7 Jan 2020 10:51:11 -0800
Subject: [PATCH 03/43] IMP `l10n_us_hr_payroll` Refactor SUTA tests into
generic test. (Reworked Florida 2020)
---
l10n_us_hr_payroll/tests/common.py | 57 ++++++++++++++
.../tests/test_us_fl_florida_payslip_2020.py | 78 ++-----------------
2 files changed, 62 insertions(+), 73 deletions(-)
diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py
index 57dd216f..1a15fa85 100755
--- a/l10n_us_hr_payroll/tests/common.py
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -3,9 +3,11 @@
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):
@@ -166,3 +168,58 @@ class TestUsPayslip(common.TransactionCase):
], limit=1)
cache[code] = us_state
return us_state
+
+ def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract):
+ 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)
+ self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0)
+
+ contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_BASIC
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('ER_US_SUTA', 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)
+ self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), wage * rate)
+
+ if wage_base:
+ process_payslip(payslip)
+
+ remaining_unemp_wages = wage_base - wage
+ self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative
+ payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60))
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('ER_US_SUTA', 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)
+ 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
index b38e77d6..b32c1030 100755
--- 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
@@ -1,5 +1,5 @@
-from .common import TestUsPayslip, process_payslip
-from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+from datetime import date
+from .common import TestUsPayslip
class TestUsFlPayslip(TestUsPayslip):
@@ -7,76 +7,8 @@ class TestUsFlPayslip(TestUsPayslip):
# 2020 Taxes and Rates
###
FL_UNEMP_MAX_WAGE = 7000.0
- FL_UNEMP = -2.7 / 100.0
+ FL_UNEMP = 2.7
def test_2020_taxes(self):
- salary = 5000.0
-
- employee = self._createEmployee()
- contract = self._createContract(employee,
- wage=salary,
- state_id=self.get_us_state('FL'))
-
- self._log('2020 Florida 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_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('2020 Florida tax second payslip:')
- payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_fl_unemp_wages * self.FL_UNEMP)
-
- def test_2020_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('2020 Forida_external 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_SUTA'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP)
-
- def test_2020_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('2020 Forida_external tax first payslip:')
- payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0)
+ # Only has state unemployment
+ self._test_er_suta('FL', self.FL_UNEMP, date(2020, 1, 1), wage_base=self.FL_UNEMP_MAX_WAGE)
From 64c160035ed6a0993d0248257990deacf254c9c7 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Tue, 7 Jan 2020 14:58:58 -0800
Subject: [PATCH 04/43] IMP `l10n_us_hr_payroll` Add Generic SIT Category and
method, add PA Pennsylvania (unemployment (ER, EE), income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/base.xml | 7 +
.../data/state/pa_pennsylvania.xml | 131 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 4 +-
l10n_us_hr_payroll/models/state/general.py | 92 ++++++++----
l10n_us_hr_payroll/tests/__init__.py | 2 +
l10n_us_hr_payroll/tests/common.py | 31 +++--
.../tests/test_us_fl_florida_payslip_2019.py | 2 +
.../tests/test_us_fl_florida_payslip_2020.py | 2 +
.../test_us_pa_pennsylvania_payslip_2019.py | 33 +++++
.../test_us_pa_pennsylvania_payslip_2020.py | 31 +++++
11 files changed, 295 insertions(+), 41 deletions(-)
create mode 100644 l10n_us_hr_payroll/data/state/pa_pennsylvania.xml
create mode 100755 l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 01fb7563..6c3e9aa2 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -26,6 +26,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
+ 'data/state/pa_pennsylvania.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
],
diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml
index a614f313..6bd03f57 100644
--- a/l10n_us_hr_payroll/data/base.xml
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -29,4 +29,11 @@
+
+
+ EE: State Income Tax Withholding
+ EE_US_SIT
+
+
+
\ 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..53312b39
--- /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 (RT-6)
+ 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 (RT-6)
+ 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
+ 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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 2865ee56..7d92f5b4 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -9,7 +9,8 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
er_us_941_fica_ss, \
er_us_941_fica_m, \
ee_us_941_fit
-from .state.general import general_state_unemployment
+from .state.general import general_state_unemployment, \
+ general_state_income_withholding
class HRPayslip(models.Model):
@@ -39,6 +40,7 @@ class HRPayslip(models.Model):
'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,
})
return res
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index f576510d..0185aea8 100644
--- a/l10n_us_hr_payroll/models/state/general.py
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -5,26 +5,17 @@ from odoo.exceptions import UserError
# _logger = logging.getLogger(__name__)
-def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None):
+def _state_applies(payslip, state_code):
+ return state_code == payslip.contract_id.us_payroll_config_value('state_code')
+
+
+def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, rate=None):
"""
- Returns SUTA eligible wage and rate.
- WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT
-
- The contract's `futa_type` determines if SUTA should be collected.
-
Function parameters:
wage_base, wage_start, rate can either be strings (rule_parameters) or floats
-
- :return: result, result_rate (wage, percent)
+ :return: result, result_rate(wage, percent)
"""
- if state_code != payslip.contract_id.us_payroll_config_value('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
-
# Resolve parameters. On exception, return (probably missing a year, would rather not have exception)
if wage_base and isinstance(wage_base, str):
try:
@@ -50,15 +41,6 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba
# Rate assumed positive percentage!
rate = -rate
- # Determine 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('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
-
- wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT
- #_logger.warn('ytd_wage: ' + str(ytd_wage) + ' wage: ' + str(wage))
-
if wage_base:
remaining = wage_base - ytd_wage
if remaining < 0.0:
@@ -68,18 +50,70 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba
else:
result = wage
- #_logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate))
+ # _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))
+ # _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))
+ # _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))
+ # _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))
+ # _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 - WAGE_US_940_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
+ 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('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage = categories.GROSS - categories.WAGE_US_940_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 SUTA eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+
+ The Federal Income Tax Filing status (W4) is used for Exemption.
+
+ :return: result, result_rate (wage, percent)
+ """
+ if not _state_applies(payslip, state_code):
+ return 0.0, 0.0
+
+ if not payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status'):
+ return 0.0, 0.0
+
+ # Determine 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('WAGE_US_941_FIT_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01')
+ ytd_wage += payslip.contract_id.external_wages
+
+ wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ return _general_rate(payslip, wage, ytd_wage, wage_base=wage_base, wage_start=wage_start, rate=rate)
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 10545ad0..57f8e5e1 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -6,3 +6,5 @@ from . import test_us_payslip_2020
from . import test_us_fl_florida_payslip_2019
from . import test_us_fl_florida_payslip_2020
+from . import test_us_pa_pennsylvania_payslip_2019
+from . import test_us_pa_pennsylvania_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py
index 1a15fa85..c20bc34d 100755
--- a/l10n_us_hr_payroll/tests/common.py
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -169,7 +169,7 @@ class TestUsPayslip(common.TransactionCase):
cache[code] = us_state
return us_state
- def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract):
+ def _test_suta(self, category, state_code, rate, date, wage_base=None, **extra_contract):
if wage_base:
# Slightly larger than 1/2 the wage_base
wage = round(wage_base / 2.0) + 100.0
@@ -192,28 +192,29 @@ class TestUsPayslip(common.TransactionCase):
contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_EXEMPT
payslip.compute_sheet()
cats = self._getCategories(payslip)
- self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0)
+ self.assertPayrollEqual(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)
- self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0)
+ self.assertPayrollEqual(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)
- self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), wage * rate)
+ self.assertPayrollEqual(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:
- process_payslip(payslip)
-
remaining_unemp_wages = wage_base - wage
self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative
- payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60))
- payslip.compute_sheet()
- cats = self._getCategories(payslip)
- self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), remaining_unemp_wages * rate)
+ self.assertPayrollEqual(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)
@@ -222,4 +223,12 @@ class TestUsPayslip(common.TransactionCase):
contract.external_wages = wage
payslip.compute_sheet()
cats = self._getCategories(payslip)
- self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0)
+ self.assertPayrollEqual(cats.get(category, 0.0), 0.0)
+ else:
+ self.assertPayrollEqual(cats.get(category, 0.0), wage * rate)
+
+ def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract):
+ self._test_suta('ER_US_SUTA', state_code, rate, date, wage_base=wage_base, **extra_contract)
+
+ def _test_ee_suta(self, state_code, rate, date, wage_base=None, **extra_contract):
+ self._test_suta('EE_US_SUTA', state_code, rate, date, wage_base=wage_base, **extra_contract)
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
index 981e9ce0..419be377 100755
--- 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
@@ -1,3 +1,5 @@
+# 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
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
index b32c1030..5952eb1f 100755
--- 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
@@ -1,3 +1,5 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
from datetime import date
from .common import TestUsPayslip
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..9433e805
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py
@@ -0,0 +1,31 @@
+# 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)
From b409dba9103a2567a47c5c9da1c1cdbbfa48593f Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Tue, 7 Jan 2020 16:06:31 -0800
Subject: [PATCH 05/43] IMP `l10n_us_hr_payroll` Implement generic state income
tax exempt and additional fields. Include in PA Tests and State Form
section.
---
l10n_us_hr_payroll/data/state/pa_pennsylvania.xml | 6 +++---
l10n_us_hr_payroll/models/state/general.py | 12 ++++++++----
l10n_us_hr_payroll/models/us_payroll_config.py | 2 ++
.../tests/test_us_pa_pennsylvania_payslip_2020.py | 12 ++++++++++++
l10n_us_hr_payroll/views/us_payroll_config_views.xml | 4 ++++
5 files changed, 29 insertions(+), 7 deletions(-)
diff --git a/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml b/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml
index 53312b39..f46a92b4 100644
--- a/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml
+++ b/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml
@@ -90,7 +90,7 @@
- ER: US PA Pennsylvania State Unemployment (RT-6)
+ 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')
@@ -104,7 +104,7 @@
- EE: US PA Pennsylvania State Unemployment (RT-6)
+ 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')
@@ -118,7 +118,7 @@
- EE: US PA Pennsylvania State Income Tax Withholding
+ 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')
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index 0185aea8..eff29da8 100644
--- a/l10n_us_hr_payroll/models/state/general.py
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -99,14 +99,12 @@ def general_state_income_withholding(payslip, categories, worked_days, inputs, w
Returns SUTA eligible wage and rate.
WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
- The Federal Income Tax Filing status (W4) is used for Exemption.
-
:return: result, result_rate (wage, percent)
"""
if not _state_applies(payslip, state_code):
return 0.0, 0.0
- if not payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status'):
+ if payslip.contract_id.us_payroll_config_value('state_income_tax_exempt'):
return 0.0, 0.0
# Determine Wage
@@ -116,4 +114,10 @@ def general_state_income_withholding(payslip, categories, worked_days, inputs, w
ytd_wage += payslip.contract_id.external_wages
wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
- return _general_rate(payslip, wage, ytd_wage, wage_base=wage_base, wage_start=wage_start, rate=rate)
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index d87ebcac..e75b5210 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -15,6 +15,8 @@ class HRContractUSPayrollConfig(models.Model):
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')
fed_940_type = fields.Selection([
(FUTA_TYPE_EXEMPT, 'Exempt (0%)'),
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
index 9433e805..3dd3fd27 100755
--- 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
@@ -29,3 +29,15 @@ class TestUsPAPayslip(TestUsPayslip):
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index e7ae0338..701920b3 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -44,6 +44,10 @@
No additional fields.
+
+
+
+
From dc2e895b40d9bc9354f99410048491977d7e1e9c Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 8 Jan 2020 08:04:07 -0800
Subject: [PATCH 06/43] IMP `l10n_us_hr_payroll` Add MT Montana (unemployment
(with AFT), income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/mt_montana.xml | 176 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/general.py | 2 +-
l10n_us_hr_payroll/models/state/mt_montana.py | 41 ++++
.../models/us_payroll_config.py | 12 ++
l10n_us_hr_payroll/tests/__init__.py | 2 +
.../tests/test_us_mt_montana_payslip_2019.py | 139 ++++++++++++++
.../tests/test_us_mt_montana_payslip_2020.py | 17 ++
.../views/us_payroll_config_views.xml | 6 +
10 files changed, 397 insertions(+), 1 deletion(-)
create mode 100644 l10n_us_hr_payroll/data/state/mt_montana.xml
create mode 100644 l10n_us_hr_payroll/models/state/mt_montana.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 6c3e9aa2..4c9df42a 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -26,6 +26,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
+ 'data/state/mt_montana.xml',
'data/state/pa_pennsylvania.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
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..ec18a955
--- /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_suta_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_suta_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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 7d92f5b4..93e4f64b 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -11,6 +11,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
ee_us_941_fit
from .state.general import general_state_unemployment, \
general_state_income_withholding
+from .state.mt_montana import mt_montana_state_income_withholding
class HRPayslip(models.Model):
@@ -41,6 +42,7 @@ class HRPayslip(models.Model):
'ee_us_941_fit': ee_us_941_fit,
'general_state_unemployment': general_state_unemployment,
'general_state_income_withholding': general_state_income_withholding,
+ 'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
})
return res
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index eff29da8..faf3f477 100644
--- a/l10n_us_hr_payroll/models/state/general.py
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -96,7 +96,7 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba
def general_state_income_withholding(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None):
"""
- Returns SUTA eligible wage and rate.
+ Returns SIT eligible wage and rate.
WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
:return: result, result_rate (wage, percent)
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..727c56e9
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/mt_montana.py
@@ -0,0 +1,41 @@
+from .general import _state_applies
+
+
+def mt_montana_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 - WAGE_US_941_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 = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ 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_suta_sit_exemption_rate').get(schedule_pay)
+ withholding_rate = payslip.rule_parameter('us_mt_suta_sit_rate').get(schedule_pay)
+ if not exemption_rate or not withholding_rate or wage == 0.0:
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index e75b5210..b90af562 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -46,3 +46,15 @@ class HRContractUSPayrollConfig(models.Model):
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)')
+
+ 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')
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 57f8e5e1..cc0a4321 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -6,5 +6,7 @@ from . import test_us_payslip_2020
from . import test_us_fl_florida_payslip_2019
from . import test_us_fl_florida_payslip_2020
+from . import test_us_mt_montana_payslip_2019
+from . import test_us_mt_montana_payslip_2020
from . import test_us_pa_pennsylvania_payslip_2019
from . import test_us_pa_pennsylvania_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 701920b3..846f5ca4 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -44,6 +44,12 @@
No additional fields.
+
+ Form MT-4 - State Income Tax
+
+
+
+
From f788d0f0fa651eebe742f26508cd9edeeaf8e30f Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 8 Jan 2020 11:22:19 -0800
Subject: [PATCH 07/43] IMP `l10n_us_hr_payroll` Add OH Ohio (unemployment,
income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/mt_montana.xml | 4 +-
l10n_us_hr_payroll/data/state/oh_ohio.xml | 157 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/mt_montana.py | 5 +-
l10n_us_hr_payroll/models/state/oh_ohio.py | 44 +++++
.../models/us_payroll_config.py | 4 +
l10n_us_hr_payroll/tests/__init__.py | 5 +
.../tests/test_us_oh_ohio_payslip_2019.py | 96 +++++++++++
.../tests/test_us_oh_ohio_payslip_2020.py | 108 ++++++++++++
.../views/us_payroll_config_views.xml | 6 +
11 files changed, 427 insertions(+), 5 deletions(-)
create mode 100644 l10n_us_hr_payroll/data/state/oh_ohio.xml
create mode 100644 l10n_us_hr_payroll/models/state/oh_ohio.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 4c9df42a..cca06ffe 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -27,6 +27,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
'data/state/mt_montana.xml',
+ 'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
diff --git a/l10n_us_hr_payroll/data/state/mt_montana.xml b/l10n_us_hr_payroll/data/state/mt_montana.xml
index ec18a955..b420c4fe 100644
--- a/l10n_us_hr_payroll/data/state/mt_montana.xml
+++ b/l10n_us_hr_payroll/data/state/mt_montana.xml
@@ -58,7 +58,7 @@
US MT Montana SIT Rate Table
- us_mt_suta_sit_rate
+ us_mt_sit_rate
@@ -102,7 +102,7 @@
US MT Montana SIT Exemption Rate Table
- us_mt_suta_sit_exemption_rate
+ us_mt_sit_exemption_rate
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 93e4f64b..2abc4afa 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
from .state.general import general_state_unemployment, \
general_state_income_withholding
from .state.mt_montana import mt_montana_state_income_withholding
+from .state.oh_ohio import oh_ohio_state_income_withholding
class HRPayslip(models.Model):
@@ -43,6 +44,7 @@ class HRPayslip(models.Model):
'general_state_unemployment': general_state_unemployment,
'general_state_income_withholding': general_state_income_withholding,
'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
+ 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
})
return res
diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py
index 727c56e9..b46380b1 100644
--- a/l10n_us_hr_payroll/models/state/mt_montana.py
+++ b/l10n_us_hr_payroll/models/state/mt_montana.py
@@ -2,7 +2,6 @@ from .general import _state_applies
def mt_montana_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 - WAGE_US_941_FIT_EXEMPT
@@ -21,8 +20,8 @@ def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs
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_suta_sit_exemption_rate').get(schedule_pay)
- withholding_rate = payslip.rule_parameter('us_mt_suta_sit_rate').get(schedule_pay)
+ 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 or wage == 0.0:
return 0.0, 0.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..d279ce1f
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/oh_ohio.py
@@ -0,0 +1,44 @@
+from .general import _state_applies
+
+
+def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs):
+ """
+ Returns SIT eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_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 = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ 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')
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index b90af562..e0d56a6c 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -58,3 +58,7 @@ class HRContractUSPayrollConfig(models.Model):
('north_dakota', 'North Dakota'),
('montana_for_marriage', 'Montana for Marriage'),
], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2')
+
+ # Ohio will use generic SIT exempt and additional fields
+ oh_it4_sit_exemptions = fields.Integer(string='Ohio IT-4 Exemptions',
+ help='Line 4')
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index cc0a4321..a2953520 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -6,7 +6,12 @@ from . import test_us_payslip_2020
from . import test_us_fl_florida_payslip_2019
from . import test_us_fl_florida_payslip_2020
+
from . import test_us_mt_montana_payslip_2019
from . import test_us_mt_montana_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
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..bf38f4d5
--- /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.assertAlmostEqual(cats['EE_US_SIT'], -wd, 1) # 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..04256afa
--- /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.assertAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected, 1)
+ 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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 846f5ca4..d9222493 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -50,6 +50,12 @@
+
+ Form IT-4 - State Income Tax
+
+
+
+
From 44a18c8f8b4819593f837750711ea7a1f2793468 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 8 Jan 2020 18:14:38 -0800
Subject: [PATCH 08/43] IMP `l10n_us_hr_payroll` Add WA Washington
(unemployment, lni, fml)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/wa_washington.xml | 211 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 8 +-
l10n_us_hr_payroll/models/state/general.py | 4 +
l10n_us_hr_payroll/models/state/mt_montana.py | 2 +
l10n_us_hr_payroll/models/state/oh_ohio.py | 2 +
.../models/state/wa_washington.py | 27 +++
.../models/us_payroll_config.py | 4 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
l10n_us_hr_payroll/tests/common.py | 3 +
.../tests/test_us_oh_ohio_payslip_2019.py | 2 +-
.../tests/test_us_oh_ohio_payslip_2020.py | 2 +-
.../test_us_wa_washington_payslip_2019.py | 92 ++++++++
.../test_us_wa_washington_payslip_2020.py | 90 ++++++++
.../views/us_payroll_config_views.xml | 9 +-
15 files changed, 456 insertions(+), 4 deletions(-)
create mode 100644 l10n_us_hr_payroll/data/state/wa_washington.xml
create mode 100644 l10n_us_hr_payroll/models/state/wa_washington.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index cca06ffe..180fe010 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -29,6 +29,7 @@ United States of America - Payroll Rules.
'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
+ 'data/state/wa_washington.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
],
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..d76d0b18
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/wa_washington.xml
@@ -0,0 +1,211 @@
+
+
+
+
+ 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_er_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
+
+hours = worked_days.WORK100.number_of_hours
+rate = payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_er_code'))
+try:
+ # Redo employee withholding calculation
+ ee_withholding = worked_days.WORK100.number_of_hours * -payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_ee_code')) / 100.0
+except:
+ ee_withholding = 0.0
+er_withholding = -(hours * (rate / 100.0)) - ee_withholding
+result = hours
+result_rate = (er_withholding / hours) * 100.0
+
+
+
+
+
+
+
+
+
+ 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/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 2abc4afa..021142a6 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -10,9 +10,12 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
er_us_941_fica_m, \
ee_us_941_fit
from .state.general import general_state_unemployment, \
- general_state_income_withholding
+ general_state_income_withholding, \
+ is_us_state
from .state.mt_montana import mt_montana_state_income_withholding
from .state.oh_ohio import oh_ohio_state_income_withholding
+from .state.wa_washington import wa_washington_fml_er, \
+ wa_washington_fml_ee
class HRPayslip(models.Model):
@@ -43,8 +46,11 @@ class HRPayslip(models.Model):
'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,
'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
+ 'wa_washington_fml_er': wa_washington_fml_er,
+ 'wa_washington_fml_ee': wa_washington_fml_ee,
})
return res
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index faf3f477..c10133fb 100644
--- a/l10n_us_hr_payroll/models/state/general.py
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -9,6 +9,10 @@ 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:
diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py
index b46380b1..742d7607 100644
--- a/l10n_us_hr_payroll/models/state/mt_montana.py
+++ b/l10n_us_hr_payroll/models/state/mt_montana.py
@@ -1,3 +1,5 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
from .general import _state_applies
diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py
index d279ce1f..ed4ca8e2 100644
--- a/l10n_us_hr_payroll/models/state/oh_ohio.py
+++ b/l10n_us_hr_payroll/models/state/oh_ohio.py
@@ -1,3 +1,5 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
from .general import _state_applies
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
index e0d56a6c..5ba5f66b 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -17,6 +17,10 @@ class HRContractUSPayrollConfig(models.Model):
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%)'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index a2953520..7ffc630a 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -15,3 +15,6 @@ 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_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
index c20bc34d..26c6874b 100755
--- a/l10n_us_hr_payroll/tests/common.py
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -146,6 +146,9 @@ class TestUsPayslip(common.TransactionCase):
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 test_semi_monthly(self):
salary = 80000.0
employee = self._createEmployee()
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
index bf38f4d5..d1f65f05 100755
--- 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
@@ -34,7 +34,7 @@ class TestUsOhPayslip(TestUsPayslip):
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.OH_UNEMP)
- self.assertAlmostEqual(cats['EE_US_SIT'], -wd, 1) # Off by 0.6 cents so it rounds off by a penny
+ 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)
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
index 04256afa..9026da92 100755
--- 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
@@ -48,7 +48,7 @@ class TestUsOhPayslip(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
# Instead of PayrollEqual after initial first round of testing.
- self.assertAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected, 1)
+ self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected)
return payslip
def test_2020_sit_1(self):
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..b686cb1e
--- /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) - rules['EE_US_WA_LNI'])
+ # 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..d9983fb0
--- /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) - rules['EE_US_WA_LNI'])
+ # 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/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index d9222493..fbee1ac6 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -26,7 +26,6 @@
-
Form 940 - Federal Unemployment
Form 941 / W4 - Federal Income Tax
@@ -39,6 +38,10 @@
+ State Information and Extra
+
+
+
@@ -60,6 +63,10 @@
+
+ No additional fields.
+ Ensure that your Employee and Employer workers' comp code fields are filled in for WA LNI withholding.
+
From 2ee3590824ff14f3a907e0fc1ea2af084c24bcde Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 8 Jan 2020 19:33:59 -0800
Subject: [PATCH 09/43] IMP `l10n_us_hr_payroll` Add TX Texas (unemployment,
OA, ETIA)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/tx_texas.xml | 127 ++++++++++++++++++
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_tx_texas_payslip_2019.py | 100 ++++++++++++++
.../tests/test_us_tx_texas_payslip_2020.py | 17 +++
.../views/us_payroll_config_views.xml | 3 +
6 files changed, 251 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/tx_texas.xml
create mode 100755 l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 180fe010..1fe03246 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -29,6 +29,7 @@ United States of America - Payroll Rules.
'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
+ 'data/state/tx_texas.xml',
'data/state/wa_washington.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 7ffc630a..90d6dfef 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -16,5 +16,8 @@ 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_wa_washington_payslip_2019
from . import test_us_wa_washington_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index fbee1ac6..7e7aaca9 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -63,6 +63,9 @@
+
+ No additional fields.
+
No additional fields.
Ensure that your Employee and Employer workers' comp code fields are filled in for WA LNI withholding.
From 8134c473bd8b00d469bd4258f7f4ad524fe3fd48 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Thu, 9 Jan 2020 08:45:39 -0800
Subject: [PATCH 10/43] IMP `l10n_us_hr_payroll` Add VA Virginia (unemployment,
income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/va_virginia.xml | 138 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/va_virginia.py | 43 ++++++
.../models/us_payroll_config.py | 5 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_va_virginia_payslip_2019.py | 133 +++++++++++++++++
.../tests/test_us_va_virginia_payslip_2020.py | 116 +++++++++++++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 447 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/va_virginia.xml
create mode 100644 l10n_us_hr_payroll/models/state/va_virginia.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 1fe03246..ae62eff5 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -30,6 +30,7 @@ United States of America - Payroll Rules.
'data/state/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/us_payroll_config_views.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 021142a6..d42d60c7 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -14,6 +14,7 @@ from .state.general import general_state_unemployment, \
is_us_state
from .state.mt_montana import mt_montana_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
@@ -49,6 +50,7 @@ class HRPayslip(models.Model):
'is_us_state': is_us_state,
'mt_montana_state_income_withholding': mt_montana_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,
})
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..163ba0c3
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/va_virginia.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
+
+
+def va_virginia_state_income_withholding(payslip, categories, worked_days, inputs):
+ """
+ Returns SIT eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_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 = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ 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')
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 5ba5f66b..4d86a1b7 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -66,3 +66,8 @@ class HRContractUSPayrollConfig(models.Model):
# 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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 90d6dfef..ef846086 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -19,5 +19,8 @@ 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/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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 7e7aaca9..de910709 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -66,6 +66,12 @@
No additional fields.
+
+
+
+
+
+
No additional fields.
Ensure that your Employee and Employer workers' comp code fields are filled in for WA LNI withholding.
From c6ca33cad20f751c90093d225199d5bbb71ec37c Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Thu, 9 Jan 2020 09:03:41 -0800
Subject: [PATCH 11/43] IMP `l10n_us_hr_payroll` Add form name in Virginia's
state box.
---
l10n_us_hr_payroll/views/us_payroll_config_views.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index de910709..987c6958 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -67,6 +67,7 @@
No additional fields.
+ Form VA-4/VA-4P - State Income Tax
From fe05ac55c4bbbdb584ced60be3984f0eba9d3705 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Thu, 9 Jan 2020 11:04:38 -0800
Subject: [PATCH 12/43] IMP `l10n_us_hr_payroll` Add GA Georgia (unemployment,
income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/ga_georgia.xml | 279 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/ga_georgia.py | 52 ++++
.../models/us_payroll_config.py | 13 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_ga_georgia_payslip_2019.py | 135 +++++++++
.../tests/test_us_ga_georgia_payslip_2020.py | 148 ++++++++++
.../views/us_payroll_config_views.xml | 8 +
9 files changed, 641 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ga_georgia.xml
create mode 100644 l10n_us_hr_payroll/models/state/ga_georgia.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index ae62eff5..683b8752 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -26,6 +26,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
+ 'data/state/ga_georgia.xml',
'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index d42d60c7..7b00f540 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
+from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.mt_montana import mt_montana_state_income_withholding
from .state.oh_ohio import oh_ohio_state_income_withholding
from .state.va_virginia import va_virginia_state_income_withholding
@@ -48,6 +49,7 @@ class HRPayslip(models.Model):
'general_state_unemployment': general_state_unemployment,
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
+ 'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
'va_virginia_state_income_withholding': va_virginia_state_income_withholding,
diff --git a/l10n_us_hr_payroll/models/state/ga_georgia.py b/l10n_us_hr_payroll/models/state/ga_georgia.py
new file mode 100644
index 00000000..42c24cd4
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/ga_georgia.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
+
+
+def ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs):
+ """
+ Returns SIT eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+
+ :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 = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ 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)) or wage == 0.0:
+ return 0.0, 0.0
+
+ if wage == 0.0:
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 4d86a1b7..885846e2 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -51,6 +51,19 @@ class HRContractUSPayrollConfig(models.Model):
fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]',
help='Form W4 (2020+) 4(c)')
+ 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.')
+
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
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index ef846086..43613969 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -7,6 +7,9 @@ from . import test_us_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_mt_montana_payslip_2019
from . import test_us_mt_montana_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 987c6958..395b008b 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -47,6 +47,14 @@
No additional fields.
+
+ Form G-4 - State Income Tax
+
+
+
+
+
+
Form MT-4 - State Income Tax
From 0bb2a9884cecbee01c1a5f2c12862aacb43a0b40 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Thu, 9 Jan 2020 13:07:03 -0800
Subject: [PATCH 13/43] IMP `l10n_us_hr_payroll` Add MS Mississippi
(unemployment, income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/ms_mississippi.xml | 125 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/ms_mississippi.py | 46 +++++++
.../models/us_payroll_config.py | 10 ++
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../test_us_ms_mississippi_payslip_2019.py | 94 +++++++++++++
.../test_us_ms_mississippi_payslip_2020.py | 120 +++++++++++++++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 407 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ms_mississippi.xml
create mode 100644 l10n_us_hr_payroll/models/state/ms_mississippi.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 683b8752..74a9fd12 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -27,6 +27,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
+ 'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 7b00f540..b487d6cd 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -13,6 +13,7 @@ from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
from .state.ga_georgia import ga_georgia_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.oh_ohio import oh_ohio_state_income_withholding
from .state.va_virginia import va_virginia_state_income_withholding
@@ -50,6 +51,7 @@ class HRPayslip(models.Model):
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
+ 'ms_mississippi_state_income_withholding': ms_mississippi_state_income_withholding,
'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
'va_virginia_state_income_withholding': va_virginia_state_income_withholding,
diff --git a/l10n_us_hr_payroll/models/state/ms_mississippi.py b/l10n_us_hr_payroll/models/state/ms_mississippi.py
new file mode 100644
index 00000000..dfd9872a
--- /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
+
+
+def ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs):
+ """
+ Returns SIT eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_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 = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ 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')
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 885846e2..b3663dc5 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -64,6 +64,16 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ 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
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 43613969..3e4cb355 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -10,6 +10,9 @@ 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_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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 395b008b..f94f820b 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -55,6 +55,12 @@
+
+ Form 89-350 - State Income Tax
+
+
+
+
Form MT-4 - State Income Tax
From 19cb2987b189933b5dcdeb29ffe5f4e97b1aa3ab Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Fri, 10 Jan 2020 06:20:27 -0800
Subject: [PATCH 14/43] IMP `l10n_us_hr_payroll` Refactor to simply tax exempt
deductions.
---
l10n_us_hr_payroll/data/base.xml | 25 +++++++++++++++++++
.../data/federal/fed_940_futa_rules.xml | 6 -----
.../data/federal/fed_941_fica_rules.xml | 6 -----
.../data/federal/fed_941_fit_rules.xml | 5 ----
l10n_us_hr_payroll/models/federal/fed_940.py | 6 ++---
l10n_us_hr_payroll/models/federal/fed_941.py | 20 +++++++--------
l10n_us_hr_payroll/models/state/ga_georgia.py | 4 +--
l10n_us_hr_payroll/models/state/general.py | 12 ++++-----
.../models/state/ms_mississippi.py | 4 +--
l10n_us_hr_payroll/models/state/mt_montana.py | 4 +--
l10n_us_hr_payroll/models/state/oh_ohio.py | 4 +--
.../models/state/va_virginia.py | 4 +--
12 files changed, 54 insertions(+), 46 deletions(-)
diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml
index 6bd03f57..09172433 100644
--- a/l10n_us_hr_payroll/data/base.xml
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -36,4 +36,29 @@
+
+
+
+
+
+ Deduction: Federal Income Tax Exempt
+ DED_US_FIT_EXEMPT
+
+
+
+
+
+
+ Deduction: FICA Exempt
+ DED_FICA_EXEMPT
+
+
+
+
+
+ Deduction: FUTA Exempt
+ DED_FUTA_EXEMPT
+
+
+
\ 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
index 5b315100..6a153efb 100644
--- a/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml
+++ b/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml
@@ -11,12 +11,6 @@
-
-
- WAGE: Federal 940 FUTA Exempt
- WAGE_US_940_FUTA_EXEMPT
-
-
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
index 324958a4..64c91607 100644
--- a/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml
@@ -17,12 +17,6 @@
-
-
- WAGE: Federal 941 FICA Exempt
- WAGE_US_941_FICA_EXEMPT
-
-
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
index a7751adf..4e3cb28c 100644
--- a/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml
@@ -1,10 +1,5 @@
-
-
- WAGE: Federal 941 Income Tax Exempt
- WAGE_US_941_FIT_EXEMPT
-
EE: Federal 941 Income Tax Withholding
diff --git a/l10n_us_hr_payroll/models/federal/fed_940.py b/l10n_us_hr_payroll/models/federal/fed_940.py
index 1cf042c7..7dafd750 100644
--- a/l10n_us_hr_payroll/models/federal/fed_940.py
+++ b/l10n_us_hr_payroll/models/federal/fed_940.py
@@ -3,7 +3,7 @@
def er_us_940_futa(payslip, categories, worked_days, inputs):
"""
Returns FUTA eligible wage and rate.
- WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT
+ WAGE = GROSS + DED_FUTA_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -19,13 +19,13 @@ def er_us_940_futa(payslip, categories, worked_days, inputs):
# Determine 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('WAGE_US_940_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')
ytd_wage += payslip.contract_id.external_wages
wage_base = payslip.rule_parameter('fed_940_futa_wage_base')
remaining = wage_base - ytd_wage
- wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT
+ wage = categories.GROSS + categories.DED_FUTA_EXEMPT
if remaining < 0.0:
result = 0.0
diff --git a/l10n_us_hr_payroll/models/federal/fed_941.py b/l10n_us_hr_payroll/models/federal/fed_941.py
index e6288f88..256c67c1 100644
--- a/l10n_us_hr_payroll/models/federal/fed_941.py
+++ b/l10n_us_hr_payroll/models/federal/fed_941.py
@@ -7,7 +7,7 @@
def ee_us_941_fica_ss(payslip, categories, worked_days, inputs):
"""
Returns FICA Social Security eligible wage and rate.
- WAGE = GROSS - WAGE_US_941_FICA_EXEMPT
+ WAGE = GROSS + DED_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
@@ -20,13 +20,13 @@ def ee_us_941_fica_ss(payslip, categories, worked_days, inputs):
# Determine 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('WAGE_US_941_FICA_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')
ytd_wage += payslip.contract_id.external_wages
wage_base = payslip.rule_parameter('fed_941_fica_ss_wage_base')
remaining = wage_base - ytd_wage
- wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
+ wage = categories.GROSS + categories.DED_FICA_EXEMPT
if remaining < 0.0:
result = 0.0
@@ -44,7 +44,7 @@ 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.
- WAGE = GROSS - WAGE_US_941_FICA_EXEMPT
+ WAGE = GROSS + DED_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
@@ -57,13 +57,13 @@ def ee_us_941_fica_m(payslip, categories, worked_days, inputs):
# Determine 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('WAGE_US_941_FICA_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')
ytd_wage += payslip.contract_id.external_wages
wage_base = float(payslip.rule_parameter('fed_941_fica_m_wage_base')) # inf
remaining = wage_base - ytd_wage
- wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
+ wage = categories.GROSS + categories.DED_FICA_EXEMPT
if remaining < 0.0:
result = 0.0
@@ -95,13 +95,13 @@ def ee_us_941_fica_m_add(payslip, categories, worked_days, inputs):
# Determine 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('WAGE_US_941_FICA_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')
ytd_wage += payslip.contract_id.external_wages
wage_start = payslip.rule_parameter('fed_941_fica_m_add_wage_start')
existing_wage = ytd_wage - wage_start
- wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
+ wage = categories.GROSS + categories.DED_FICA_EXEMPT
if existing_wage >= 0.0:
result = wage
@@ -117,7 +117,7 @@ def ee_us_941_fica_m_add(payslip, categories, worked_days, inputs):
def ee_us_941_fit(payslip, categories, worked_days, inputs):
"""
Returns Wage and rate that is computed given the amount to withhold.
- WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
filing_status = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status')
@@ -125,7 +125,7 @@ def ee_us_941_fit(payslip, categories, worked_days, inputs):
return 0.0, 0.0
schedule_pay = payslip.contract_id.schedule_pay
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
#_logger.warn('initial gross wage: ' + str(wage))
year = payslip.dict.get_year()
if year >= 2020:
diff --git a/l10n_us_hr_payroll/models/state/ga_georgia.py b/l10n_us_hr_payroll/models/state/ga_georgia.py
index 42c24cd4..1ea2d560 100644
--- a/l10n_us_hr_payroll/models/state/ga_georgia.py
+++ b/l10n_us_hr_payroll/models/state/ga_georgia.py
@@ -6,7 +6,7 @@ from .general import _state_applies
def ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs):
"""
Returns SIT eligible wage and rate.
- WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -18,7 +18,7 @@ def ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
schedule_pay = payslip.contract_id.schedule_pay
additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
dependent_allowances = payslip.contract_id.us_payroll_config_value('ga_g4_sit_dependent_allowances')
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index c10133fb..b2ec4c7a 100644
--- a/l10n_us_hr_payroll/models/state/general.py
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -74,7 +74,7 @@ def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, 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 - WAGE_US_940_FUTA_EXEMPT
+ WAGE = GROSS + DED_FUTA_EXEMPT
The contract's `futa_type` determines if SUTA should be collected.
@@ -91,17 +91,17 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba
# Determine 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('WAGE_US_940_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')
ytd_wage += payslip.contract_id.external_wages
- wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT
+ 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 - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -114,10 +114,10 @@ def general_state_income_withholding(payslip, categories, worked_days, inputs, w
# Determine 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('WAGE_US_941_FIT_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')
ytd_wage += payslip.contract_id.external_wages
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
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:
diff --git a/l10n_us_hr_payroll/models/state/ms_mississippi.py b/l10n_us_hr_payroll/models/state/ms_mississippi.py
index dfd9872a..cda417cf 100644
--- a/l10n_us_hr_payroll/models/state/ms_mississippi.py
+++ b/l10n_us_hr_payroll/models/state/ms_mississippi.py
@@ -6,7 +6,7 @@ from .general import _state_applies
def ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs):
"""
Returns SIT eligible wage and rate.
- WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -19,7 +19,7 @@ def ms_mississippi_state_income_withholding(payslip, categories, worked_days, in
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
if wage == 0.0:
return 0.0, 0.0
diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py
index 742d7607..b89c692f 100644
--- a/l10n_us_hr_payroll/models/state/mt_montana.py
+++ b/l10n_us_hr_payroll/models/state/mt_montana.py
@@ -6,7 +6,7 @@ from .general import _state_applies
def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs):
"""
Returns SIT eligible wage and rate.
- WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -18,7 +18,7 @@ def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
schedule_pay = payslip.contract_id.schedule_pay
additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
exemptions = payslip.contract_id.us_payroll_config_value('mt_mw4_sit_exemptions')
diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py
index ed4ca8e2..24e7cc9f 100644
--- a/l10n_us_hr_payroll/models/state/oh_ohio.py
+++ b/l10n_us_hr_payroll/models/state/oh_ohio.py
@@ -6,7 +6,7 @@ from .general import _state_applies
def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs):
"""
Returns SIT eligible wage and rate.
- WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -18,7 +18,7 @@ def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs):
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
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')
diff --git a/l10n_us_hr_payroll/models/state/va_virginia.py b/l10n_us_hr_payroll/models/state/va_virginia.py
index 163ba0c3..018e56a3 100644
--- a/l10n_us_hr_payroll/models/state/va_virginia.py
+++ b/l10n_us_hr_payroll/models/state/va_virginia.py
@@ -6,7 +6,7 @@ from .general import _state_applies
def va_virginia_state_income_withholding(payslip, categories, worked_days, inputs):
"""
Returns SIT eligible wage and rate.
- WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+ WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -18,7 +18,7 @@ def va_virginia_state_income_withholding(payslip, categories, worked_days, input
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
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')
From d797b2b4d8a3c20a5be6eb3641367b2cf5185b7e Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Fri, 10 Jan 2020 07:45:33 -0800
Subject: [PATCH 15/43] FIX `l10n_us_hr_payroll` Missing Parent Category and
Code not matching pattern.
---
l10n_us_hr_payroll/data/base.xml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml
index 09172433..36981c31 100644
--- a/l10n_us_hr_payroll/data/base.xml
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -41,22 +41,22 @@
- Deduction: Federal Income Tax Exempt
- DED_US_FIT_EXEMPT
-
+ Deduction: US Federal Income Tax Exempt
+ DED_FIT_EXEMPT
+
- Deduction: FICA Exempt
+ Deduction: US FICA Exempt
DED_FICA_EXEMPT
- Deduction: FUTA Exempt
+ Deduction: US FUTA Exempt
DED_FUTA_EXEMPT
From 6cb884172521028cdf589a164ca94bb244ecabb0 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Fri, 10 Jan 2020 09:22:03 -0800
Subject: [PATCH 16/43] IMP `l10n_us_hr_payroll` Use the raw ER rate for
Washington LNI (instead of the combined rate and removing EE portion)
---
l10n_us_hr_payroll/data/state/wa_washington.xml | 15 ++-------------
.../tests/test_us_wa_washington_payslip_2019.py | 2 +-
.../tests/test_us_wa_washington_payslip_2020.py | 2 +-
3 files changed, 4 insertions(+), 15 deletions(-)
diff --git a/l10n_us_hr_payroll/data/state/wa_washington.xml b/l10n_us_hr_payroll/data/state/wa_washington.xml
index d76d0b18..3307689c 100644
--- a/l10n_us_hr_payroll/data/state/wa_washington.xml
+++ b/l10n_us_hr_payroll/data/state/wa_washington.xml
@@ -176,20 +176,9 @@
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_er_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'))
+ 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
-
-hours = worked_days.WORK100.number_of_hours
-rate = payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_er_code'))
-try:
- # Redo employee withholding calculation
- ee_withholding = worked_days.WORK100.number_of_hours * -payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_ee_code')) / 100.0
-except:
- ee_withholding = 0.0
-er_withholding = -(hours * (rate / 100.0)) - ee_withholding
-result = hours
-result_rate = (er_withholding / hours) * 100.0
-
+ result, result_rate = worked_days.WORK100.number_of_hours, -payslip.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_er_code'))
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
index b686cb1e..b67f69c6 100755
--- 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
@@ -69,7 +69,7 @@ class TestUsWAPayslip(TestUsPayslip):
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) - rules['EE_US_WA_LNI'])
+ 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)))
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
index d9983fb0..509e19b1 100755
--- 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
@@ -67,7 +67,7 @@ class TestUsWAPayslip(TestUsPayslip):
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) - rules['EE_US_WA_LNI'])
+ 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)))
From 12ec84b44255eb5410aff72ba388d967b884b7d9 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 14 Jan 2020 14:09:41 -0500
Subject: [PATCH 17/43] IMP `l10n_us_hr_payroll` Add MO Missouri (unemployment,
income tax)
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/mo_missouri.xml | 145 ++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/mo_missouri.py | 52 +++++
.../models/us_payroll_config.py | 10 +-
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_mo_missouri_payslip_2019.py | 188 ++++++++++++++++++
.../tests/test_us_mo_missouri_payslip_2020.py | 105 ++++++++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 511 insertions(+), 1 deletion(-)
create mode 100644 l10n_us_hr_payroll/data/state/mo_missouri.xml
create mode 100644 l10n_us_hr_payroll/models/state/mo_missouri.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mo_missouri_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 74a9fd12..45a6ae45 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -27,6 +27,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
+ 'data/state/mo_missouri.xml',
'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
'data/state/oh_ohio.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index b487d6cd..f74cf922 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -13,6 +13,7 @@ from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
from .state.ga_georgia import ga_georgia_state_income_withholding
+from .state.mo_missouri import mo_missouri_state_income_withholding
from .state.ms_mississippi import ms_mississippi_state_income_withholding
from .state.mt_montana import mt_montana_state_income_withholding
from .state.oh_ohio import oh_ohio_state_income_withholding
@@ -51,6 +52,7 @@ class HRPayslip(models.Model):
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
'ga_georgia_state_income_withholding': ga_georgia_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,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
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..c6018df0
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/mo_missouri.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
+
+
+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 = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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]
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index b3663dc5..eea9e916 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -64,6 +64,14 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ 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'),
@@ -93,4 +101,4 @@ class HRContractUSPayrollConfig(models.Model):
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)')
+ help='VA-4(P) 1(b)')
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 3e4cb355..6275549a 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -10,6 +10,9 @@ 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_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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index f94f820b..f38baedc 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -55,6 +55,12 @@
+
+ Form MO W-4 - State Income Tax
+
+
+
+
Form 89-350 - State Income Tax
From 5409b13c39a7fefc7421e9c76295811feb84877d Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Fri, 17 Jan 2020 11:55:08 -0500
Subject: [PATCH 18/43] IMP `l10n_us_hr_payroll` Port `l10n_us_nj_hr_payroll`
NJ New Jersey including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/nj_newjersey.xml | 458 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/nj_newjersey.py | 53 ++
.../models/us_payroll_config.py | 17 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
l10n_us_hr_payroll/tests/common.py | 26 +-
.../test_us_nj_newjersey_payslip_2019.py | 128 +++++
.../test_us_nj_newjersey_payslip_2020.py | 48 ++
.../views/us_payroll_config_views.xml | 7 +
10 files changed, 732 insertions(+), 11 deletions(-)
create mode 100644 l10n_us_hr_payroll/data/state/nj_newjersey.xml
create mode 100644 l10n_us_hr_payroll/models/state/nj_newjersey.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_nj_newjersey_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 45a6ae45..3aaa137b 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -30,6 +30,7 @@ United States of America - Payroll Rules.
'data/state/mo_missouri.xml',
'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
+ 'data/state/nj_newjersey.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
'data/state/tx_texas.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index f74cf922..c3dffe34 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -16,6 +16,7 @@ from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.mo_missouri import mo_missouri_state_income_withholding
from .state.ms_mississippi import ms_mississippi_state_income_withholding
from .state.mt_montana import mt_montana_state_income_withholding
+from .state.nj_newjersey import nj_newjersey_state_income_withholding
from .state.oh_ohio import oh_ohio_state_income_withholding
from .state.va_virginia import va_virginia_state_income_withholding
from .state.wa_washington import wa_washington_fml_er, \
@@ -55,6 +56,7 @@ class HRPayslip(models.Model):
'mo_missouri_state_income_withholding': mo_missouri_state_income_withholding,
'ms_mississippi_state_income_withholding': ms_mississippi_state_income_withholding,
'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
+ 'nj_newjersey_state_income_withholding': nj_newjersey_state_income_withholding,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
'va_virginia_state_income_withholding': va_virginia_state_income_withholding,
'wa_washington_fml_er': wa_washington_fml_er,
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..b69ffcef
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/nj_newjersey.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
+
+
+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 = categories.GROSS + categories.DED_FIT_EXEMPT
+
+ 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
+
+ if wage == 0.0:
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index eea9e916..26aec499 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -94,6 +94,23 @@ class HRContractUSPayrollConfig(models.Model):
('montana_for_marriage', 'Montana for Marriage'),
], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2')
+ 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')
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 6275549a..f0e84ff2 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -19,6 +19,9 @@ from . import test_us_ms_mississippi_payslip_2020
from . import test_us_mt_montana_payslip_2019
from . import test_us_mt_montana_payslip_2020
+from . import test_us_nj_newjersey_payslip_2019
+from . import test_us_nj_newjersey_payslip_2020
+
from . import test_us_oh_ohio_payslip_2019
from . import test_us_oh_ohio_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py
index 26c6874b..014d3719 100755
--- a/l10n_us_hr_payroll/tests/common.py
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -172,7 +172,11 @@ class TestUsPayslip(common.TransactionCase):
cache[code] = us_state
return us_state
- def _test_suta(self, category, state_code, rate, date, wage_base=None, **extra_contract):
+ 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
@@ -195,18 +199,18 @@ class TestUsPayslip(common.TransactionCase):
contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_EXEMPT
payslip.compute_sheet()
cats = self._getCategories(payslip)
- self.assertPayrollEqual(cats.get(category, 0.0), 0.0)
+ _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)
- self.assertPayrollEqual(cats.get(category, 0.0), 0.0)
+ _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)
- self.assertPayrollEqual(cats.get(category, 0.0), wage * rate)
+ _assert(cats.get(category, 0.0), wage * rate)
process_payslip(payslip)
# Second Payslip
@@ -217,7 +221,7 @@ class TestUsPayslip(common.TransactionCase):
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
- self.assertPayrollEqual(cats.get(category, 0.0), remaining_unemp_wages * rate)
+ _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)
@@ -226,12 +230,12 @@ class TestUsPayslip(common.TransactionCase):
contract.external_wages = wage
payslip.compute_sheet()
cats = self._getCategories(payslip)
- self.assertPayrollEqual(cats.get(category, 0.0), 0.0)
+ _assert(cats.get(category, 0.0), 0.0)
else:
- self.assertPayrollEqual(cats.get(category, 0.0), wage * rate)
+ _assert(cats.get(category, 0.0), wage * rate)
- def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract):
- self._test_suta('ER_US_SUTA', state_code, rate, date, wage_base=wage_base, **extra_contract)
+ 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, **extra_contract):
- self._test_suta('EE_US_SUTA', state_code, rate, date, wage_base=wage_base, **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_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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index f38baedc..a96d5f57 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -73,6 +73,13 @@
+
+ Form NJ-W4 - State Income Tax
+
+
+
+
+
Form IT-4 - State Income Tax
From ae4d410b9d77414c803c0a57e303e3b8f18c1a35 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Fri, 24 Jan 2020 19:02:20 -0500
Subject: [PATCH 19/43] IMP `l10n_us_hr_payroll` Port `l10n_us_nc_hr_payroll`
NC North Carolina including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/nc_northcarolina.xml | 109 +++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/nc_northcarolina.py | 37 +++
.../models/us_payroll_config.py | 9 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../test_us_nc_northcarolina_payslip_2019.py | 270 ++++++++++++++++++
.../test_us_nc_northcarolina_payslip_2020.py | 36 +++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 473 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/nc_northcarolina.xml
create mode 100644 l10n_us_hr_payroll/models/state/nc_northcarolina.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 3aaa137b..5e752f94 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -30,6 +30,7 @@ United States of America - Payroll Rules.
'data/state/mo_missouri.xml',
'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
+ 'data/state/nc_northcarolina.xml',
'data/state/nj_newjersey.xml',
'data/state/oh_ohio.xml',
'data/state/pa_pennsylvania.xml',
diff --git a/l10n_us_hr_payroll/data/state/nc_northcarolina.xml b/l10n_us_hr_payroll/data/state/nc_northcarolina.xml
new file mode 100644
index 00000000..597fd543
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/nc_northcarolina.xml
@@ -0,0 +1,109 @@
+
+
+
+
+ US NC North Carolina SUTA Wage Base
+ us_nc_suta_wage_base
+
+
+
+
+ 24300.0
+
+
+
+
+ 25200.0
+
+
+
+
+
+
+
+ US NC North Carolina SUTA Rate
+ us_nc_suta_rate
+
+
+
+
+ 1.0
+
+
+
+
+ 1.0
+
+
+
+
+
+
+ US NC North Carolina Allowance Rate
+ us_nc_sit_allowance_rate
+
+
+
+
+ {
+ 'weekly': {'allowance': 48.08, 'standard_deduction': 192.31, 'standard_deduction_hh': 288.46},
+ 'bi-weekly': {'allowance': 96.15, 'standard_deduction': 384.62, 'standard_deduction_hh': 576.92},
+ 'semi-monthly': {'allowance': 104.17, 'standard_deduction': 416.67, 'standard_deduction_hh': 625.00},
+ 'monthly': {'allowance': 208.33, 'standard_deduction': 833.33, 'standard_deduction_hh': 1250.00},
+ }
+
+
+
+
+ {
+ 'weekly': {'allowance': 48.08, 'standard_deduction': 206.73, 'standard_deduction_hh': 310.10},
+ 'bi-weekly': {'allowance': 96.15, 'standard_deduction': 413.46, 'standard_deduction_hh': 620.19},
+ 'semi-monthly': {'allowance': 104.17, 'standard_deduction': 447.92, 'standard_deduction_hh': 671.88},
+ 'monthly': {'allowance': 208.33, 'standard_deduction': 895.83, 'standard_deduction_hh': 1343.75},
+ }
+
+
+
+
+
+
+
+ US North Carolina - Department of Taxation - Unemployment Tax
+
+
+
+ US North Carolina - Department of Taxation - Income Tax
+
+
+
+
+
+
+
+
+
+ ER: US NC North Carolina State Unemployment
+ ER_US_NC_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nc_suta_wage_base', rate='us_nc_suta_rate', state_code='NC')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_nc_suta_wage_base', rate='us_nc_suta_rate', state_code='NC')
+
+
+
+
+
+
+
+
+ EE: US NC North Carolina State Income Tax Withholding
+ EE_US_NC_SIT
+ python
+ result, _ = nc_northcarolina_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = nc_northcarolina_state_income_withholding(payslip, categories, worked_days, inputs)
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index c3dffe34..7e4a32d0 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -16,6 +16,7 @@ from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.mo_missouri import mo_missouri_state_income_withholding
from .state.ms_mississippi import ms_mississippi_state_income_withholding
from .state.mt_montana import mt_montana_state_income_withholding
+from .state.nc_northcarolina import nc_northcarolina_state_income_withholding
from .state.nj_newjersey import nj_newjersey_state_income_withholding
from .state.oh_ohio import oh_ohio_state_income_withholding
from .state.va_virginia import va_virginia_state_income_withholding
@@ -56,6 +57,7 @@ class HRPayslip(models.Model):
'mo_missouri_state_income_withholding': mo_missouri_state_income_withholding,
'ms_mississippi_state_income_withholding': ms_mississippi_state_income_withholding,
'mt_montana_state_income_withholding': mt_montana_state_income_withholding,
+ 'nc_northcarolina_state_income_withholding': nc_northcarolina_state_income_withholding,
'nj_newjersey_state_income_withholding': nj_newjersey_state_income_withholding,
'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding,
'va_virginia_state_income_withholding': va_virginia_state_income_withholding,
diff --git a/l10n_us_hr_payroll/models/state/nc_northcarolina.py b/l10n_us_hr_payroll/models/state/nc_northcarolina.py
new file mode 100644
index 00000000..8b9be103
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/nc_northcarolina.py
@@ -0,0 +1,37 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .general import _state_applies
+
+
+def nc_northcarolina_state_income_withholding(payslip, categories, worked_days, inputs):
+ """
+ Returns SIT eligible wage and rate.
+ WAGE = GROSS + DED_FIT_EXEMPT
+
+ :return: result, result_rate (wage, percent)
+ """
+ state_code = 'NC'
+ if not _state_applies(payslip, state_code):
+ return 0.0, 0.0
+
+ filing_status = payslip.contract_id.us_payroll_config_value('nc_nc4_sit_filing_status')
+ if not filing_status:
+ return 0.0, 0.0
+
+ # Determine Wage
+ wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ schedule_pay = payslip.contract_id.schedule_pay
+ additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ allowances = payslip.contract_id.us_payroll_config_value('nc_nc4_sit_allowances')
+ allowances_rate = payslip.rule_parameter('us_nc_sit_allowance_rate').get(schedule_pay)['allowance']
+ deduction = payslip.rule_parameter('us_nc_sit_allowance_rate').get(schedule_pay)['standard_deduction'] if filing_status != 'head_household' else payslip.rule_parameter('us_nc_sit_allowance_rate').get(schedule_pay)['standard_deduction_hh']
+
+ if wage == 0.0:
+ return 0.0, 0.0
+ taxable_wage = round((wage - (deduction + (allowances * allowances_rate))) * 0.0535)
+ withholding = 0.0
+ if taxable_wage < 0.0:
+ withholding -= taxable_wage
+ withholding = taxable_wage
+ withholding += additional
+ return wage, -((withholding / wage) * 100.0)
diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 26aec499..49d1169b 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -94,6 +94,15 @@ class HRContractUSPayrollConfig(models.Model):
('montana_for_marriage', 'Montana for Marriage'),
], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2')
+ nc_nc4_sit_filing_status = fields.Selection([
+ ('', 'Exempt'),
+ ('single', 'Single'),
+ ('married', 'Married'),
+ ('surviving_spouse', 'Surviving Spouse'),
+ ('head_household', 'Head of Household')
+ ], string='North Carolina NC-4 Filing Status', help='NC-4')
+ nc_nc4_sit_allowances = fields.Integer(string='North Carolina NC-4 Allowances', help='NC-4 1.')
+
nj_njw4_sit_filing_status = fields.Selection([
('', 'Exempt'),
('single', 'Single'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index f0e84ff2..3844fbcf 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -19,6 +19,9 @@ from . import test_us_ms_mississippi_payslip_2020
from . import test_us_mt_montana_payslip_2019
from . import test_us_mt_montana_payslip_2020
+from . import test_us_nc_northcarolina_payslip_2019
+from . import test_us_nc_northcarolina_payslip_2020
+
from . import test_us_nj_newjersey_payslip_2019
from . import test_us_nj_newjersey_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py
new file mode 100755
index 00000000..14c1c5b2
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2019.py
@@ -0,0 +1,270 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsNCPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ NC_UNEMP_MAX_WAGE = 24300.0
+ NC_UNEMP = -1.0 / 100.0
+ NC_INC_TAX = -0.0535
+
+
+ def test_2019_taxes_weekly(self):
+ salary = 20000.0
+ # allowance_multiplier and Portion of Standard Deduction for weekly
+ allowance_multiplier = 48.08
+ PST = 192.31
+ exemption = 1
+ # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf
+ wh = -round((salary - (PST + (allowance_multiplier * exemption))) * -self.NC_INC_TAX)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status='married',
+ state_income_tax_additional_withholding=0.0,
+ nc_nc4_sit_allowances=1.0,
+ schedule_pay='weekly')
+
+ self._log('2019 North Carolina tax first payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+ self._log('2019 North Carolina tax second payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP)
+
+ def test_2019_taxes_with_external_weekly(self):
+ salary = 5000.0
+ schedule_pay = 'weekly'
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status='married',
+ state_income_tax_additional_withholding=0.0,
+ nc_nc4_sit_allowances=1.0,
+ schedule_pay='weekly')
+
+ self._log('2019 NorthCarolina_external tax first payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.NC_UNEMP)
+
+ def test_2019_taxes_biweekly(self):
+ salary = 5000.0
+ schedule_pay = 'bi-weekly'
+ # allowance_multiplier and Portion of Standard Deduction for weekly
+ allowance_multiplier = 96.15
+ PST = 384.62
+
+ allowances = 2
+ # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf
+
+ wh = -round((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX)
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status='married',
+ state_income_tax_additional_withholding=0.0,
+ nc_nc4_sit_allowances=2.0,
+ schedule_pay='bi-weekly')
+
+ self._log('2019 North Carolina tax first payslip bi-weekly:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.NC_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 North Carolina tax second payslip bi-weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP)
+
+ def test_2019_taxes_semimonthly(self):
+ salary = 4000.0
+ # allowance_multiplier and Portion of Standard Deduction for weekly
+ allowance_multiplier = 104.17
+ PST = 625.00
+
+ allowances = 1
+ # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf
+
+ wh = -round((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX)
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status='head_household',
+ state_income_tax_additional_withholding=0.0,
+ nc_nc4_sit_allowances=1.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 North Carolina tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.NC_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2 * salary < salary) \
+ else salary
+
+ self._log('2019 North Carolina tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP)
+
+ def test_2019_taxes_monthly(self):
+ salary = 4000.0
+ schedule_pay = 'monthly'
+ # allowance_multiplier and Portion of Standard Deduction for weekly
+ allowance_multiplier = 208.33
+ PST = 833.33
+
+ allowances = 1
+ # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf
+
+ wh = -round((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX)
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status='single',
+ state_income_tax_additional_withholding=0.0,
+ nc_nc4_sit_allowances=1.0,
+ schedule_pay='monthly')
+
+ self._log('2019 North Carolina tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.NC_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (
+ self.NC_UNEMP_MAX_WAGE - 2 * salary < salary) \
+ else salary
+
+ self._log('2019 North Carolina tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP)
+
+ def test_additional_withholding(self):
+ salary = 4000.0
+ # allowance_multiplier and Portion of Standard Deduction for weekly
+ allowance_multiplier = 48.08
+ PST = 192.31
+ additional_wh = 40.0
+
+ #4000 - (168.27 + (48.08 * 1)
+
+ allowances = 1
+ # Algorithm derived from percentage method in https://files.nc.gov/ncdor/documents/files/nc-30_book_web.pdf
+
+ wh = -round(((salary - (PST + (allowance_multiplier * allowances))) * -self.NC_INC_TAX) + additional_wh)
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status='married',
+ state_income_tax_additional_withholding=40.0,
+ nc_nc4_sit_allowances=1.0,
+ schedule_pay='weekly')
+
+ self._log('2019 North Carolina tax first payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_NC_UNEMP_wages = self.NC_UNEMP_MAX_WAGE - salary if (self.NC_UNEMP_MAX_WAGE - 2 * salary < salary) \
+ else salary
+
+ self._log('2019 North Carolina tax second payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_NC_UNEMP_wages * self.NC_UNEMP)
diff --git a/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py
new file mode 100755
index 00000000..2c484ac4
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_nc_northcarolina_payslip_2020.py
@@ -0,0 +1,36 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date, timedelta
+from .common import TestUsPayslip
+
+
+class TestUsNCPayslip(TestUsPayslip):
+ ###
+ # 2020 Taxes and Rates
+ ###
+ NC_UNEMP_MAX_WAGE = 25200.0
+ NC_UNEMP = 1.0
+ NC_INC_TAX = 0.0535
+
+ def _test_sit(self, wage, filing_status, allowances, additional_withholding, schedule_pay, date_start, expected_withholding):
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ state_id=self.get_us_state('NC'),
+ nc_nc4_sit_filing_status=filing_status,
+ nc_nc4_sit_allowances=allowances,
+ state_income_tax_additional_withholding=additional_withholding,
+ schedule_pay=schedule_pay)
+ payslip = self._createPayslip(employee, date_start, date_start + timedelta(days=7))
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self._log('Computed period tax: ' + str(expected_withholding))
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), -expected_withholding if filing_status else 0.0)
+
+ def test_2020_taxes_example(self):
+ self._test_er_suta('NC', self.NC_UNEMP, date(2020, 1, 1), wage_base=self.NC_UNEMP_MAX_WAGE)
+ self._test_sit(20000.0, 'single', 1, 100.0, 'weekly', date(2020, 1, 1), 1156.0)
+ self._test_sit(5000.0, 'married', 1, 0.0, 'weekly', date(2020, 1, 1), 254.0)
+ self._test_sit(4000.0, 'head_household', 1, 5.0, 'semi-monthly', date(2020, 1, 1), 177.0)
diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index a96d5f57..394f3e80 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -73,6 +73,12 @@
+
+ Form NC-4 - State Income Tax
+
+
+
+
Form NJ-W4 - State Income Tax
From 714587c39a801013fc2685441f53595e4427447c Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Wed, 29 Jan 2020 11:13:38 -0500
Subject: [PATCH 20/43] IMP `l10n_us_hr_payroll` Port `l10n_us_mi_hr_payroll`
MI Michigan including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/mi_michigan.xml | 99 +++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/mi_michigan.py | 35 ++++
.../models/us_payroll_config.py | 2 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_mi_michigan_payslip_2019.py | 194 ++++++++++++++++++
.../tests/test_us_mi_michigan_payslip_2020.py | 35 ++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 377 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/mi_michigan.xml
create mode 100644 l10n_us_hr_payroll/models/state/mi_michigan.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mi_michigan_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 5e752f94..8a54c1e5 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -27,6 +27,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_rules.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
+ 'data/state/mi_michigan.xml',
'data/state/mo_missouri.xml',
'data/state/ms_mississippi.xml',
'data/state/mt_montana.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 7e4a32d0..56fd8981 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -13,6 +13,7 @@ from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
from .state.ga_georgia import ga_georgia_state_income_withholding
+from .state.mi_michigan import mi_michigan_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
@@ -54,6 +55,7 @@ class HRPayslip(models.Model):
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
+ 'mi_michigan_state_income_withholding': mi_michigan_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,
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..ba556c06
--- /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
+
+
+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 = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
+
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 49d1169b..3e6950d4 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -64,6 +64,8 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ mi_w4_sit_exemptions = fields.Integer(string='Michigan MI W-4 Exemptions', help='MI-W4 6.')
+
mo_mow4_sit_filing_status = fields.Selection([
('', 'Exempt'),
('single', 'Single or Married Spouse Works or Married Filing Separate'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 3844fbcf..65616a6a 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -10,6 +10,9 @@ 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_mi_michigan_payslip_2019
+from . import test_us_mi_michigan_payslip_2020
+
from . import test_us_mo_missouri_payslip_2019
from . import test_us_mo_missouri_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 394f3e80..62dfe6b9 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -55,6 +55,12 @@
+
+ Form MI-W4 - State Income Tax
+
+
+
+
Form MO W-4 - State Income Tax
From 79177549f4e70dd6362311cd4a11f5b3ab413f37 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Thu, 30 Jan 2020 11:02:37 -0500
Subject: [PATCH 21/43] IMP `l10n_us_hr_payroll` Port `l10n_us_mn_hr_payroll`
MN Minnesota including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/mn_minnesota.xml | 143 ++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/mn_minnesota.py | 43 +++++
.../models/us_payroll_config.py | 7 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../test_us_mn_minnesota_payslip_2019.py | 159 ++++++++++++++++++
.../test_us_mn_minnesota_payslip_2020.py | 36 ++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 400 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/mn_minnesota.xml
create mode 100644 l10n_us_hr_payroll/models/state/mn_minnesota.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mn_minnesota_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_mn_minnesota_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 8a54c1e5..82e8b55e 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -28,6 +28,7 @@ United States of America - Payroll Rules.
'data/state/fl_florida.xml',
'data/state/ga_georgia.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',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 56fd8981..48d04270 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -14,6 +14,7 @@ from .state.general import general_state_unemployment, \
is_us_state
from .state.ga_georgia import ga_georgia_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
@@ -56,6 +57,7 @@ class HRPayslip(models.Model):
'is_us_state': is_us_state,
'ga_georgia_state_income_withholding': ga_georgia_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,
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..5c65a0a9
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/mn_minnesota.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
+
+
+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 = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
+ if wage == 0.0:
+ return 0.0, 0.0
+
+ 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 3e6950d4..becaaaa8 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -66,6 +66,13 @@ class HRContractUSPayrollConfig(models.Model):
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'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 65616a6a..711bb35c 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -13,6 +13,9 @@ from . import test_us_ga_georgia_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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 62dfe6b9..0e78dd83 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -61,6 +61,12 @@
+
+ Form W-4MN - State Income Tax
+
+
+
+
Form MO W-4 - State Income Tax
From 5d5712c15c96442846d6e39870b5ea2492ae3523 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Sat, 1 Feb 2020 05:52:50 -0800
Subject: [PATCH 22/43] IMP `l10n_us_hr_payroll` Create tax exempt categories
for table coverage from IRS Pub. 15-B
---
l10n_us_hr_payroll/data/base.xml | 82 ++++++++++--
l10n_us_hr_payroll/models/federal/fed_940.py | 56 +++++++-
l10n_us_hr_payroll/models/federal/fed_941.py | 126 ++++++++++++++----
l10n_us_hr_payroll/models/state/ga_georgia.py | 13 +-
l10n_us_hr_payroll/models/state/general.py | 23 ++--
.../models/state/mi_michigan.py | 10 +-
.../models/state/mn_minnesota.py | 9 +-
.../models/state/mo_missouri.py | 9 +-
.../models/state/ms_mississippi.py | 6 +-
l10n_us_hr_payroll/models/state/mt_montana.py | 9 +-
.../models/state/nc_northcarolina.py | 9 +-
.../models/state/nj_newjersey.py | 9 +-
l10n_us_hr_payroll/models/state/oh_ohio.py | 9 +-
.../models/state/va_virginia.py | 9 +-
14 files changed, 287 insertions(+), 92 deletions(-)
diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml
index 36981c31..2e29934e 100644
--- a/l10n_us_hr_payroll/data/base.xml
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -36,25 +36,89 @@
-
+
+
+
+
+ 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 Federal Income Tax 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
diff --git a/l10n_us_hr_payroll/models/federal/fed_940.py b/l10n_us_hr_payroll/models/federal/fed_940.py
index 7dafd750..bbd4be17 100644
--- a/l10n_us_hr_payroll/models/federal/fed_940.py
+++ b/l10n_us_hr_payroll/models/federal/fed_940.py
@@ -1,9 +1,53 @@
# 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.
- WAGE = GROSS + DED_FUTA_EXEMPT
:return: result, result_rate (wage, percent)
"""
@@ -17,16 +61,14 @@ def er_us_940_futa(payslip, categories, worked_days, inputs):
result_rate = -payslip.rule_parameter('fed_940_futa_rate_normal')
# Determine 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('DED_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
+ 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
- wage = categories.GROSS + categories.DED_FUTA_EXEMPT
-
if remaining < 0.0:
result = 0.0
elif remaining < wage:
diff --git a/l10n_us_hr_payroll/models/federal/fed_941.py b/l10n_us_hr_payroll/models/federal/fed_941.py
index 256c67c1..517df6c0 100644
--- a/l10n_us_hr_payroll/models/federal/fed_941.py
+++ b/l10n_us_hr_payroll/models/federal/fed_941.py
@@ -4,10 +4,53 @@
# _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.
- WAGE = GROSS + DED_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
@@ -18,16 +61,14 @@ def ee_us_941_fica_ss(payslip, categories, worked_days, inputs):
result_rate = -payslip.rule_parameter('fed_941_fica_ss_rate')
# Determine 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('DED_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
+ 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
- wage = categories.GROSS + categories.DED_FICA_EXEMPT
-
if remaining < 0.0:
result = 0.0
elif remaining < wage:
@@ -44,7 +85,6 @@ 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.
- WAGE = GROSS + DED_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
@@ -55,16 +95,14 @@ def ee_us_941_fica_m(payslip, categories, worked_days, inputs):
result_rate = -payslip.rule_parameter('fed_941_fica_m_rate')
# Determine 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('DED_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
+ 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
- wage = categories.GROSS + categories.DED_FICA_EXEMPT
-
if remaining < 0.0:
result = 0.0
elif remaining < wage:
@@ -81,8 +119,6 @@ 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.
- Note that this wage is not capped like the above rules.
- WAGE = GROSS - WAGE_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
@@ -93,16 +129,14 @@ def ee_us_941_fica_m_add(payslip, categories, worked_days, inputs):
result_rate = -payslip.rule_parameter('fed_941_fica_m_add_rate')
# Determine 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('DED_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
+ 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
- wage = categories.GROSS + categories.DED_FICA_EXEMPT
-
if existing_wage >= 0.0:
result = wage
elif wage + existing_wage > 0.0:
@@ -113,11 +147,54 @@ def ee_us_941_fica_m_add(payslip, categories, worked_days, inputs):
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.
- WAGE = GROSS + DED_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
filing_status = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status')
@@ -125,7 +202,8 @@ def ee_us_941_fit(payslip, categories, worked_days, inputs):
return 0.0, 0.0
schedule_pay = payslip.contract_id.schedule_pay
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ wage = fit_wage(payslip, categories)
+
#_logger.warn('initial gross wage: ' + str(wage))
year = payslip.dict.get_year()
if year >= 2020:
diff --git a/l10n_us_hr_payroll/models/state/ga_georgia.py b/l10n_us_hr_payroll/models/state/ga_georgia.py
index 1ea2d560..66503e35 100644
--- a/l10n_us_hr_payroll/models/state/ga_georgia.py
+++ b/l10n_us_hr_payroll/models/state/ga_georgia.py
@@ -1,12 +1,11 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def ga_georgia_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)
"""
@@ -18,7 +17,10 @@ def ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
@@ -27,10 +29,7 @@ def ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs
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)) or wage == 0.0:
- return 0.0, 0.0
-
- if wage == 0.0:
+ if not all((dependent_allowance_rate, personal_allowance, deduction, withholding_rate)):
return 0.0, 0.0
after_standard_deduction = wage - deduction
diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py
index b2ec4c7a..44d2aeb5 100644
--- a/l10n_us_hr_payroll/models/state/general.py
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -1,9 +1,16 @@
# 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')
@@ -89,10 +96,11 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba
return 0.0, 0.0
# Determine 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('DED_FUTA_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
+ 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)
@@ -112,12 +120,9 @@ def general_state_income_withholding(payslip, categories, worked_days, inputs, w
return 0.0, 0.0
# Determine 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('DED_FIT_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01')
- ytd_wage += payslip.contract_id.external_wages
+ ytd_wage = sit_wage_ytd(payslip, categories)
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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:
diff --git a/l10n_us_hr_payroll/models/state/mi_michigan.py b/l10n_us_hr_payroll/models/state/mi_michigan.py
index ba556c06..f9656529 100644
--- a/l10n_us_hr_payroll/models/state/mi_michigan.py
+++ b/l10n_us_hr_payroll/models/state/mi_michigan.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def mi_michigan_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -18,15 +18,15 @@ def mi_michigan_state_income_withholding(payslip, categories, worked_days, input
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
- if wage == 0.0:
- return 0.0, 0.0
-
annual_exemption = (exemption * exemption_rate) / pay_periods
withholding = ((wage - annual_exemption) * 0.0425)
if withholding < 0.0:
diff --git a/l10n_us_hr_payroll/models/state/mn_minnesota.py b/l10n_us_hr_payroll/models/state/mn_minnesota.py
index 5c65a0a9..c626bc3b 100644
--- a/l10n_us_hr_payroll/models/state/mn_minnesota.py
+++ b/l10n_us_hr_payroll/models/state/mn_minnesota.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def mn_minnesota_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -19,14 +19,15 @@ def mn_minnesota_state_income_withholding(payslip, categories, worked_days, inpu
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
- if wage == 0.0:
- return 0.0, 0.0
taxable_income = (wage * pay_periods) - (allowances * allowances_rate)
withholding = 0.0
diff --git a/l10n_us_hr_payroll/models/state/mo_missouri.py b/l10n_us_hr_payroll/models/state/mo_missouri.py
index c6018df0..47e56639 100644
--- a/l10n_us_hr_payroll/models/state/mo_missouri.py
+++ b/l10n_us_hr_payroll/models/state/mo_missouri.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def mo_missouri_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -19,7 +19,10 @@ def mo_missouri_state_income_withholding(payslip, categories, worked_days, input
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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)
@@ -28,8 +31,6 @@ def mo_missouri_state_income_withholding(payslip, categories, worked_days, input
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]
- if wage == 0.0:
- return 0.0, 0.0
gross_taxable_income = wage * pay_periods
gross_taxable_income -= deduction
diff --git a/l10n_us_hr_payroll/models/state/ms_mississippi.py b/l10n_us_hr_payroll/models/state/ms_mississippi.py
index cda417cf..10f30ee2 100644
--- a/l10n_us_hr_payroll/models/state/ms_mississippi.py
+++ b/l10n_us_hr_payroll/models/state/ms_mississippi.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -19,8 +19,8 @@ def ms_mississippi_state_income_withholding(payslip, categories, worked_days, in
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
- if wage == 0.0:
+ wage = sit_wage(payslip, categories)
+ if not wage:
return 0.0, 0.0
pay_periods = payslip.dict.get_pay_periods_in_year()
diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py
index b89c692f..6e33261a 100644
--- a/l10n_us_hr_payroll/models/state/mt_montana.py
+++ b/l10n_us_hr_payroll/models/state/mt_montana.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -18,13 +18,16 @@ def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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 or wage == 0.0:
+ if not exemption_rate or not withholding_rate:
return 0.0, 0.0
adjusted_wage = wage - (exemption_rate * (exemptions or 0))
diff --git a/l10n_us_hr_payroll/models/state/nc_northcarolina.py b/l10n_us_hr_payroll/models/state/nc_northcarolina.py
index 8b9be103..056d1fe8 100644
--- a/l10n_us_hr_payroll/models/state/nc_northcarolina.py
+++ b/l10n_us_hr_payroll/models/state/nc_northcarolina.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def nc_northcarolina_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -19,15 +19,16 @@ def nc_northcarolina_state_income_withholding(payslip, categories, worked_days,
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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']
- if wage == 0.0:
- return 0.0, 0.0
taxable_wage = round((wage - (deduction + (allowances * allowances_rate))) * 0.0535)
withholding = 0.0
if taxable_wage < 0.0:
diff --git a/l10n_us_hr_payroll/models/state/nj_newjersey.py b/l10n_us_hr_payroll/models/state/nj_newjersey.py
index b69ffcef..f0a805b9 100644
--- a/l10n_us_hr_payroll/models/state/nj_newjersey.py
+++ b/l10n_us_hr_payroll/models/state/nj_newjersey.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def nj_newjersey_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -19,7 +19,9 @@ def nj_newjersey_state_income_withholding(payslip, categories, worked_days, inpu
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
@@ -34,9 +36,6 @@ def nj_newjersey_state_income_withholding(payslip, categories, worked_days, inpu
if not allowances:
return 0.0, 0.0
- if wage == 0.0:
- return 0.0, 0.0
-
gross_taxable_income = wage - (allowance_value * allowances)
withholding = 0.0
prior_wage_base = 0.0
diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py
index 24e7cc9f..5a7c3869 100644
--- a/l10n_us_hr_payroll/models/state/oh_ohio.py
+++ b/l10n_us_hr_payroll/models/state/oh_ohio.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -18,15 +18,16 @@ def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs):
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
- if wage == 0.0:
- return 0.0, 0.0
taxable_wage = (wage * pay_periods) - (exemption_rate * (exemptions or 0))
withholding = 0.0
diff --git a/l10n_us_hr_payroll/models/state/va_virginia.py b/l10n_us_hr_payroll/models/state/va_virginia.py
index 018e56a3..a09f80a0 100644
--- a/l10n_us_hr_payroll/models/state/va_virginia.py
+++ b/l10n_us_hr_payroll/models/state/va_virginia.py
@@ -1,6 +1,6 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from .general import _state_applies
+from .general import _state_applies, sit_wage
def va_virginia_state_income_withholding(payslip, categories, worked_days, inputs):
@@ -18,7 +18,10 @@ def va_virginia_state_income_withholding(payslip, categories, worked_days, input
return 0.0, 0.0
# Determine Wage
- wage = categories.GROSS + categories.DED_FIT_EXEMPT
+ 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')
@@ -27,8 +30,6 @@ def va_virginia_state_income_withholding(payslip, categories, worked_days, input
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')
- if wage == 0.0:
- return 0.0, 0.0
taxable_wage = (wage * pay_periods) - (deduction + (personal_exemptions * personal_exemption_rate) + (other_exemptions * other_exemption_rate))
withholding = 0.0
From cc5267452eab1793c6e2a285c2e86b3c4c69b9bb Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 4 Feb 2020 11:25:55 -0500
Subject: [PATCH 23/43] IMP `l10n_us_hr_payroll` Port `l10n_us_ar_hr_payroll`
AR Arkansas including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/ar_arkansas.xml | 143 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/ar_arkansas.py | 47 ++++++
.../models/us_payroll_config.py | 2 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_ar_arkansas_payslip_2019.py | 72 +++++++++
.../tests/test_us_ar_arkansas_payslip_2020.py | 35 +++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 311 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ar_arkansas.xml
create mode 100644 l10n_us_hr_payroll/models/state/ar_arkansas.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ar_arkansas_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 82e8b55e..0be1d505 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -25,6 +25,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fica_rules.xml',
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
+ 'data/state/ar_arkansas.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
'data/state/mi_michigan.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 48d04270..1a24442c 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
+from .state.ar_arkansas import ar_arkansas_state_income_withholding
from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.mi_michigan import mi_michigan_state_income_withholding
from .state.mn_minnesota import mn_minnesota_state_income_withholding
@@ -55,6 +56,7 @@ class HRPayslip(models.Model):
'general_state_unemployment': general_state_unemployment,
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
+ 'ar_arkansas_state_income_withholding': ar_arkansas_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
'mi_michigan_state_income_withholding': mi_michigan_state_income_withholding,
'mn_minnesota_state_income_withholding': mn_minnesota_state_income_withholding,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index becaaaa8..06a18b6e 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -51,6 +51,8 @@ class HRContractUSPayrollConfig(models.Model):
fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]',
help='Form W4 (2020+) 4(c)')
+ ar_ar4ec_sit_allowances = fields.Integer(string='Arkansas AR4EC allowances', help='AR4EC 3.')
+
ga_g4_sit_filing_status = fields.Selection([
('exempt', 'Exempt'),
('single', 'Single'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 711bb35c..83e839d9 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -4,6 +4,9 @@ from . import common
from . import test_us_payslip_2019
from . import test_us_payslip_2020
+from . import test_us_ar_arkansas_payslip_2019
+from . import test_us_ar_arkansas_payslip_2020
+
from . import test_us_fl_florida_payslip_2019
from . import test_us_fl_florida_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 0e78dd83..33eb7323 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -44,6 +44,12 @@
+
+ Form AR4EC - State Income Tax
+
+
+
+
No additional fields.
From dad1716a976a91b904ef6a2e9b1ec043ef6d75fe Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 11 Feb 2020 10:11:30 -0500
Subject: [PATCH 24/43] IMP `l10n_us_hr_payroll` Port `l10n_us_il_hr_payroll`
IL Illinois including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/il_illinois.xml | 117 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/il_illinois.py | 35 ++++++
.../models/us_payroll_config.py | 3 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_il_illinois_payslip_2019.py | 71 +++++++++++
.../tests/test_us_il_illinois_payslip_2020.py | 36 ++++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 274 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/il_illinois.xml
create mode 100644 l10n_us_hr_payroll/models/state/il_illinois.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_il_illinois_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 0be1d505..7a007cc4 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -28,6 +28,7 @@ United States of America - Payroll Rules.
'data/state/ar_arkansas.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
+ 'data/state/il_illinois.xml',
'data/state/mi_michigan.xml',
'data/state/mn_minnesota.xml',
'data/state/mo_missouri.xml',
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..7fe9108e
--- /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.130
+
+
+
+
+
+
+ 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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 1a24442c..8de8bfe9 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -14,6 +14,7 @@ from .state.general import general_state_unemployment, \
is_us_state
from .state.ar_arkansas import ar_arkansas_state_income_withholding
from .state.ga_georgia import ga_georgia_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
@@ -58,6 +59,7 @@ class HRPayslip(models.Model):
'is_us_state': is_us_state,
'ar_arkansas_state_income_withholding': ar_arkansas_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_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,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 06a18b6e..6edfd76d 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -66,6 +66,9 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ 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([
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 83e839d9..56d2a837 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -13,6 +13,9 @@ 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_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
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..244c383c
--- /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
+ MI_UNEMP_MAX_WAGE = 12740.0
+ MI_UNEMP = 3.130
+
+ 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.MI_UNEMP, date(2020, 1, 1), wage_base=self.MI_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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 33eb7323..143d6d5a 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -61,6 +61,12 @@
+
+ Form IL-W-4 - State Income Tax
+
+
+
+
Form MI-W4 - State Income Tax
From 01fc541648ba0b581c18de1617ad52d3004afcdd Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Thu, 13 Feb 2020 11:06:09 -0500
Subject: [PATCH 25/43] IMP `l10n_us_hr_payroll` Port `l10n_us_az_hr_payroll`
AZ Arizona including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/az_arizona.xml | 81 +++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/az_arizona.py | 35 ++++++++
.../models/us_payroll_config.py | 4 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_az_arizona_payslip_2019.py | 72 +++++++++++++++++
.../tests/test_us_az_arizona_payslip_2020.py | 33 ++++++++
.../views/us_payroll_config_views.xml | 5 ++
9 files changed, 236 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/az_arizona.xml
create mode 100644 l10n_us_hr_payroll/models/state/az_arizona.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_az_arizona_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 7a007cc4..6db804bf 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -26,6 +26,7 @@ United States of America - Payroll Rules.
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
'data/state/ar_arkansas.xml',
+ 'data/state/az_arizona.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
'data/state/il_illinois.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 8de8bfe9..4141a0f5 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -13,6 +13,7 @@ from .state.general import general_state_unemployment, \
general_state_income_withholding, \
is_us_state
from .state.ar_arkansas import ar_arkansas_state_income_withholding
+from .state.az_arizona import az_arizona_state_income_withholding
from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.il_illinois import il_illinois_state_income_withholding
from .state.mi_michigan import mi_michigan_state_income_withholding
@@ -58,6 +59,7 @@ class HRPayslip(models.Model):
'general_state_income_withholding': general_state_income_withholding,
'is_us_state': is_us_state,
'ar_arkansas_state_income_withholding': ar_arkansas_state_income_withholding,
+ 'az_arizona_state_income_withholding': az_arizona_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
'il_illinois_state_income_withholding': il_illinois_state_income_withholding,
'mi_michigan_state_income_withholding': mi_michigan_state_income_withholding,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 6edfd76d..bc76715a 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -53,6 +53,10 @@ class HRContractUSPayrollConfig(models.Model):
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.')
+
ga_g4_sit_filing_status = fields.Selection([
('exempt', 'Exempt'),
('single', 'Single'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 56d2a837..7d4abae5 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -7,6 +7,9 @@ from . import test_us_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_fl_florida_payslip_2019
from . import test_us_fl_florida_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 143d6d5a..dafaf285 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -50,6 +50,11 @@
+
+ Form A-4 - State Income Tax
+
+
+
No additional fields.
From 8e8aae056d90bfffaae7ab9215418f2580475260 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Thu, 13 Feb 2020 17:20:20 -0500
Subject: [PATCH 26/43] FIX `l10n_us_hr_payroll` Changed SUTA Rate for Illinois
2020.
---
l10n_us_hr_payroll/data/state/il_illinois.xml | 2 +-
.../tests/test_us_il_illinois_payslip_2020.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/l10n_us_hr_payroll/data/state/il_illinois.xml b/l10n_us_hr_payroll/data/state/il_illinois.xml
index 7fe9108e..840b2a9b 100644
--- a/l10n_us_hr_payroll/data/state/il_illinois.xml
+++ b/l10n_us_hr_payroll/data/state/il_illinois.xml
@@ -32,7 +32,7 @@
- 3.130
+ 3.125
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
index 244c383c..ead932e4 100644
--- 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
@@ -6,8 +6,8 @@ from .common import TestUsPayslip
class TestUsILPayslip(TestUsPayslip):
# Taxes and Rates
- MI_UNEMP_MAX_WAGE = 12740.0
- MI_UNEMP = 3.130
+ 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):
@@ -27,7 +27,7 @@ class TestUsILPayslip(TestUsPayslip):
self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), -expected_withholding)
def test_2020_taxes_example(self):
- self._test_er_suta('IL', self.MI_UNEMP, date(2020, 1, 1), wage_base=self.MI_UNEMP_MAX_WAGE)
+ 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)
From 043d0f8acdfca336d1d5312611d2efa34da3b260 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Mon, 17 Feb 2020 10:49:53 -0500
Subject: [PATCH 27/43] IMP `l10n_us_hr_payroll` Port `l10n_us_ak_hr_payroll`
AK Alaska including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/ak_alaska.xml | 95 +++++++++++++++++++
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_ak_alaska_payslip_2019.py | 61 ++++++++++++
.../tests/test_us_ak_alaska_payslip_2020.py | 15 +++
5 files changed, 175 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ak_alaska.xml
create mode 100644 l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ak_alaska_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 6db804bf..77b506fd 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -25,6 +25,7 @@ United States of America - Payroll Rules.
'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/ar_arkansas.xml',
'data/state/az_arizona.xml',
'data/state/fl_florida.xml',
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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 7d4abae5..e5b8fa04 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -4,6 +4,9 @@ from . import common
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_ar_arkansas_payslip_2019
from . import test_us_ar_arkansas_payslip_2020
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)
From 5068a883e36204523d4cf32b2e5c1c9c16f83bec Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Mon, 17 Feb 2020 13:05:15 -0500
Subject: [PATCH 28/43] IMP `l10n_us_hr_payroll` Port `l10n_us_al_hr_payroll`
AL Alabama including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/al_alabama.xml | 191 +++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/al_alabama.py | 77 +++++
.../models/us_payroll_config.py | 9 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_al_alabama_payslip_2019.py | 264 ++++++++++++++++++
.../tests/test_us_al_alabama_payslip_2020.py | 36 +++
.../views/us_payroll_config_views.xml | 7 +
9 files changed, 590 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/al_alabama.xml
create mode 100644 l10n_us_hr_payroll/models/state/al_alabama.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_al_alabama_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 77b506fd..3f26f989 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -26,6 +26,7 @@ United States of America - Payroll Rules.
'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/fl_florida.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 4141a0f5..3d52d04c 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \
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.ga_georgia import ga_georgia_state_income_withholding
@@ -58,6 +59,7 @@ class HRPayslip(models.Model):
'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,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index bc76715a..69dc69c2 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -51,6 +51,15 @@ class HRContractUSPayrollConfig(models.Model):
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(
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index e5b8fa04..1ded7fb4 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -7,6 +7,9 @@ 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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index dafaf285..082aedcb 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -44,6 +44,13 @@
+
+ Form A4 - State Income Tax
+
+
+
+
+
Form AR4EC - State Income Tax
From 757b03195ddbb99ca9fd266305db25e02217e9e8 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Fri, 21 Feb 2020 17:05:11 -0500
Subject: [PATCH 29/43] IMP `l10n_us_hr_payroll` Port `l10n_us_ct_hr_payroll`
CT Connecticut including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/ct_connecticut.xml | 1227 +++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/ct_connecticut.py | 76 +
.../models/us_payroll_config.py | 8 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../test_us_ct_connecticut_payslip_2019.py | 121 ++
.../test_us_ct_connecticut_payslip_2020.py | 34 +
.../views/us_payroll_config_views.xml | 5 +
9 files changed, 1477 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ct_connecticut.xml
create mode 100644 l10n_us_hr_payroll/models/state/ct_connecticut.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2019.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ct_connecticut_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 3f26f989..d9aa37e5 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -29,6 +29,7 @@ United States of America - Payroll Rules.
'data/state/al_alabama.xml',
'data/state/ar_arkansas.xml',
'data/state/az_arizona.xml',
+ 'data/state/ct_connecticut.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
'data/state/il_illinois.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 3d52d04c..330ae93e 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -15,6 +15,7 @@ from .state.general import general_state_unemployment, \
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.ct_connecticut import ct_connecticut_state_income_withholding
from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.il_illinois import il_illinois_state_income_withholding
from .state.mi_michigan import mi_michigan_state_income_withholding
@@ -62,6 +63,7 @@ class HRPayslip(models.Model):
'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,
+ 'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
'il_illinois_state_income_withholding': il_illinois_state_income_withholding,
'mi_michigan_state_income_withholding': mi_michigan_state_income_withholding,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 69dc69c2..c038396c 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -66,6 +66,14 @@ class HRContractUSPayrollConfig(models.Model):
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.')
+ 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.')
+
ga_g4_sit_filing_status = fields.Selection([
('exempt', 'Exempt'),
('single', 'Single'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 1ded7fb4..92176b7f 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -16,6 +16,9 @@ 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_ct_connecticut_payslip_2019
+from . import test_us_ct_connecticut_payslip_2020
+
from . import test_us_fl_florida_payslip_2019
from . import test_us_fl_florida_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 082aedcb..a59bf54f 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -62,6 +62,11 @@
+
+ Form CT-W4 - State Income Tax
+
+
+
No additional fields.
From ee6537bddd31a341d5d89c2da7eccc659a6775af Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 3 Mar 2020 18:15:21 -0500
Subject: [PATCH 30/43] IMP `l10n_us_hr_payroll` Port `l10n_us_ca_hr_payroll`
CA California including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/ca_california.xml | 357 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/ca_california.py | 98 +++++
.../models/us_payroll_config.py | 11 +
.../test_us_ca_california_payslip_2019.py | 245 ++++++++++++
.../test_us_ca_california_payslip_2020.py | 42 +++
.../views/us_payroll_config_views.xml | 7 +
8 files changed, 763 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ca_california.xml
create mode 100644 l10n_us_hr_payroll/models/state/ca_california.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_ca_california_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index d9aa37e5..1a159fa0 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -29,6 +29,7 @@ United States of America - Payroll Rules.
'data/state/al_alabama.xml',
'data/state/ar_arkansas.xml',
'data/state/az_arizona.xml',
+ 'data/state/ca_california.xml',
'data/state/ct_connecticut.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 330ae93e..57419af1 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -15,6 +15,7 @@ from .state.general import general_state_unemployment, \
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.ct_connecticut import ct_connecticut_state_income_withholding
from .state.ga_georgia import ga_georgia_state_income_withholding
from .state.il_illinois import il_illinois_state_income_withholding
@@ -63,6 +64,7 @@ class HRPayslip(models.Model):
'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,
'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
'il_illinois_state_income_withholding': il_illinois_state_income_withholding,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index c038396c..6b585075 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -66,6 +66,17 @@ class HRContractUSPayrollConfig(models.Model):
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'),
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index a59bf54f..64d88b27 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -62,6 +62,13 @@
+
+ Form W-4 - State Income Tax
+
+
+
+
+
Form CT-W4 - State Income Tax
From c5c923a8202ea5d02d63c9b3ffbf0df931b688e7 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 3 Mar 2020 20:04:52 -0500
Subject: [PATCH 31/43] IMP `l10n_us_hr_payroll` Port `l10n_us_id_hr_payroll`
ID Idaho including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/id_idaho.xml | 176 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/id_idaho.py | 41 ++++
.../models/us_payroll_config.py | 7 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_id_idaho_payslip_2019.py | 85 +++++++++
.../tests/test_us_id_idaho_payslip_2020.py | 35 ++++
.../views/us_payroll_config_views.xml | 5 +
9 files changed, 355 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/id_idaho.xml
create mode 100644 l10n_us_hr_payroll/models/state/id_idaho.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_id_idaho_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 1a159fa0..8f7b1131 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -33,6 +33,7 @@ United States of America - Payroll Rules.
'data/state/ct_connecticut.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
+ 'data/state/id_idaho.xml',
'data/state/il_illinois.xml',
'data/state/mi_michigan.xml',
'data/state/mn_minnesota.xml',
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..66195251
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/id_idaho.xml
@@ -0,0 +1,176 @@
+
+
+
+
+ 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
+ 1
+
+
+
+ US Idaho - State Tax Commission (ISTC) - Income Tax
+ 1
+
+
+
+
+
+
+
+
+
+ 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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 57419af1..32f24ce7 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -18,6 +18,7 @@ from .state.az_arizona import az_arizona_state_income_withholding
from .state.ca_california import ca_california_state_income_withholding
from .state.ct_connecticut import ct_connecticut_state_income_withholding
from .state.ga_georgia import ga_georgia_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
@@ -67,6 +68,7 @@ class HRPayslip(models.Model):
'ca_california_state_income_withholding': ca_california_state_income_withholding,
'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_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,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 6b585075..c61def5e 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -98,6 +98,13 @@ class HRContractUSPayrollConfig(models.Model):
ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances',
help='G-4 5.')
+ 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.')
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 92176b7f..86f3112d 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -25,6 +25,9 @@ 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_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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 64d88b27..a3fda1fc 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -85,6 +85,11 @@
+
+ Form ID W-4 - State Income Tax
+
+
+
Form IL-W-4 - State Income Tax
From 2ddaaa52d7bf3562efa6c38c26149eebcc495cf1 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 3 Mar 2020 20:11:33 -0500
Subject: [PATCH 32/43] FIX `l10n_us_hr_payroll` Port `l10n_us_ca_hr_payroll`
Added test case on file.
---
l10n_us_hr_payroll/tests/__init__.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 86f3112d..6bdb478d 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -16,6 +16,9 @@ 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_ct_connecticut_payslip_2019
from . import test_us_ct_connecticut_payslip_2020
From 829c152d87ffef9228bf7cfde34e3bab741045f1 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Wed, 4 Mar 2020 16:50:11 -0500
Subject: [PATCH 33/43] FIX `l10n_us_hr_payroll` Port `l10n_us_id_hr_payroll`
Remove supplier from the Partners..
---
l10n_us_hr_payroll/data/state/id_idaho.xml | 2 --
1 file changed, 2 deletions(-)
diff --git a/l10n_us_hr_payroll/data/state/id_idaho.xml b/l10n_us_hr_payroll/data/state/id_idaho.xml
index 66195251..ef908d13 100644
--- a/l10n_us_hr_payroll/data/state/id_idaho.xml
+++ b/l10n_us_hr_payroll/data/state/id_idaho.xml
@@ -134,12 +134,10 @@
US Idaho - Department of Labor (IDOL) - Unemployment Tax
- 1
US Idaho - State Tax Commission (ISTC) - Income Tax
- 1
From d41bff78f367ffefc989de0b2450409f019329a5 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 4 Mar 2020 15:45:48 -0800
Subject: [PATCH 34/43] FIX `l10n_us_hr_payroll` Don't give error on Zero wage
in FIT
---
l10n_us_hr_payroll/models/federal/fed_941.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/l10n_us_hr_payroll/models/federal/fed_941.py b/l10n_us_hr_payroll/models/federal/fed_941.py
index 517df6c0..c154bca7 100644
--- a/l10n_us_hr_payroll/models/federal/fed_941.py
+++ b/l10n_us_hr_payroll/models/federal/fed_941.py
@@ -203,6 +203,8 @@ def ee_us_941_fit(payslip, categories, worked_days, inputs):
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()
From 65791b0fbddef1c744cd69eab6bc88f7146138d9 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Fri, 6 Mar 2020 13:41:48 -0500
Subject: [PATCH 35/43] IMP `l10n_us_hr_payroll` Port `l10n_us_hi_hr_payroll`
HI Hawaii including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/hi_hawaii.xml | 125 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/hi_hawaii.py | 43 ++++++
.../models/us_payroll_config.py | 8 ++
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_hi_hawaii_payslip_2019.py | 93 +++++++++++++
.../tests/test_us_hi_hawaii_payslip_2020.py | 35 +++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 316 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/hi_hawaii.xml
create mode 100644 l10n_us_hr_payroll/models/state/hi_hawaii.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_hi_hawaii_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 8f7b1131..f023e2df 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -33,6 +33,7 @@ United States of America - Payroll Rules.
'data/state/ct_connecticut.xml',
'data/state/fl_florida.xml',
'data/state/ga_georgia.xml',
+ 'data/state/hi_hawaii.xml',
'data/state/id_idaho.xml',
'data/state/il_illinois.xml',
'data/state/mi_michigan.xml',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 32f24ce7..3aa4b4b2 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -18,6 +18,7 @@ from .state.az_arizona import az_arizona_state_income_withholding
from .state.ca_california import ca_california_state_income_withholding
from .state.ct_connecticut import ct_connecticut_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.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
@@ -68,6 +69,7 @@ class HRPayslip(models.Model):
'ca_california_state_income_withholding': ca_california_state_income_withholding,
'ct_connecticut_state_income_withholding': ct_connecticut_state_income_withholding,
'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding,
+ 'hi_hawaii_state_income_withholding': hi_hawaii_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,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index c61def5e..de8e3daf 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -98,6 +98,14 @@ class HRContractUSPayrollConfig(models.Model):
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.')
+
id_w4_sit_filing_status = fields.Selection([
('single', 'Single'),
('married', 'Married'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 6bdb478d..d6e43e9e 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -28,6 +28,9 @@ 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_id_idaho_payslip_2019
from . import test_us_id_idaho_payslip_2020
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index a3fda1fc..99889df7 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -85,6 +85,12 @@
+
+ Form HI HW-4 - State Income Tax
+
+
+
+
Form ID W-4 - State Income Tax
From 99ca9a55a65825135d40686931c869a5c6eb52f0 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Mon, 9 Mar 2020 18:49:24 -0400
Subject: [PATCH 36/43] IMP `l10n_us_hr_payroll` Port `l10n_us_de_hr_payroll`
DE Delaware including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/de_delaware.xml | 119 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/de_delaware.py | 49 ++++++++
.../models/us_payroll_config.py | 6 +
l10n_us_hr_payroll/tests/__init__.py | 2 +
.../tests/test_us_de_delaware_payslip_2020.py | 36 ++++++
.../views/us_payroll_config_views.xml | 6 +
8 files changed, 221 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/de_delaware.xml
create mode 100644 l10n_us_hr_payroll/models/state/de_delaware.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_de_delaware_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index f023e2df..adbd9306 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -31,6 +31,7 @@ United States of America - Payroll Rules.
'data/state/az_arizona.xml',
'data/state/ca_california.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',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 3aa4b4b2..1a61e91d 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -17,6 +17,7 @@ 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.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.id_idaho import id_idaho_state_income_withholding
@@ -68,6 +69,7 @@ class HRPayslip(models.Model):
'az_arizona_state_income_withholding': az_arizona_state_income_withholding,
'ca_california_state_income_withholding': ca_california_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,
'id_idaho_state_income_withholding': id_idaho_state_income_withholding,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index de8e3daf..55c48951 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -85,6 +85,12 @@ class HRContractUSPayrollConfig(models.Model):
('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'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index d6e43e9e..ed39aac5 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -22,6 +22,8 @@ from . import test_us_ca_california_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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index 99889df7..f978c203 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -69,6 +69,12 @@
+
+ Form DE W-4 - State Income Tax
+
+
+
+
Form CT-W4 - State Income Tax
From 0d447d67b4b754dbe4b7b3446b5ab08c1fc9a427 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Thu, 12 Mar 2020 12:20:09 -0400
Subject: [PATCH 37/43] IMP `l10n_us_hr_payroll` for Colorado 13.0
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/co_colorado.xml | 97 +++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/co_colorado.py | 45 +++++++++
l10n_us_hr_payroll/tests/__init__.py | 2 +
.../tests/test_us_co_colorado_payslip_2020.py | 36 +++++++
.../views/us_payroll_config_views.xml | 5 +
7 files changed, 188 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/co_colorado.xml
create mode 100644 l10n_us_hr_payroll/models/state/co_colorado.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_co_colorado_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index adbd9306..1a3214ff 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -30,6 +30,7 @@ United States of America - Payroll Rules.
'data/state/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',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 1a61e91d..c0667415 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -16,6 +16,7 @@ from .state.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
@@ -68,6 +69,7 @@ class HRPayslip(models.Model):
'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,
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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index ed39aac5..fd80b46d 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -19,6 +19,8 @@ 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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index f978c203..eaa0c8d0 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -69,6 +69,11 @@
+
+ Form W-4 - State Income Tax
+
+
+
Form DE W-4 - State Income Tax
From f6beed71652efafc87323ff3a0774bd885cad211 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Tue, 24 Mar 2020 15:50:48 -0400
Subject: [PATCH 38/43] IMP `l10n_us_hr_payroll` Port `l10n_us_ia_hr_payroll`
IA Iowa including migration
---
l10n_us_hr_payroll/__manifest__.py | 1 +
l10n_us_hr_payroll/data/state/ia_iowa.xml | 177 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
l10n_us_hr_payroll/models/state/ia_iowa.py | 44 +++++
.../models/us_payroll_config.py | 2 +
l10n_us_hr_payroll/tests/__init__.py | 3 +
.../tests/test_us_ia_iowa_payslip_2019.py | 152 +++++++++++++++
.../tests/test_us_ia_iowa_payslip_2020.py | 33 ++++
.../views/us_payroll_config_views.xml | 6 +
9 files changed, 420 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/ia_iowa.xml
create mode 100644 l10n_us_hr_payroll/models/state/ia_iowa.py
create mode 100644 l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2019.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_ia_iowa_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 1a3214ff..d04c4772 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -36,6 +36,7 @@ United States of America - Payroll Rules.
'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',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index c0667415..24bf0548 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -21,6 +21,7 @@ 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
@@ -74,6 +75,7 @@ class HRPayslip(models.Model):
'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,
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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
index 55c48951..b1aa0699 100644
--- a/l10n_us_hr_payroll/models/us_payroll_config.py
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -112,6 +112,8 @@ class HRContractUSPayrollConfig(models.Model):
], 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'),
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index fd80b46d..ffbcfba4 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -35,6 +35,9 @@ 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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index eaa0c8d0..a1afad81 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -102,6 +102,12 @@
+
+ Form IA W-4 - State Income Tax
+
+
+
+
Form ID W-4 - State Income Tax
From 9edbd40bc9cd1ea4e7554c2f0ecb64bb39589398 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Fri, 27 Mar 2020 13:56:06 -0400
Subject: [PATCH 39/43] IMP `l10n_us_hr_payroll` for New Mexico 13.0
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/nm_new_mexico.xml | 113 ++++++++++++++++++
l10n_us_hr_payroll/models/hr_payslip.py | 2 +
.../models/state/nm_new_mexico.py | 40 +++++++
l10n_us_hr_payroll/tests/__init__.py | 2 +
.../test_us_nm_new_mexico_payslip_2020.py | 35 ++++++
.../views/us_payroll_config_views.xml | 4 +
7 files changed, 197 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/nm_new_mexico.xml
create mode 100644 l10n_us_hr_payroll/models/state/nm_new_mexico.py
create mode 100755 l10n_us_hr_payroll/tests/test_us_nm_new_mexico_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index d04c4772..672a620e 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -46,6 +46,7 @@ United States of America - Payroll Rules.
'data/state/mt_montana.xml',
'data/state/nc_northcarolina.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',
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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py
index 24bf0548..888a9ebc 100644
--- a/l10n_us_hr_payroll/models/hr_payslip.py
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -31,6 +31,7 @@ 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, \
@@ -85,6 +86,7 @@ class HRPayslip(models.Model):
'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,
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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index ffbcfba4..d8499ab3 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -65,6 +65,8 @@ from . import test_us_nc_northcarolina_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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index a1afad81..c16391ae 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -162,6 +162,10 @@
+
+ Form NM W-4 - State Income Tax
+
+
Form IT-4 - State Income Tax
From 0a09a060838e4d1f66266ad7e89584fe921c9067 Mon Sep 17 00:00:00 2001
From: Bhoomi Vaishnani
Date: Fri, 27 Mar 2020 14:25:11 -0400
Subject: [PATCH 40/43] IMP `l10n_us_hr_payroll` for New Hampshire 13.0
---
l10n_us_hr_payroll/__manifest__.py | 1 +
.../data/state/nh_new_hampshire.xml | 51 +++++++++++++++++++
l10n_us_hr_payroll/tests/__init__.py | 2 +
.../test_us_nh_new_hampshire_payslip_2020.py | 13 +++++
.../views/us_payroll_config_views.xml | 3 ++
5 files changed, 70 insertions(+)
create mode 100644 l10n_us_hr_payroll/data/state/nh_new_hampshire.xml
create mode 100644 l10n_us_hr_payroll/tests/test_us_nh_new_hampshire_payslip_2020.py
diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py
index 672a620e..2785d47b 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -45,6 +45,7 @@ United States of America - Payroll Rules.
'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',
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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index d8499ab3..11a045e2 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -62,6 +62,8 @@ 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
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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
index c16391ae..a91f80e4 100644
--- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -155,6 +155,9 @@
+
+ No additional fields.
+
Form NJ-W4 - State Income Tax
From 310bcf1c4ab1a7b8a1de6e83e099d6ca7c5ea9c1 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Sat, 18 Apr 2020 15:35:43 -0700
Subject: [PATCH 41/43] IMP `l10n_us_hr_payroll` Allow configurable changes to
payslip summing behavior.
In stock Odoo, summing anything in payroll rules (but most importantly rule amounts and category amounts by code), the considered payslips are referenced from their `date_from` field. However in the USA, it is in fact the `date_to` that is more important (or accounting date). A Payslip made for 2019-12-20 to 2020-01-04 should in fact be considered a '2020' payslip, and thus the summation on other '2020' payslips must find it by considering payslips `date_to`.
---
l10n_us_hr_payroll/__init__.py | 9 ++
l10n_us_hr_payroll/__manifest__.py | 2 +
l10n_us_hr_payroll/models/__init__.py | 2 +
l10n_us_hr_payroll/models/browsable_object.py | 122 ++++++++++++++++++
.../models/res_config_settings.py | 24 ++++
l10n_us_hr_payroll/tests/__init__.py | 3 +
l10n_us_hr_payroll/tests/common.py | 13 +-
l10n_us_hr_payroll/tests/test_special.py | 65 ++++++++++
.../views/res_config_settings_views.xml | 32 +++++
9 files changed, 263 insertions(+), 9 deletions(-)
create mode 100644 l10n_us_hr_payroll/models/browsable_object.py
create mode 100644 l10n_us_hr_payroll/models/res_config_settings.py
create mode 100644 l10n_us_hr_payroll/tests/test_special.py
create mode 100644 l10n_us_hr_payroll/views/res_config_settings_views.xml
diff --git a/l10n_us_hr_payroll/__init__.py b/l10n_us_hr_payroll/__init__.py
index 09434554..013f4e73 100644
--- a/l10n_us_hr_payroll/__init__.py
+++ b/l10n_us_hr_payroll/__init__.py
@@ -1,3 +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
index 2785d47b..0084f427 100644
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -54,10 +54,12 @@ United States of America - Payroll Rules.
'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/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py
index c6d607ff..8611b49a 100644
--- a/l10n_us_hr_payroll/models/__init__.py
+++ b/l10n_us_hr_payroll/models/__init__.py
@@ -1,5 +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/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/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index 11a045e2..d2b95c24 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -1,6 +1,9 @@
# 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
diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py
index 014d3719..df6491a6 100755
--- a/l10n_us_hr_payroll/tests/common.py
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -22,6 +22,10 @@ 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):
@@ -149,15 +153,6 @@ class TestUsPayslip(common.TransactionCase):
def assertPayrollAlmostEqual(self, first, second):
self.assertAlmostEqual(first, second, self.payroll_digits-1)
- 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 get_us_state(self, code, cache={}):
country_key = 'US_COUNTRY'
if code in cache:
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/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.
+
+
+
+
+
+
+
+
+
+
From 7fe37304dba08c58912b3e330b1a52641972c81e Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Wed, 10 Jun 2020 15:41:14 -0700
Subject: [PATCH 42/43] [ADD] l10n_us_hr_payroll_401k: initial commit for Odoo
13.0
---
l10n_us_hr_payroll_401k/__init__.py | 3 +
l10n_us_hr_payroll_401k/__manifest__.py | 24 ++++
l10n_us_hr_payroll_401k/data/payroll.xml | 119 ++++++++++++++++
l10n_us_hr_payroll_401k/models/__init__.py | 4 +
l10n_us_hr_payroll_401k/models/contract.py | 21 +++
l10n_us_hr_payroll_401k/models/payslip.py | 83 +++++++++++
l10n_us_hr_payroll_401k/tests/__init__.py | 3 +
l10n_us_hr_payroll_401k/tests/test_payroll.py | 132 ++++++++++++++++++
.../views/contract_views.xml | 20 +++
9 files changed, 409 insertions(+)
create mode 100644 l10n_us_hr_payroll_401k/__init__.py
create mode 100644 l10n_us_hr_payroll_401k/__manifest__.py
create mode 100644 l10n_us_hr_payroll_401k/data/payroll.xml
create mode 100644 l10n_us_hr_payroll_401k/models/__init__.py
create mode 100644 l10n_us_hr_payroll_401k/models/contract.py
create mode 100644 l10n_us_hr_payroll_401k/models/payslip.py
create mode 100644 l10n_us_hr_payroll_401k/tests/__init__.py
create mode 100644 l10n_us_hr_payroll_401k/tests/test_payroll.py
create mode 100644 l10n_us_hr_payroll_401k/views/contract_views.xml
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/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
From 314fceb4d6b267b42c094004eda09143a1fd6f79 Mon Sep 17 00:00:00 2001
From: Jared Kipe
Date: Tue, 29 Sep 2020 14:41:58 -0700
Subject: [PATCH 43/43] [IMP] l10n_us_hr_payroll_401k: Add migration code to
handle known issues from Odoo S.A. migrations.
---
.../migrations/13.0.0.0.1/pre-migration.py | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 l10n_us_hr_payroll_401k/migrations/13.0.0.0.1/pre-migration.py
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})