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 00000000..2a58a381
Binary files /dev/null and b/l10n_us_hr_payroll/static/description/icon.png differ
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
new file mode 100755
index 00000000..23702419
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -0,0 +1,5 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import common
+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
new file mode 100755
index 00000000..fc84b0fe
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -0,0 +1,150 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from logging import getLogger
+from sys import float_info as sys_float_info
+from collections import defaultdict
+
+from odoo.tests import common
+from odoo.tools.float_utils import float_round as odoo_float_round
+
+
+def process_payslip(payslip):
+ try:
+ payslip.action_payslip_done()
+ except AttributeError:
+ # v9
+ payslip.process_sheet()
+
+
+class TestUsPayslip(common.TransactionCase):
+ debug = False
+ _logger = getLogger(__name__)
+
+ float_info = sys_float_info
+
+ def float_round(self, value, digits):
+ return odoo_float_round(value, digits)
+
+ _payroll_digits = -1
+
+ @property
+ def payroll_digits(self):
+ if self._payroll_digits == -1:
+ self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll')
+ return self._payroll_digits
+
+ def _log(self, message):
+ if self.debug:
+ self._logger.warn(message)
+
+ def _createEmployee(self):
+ return self.env['hr.employee'].create({
+ 'birthday': '1985-03-14',
+ 'country_id': self.ref('base.us'),
+ 'department_id': self.ref('hr.dep_rd'),
+ 'gender': 'male',
+ 'name': 'Jared'
+ })
+
+ def _createContract(self, employee, **kwargs):
+ if not 'schedule_pay' in kwargs:
+ kwargs['schedule_pay'] = 'monthly'
+ schedule_pay = kwargs['schedule_pay']
+ config_model = self.env['hr.contract.us_payroll_config']
+ contract_model = self.env['hr.contract']
+ config_values = {
+ 'name': 'Test Config Values',
+ 'employee_id': employee.id,
+ }
+ contract_values = {
+ 'name': 'Test Contract',
+ 'employee_id': employee.id,
+ }
+
+ for key, val in kwargs.items():
+ # Assume any Odoo object is in a Many2one
+ if hasattr(val, 'id'):
+ val = val.id
+ found = False
+ if hasattr(contract_model, key):
+ contract_values[key] = val
+ found = True
+ if hasattr(config_model, key):
+ config_values[key] = val
+ found = True
+ if not found:
+ self._logger.warn('cannot locate attribute names "%s" on contract or payroll config' % (key, ))
+
+ # US Payroll Config Defaults Should be set on the Model
+ config = config_model.create(config_values)
+ contract_values['us_payroll_config_id'] = config.id
+
+ # Some Basic Defaults
+ if not contract_values.get('state'):
+ contract_values['state'] = 'open' # Running
+ if not contract_values.get('structure_type_id'):
+ contract_values['structure_type_id'] = self.ref('l10n_us_hr_payroll.structure_type_employee')
+ if not contract_values.get('date_start'):
+ contract_values['date_start'] = '2016-01-01'
+ if not contract_values.get('date_end'):
+ contract_values['date_end'] = '2030-12-31'
+ if not contract_values.get('resource_calendar_id'):
+ contract_values['resource_calendar_id'] = self.ref('resource.resource_calendar_std')
+
+ # Compatibility with earlier Odoo versions
+ if not contract_values.get('journal_id') and hasattr(contract_model, 'journal_id'):
+ try:
+ contract_values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id
+ except KeyError:
+ # Accounting not installed
+ pass
+
+ contract = contract_model.create(contract_values)
+
+ # Compatibility with Odoo 13
+ contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay
+ return contract
+
+ def _createPayslip(self, employee, date_from, date_to):
+ slip = self.env['hr.payslip'].create({
+ 'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to),
+ 'employee_id': employee.id,
+ 'date_from': date_from,
+ 'date_to': date_to
+ })
+ slip._onchange_employee()
+ return slip
+
+ def _getCategories(self, payslip):
+ categories = defaultdict(float)
+ for line in payslip.line_ids:
+ self._log(' line code: ' + str(line.code) +
+ ' category code: ' + line.category_id.code +
+ ' total: ' + str(line.total) +
+ ' rate: ' + str(line.rate) +
+ ' amount: ' + str(line.amount))
+ category_id = line.category_id
+ category_code = line.category_id.code
+ while category_code:
+ categories[category_code] += line.total
+ category_id = category_id.parent_id
+ category_code = category_id.code
+ return categories
+
+ def _getRules(self, payslip):
+ rules = defaultdict(float)
+ for line in payslip.line_ids:
+ rules[line.code] += line.total
+ return rules
+
+ def assertPayrollEqual(self, first, second):
+ self.assertAlmostEqual(first, second, self.payroll_digits)
+
+ def 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()
diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py
new file mode 100644
index 00000000..db6004e9
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py
@@ -0,0 +1,338 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+from sys import float_info
+
+
+class TestUsPayslip2019(TestUsPayslip):
+ # FUTA Constants
+ FUTA_RATE_NORMAL = 0.6
+ FUTA_RATE_BASIC = 6.0
+ FUTA_RATE_EXEMPT = 0.0
+
+ # Wage caps
+ FICA_SS_MAX_WAGE = 132900.0
+ FICA_M_MAX_WAGE = float_info.max
+ FICA_M_ADD_START_WAGE = 200000.0
+ FUTA_MAX_WAGE = 7000.0
+
+ # Rates
+ FICA_SS = 6.2 / -100.0
+ FICA_M = 1.45 / -100.0
+ FUTA = FUTA_RATE_NORMAL / -100.0
+ FICA_M_ADD = 0.9 / -100.0
+
+ ###
+ # 2019 Taxes and Rates
+ ###
+
+ def test_2019_taxes(self):
+ self.debug = False
+ # salary is high so that second payslip runs over max
+ # social security salary
+ salary = 80000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee, wage=salary)
+ self._log(contract.read())
+
+ self._log('2018 tax last slip')
+ payslip = self._createPayslip(employee, '2018-12-01', '2018-12-31')
+ payslip.compute_sheet()
+ self._log(payslip.read())
+ process_payslip(payslip)
+
+ # Ensure amounts are there, they shouldn't be added in the next year...
+ cats = self._getCategories(payslip)
+ self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
+
+ self._log('2019 tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ # Employee
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
+ # Employer
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have reached Medicare Additional (employee only)
+ remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
+ remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
+
+ self._log('2019 tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have reached Medicare Additional (employee only)
+ self._log('2019 tax third payslip:')
+ payslip = self._createPayslip(employee, '2019-03-01', '2019-03-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have all salary as Medicare Additional
+
+ self._log('2019 tax fourth payslip:')
+ payslip = self._createPayslip(employee, '2019-04-01', '2019-04-30')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD)
+
+ process_payslip(payslip)
+
+ def test_2019_fed_income_withholding_single(self):
+ self.debug = False
+
+ salary = 6000.00
+ schedule_pay = 'monthly'
+ w4_allowances = 3
+ w4_allowance_amt = 350.00 * w4_allowances
+ adjusted_salary = salary - w4_allowance_amt # should be 4962.60, but would work over a wide value for the rate
+ ###
+ # Single MONTHLY form Publication 15
+ expected_withholding = self.float_round(-(378.52 + ((adjusted_salary - 3606) * 0.22)), self.payroll_digits)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_filing_status='single',
+ fed_941_fit_w4_allowances=w4_allowances
+ )
+
+ self._log('2019 fed income single payslip: adjusted_salary: ' + str(adjusted_salary))
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
+
+ def test_2019_fed_income_withholding_married_as_single(self):
+ salary = 500.00
+ schedule_pay = 'weekly'
+ w4_allowances = 1
+ w4_allowance_amt = 80.80 * w4_allowances
+ adjusted_salary = salary - w4_allowance_amt # should be 420.50, but would work over a wide value for the rate
+ ###
+ expected_withholding = self.float_round(-(18.70 + ((adjusted_salary - 260) * 0.12)), self.payroll_digits)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_filing_status='married_as_single',
+ fed_941_fit_w4_allowances=w4_allowances,
+ )
+
+ self._log('2019 fed income married_as_single payslip: adjusted_salary: ' + str(adjusted_salary))
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
+
+ def test_2019_fed_income_withholding_married(self):
+ salary = 14000.00
+ schedule_pay = 'bi-weekly'
+ w4_allowances = 2
+ w4_allowance_amt = 161.50 * w4_allowances
+ adjusted_salary = salary - w4_allowance_amt # should be 13680.80, but would work over a wide value for the rate
+ ###
+ # Single MONTHLY form Publication 15
+ expected_withholding = self.float_round(-(2519.06 + ((adjusted_salary - 12817) * 0.32)), self.payroll_digits)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_filing_status='married',
+ fed_941_fit_w4_allowances=w4_allowances
+ )
+
+ self._log('2019 fed income married payslip: adjusted_salary: ' + str(adjusted_salary))
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ # This is off by 1 penny given our new reporting of adjusted wage * computed percentage
+ #self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
+ self.assertTrue(abs(cats['EE_US_941_FIT'] - expected_withholding) < 0.011)
+
+ def test_2019_taxes_with_external(self):
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ self._createContract(employee, wage=salary, external_wages=external_wages)
+
+ self._log('2019 tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD)
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA)
+
+ def test_2019_taxes_with_full_futa(self):
+ futa_rate = self.FUTA_RATE_BASIC / -100.0
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+
+ employee = self._createEmployee()
+
+ self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC)
+
+ self._log('2019 tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD)
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate)
+
+ def test_2019_taxes_with_futa_exempt(self):
+ futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
+
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+ employee = self._createEmployee()
+ self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT)
+ self._log('2019 tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
+
+ def test_2019_fed_income_withholding_nonresident_alien(self):
+ salary = 3500.00
+ schedule_pay = 'quarterly'
+ w4_allowances = 1
+ w4_allowance_amt = 1050.0 * w4_allowances
+ nra_adjustment = 2000.0 # for quarterly
+ adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4450
+
+ ###
+ # Single QUARTERLY form Publication 15
+ expected_withholding = self.float_round(-(242.50 + ((adjusted_salary - 3375) * 0.12)), self.payroll_digits)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_allowances=w4_allowances,
+ fed_941_fit_w4_is_nonresident_alien=True,
+ )
+
+ self._log('2019 fed income single payslip nonresident alien: adjusted_salary: ' + str(adjusted_salary))
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
+
+ def test_2019_fed_income_additional_withholding(self):
+ salary = 50000.00
+ schedule_pay = 'annually'
+ w4_additional_withholding = 5000.0
+ w4_allowances = 2
+ w4_allowance_amt = 4200.0 * w4_allowances
+ adjusted_salary = salary - w4_allowance_amt # 41700
+
+ ###
+ # Single ANNUAL form Publication 15
+ expected_withholding = \
+ self.float_round(-((1940.00 + ((adjusted_salary - 31200) * 0.12)) + w4_additional_withholding),
+ self.payroll_digits)
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_filing_status='married',
+ fed_941_fit_w4_allowances=w4_allowances,
+ fed_941_fit_w4_additional_withholding=w4_additional_withholding,
+ )
+
+ self._log('2019 fed income married payslip additional withholding: adjusted_salary: ' + str(adjusted_salary))
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
+
+ def test_2019_taxes_with_w4_exempt(self):
+ salary = 6000.0
+ schedule_pay = 'bi-weekly'
+ w4_allowances = 0
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_allowances=w4_allowances,
+ fed_941_fit_w4_filing_status='',
+ )
+
+ self._log('2019 tax w4 exempt payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FIT'], 0.0)
+
+ def test_2019_taxes_with_fica_exempt(self):
+ salary = 6000.0
+ schedule_pay = 'bi-weekly'
+ employee = self._createEmployee()
+ contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay)
+ contract.us_payroll_config_id.fed_941_fica_exempt = True
+
+ self._log('2019 tax w4 exempt payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_payslip_2020.py
new file mode 100644
index 00000000..c10a230b
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_payslip_2020.py
@@ -0,0 +1,302 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+from sys import float_info
+
+
+class TestUsPayslip2020(TestUsPayslip):
+ # FUTA Constants
+ FUTA_RATE_NORMAL = 0.6
+ FUTA_RATE_BASIC = 6.0
+ FUTA_RATE_EXEMPT = 0.0
+
+ # Wage caps
+ FICA_SS_MAX_WAGE = 137700.0
+ FICA_M_MAX_WAGE = float_info.max
+ FICA_M_ADD_START_WAGE = 200000.0
+ FUTA_MAX_WAGE = 7000.0
+
+ # Rates
+ FICA_SS = 6.2 / -100.0
+ FICA_M = 1.45 / -100.0
+ FUTA = FUTA_RATE_NORMAL / -100.0
+ FICA_M_ADD = 0.9 / -100.0
+
+ ###
+ # 2020 Taxes and Rates
+ ###
+
+ def test_2020_taxes(self):
+ self.debug = False
+ # salary is high so that second payslip runs over max
+ # social security salary
+ salary = 80000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee, wage=salary)
+ self._log(contract.read())
+
+ self._log('2019 tax last slip')
+ payslip = self._createPayslip(employee, '2019-12-01', '2019-12-31')
+ payslip.compute_sheet()
+ self._log(payslip.read())
+ process_payslip(payslip)
+
+ # Ensure amounts are there, they shouldn't be added in the next year...
+ cats = self._getCategories(payslip)
+ self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
+
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ # Employee
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
+ # Employer
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have reached Medicare Additional (employee only)
+ remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
+ remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
+
+ self._log('2020 tax second payslip:')
+ payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have reached Medicare Additional (employee only)
+ self._log('2020 tax third payslip:')
+ payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have all salary as Medicare Additional
+
+ self._log('2020 tax fourth payslip:')
+ payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD)
+
+ process_payslip(payslip)
+
+ def test_2020_taxes_with_external(self):
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ self._createContract(employee, wage=salary, external_wages=external_wages)
+
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD)
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA)
+
+ def test_2020_taxes_with_full_futa(self):
+ futa_rate = self.FUTA_RATE_BASIC / -100.0
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+
+ employee = self._createEmployee()
+
+ self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC)
+
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD)
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate)
+
+ def test_2020_taxes_with_futa_exempt(self):
+ futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
+
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+ employee = self._createEmployee()
+ self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT)
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
+
+ def test_2020_taxes_with_fica_exempt(self):
+ salary = 6000.0
+ schedule_pay = 'bi-weekly'
+ employee = self._createEmployee()
+ contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay)
+ contract.us_payroll_config_id.fed_941_fica_exempt = True
+
+ self._log('2020 tax w4 exempt payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)
+
+ """
+ For Federal Income Tax Withholding, we are utilizing the calculations from the new IRS Excel calculator.
+ Given that you CAN round, we will round to compare even though we will calculate as close to the penny as possible
+ with the wage * computed_percent method.
+ """
+
+ def _run_test_fit(self,
+ wage=0.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=0.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ expected_standard=0.0,
+ expected_higher=0.0,
+ ):
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien,
+ fed_941_fit_w4_filing_status=filing_status,
+ fed_941_fit_w4_multiple_jobs_higher=False,
+ fed_941_fit_w4_dependent_credit=dependent_credit,
+ fed_941_fit_w4_other_income=other_income,
+ fed_941_fit_w4_deductions=deductions,
+ fed_941_fit_w4_additional_withholding=additional_withholding,
+ )
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_standard)
+
+ contract.us_payroll_config_id.fed_941_fit_w4_multiple_jobs_higher = True
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_higher)
+ return payslip
+
+ def test_2020_fed_income_withholding_single(self):
+ _ = self._run_test_fit(
+ wage=6000.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=100.0,
+ other_income=200.0,
+ deductions=300.0,
+ additional_withholding=400.0,
+ is_nonresident_alien=False,
+ expected_standard=1132.0,
+ expected_higher=1459.0,
+ )
+
+ def test_2020_fed_income_withholding_married_as_single(self):
+ # This is "Head of Household" though the field name is the same for historical reasons.
+ _ = self._run_test_fit(
+ wage=500.0,
+ schedule_pay='weekly',
+ filing_status='married_as_single',
+ dependent_credit=20.0,
+ other_income=30.0,
+ deductions=40.0,
+ additional_withholding=10.0,
+ is_nonresident_alien=False,
+ expected_standard=24.0,
+ expected_higher=45.0,
+ )
+
+ def test_2020_fed_income_withholding_married(self):
+ _ = self._run_test_fit(
+ wage=14000.00,
+ schedule_pay='bi-weekly',
+ filing_status='married',
+ dependent_credit=2500.0,
+ other_income=1200.0,
+ deductions=1000.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ expected_standard=2621.0,
+ expected_higher=3702.0,
+ )
+
+ def test_2020_fed_income_withholding_nonresident_alien(self):
+ # Monthly NRA additional wage is 1033.30
+ # Wage input on IRS Form entered as (3500+1033.30)=4533.30, not 3500.0
+ _ = self._run_test_fit(
+ wage=3500.00,
+ schedule_pay='monthly',
+ filing_status='married',
+ dependent_credit=340.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=True,
+ expected_standard=235.0,
+ expected_higher=391.0,
+ )
+
+ def test_2020_taxes_with_w4_exempt(self):
+ _ = self._run_test_fit(
+ wage=3500.00,
+ schedule_pay='monthly',
+ filing_status='', # Exempt
+ dependent_credit=340.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=True,
+ expected_standard=0.0,
+ expected_higher=0.0,
+ )
diff --git a/l10n_us_hr_payroll/views/hr_contract_views.xml b/l10n_us_hr_payroll/views/hr_contract_views.xml
new file mode 100644
index 00000000..001b19cd
--- /dev/null
+++ b/l10n_us_hr_payroll/views/hr_contract_views.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ hr.contract.form.inherit
+ hr.contract
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/views/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
+
+
+
+
+
+