diff --git a/LICENSE_PROFESSIONAL b/LICENSE_PROFESSIONAL
new file mode 100644
index 00000000..5cbd3023
--- /dev/null
+++ b/LICENSE_PROFESSIONAL
@@ -0,0 +1,29 @@
+Odoo Proprietary License v1.0
+
+This software and associated files (the "Software") may only be used
+(executed, modified, executed after modifications) if you have purchased
+a valid license from the authors, typically via Odoo Apps, or if you
+have received a written agreement from the authors of the Software
+(see the COPYRIGHT file).
+
+You may develop Odoo modules that use the Software as a library
+(typically by depending on it, importing it and using its resources),
+but without copying any source code or material from the Software.
+You may distribute those modules under the license of your choice,
+provided that this license is compatible with the terms of the Odoo
+Proprietary License (For example: LGPL, MIT, or proprietary licenses
+similar to this one).
+
+It is forbidden to publish, distribute, sublicense, or sell copies
+of the Software or modified copies of the Software.
+
+The above copyright notice and this permission notice must be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/hr_payroll_rate/models/payroll.py b/hr_payroll_rate/models/payroll.py
index 66a89963..d1280593 100644
--- a/hr_payroll_rate/models/payroll.py
+++ b/hr_payroll_rate/models/payroll.py
@@ -1,23 +1,41 @@
+import ast
from odoo import api, fields, models
+from odoo.tools import ormcache
+from odoo.exceptions import UserError
class PayrollRate(models.Model):
_name = 'hr.payroll.rate'
_description = 'Payroll Rate'
+ _order = 'date_from DESC, company_id ASC'
active = fields.Boolean(string='Active', default=True)
name = fields.Char(string='Name')
- date_from = fields.Date(string='Date From', required=True)
+ date_from = fields.Date(string='Date From', index=True, required=True)
date_to = fields.Date(string='Date To')
company_id = fields.Many2one('res.company', string='Company', copy=False,
default=False)
- rate = fields.Float(string='Rate', digits=(12, 6), required=True)
- code = fields.Char(string='Code', required=True)
+ rate = fields.Float(string='Rate', digits=(12, 6), default=0.0, required=True)
+ code = fields.Char(string='Code', index=True, required=True)
limit_payslip = fields.Float(string='Payslip Limit')
limit_year = fields.Float(string='Year Limit')
wage_limit_payslip = fields.Float(string='Payslip Wage Limit')
wage_limit_year = fields.Float(string='Year Wage Limit')
+ parameter_value = fields.Text(help="Python data structure")
+
+ @api.model
+ @ormcache('code', 'date', 'company_id', 'self.env.user.company_id.id')
+ def _get_parameter_from_code(self, code, company_id, date=None):
+ if not date:
+ date = fields.Date.today()
+ rate = self.search([
+ ('code', '=', code),
+ ('date_from', '<=', date),
+ ], limit=1)
+ if not rate:
+ raise UserError(_("No rule parameter with code '%s' was found for %s ") % (code, date))
+ return ast.literal_eval(rate.parameter_value)
class Payslip(models.Model):
@@ -35,3 +53,6 @@ class Payslip(models.Model):
self.ensure_one()
return self.env['hr.payroll.rate'].search(
self._get_rate_domain(code), limit=1, order='date_from DESC, company_id ASC')
+
+ def rule_parameter(self, code):
+ return self.env['hr.payroll.rate']._get_parameter_from_code(code, self.company_id.id, self.date_to)
diff --git a/hr_payroll_rate/tests/test_payroll_rate.py b/hr_payroll_rate/tests/test_payroll_rate.py
index c86fe446..5354cfe1 100644
--- a/hr_payroll_rate/tests/test_payroll_rate.py
+++ b/hr_payroll_rate/tests/test_payroll_rate.py
@@ -44,6 +44,15 @@ class TestPayrollRate(common.TransactionCase):
rate = self.payslip.get_rate('TEST')
self.assertEqual(rate, test_rate)
+ test_rate.parameter_value = """[
+ (1, 2, 3),
+ (4, 5, 6),
+ ]"""
+
+ value = self.payslip.rule_parameter('TEST')
+ self.assertEqual(len(value), 2)
+ self.assertEqual(value[0], (1, 2, 3))
+
def test_payroll_rate_multicompany(self):
test_rate_other = self.env['hr.payroll.rate'].create({
'name': 'Test Rate',
diff --git a/hr_payroll_rate/views/payroll_views.xml b/hr_payroll_rate/views/payroll_views.xml
index 4132846e..49815884 100644
--- a/hr_payroll_rate/views/payroll_views.xml
+++ b/hr_payroll_rate/views/payroll_views.xml
@@ -27,6 +27,7 @@
+
diff --git a/l10n_us_ca_hr_payroll/__manifest__.py b/l10n_us_ca_hr_payroll/__manifest__.py
index b4dd1c3a..eeeeb590 100755
--- a/l10n_us_ca_hr_payroll/__manifest__.py
+++ b/l10n_us_ca_hr_payroll/__manifest__.py
@@ -28,5 +28,5 @@ USA::California Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_fl_hr_payroll/__manifest__.py b/l10n_us_fl_hr_payroll/__manifest__.py
index 4c1312d4..354fdc7d 100755
--- a/l10n_us_fl_hr_payroll/__manifest__.py
+++ b/l10n_us_fl_hr_payroll/__manifest__.py
@@ -23,5 +23,5 @@ USA::Florida Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_hr_payroll/__init__.py b/l10n_us_hr_payroll/__init__.py
index 0650744f..09434554 100755
--- a/l10n_us_hr_payroll/__init__.py
+++ b/l10n_us_hr_payroll/__init__.py
@@ -1 +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
index dd863641..54791cef 100755
--- a/l10n_us_hr_payroll/__manifest__.py
+++ b/l10n_us_hr_payroll/__manifest__.py
@@ -1,10 +1,9 @@
{
'name': 'USA - Payroll',
'author': 'Hibou Corp. ',
- 'license': 'AGPL-3',
'category': 'Localization',
'depends': ['hr_payroll', 'hr_payroll_rate'],
- 'version': '12.0.2019.1.0',
+ 'version': '12.0.2020.1.0',
'description': """
USA Payroll Rules.
==================
@@ -20,11 +19,28 @@ USA Payroll Rules.
'auto_install': False,
'website': 'https://hibou.io/',
'data': [
- 'views/l10n_us_hr_payroll_view.xml',
+ 'security/ir.model.access.csv',
'data/base.xml',
- 'data/rates.xml',
- 'data/rules.xml',
+ 'data/integration_rules.xml',
+ 'data/federal/fed_940_futa_parameters.xml',
+ 'data/federal/fed_940_futa_rules.xml',
+ 'data/federal/fed_941_fica_parameters.xml',
+ 'data/federal/fed_941_fica_rules.xml',
+ 'data/federal/fed_941_fit_parameters.xml',
+ 'data/federal/fed_941_fit_rules.xml',
+ 'data/state/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',
+ 'data/state/tx_texas.xml',
+ 'data/state/va_virginia.xml',
+ 'data/state/wa_washington.xml',
'data/final.xml',
+ 'views/hr_contract_views.xml',
+ 'views/us_payroll_config_views.xml',
],
- 'installable': True
+ 'installable': True,
+ 'license': 'OPL-1',
}
diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml
old mode 100755
new mode 100644
index 0579e8f3..7327729f
--- a/l10n_us_hr_payroll/data/base.xml
+++ b/l10n_us_hr_payroll/data/base.xml
@@ -1,83 +1,23 @@
-
+
-
-
-
- EFTPS - Form 941
- 1
-
-
-
- EFTPS - Form 940
- 1
-
-
-
- EFTPS - 941 (FICA + Federal Witholding)
- Electronic Federal Tax Payment System - Form 941
-
-
-
- EFTPS - 940 (FUTA)
- Electronic Federal Tax Payment System - Form 940
-
-
-
-
-
- Wage: US FICA Social Security
- WAGE_US_FICA_SS
-
-
- Wage: US FICA Medicare
- WAGE_US_FICA_M
-
-
- Wage: US FICA Medicare Additional
- WAGE_US_FICA_M_ADD
-
-
- Wage: US FUTA Federal Unemployment
- WAGE_US_FUTA
-
-
-
- EE: US FICA Social Security
- EE_US_FICA_SS
+
+
+ EE: State Unemployment SUTA
+ EE_US_SUTA
-
- EE: US FICA Medicare
- EE_US_FICA_M
-
-
-
- EE: US FICA Medicare Additional
- EE_US_FICA_M_ADD
-
-
-
- EE: US Federal Income Tax Withholding
- EE_US_FED_INC_WITHHOLD
-
-
-
-
- ER: US FICA Social Security
- ER_US_FICA_SS
+
+ ER: State Unemployment SUTA
+ ER_US_SUTA
-
- ER: US FICA Medicare
- ER_US_FICA_M
-
+
+
+
+ EE: State Income Tax Withholding
+ EE_US_SIT
+
-
- ER: US FUTA Federal Unemployment
- ER_US_FUTA
-
-
-
-
-
+
+
\ 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..e2ed116b
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_940_futa_parameters.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ 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..c1073b06
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ US Federal 940 - EFTPS
+
+
+
+ EFTPS - 940 (FUTA)
+ Electronic Federal Tax Payment System - Form 940
+
+
+
+
+ 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..8a25dde0
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fica_parameters.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Federal 941 FICA Social Security Wage Base
+ fed_941_fica_ss_wage_base
+ 128400.0
+
+
+
+ Federal 941 FICA Social Security Wage Base
+ fed_941_fica_ss_wage_base
+ 132900.0
+
+
+
+ Federal 941 FICA Social Security Wage Base
+ fed_941_fica_ss_wage_base
+ 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..6ce417fa
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml
@@ -0,0 +1,101 @@
+
+
+
+
+ US Federal 941 - EFTPS
+
+
+
+ EFTPS - 941 (FICA + Federal Witholding)
+ Electronic Federal Tax Payment System - Form 941
+
+
+
+
+ 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..89330246
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fit_parameters.xml
@@ -0,0 +1,492 @@
+
+
+
+
+
+ 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,
+ }
+
+
+
+ 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,
+ }
+
+
+
+ Federal 941 FIT Allowance
+ fed_941_fit_allowance
+
+ 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,
+ }
+
+
+
+ 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,
+ }
+
+
+
+ Federal 941 FIT NRA Additional
+ fed_941_fit_nra_additional
+ {
+ '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),
+ ],
+ }
+
+
+
+ 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),
+ ],
+ }
+
+
+
+ Federal 941 FIT Table Single
+ fed_941_fit_table_single
+
+
+ {
+ '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),
+ ],
+ }
+
+
+
+ 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),
+ ],
+ }
+
+
+
+ Federal 941 FIT Table Married
+ fed_941_fit_table_married
+
+
+ {
+ '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..8a4612af
--- /dev/null
+++ b/l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ 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/final.xml b/l10n_us_hr_payroll/data/final.xml
old mode 100755
new mode 100644
index a1ae2fd8..9bdeeef6
--- a/l10n_us_hr_payroll/data/final.xml
+++ b/l10n_us_hr_payroll/data/final.xml
@@ -1,29 +1,58 @@
-
-
+
US_EMP
USA Employee
-
+
-
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..9ecec93b
--- /dev/null
+++ b/l10n_us_hr_payroll/data/integration_rules.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ python
+ result = inputs.COMMISSION.amount > 0.0 if inputs.COMMISSION else False
+ code
+ result = inputs.COMMISSION.amount if inputs.COMMISSION else 0
+ BASIC_COM
+
+ Commissions
+
+
+
+
+
+ python
+ result = inputs.BADGES.amount > 0.0 if inputs.BADGES else False
+ code
+ result = inputs.BADGES.amount if inputs.BADGES else 0
+ BASIC_BADGES
+
+ Badges
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/rates.xml b/l10n_us_hr_payroll/data/rates.xml
deleted file mode 100644
index 6af3f48e..00000000
--- a/l10n_us_hr_payroll/data/rates.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
- US FUTA Exempt
- US_FUTA_EXEMPT
- 0.0
- 2016-01-01
-
-
-
- US FUTA Normal
- US_FUTA_NORMAL
- 0.6
- 2016-01-01
-
-
-
- US FUTA Basic
- US_FUTA_BASIC
- 6.0
- 2016-01-01
-
-
-
-
-
-
- US FICA Social Security
- US_FICA_SS
- 6.2
- 2016-01-01
- 2017-12-31
-
-
-
- US FICA Social Security
- US_FICA_SS
- 6.2
- 2018-01-01
- 2018-12-31
-
-
-
- US FICA Social Security
- US_FICA_SS
- 6.2
- 2019-01-01
- 2019-12-31
-
-
-
-
- US FICA Medicare
- US_FICA_M
- 1.45
- 2016-01-01
-
-
-
- US FICA Medicare Additional
- US_FICA_M_ADD
- 0.9
- 2016-01-01
- 200000.0
-
-
-
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/rules.xml b/l10n_us_hr_payroll/data/rules.xml
deleted file mode 100755
index 469f62f7..00000000
--- a/l10n_us_hr_payroll/data/rules.xml
+++ /dev/null
@@ -1,1058 +0,0 @@
-
-
-
-
-
-
-
-
-
- Wage: US FICA Social Security
- WAGE_US_FICA_SS
- python
- result = not contract.fica_exempt
- code
-
-###
-year = payslip.dict.date_to.year
-ytd = payslip.sum('WAGE_US_FICA_SS', str(year) + '-01-01', str(year+1) + '-01-01')
-ytd += contract.external_wages
-rate = payslip.dict.get_rate('US_FICA_SS')
-remaining = rate.wage_limit_year - ytd
-
-if remaining <= 0.0:
- result = 0
-elif remaining < categories.BASIC:
- result = remaining
-else:
- result = categories.BASIC
-
-
-
-
-
-
- Wage: US FICA Medicare
- WAGE_US_FICA_M
- python
- result = not contract.fica_exempt
- code
- result = categories.BASIC
-
-
-
-
-
- Wage: US FICA Medicare Additional
- WAGE_US_FICA_M_ADD
- python
- result = not contract.fica_exempt
- code
-
-###
-rate = payslip.dict.get_rate('US_FICA_M_ADD')
-ADD_M = rate.wage_limit_year
-year = payslip.dict.date_to.year
-norm_med_ytd = payslip.sum('WAGE_US_FICA_M', str(year) + '-01-01', str(year+1) + '-01-01')
-norm_med_cur = categories.WAGE_US_FICA_M
-
-if ADD_M > norm_med_ytd:
- diff = ADD_M - norm_med_ytd
- if norm_med_cur > diff:
- result = norm_med_cur - diff
- else:
- result = 0 # normal condition
-else:
- result = norm_med_cur # after YTD wages have passed the max
-
-
-
-
-
-
-
- EE: US FICA Social Security
- EE_US_FICA_SS
- python
- result = not contract.fica_exempt
- code
-
-rate = payslip.dict.get_rate('US_FICA_SS')
-result_rate = -rate.rate
-result = categories.WAGE_US_FICA_SS
-
-
-
-
-
-
- EE: US FICA Medicare
- EE_US_FICA_M
- python
- result = not contract.fica_exempt
- code
-
-rate = payslip.dict.get_rate('US_FICA_M')
-result_rate = -rate.rate
-result = categories.WAGE_US_FICA_M
-
-
-
-
-
-
- EE: US FICA Medicare Additional
- EE_US_FICA_M_ADD
- python
- result = not contract.fica_exempt
- code
-
-rate = payslip.dict.get_rate('US_FICA_M_ADD')
-result_rate = -rate.rate
-result = categories.WAGE_US_FICA_M_ADD
-
-
-
-
-
-
-
-
- EE: US Federal Income Tax Withholding - Single
- EE_US_FED_INC_WITHHOLD_S
- python
- result = (contract.w4_filing_status != 'married' and contract.w4_filing_status)
- code
-
-year = payslip.dict.date_to.year
-wages = categories.GROSS
-allowances = contract.w4_allowances
-is_nra = contract.w4_is_nonresident_alien
-schedule_pay = contract.schedule_pay
-val = 0.00
-additional = contract.w4_additional_withholding
-
-if year == 2018:
- ###
- # Single WEEKLY
- ###
- if 'weekly' == schedule_pay:
- wages -= allowances * 79.80
- if is_nra:
- wages += 151.00
-
- if wages > 71 and wages <= 254:
- val = 0.00 + ((wages - 71) * 0.10)
-
- elif wages > 254 and wages <= 815:
- val = 18.30 + ((wages - 254) * 0.12)
-
- elif wages > 815 and wages <= 1658:
- val = 85.62 + ((wages - 815) * 0.22)
-
- elif wages > 1658 and wages <= 3100:
- val = 271.08 + ((wages - 1658) * 0.24)
-
- elif wages > 3100 and wages <= 3917:
- val = 617.16 + ((wages - 3100) * 0.32)
-
- elif wages > 3917 and wages <= 9687:
- val = 878.60 + ((wages - 3917) * 0.35)
-
- elif wages > 9687:
- val = 2898.10 + ((wages - 9687) * 0.37)
-
- ###
- # Single BIWEEKLY
- ###
- elif 'bi-weekly' == schedule_pay:
- wages -= allowances * 159.60
- if is_nra:
- wages += 301.90
-
- if wages > 142 and wages <= 509:
- val = 0.00 + ((wages - 142) * 0.10)
-
- elif wages > 509 and wages <= 1631:
- val = 36.70 + ((wages - 509) * 0.12)
-
- elif wages > 1631 and wages <= 3315:
- val = 171.34 + ((wages - 1631) * 0.22)
-
- elif wages > 3315 and wages <= 6200:
- val = 541.82 + ((wages - 3315) * 0.24)
-
- elif wages > 6200 and wages <= 7835:
- val = 1234.22 + ((wages - 6200) * 0.32)
-
- elif wages > 7835 and wages <= 19373:
- val = 1757.42 + ((wages - 7835) * 0.35)
-
- elif wages > 19373:
- val = 5795.72 + ((wages - 19373) * 0.37)
-
- ###
- # Single SEMIMONTHLY
- ###
- elif 'semi-monthly' == schedule_pay:
- wages -= allowances * 172.90
- if is_nra:
- wages += 327.10
-
- if wages > 154 and wages <= 551:
- val = 0.00 + ((wages - 154) * 0.10)
-
- elif wages > 551 and wages <= 1767:
- val = 39.70 + ((wages - 551) * 0.12)
-
- elif wages > 1767 and wages <= 3592:
- val = 185.62 + ((wages - 1767) * 0.22)
-
- elif wages > 3592 and wages <= 6717:
- val = 587.12 + ((wages - 3592) * 0.24)
-
- elif wages > 6717 and wages <= 8488:
- val = 1337.12 + ((wages - 6717) * 0.32)
-
- elif wages > 8488 and wages <= 20988:
- val = 1903.84 + ((wages - 8488) * 0.35)
-
- elif wages > 20988:
- val = 6278.84 + ((wages - 20988) * 0.37)
-
- ###
- # Single MONTHLY
- ###
- elif 'monthly' == schedule_pay:
- wages -= allowances * 345.80
- if is_nra:
- wages += 654.20
-
- if wages > 308 and wages <= 1102:
- val = 0.00 + ((wages - 308) * 0.10)
-
- elif wages > 1102 and wages <= 3533:
- val = 79.40 + ((wages - 1102) * 0.12)
-
- elif wages > 3533 and wages <= 7183:
- val = 371.12 + ((wages - 3533) * 0.22)
-
- elif wages > 7183 and wages <= 13433:
- val = 1174.12 + ((wages - 7183) * 0.24)
-
- elif wages > 13433 and wages <= 16975:
- val = 2674.12 + ((wages - 13433) * 0.32)
-
- elif wages > 16975 and wages <= 41975:
- val = 3807.56 + ((wages - 16975) * 0.35)
-
- elif wages > 41975:
- val = 12557.56 + ((wages - 41975) * 0.37)
-
- ###
- # Single QUARTERLY
- ###
- elif 'quarterly' == schedule_pay:
- wages -= allowances * 1037.50
- if is_nra:
- wages += 1962.50
-
- if wages > 925 and wages <= 3306:
- val = 0.00 + ((wages - 925) * 0.10)
-
- elif wages > 3306 and wages <= 10600:
- val = 238.10 + ((wages - 3306) * 0.12)
-
- elif wages > 10600 and wages <= 21550:
- val = 1113.38 + ((wages - 10600) * 0.22)
-
- elif wages > 21550 and wages <= 40300:
- val = 3522.38 + ((wages - 21550) * 0.24)
-
- elif wages > 40300 and wages <= 50925:
- val = 8022.38 + ((wages - 40300) * 0.32)
-
- elif wages > 50925 and wages <= 125925:
- val = 11422.38 + ((wages - 50925) * 0.35)
-
- elif wages > 125925:
- val = 37672.38 + ((wages - 125925) * 0.37)
-
- ###
- # Single SEMIANNUAL
- ###
- elif 'semi-annually' == schedule_pay:
- wages -= allowances * 2075.00
- if is_nra:
- wages += 3925.00
-
- if wages > 1850 and wages <= 6613:
- val = 0.00 + ((wages - 1850) * 0.10)
-
- elif wages > 6613 and wages <= 21200:
- val = 476.30 + ((wages - 6613) * 0.12)
-
- elif wages > 21200 and wages <= 43100:
- val = 2226.74 + ((wages - 21200) * 0.22)
-
- elif wages > 43100 and wages <= 80600:
- val = 7044.74 + ((wages - 43100) * 0.24)
-
- elif wages > 80600 and wages <= 101850:
- val = 16044.74 + ((wages - 80600) * 0.32)
-
- elif wages > 101850 and wages <= 251850:
- val = 22844.74 + ((wages - 101850) * 0.35)
-
- elif wages > 251850:
- val = 75344.74 + ((wages - 251850) * 0.37)
-
- ###
- # Single ANNUAL
- ###
- elif 'annually' == schedule_pay:
- wages -= allowances * 4150.00
- if is_nra:
- wages += 7850.00
-
- if wages > 3700 and wages <= 13225:
- val = 0.00 + ((wages - 3700) * 0.10)
-
- elif wages > 13225 and wages <= 42400:
- val = 952.50 + ((wages - 13225) * 0.12)
-
- elif wages > 42400 and wages <= 86200:
- val = 4453.50 + ((wages - 42400) * 0.22)
-
- elif wages > 86200 and wages <= 161200:
- val = 14089.50 + ((wages - 86200) * 0.24)
-
- elif wages > 161200 and wages <= 203700:
- val = 32089.50 + ((wages - 161200) * 0.32)
-
- elif wages > 203700 and wages <= 503700:
- val = 45689.50 + ((wages - 203700) * 0.35)
-
- elif wages > 503700:
- val = 150689.50 + ((wages - 503700) * 0.37)
-
- else:
- raise Exception('Invalid schedule_pay="' + schedule_pay + '" for W4 Allowance calculation')
-else:
- ########
- # 2019 #
- ########
- # Single WEEKLY
- ###
- if 'weekly' == schedule_pay:
- wages -= allowances * 80.80
- if is_nra:
- wages += 153.80
-
- if wages > 73 and wages <= 260:
- val = 0.00 + ((wages - 73) * 0.10)
-
- elif wages > 260 and wages <= 832:
- val = 18.70 + ((wages - 260) * 0.12)
-
- elif wages > 832 and wages <= 1692:
- val = 87.34 + ((wages - 832) * 0.22)
-
- elif wages > 1692 and wages <= 3164:
- val = 276.54 + ((wages - 1692) * 0.24)
-
- elif wages > 3164 and wages <= 3998:
- val = 629.82 + ((wages - 3164) * 0.32)
-
- elif wages > 3998 and wages <= 9887:
- val = 896.70 + ((wages - 3998) * 0.35)
-
- elif wages > 9887:
- val = 2957.85 + ((wages - 9887) * 0.37)
-
- ###
- # Single BIWEEKLY
- ###
- elif 'bi-weekly' == schedule_pay:
- wages -= allowances * 161.50
- if is_nra:
- wages += 307.70
-
- if wages > 146 and wages <= 519:
- val = 0.00 + ((wages - 146) * 0.10)
-
- elif wages > 519 and wages <= 1664:
- val = 37.30 + ((wages - 519) * 0.12)
-
- elif wages > 1664 and wages <= 3385:
- val = 174.70 + ((wages - 1664) * 0.22)
-
- elif wages > 3385 and wages <= 6328:
- val = 553.32 + ((wages - 3385) * 0.24)
-
- elif wages > 6328 and wages <= 7996:
- val = 1259.64 + ((wages - 6328) * 0.32)
-
- elif wages > 7996 and wages <= 19773:
- val = 1793.40 + ((wages - 7996) * 0.35)
-
- elif wages > 19773:
- val = 5915.35 + ((wages - 19773) * 0.37)
-
- ###
- # Single SEMIMONTHLY
- ###
- elif 'semi-monthly' == schedule_pay:
- wages -= allowances * 175.00
- if is_nra:
- wages += 333.30
-
- if wages > 158 and wages <= 563:
- val = 0.00 + ((wages - 158) * 0.10)
-
- elif wages > 563 and wages <= 1803:
- val = 40.50 + ((wages - 563) * 0.12)
-
- elif wages > 1803 and wages <= 3667:
- val = 189.30 + ((wages - 1803) * 0.22)
-
- elif wages > 3667 and wages <= 6855:
- val = 599.38 + ((wages - 3667) * 0.24)
-
- elif wages > 6855 and wages <= 8663:
- val = 1364.50 + ((wages - 6855) * 0.32)
-
- elif wages > 8663 and wages <= 21421:
- val = 1943.06 + ((wages - 8663) * 0.35)
-
- elif wages > 21421:
- val = 6408.36 + ((wages - 21421) * 0.37)
-
- ###
- # Single MONTHLY
- ###
- elif 'monthly' == schedule_pay:
- wages -= allowances * 350.00
- if is_nra:
- wages += 666.70
-
- if wages > 317 and wages <= 1125:
- val = 0.00 + ((wages - 317) * 0.10)
-
- elif wages > 1125 and wages <= 3606:
- val = 80.80 + ((wages - 1125) * 0.12)
-
- elif wages > 3606 and wages <= 7333:
- val = 378.52 + ((wages - 3606) * 0.22)
-
- elif wages > 7333 and wages <= 13710:
- val = 1198.46 + ((wages - 7333) * 0.24)
-
- elif wages > 13710 and wages <= 17325:
- val = 2728.94 + ((wages - 13710) * 0.32)
-
- elif wages > 17325 and wages <= 42842:
- val = 3885.74 + ((wages - 17325) * 0.35)
-
- elif wages > 42842:
- val = 12816.69 + ((wages - 42842) * 0.37)
-
- ###
- # Single QUARTERLY
- ###
- elif 'quarterly' == schedule_pay:
- wages -= allowances * 1050.00
- if is_nra:
- wages += 2000.0
-
- if wages > 950 and wages <= 3375:
- val = 0.00 + ((wages - 950) * 0.10)
-
- elif wages > 3375 and wages <= 10819:
- val = 242.50 + ((wages - 3375) * 0.12)
-
- elif wages > 10819 and wages <= 22000:
- val = 1135.78 + ((wages - 10819) * 0.22)
-
- elif wages > 22000 and wages <= 41131:
- val = 3595.60 + ((wages - 22000) * 0.24)
-
- elif wages > 41131 and wages <= 51975:
- val = 8187.04 + ((wages - 41131) * 0.32)
-
- elif wages > 51975 and wages <= 128525:
- val = 11657.12 + ((wages - 51975) * 0.35)
-
- elif wages > 128525:
- val = 38449.62 + ((wages - 128525) * 0.37)
-
- ###
- # Single SEMIANNUAL
- ###
- elif 'semi-annually' == schedule_pay:
- wages -= allowances * 2100.00
- if is_nra:
- wages += 4000.00
-
- if wages > 1900 and wages <= 6750:
- val = 0.00 + ((wages - 1900) * 0.10)
-
- elif wages > 6750 and wages <= 21638:
- val = 485.00 + ((wages - 6750) * 0.12)
-
- elif wages > 21638 and wages <= 44000:
- val = 2271.56 + ((wages - 21638) * 0.22)
-
- elif wages > 44000 and wages <= 82263:
- val = 7191.20 + ((wages - 44000) * 0.24)
-
- elif wages > 82263 and wages <= 103950:
- val = 16374.32 + ((wages - 82263) * 0.32)
-
- elif wages > 103950 and wages <= 257050:
- val = 23314.16 + ((wages - 103950) * 0.35)
-
- elif wages > 257050:
- val = 76899.16 + ((wages - 257050) * 0.37)
-
- ###
- # Single ANNUAL
- ###
- elif 'annually' == schedule_pay:
- wages -= allowances * 4200.00
- if is_nra:
- wages += 8000.00
-
- if wages > 3800 and wages <= 13500:
- val = 0.00 + ((wages - 3800) * 0.10)
-
- elif wages > 13500 and wages <= 43275:
- val = 970.00 + ((wages - 13500) * 0.12)
-
- elif wages > 43275 and wages <= 88000:
- val = 4543.00 + ((wages - 43275) * 0.22)
-
- elif wages > 88000 and wages <= 164525:
- val = 14382.50 + ((wages - 88000) * 0.24)
-
- elif wages > 164525 and wages <= 207900:
- val = 32748.50 + ((wages - 164525) * 0.32)
-
- elif wages > 207900 and wages <= 514100:
- val = 46628.50 + ((wages - 207900) * 0.35)
-
- elif wages > 514100:
- val = 153798.50 + ((wages - 514100) * 0.37)
-
- else:
- raise Exception('Invalid schedule_pay="' + schedule_pay + '" for W4 Allowance calculation')
-
-result = -(val + additional)
-
-
-
-
-
-
- EE: US Federal Income Tax Withholding - Married
- EE_US_FED_INC_WITHHOLD_M
- python
- result = (contract.w4_filing_status == 'married')
- code
-
-year = payslip.dict.date_to.year
-wages = categories.GROSS
-allowances = contract.w4_allowances
-is_nra = contract.w4_is_nonresident_alien
-schedule_pay = contract.schedule_pay
-val = 0.00
-additional = contract.w4_additional_withholding
-
-if year == 2018:
- ###
- # Married WEEKLY
- ###
- if 'weekly' == schedule_pay:
- wages -= allowances * 79.80
- if is_nra:
- wages += 151.00
-
- if wages > 222 and wages <= 588:
- val = 0.00 + ((wages - 222) * 0.10)
-
- elif wages > 588 and wages <= 1711:
- val = 36.60 + ((wages - 588) * 0.12)
-
- elif wages > 1711 and wages <= 3395:
- val = 171.36 + ((wages - 1711) * 0.22)
-
- elif wages > 3395 and wages <= 6280:
- val = 541.84 + ((wages - 3395) * 0.24)
-
- elif wages > 6280 and wages <= 7914:
- val = 1234.24 + ((wages - 6280) * 0.32)
-
- elif wages > 7914 and wages <= 11761:
- val = 1757.12 + ((wages - 7914) * 0.35)
-
- elif wages > 11761:
- val = 3103.57 + ((wages - 11761) * 0.37)
-
- ###
- # Married BIWEEKLY
- ###
- elif 'bi-weekly' == schedule_pay:
- wages -= allowances * 159.60
- if is_nra:
- wages += 301.90
-
- if wages > 444 and wages <= 1177:
- val = 0.00 + ((wages - 444) * 0.10)
-
- elif wages > 1177 and wages <= 3421:
- val = 73.30 + ((wages - 1177) * 0.12)
-
- elif wages > 3421 and wages <= 6790:
- val = 342.58 + ((wages - 3421) * 0.22)
-
- elif wages > 6790 and wages <= 12560:
- val = 1083.76 + ((wages - 6790) * 0.24)
-
- elif wages > 12560 and wages <= 15829:
- val = 2468.56 + ((wages - 12560) * 0.32)
-
- elif wages > 15829 and wages <= 23521:
- val = 3514.64 + ((wages - 15829) * 0.35)
-
- elif wages > 23521:
- val = 6206.84 + ((wages - 23521) * 0.37)
-
- ###
- # Married SEMIMONTHLY
- ###
- elif 'semi-monthly' == schedule_pay:
- wages -= allowances * 172.90
- if is_nra:
- wages += 327.10
-
- if wages > 481 and wages <= 1275:
- val = 0.00 + ((wages - 481) * 0.10)
-
- elif wages > 1275 and wages <= 3706:
- val = 79.40 + ((wages - 1275) * 0.12)
-
- elif wages > 3706 and wages <= 7356:
- val = 371.12 + ((wages - 3706) * 0.22)
-
- elif wages > 7356 and wages <= 13606:
- val = 1174.12 + ((wages - 7356) * 0.24)
-
- elif wages > 13606 and wages <= 17148:
- val = 2674.12 + ((wages - 13606) * 0.32)
-
- elif wages > 17148 and wages <= 25481:
- val = 3807.56 + ((wages - 17148) * 0.35)
-
- elif wages > 25481:
- val = 6724.11 + ((wages - 25481) * 0.37)
-
- ###
- # Married MONTHLY
- ###
- elif 'monthly' == schedule_pay:
- wages -= allowances * 345.80
- if is_nra:
- wages += 654.20
-
- if wages > 963 and wages <= 2550:
- val = 0.00 + ((wages - 963) * 0.10)
-
- elif wages > 2550 and wages <= 7413:
- val = 158.70 + ((wages - 2550) * 0.12)
-
- elif wages > 7413 and wages <= 14713:
- val = 742.26 + ((wages - 7413) * 0.22)
-
- elif wages > 14713 and wages <= 27213:
- val = 2348.26 + ((wages - 14713) * 0.24)
-
- elif wages > 27213 and wages <= 34296:
- val = 5348.26 + ((wages - 27213) * 0.32)
-
- elif wages > 34296 and wages <= 50963:
- val = 7614.82 + ((wages - 34296) * 0.35)
-
- elif wages > 50963:
- val = 13448.27 + ((wages - 50963) * 0.37)
-
- ###
- # Married QUARTERLY
- ###
- elif 'quarterly' == schedule_pay:
- wages -= allowances * 1037.50
- if is_nra:
- wages += 1962.50
-
- if wages > 2888 and wages <= 7650:
- val = 0.00 + ((wages - 2888) * 0.10)
-
- elif wages > 7650 and wages <= 22238:
- val = 476.20 + ((wages - 7650) * 0.12)
-
- elif wages > 22238 and wages <= 44138:
- val = 2226.76 + ((wages - 22238) * 0.22)
-
- elif wages > 44138 and wages <= 81638:
- val = 7044.76 + ((wages - 44138) * 0.24)
-
- elif wages > 81638 and wages <= 102888:
- val = 16044.76 + ((wages - 81638) * 0.32)
-
- elif wages > 102888 and wages <= 152888:
- val = 22844.76 + ((wages - 102888) * 0.35)
-
- elif wages > 152888:
- val = 40344.76 + ((wages - 152888) * 0.37)
-
- ###
- # Married SEMIANNUAL
- ###
- elif 'semi-annually' == schedule_pay:
- wages -= allowances * 2075.00
- if is_nra:
- wages += 3925.00
-
- if wages > 5775 and wages <= 15300:
- val = 0.00 + ((wages - 5775) * 0.10)
-
- elif wages > 15300 and wages <= 44475:
- val = 952.50 + ((wages - 15300) * 0.12)
-
- elif wages > 44475 and wages <= 88275:
- val = 4453.50 + ((wages - 44475) * 0.22)
-
- elif wages > 88275 and wages <= 163275:
- val = 14089.50 + ((wages - 88275) * 0.24)
-
- elif wages > 163275 and wages <= 205775:
- val = 32089.50 + ((wages - 163275) * 0.32)
-
- elif wages > 205775 and wages <= 305775:
- val = 45689.50 + ((wages - 205775) * 0.35)
-
- elif wages > 305775:
- val = 80689.50 + ((wages - 305775) * 0.37)
-
- ###
- # Married ANNUAL
- ###
- elif 'annually' == schedule_pay:
- wages -= allowances * 4150.00
- if is_nra:
- wages += 7850.00
-
- if wages > 11550 and wages <= 30600:
- val = 0.00 + ((wages - 11550) * 0.10)
-
- elif wages > 30600 and wages <= 88950:
- val = 1905.00 + ((wages - 30600) * 0.12)
-
- elif wages > 88950 and wages <= 176550:
- val = 8907.00 + ((wages - 88950) * 0.22)
-
- elif wages > 176550 and wages <= 326550:
- val = 28179.00 + ((wages - 176550) * 0.24)
-
- elif wages > 326550 and wages <= 411550:
- val = 64179.00 + ((wages - 326550) * 0.32)
-
- elif wages > 411550 and wages <= 611550:
- val = 91379.00 + ((wages - 411550) * 0.35)
-
- elif wages > 611550:
- val = 161379.00 + ((wages - 611550) * 0.37)
-
- else:
- raise Exception('Invalid schedule_pay="' + schedule_pay + '" for W4 Allowance calculation')
-else:
- ########
- # 2019 #
- ########
- # Married WEEKLY
- ###
- if 'weekly' == schedule_pay:
- wages -= allowances * 80.80
- if is_nra:
- wages += 153.80
-
- if wages > 227 and wages <= 600:
- val = 0.00 + ((wages - 227) * 0.10)
-
- elif wages > 600 and wages <= 1745:
- val = 37.30 + ((wages - 600) * 0.12)
-
- elif wages > 1745 and wages <= 3465:
- val = 174.70 + ((wages - 1745) * 0.22)
-
- elif wages > 3465 and wages <= 6409:
- val = 553.10 + ((wages - 3465) * 0.24)
-
- elif wages > 6409 and wages <= 8077:
- val = 1259.66 + ((wages - 6409) * 0.32)
-
- elif wages > 8077 and wages <= 12003:
- val = 1793.42 + ((wages - 8077) * 0.35)
-
- elif wages > 12003:
- val = 3167.52 + ((wages - 12003) * 0.37)
-
- ###
- # Married BIWEEKLY
- ###
- elif 'bi-weekly' == schedule_pay:
- wages -= allowances * 161.50
- if is_nra:
- wages += 307.70
-
- if wages > 454 and wages <= 1200:
- val = 0.00 + ((wages - 454) * 0.10)
-
- elif wages > 1200 and wages <= 3490:
- val = 74.60 + ((wages - 1200) * 0.12)
-
- elif wages > 3490 and wages <= 6931:
- val = 349.40 + ((wages - 3490) * 0.22)
-
- elif wages > 6931 and wages <= 12817:
- val = 1106.42 + ((wages - 6931) * 0.24)
-
- elif wages > 12817 and wages <= 16154:
- val = 2519.06 + ((wages - 12817) * 0.32)
-
- elif wages > 16154 and wages <= 24006:
- val = 3586.90 + ((wages - 16154) * 0.35)
-
- elif wages > 24006:
- val = 6335.10 + ((wages - 24006) * 0.37)
-
- ###
- # Married SEMIMONTHLY
- ###
- elif 'semi-monthly' == schedule_pay:
- wages -= allowances * 175.00
- if is_nra:
- wages += 333.30
-
- if wages > 492 and wages <= 1300:
- val = 0.00 + ((wages - 492) * 0.10)
-
- elif wages > 1300 and wages <= 3781:
- val = 80.80 + ((wages - 1300) * 0.12)
-
- elif wages > 3781 and wages <= 7508:
- val = 378.52 + ((wages - 3781) * 0.22)
-
- elif wages > 7508 and wages <= 13885:
- val = 1198.46 + ((wages - 7508) * 0.24)
-
- elif wages > 13885 and wages <= 17500:
- val = 2728.94 + ((wages - 13885) * 0.32)
-
- elif wages > 17500 and wages <= 26006:
- val = 3885.74 + ((wages - 17500) * 0.35)
-
- elif wages > 26006:
- val = 6862.84 + ((wages - 26006) * 0.37)
-
- ###
- # Married MONTHLY
- ###
- elif 'monthly' == schedule_pay:
- wages -= allowances * 350.00
- if is_nra:
- wages += 666.70
-
- if wages > 983 and wages <= 2600:
- val = 0.00 + ((wages - 983) * 0.10)
-
- elif wages > 2600 and wages <= 7563:
- val = 161.70 + ((wages - 2600) * 0.12)
-
- elif wages > 7563 and wages <= 15017:
- val = 757.26 + ((wages - 7563) * 0.22)
-
- elif wages > 15017 and wages <= 27771:
- val = 2397.14 + ((wages - 15017) * 0.24)
-
- elif wages > 27771 and wages <= 35000:
- val = 5458.10 + ((wages - 27771) * 0.32)
-
- elif wages > 35000 and wages <= 50963:
- val = 7771.38 + ((wages - 35000) * 0.35)
-
- elif wages > 52013:
- val = 13725.93 + ((wages - 52013) * 0.37)
-
- ###
- # Married QUARTERLY
- ###
- elif 'quarterly' == schedule_pay:
- wages -= allowances * 1050.00
- if is_nra:
- wages += 2000.00
-
- if wages > 2950 and wages <= 7800:
- val = 0.00 + ((wages - 2950) * 0.10)
-
- elif wages > 7800 and wages <= 22688:
- val = 485.00 + ((wages - 7800) * 0.12)
-
- elif wages > 22688 and wages <= 45050:
- val = 2271.56 + ((wages - 22688) * 0.22)
-
- elif wages > 45050 and wages <= 83313:
- val = 7191.20 + ((wages - 45050) * 0.24)
-
- elif wages > 83313 and wages <= 105000:
- val = 16374.32 + ((wages - 83313) * 0.32)
-
- elif wages > 105000 and wages <= 156038:
- val = 23314.16 + ((wages - 105000) * 0.35)
-
- elif wages > 156038:
- val = 41177.46 + ((wages - 156038) * 0.37)
-
- ###
- # Married SEMIANNUAL
- ###
- elif 'semi-annually' == schedule_pay:
- wages -= allowances * 2100.00
- if is_nra:
- wages += 4000.00
-
- if wages > 5900 and wages <= 15600:
- val = 0.00 + ((wages - 5900) * 0.10)
-
- elif wages > 15600 and wages <= 45375:
- val = 970.00 + ((wages - 15600) * 0.12)
-
- elif wages > 45375 and wages <= 90100:
- val = 4543.00 + ((wages - 45375) * 0.22)
-
- elif wages > 90100 and wages <= 166625:
- val = 14382.50 + ((wages - 90100) * 0.24)
-
- elif wages > 166625 and wages <= 210000:
- val = 32748.50 + ((wages - 166625) * 0.32)
-
- elif wages > 210000 and wages <= 312075:
- val = 46628.50 + ((wages - 210000) * 0.35)
-
- elif wages > 312075:
- val = 82354.75 + ((wages - 312075) * 0.37)
-
- ###
- # Married ANNUAL
- ###
- elif 'annually' == schedule_pay:
- wages -= allowances * 4200.00
- if is_nra:
- wages += 8000.00
-
- if wages > 11800 and wages <= 31200:
- val = 0.00 + ((wages - 11800) * 0.10)
-
- elif wages > 31200 and wages <= 90750:
- val = 1940.00 + ((wages - 31200) * 0.12)
-
- elif wages > 90750 and wages <= 180200:
- val = 9086.00 + ((wages - 90750) * 0.22)
-
- elif wages > 180200 and wages <= 333250:
- val = 28765.00 + ((wages - 180200) * 0.24)
-
- elif wages > 333250 and wages <= 420000:
- val = 65497.00 + ((wages - 333250) * 0.32)
-
- elif wages > 420000 and wages <= 624150:
- val = 93257.00 + ((wages - 420000) * 0.35)
-
- elif wages > 624150:
- val = 164709.50 + ((wages - 624150) * 0.37)
-
- else:
- raise Exception('Invalid schedule_pay="' + schedule_pay + '" for W4 Allowance calculation')
-
-result = -(val + additional)
-
-
-
-
-
-
-
- Wage: US FUTA Federal Unemployment
- WAGE_US_FUTA
- python
- result = (contract.futa_type != contract.FUTA_TYPE_EXEMPT)
- code
-
-###
-rate = payslip.dict.get_futa_rate(contract)
-year = payslip.dict.date_to.year
-ytd = payslip.sum('WAGE_US_FUTA', str(year) + '-01-01', str(year+1) + '-01-01')
-ytd += contract.external_wages
-remaining = rate.wage_limit_year - ytd
-if remaining <= 0.0:
- result = 0
-elif remaining < categories.BASIC:
- result = remaining
-else:
- result = categories.BASIC
-
-
-
-
-
-
- ER: US FUTA Federal Unemployment
- ER_US_FUTA
- python
- result = (contract.futa_type != contract.FUTA_TYPE_EXEMPT)
- code
-
-year = payslip.dict.date_to.year
-rate = payslip.dict.get_futa_rate(contract)
-result_rate = -(rate.rate)
-result = categories.WAGE_US_FUTA
-
-
-
-
-
-
-
-
-
- ER: US FICA Social Security
- ER_US_FICA_SS
- none
- code
- result = categories.EE_US_FICA_SS
-
-
-
-
-
-
- ER: US FICA Medicare
- ER_US_FICA_M
- none
- code
- result = categories.EE_US_FICA_M
-
-
-
-
-
-
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..de1cc49a
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/fl_florida.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+ US FL Florida SUTA Wage Base
+ us_fl_suta_wage_base
+ 7000.00
+
+
+
+ US FL Florida SUTA Wage Base
+ us_fl_suta_wage_base
+ 7000.00
+
+
+
+
+
+
+
+ US FL Florida SUTA Rate
+ us_fl_suta_rate
+ 2.7
+
+
+
+ US FL Florida SUTA Rate
+ us_fl_suta_rate
+ 2.7
+
+
+
+
+
+
+ US Florida - Department of Revenue
+
+
+
+ US Florida - Department of Revenue (RT-6)
+
+
+
+
+
+
+
+
+
+ ER: US FL Florida State Unemployment (RT-6)
+ ER_US_FL_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_fl_suta_wage_base', rate='us_fl_suta_rate', state_code='FL')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_fl_suta_wage_base', rate='us_fl_suta_rate', state_code='FL')
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/ga_georgia.xml b/l10n_us_hr_payroll/data/state/ga_georgia.xml
new file mode 100644
index 00000000..2d07104a
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/ga_georgia.xml
@@ -0,0 +1,265 @@
+
+
+
+
+
+ US GA Georgia SUTA Wage Base
+ us_ga_suta_wage_base
+ 9500.00
+
+
+
+ US GA Georgia SUTA Wage Base
+ us_ga_suta_wage_base
+ 9500.00
+
+
+
+
+
+
+
+ US GA Georgia SUTA Rate
+ us_ga_suta_rate
+ 2.7
+
+
+
+ US GA Georgia SUTA Rate
+ us_ga_suta_rate
+ 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 Pennsylvania - Department of Revenue - Unemployment Tax
+
+
+
+
+ US Georgia - Department of Taxation - Income Tax
+
+
+
+ US Pennsylvania - Department of Revenue - Unemployment Tax
+
+
+
+
+
+
+
+
+
+ ER: US GA Georgia State Unemployment
+ ER_US_GA_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ga_suta_wage_base', rate='us_ga_suta_rate', state_code='GA')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ga_suta_wage_base', rate='us_ga_suta_rate', state_code='GA')
+
+
+
+
+
+
+
+ EE: US GA Georgia State Income Tax Withholding
+ EE_US_GA_SIT
+ python
+ result, _ = ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = ga_georgia_state_income_withholding(payslip, categories, worked_days, inputs)
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/ms_mississippi.xml b/l10n_us_hr_payroll/data/state/ms_mississippi.xml
new file mode 100644
index 00000000..7a94150d
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/ms_mississippi.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+ US MS Mississippi SUTA Wage Base
+ us_ms_suta_wage_base
+ 14000.0
+
+
+
+ US MS Mississippi SUTA Wage Base
+ us_ms_suta_wage_base
+ 14000.0
+
+
+
+
+
+
+
+ US MS Mississippi SUTA Rate
+ us_ms_suta_rate
+ 1.2
+
+
+
+ US MS Mississippi SUTA Rate
+ us_ms_suta_rate
+ 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),
+ ]
+
+
+
+ US MS Mississippi SIT Rate Table
+ us_ms_sit_rate
+ [
+ ( 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 - Department of Employment Security (Unemployment)
+
+
+
+
+ US Mississippi - Mississippi Department of Revenue (Income Tax)
+
+
+
+ US Mississippi - Mississippi Department of Revenue (Income Tax)
+
+
+
+
+
+
+
+
+
+ ER: US MS Mississippi State Unemployment
+ ER_US_MS_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ms_suta_wage_base', rate='us_ms_suta_rate', state_code='MS')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_ms_suta_wage_base', rate='us_ms_suta_rate', state_code='MS')
+
+
+
+
+
+
+
+ EE: US MS Mississippi State Income Tax Withholding
+ EE_US_MS_SIT
+ python
+ result, _ = ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = ms_mississippi_state_income_withholding(payslip, categories, worked_days, inputs)
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/mt_montana.xml b/l10n_us_hr_payroll/data/state/mt_montana.xml
new file mode 100644
index 00000000..7cca142a
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/mt_montana.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+ US MT Montana SUTA Wage Base
+ us_mt_suta_wage_base
+ 33000.00
+
+
+
+ US MT Montana SUTA Wage Base
+ us_mt_suta_wage_base
+ 34100.00
+
+
+
+
+
+
+
+ US MT Montana SUTA Rate (UI)
+ us_mt_suta_rate
+ 1.18
+
+
+
+ US MT Montana SUTA Rate (UI)
+ us_mt_suta_rate
+ 1.18
+
+
+
+
+
+
+ US MT Montana SUTA Administrative Fund Tax Rate
+ us_mt_suta_aft_rate
+ 0.13
+
+
+
+ US MT Montana SUTA Administrative Fund Tax Rate
+ us_mt_suta_aft_rate
+ 0.13
+
+
+
+
+
+
+ US MT Montana SIT Rate Table
+ us_mt_sit_rate
+ {
+ 'weekly': [
+ ( 135.00, 0.0, 1.80),
+ ( 288.00, 2.0, 4.40),
+ ( 2308.00, 9.0, 6.00),
+ ( 'inf', 130.0, 6.60),
+ ],
+ 'bi-weekly': [
+ ( 269.00, 0.0, 1.80),
+ ( 577.00, 5.0, 4.40),
+ ( 4615.00, 18.0, 6.00),
+ ( 'inf', 261.0, 6.60),
+ ],
+ 'semi-monthly': [
+ ( 292.00, 0.0, 1.80),
+ ( 625.00, 5.0, 4.40),
+ ( 5000.00, 20.0, 6.00),
+ ( 'inf', 282.0, 6.60),
+ ],
+ 'monthly': [
+ ( 583.00, 0.0, 1.80),
+ ( 1250.00, 11.0, 4.40),
+ ( 10000.00, 40.0, 6.00),
+ ( 'inf', 565.0, 6.60),
+ ],
+ 'annually': [
+ ( 7000.00, 0.0, 1.80),
+ ( 15000.00, 126.0, 4.40),
+ ( 120000.00, 478.0, 6.00),
+ ( 'inf', 6778.0, 6.60),
+ ],
+ }
+
+
+
+
+
+
+ US MT Montana SIT Exemption Rate Table
+ us_mt_sit_exemption_rate
+ {
+ 'weekly': 37.0,
+ 'bi-weekly': 73.0,
+ 'semi-monthly': 79.0,
+ 'monthly': 158.0,
+ 'annually': 1900.0,
+ }
+
+
+
+
+
+
+ US Montana - Department of Labor & Industries
+
+
+
+ US Montana - Department of Labor & Industries
+
+
+
+
+ US Montana - Department of Revenue - Income Tax
+
+
+
+ US Montana - Department of Revenue - Income Tax
+
+
+
+
+
+
+
+
+
+ ER: US MT Montana State Unemployment (UI-5)
+ ER_US_MT_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_rate', state_code='MT')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_rate', state_code='MT')
+
+
+
+
+
+
+
+ ER: US MT Montana State Unemployment Administrative Fund Tax (AFT) (UI-5)
+ ER_US_MT_SUTA_AFT
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_aft_rate', state_code='MT')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_mt_suta_wage_base', rate='us_mt_suta_aft_rate', state_code='MT')
+
+
+
+
+
+
+
+ EE: US MT Montana State Income Tax Withholding (MW-3)
+ EE_US_MT_SIT
+ python
+ result, _ = mt_montana_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = mt_montana_state_income_withholding(payslip, categories, worked_days, inputs)
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/oh_ohio.xml b/l10n_us_hr_payroll/data/state/oh_ohio.xml
new file mode 100644
index 00000000..91d16bd8
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/oh_ohio.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+ US OH Ohio SUTA Wage Base
+ us_oh_suta_wage_base
+ 9500.00
+
+
+
+ US OH Ohio SUTA Wage Base
+ us_oh_suta_wage_base
+ 9000.00
+
+
+
+
+
+
+
+ US OH Ohio SUTA Rate
+ us_oh_suta_rate
+ 2.7
+
+
+
+ US OH Ohio SUTA Rate
+ us_oh_suta_rate
+ 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),
+ ]
+
+
+
+ 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),
+ ]
+
+
+
+
+
+
+ US OH Ohio SIT Exemption Rate
+ us_oh_sit_exemption_rate
+ 650.0
+
+
+
+ US OH Ohio SIT Exemption Rate
+ us_oh_sit_exemption_rate
+ 650.0
+
+
+
+
+
+
+ US OH Ohio SIT Multiplier Value
+ us_oh_sit_multiplier
+ 1.075
+
+
+
+ US OH Ohio SIT Multiplier Value
+ us_oh_sit_multiplier
+ 1.032
+
+
+
+
+
+
+ US Ohio - OBG - Unemployment
+
+
+
+ US Ohio - OBG - Unemployment
+
+
+
+
+ US Ohio - OBG - Income Withholding
+
+
+
+ US Ohio - OBG - Income Withholding
+
+
+
+
+
+
+
+
+
+ ER: US OH Ohio State Unemployment (JFS-20125)
+ ER_US_OH_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_oh_suta_wage_base', rate='us_oh_suta_rate', state_code='OH')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_oh_suta_wage_base', rate='us_oh_suta_rate', state_code='OH')
+
+
+
+
+
+
+
+ EE: US OH Ohio State Income Tax Withholding (IT 501)
+ EE_US_OH_SIT
+ python
+ result, _ = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs)
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml b/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml
new file mode 100644
index 00000000..d0db79c9
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/pa_pennsylvania.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+ US PA Pennsylvania SUTA Wage Base (ER)
+ us_pa_suta_wage_base
+ 10000.00
+
+
+
+ US PA Pennsylvania SUTA Wage Base (ER)
+ us_pa_suta_wage_base
+ 10000.00
+
+
+
+
+
+
+
+ US PA Pennsylvania SUTA Rate
+ us_pa_suta_rate
+ 3.6890
+
+
+
+ US PA Pennsylvania SUTA Rate
+ us_pa_suta_rate
+ 3.6890
+
+
+
+
+
+
+ US PA Pennsylvania SUTA Employee Rate
+ us_pa_suta_ee_rate
+ 0.06
+
+
+
+ US PA Pennsylvania SUTA Employee Rate
+ us_pa_suta_ee_rate
+ 0.06
+
+
+
+
+
+
+ US PA Pennsylvania SIT Rate
+ us_pa_sit_rate
+ 3.07
+
+
+
+ US PA Pennsylvania SIT Rate
+ us_pa_sit_rate
+ 3.07
+
+
+
+
+
+
+ US Pennsylvania - Department of Revenue - Unemployment Tax
+
+
+
+ US Pennsylvania - Department of Revenue - Unemployment Tax
+
+
+
+
+ US Pennsylvania - Department of Revenue - Income Tax
+
+
+
+ US Pennsylvania - Department of Revenue - Income Tax
+
+
+
+
+
+
+
+
+
+ ER: US PA Pennsylvania State Unemployment (UC-2)
+ ER_US_PA_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_pa_suta_wage_base', rate='us_pa_suta_rate', state_code='PA')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_pa_suta_wage_base', rate='us_pa_suta_rate', state_code='PA')
+
+
+
+
+
+
+
+ EE: US PA Pennsylvania State Unemployment (UC-2)
+ EE_US_PA_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, rate='us_pa_suta_ee_rate', state_code='PA')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, rate='us_pa_suta_ee_rate', state_code='PA')
+
+
+
+
+
+
+
+ EE: US PA Pennsylvania State Income Tax Withholding (PA-501)
+ EE_US_PA_SIT
+ python
+ result, _ = general_state_income_withholding(payslip, categories, worked_days, inputs, rate='us_pa_sit_rate', state_code='PA')
+ code
+ result, result_rate = general_state_income_withholding(payslip, categories, worked_days, inputs, rate='us_pa_sit_rate', state_code='PA')
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/tx_texas.xml b/l10n_us_hr_payroll/data/state/tx_texas.xml
new file mode 100644
index 00000000..f8bed825
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/tx_texas.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+ US TX Texas SUTA Wage Base
+ us_tx_suta_wage_base
+ 9000.0
+
+
+
+ US TX Texas SUTA Wage Base
+ us_tx_suta_wage_base
+ 9000.0
+
+
+
+
+
+
+
+ US TX Texas SUTA Rate
+ us_tx_suta_rate
+ 2.7
+
+
+
+ US TX Texas SUTA Rate
+ us_tx_suta_rate
+ 2.7
+
+
+
+
+
+
+ US TX Texas Obligation Assessment Rate
+ us_tx_suta_oa_rate
+ 0.0
+
+
+
+ US TX Texas Obligation Assessment Rate
+ us_tx_suta_oa_rate
+ 0.0
+
+
+
+
+
+
+ US TX Texas Employment & Training Investment Assessment Rate
+ us_tx_suta_etia_rate
+ 0.1
+
+
+
+ US TX Texas Employment & Training Investment Assessment Rate
+ us_tx_suta_etia_rate
+ 0.1
+
+
+
+
+
+
+ US Texas - Workforce Commission (Unemployment)
+
+
+
+ US Texas - Workforce Commission (Unemployment)
+
+
+
+
+
+
+
+
+
+ ER: US TX Texas State Unemployment (C-3)
+ ER_US_TX_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_rate', state_code='TX')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_rate', state_code='TX')
+
+
+
+
+
+
+
+ ER: US TX Texas Obligation Assessment (C-3)
+ ER_US_TX_SUTA_OA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_oa_rate', state_code='TX')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_oa_rate', state_code='TX')
+
+
+
+
+
+
+
+ ER: US TX Texas Employment & Training Investment Assessment (C-3)
+ ER_US_TX_SUTA_ETIA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_etia_rate', state_code='TX')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_tx_suta_wage_base', rate='us_tx_suta_etia_rate', state_code='TX')
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/va_virginia.xml b/l10n_us_hr_payroll/data/state/va_virginia.xml
new file mode 100644
index 00000000..a7e04b5b
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/va_virginia.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+ US VA Virginia SUTA Wage Base
+ us_va_suta_wage_base
+ 8000.0
+
+
+
+ US VA Virginia SUTA Wage Base
+ us_va_suta_wage_base
+ 8000.0
+
+
+
+
+
+
+
+ US VA Virginia SUTA Rate
+ us_va_suta_rate
+ 2.51
+
+
+
+ US VA Virginia SUTA Rate
+ us_va_suta_rate
+ 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 - Unemployment Tax
+
+
+
+
+ US Virginia - Department of Taxation - Income Tax
+
+
+
+ US Virginia - Department of Taxation - Income Tax
+
+
+
+
+
+
+
+
+
+ ER: US VA Virginia State Unemployment
+ ER_US_VA_SUTA
+ python
+ result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_va_suta_wage_base', rate='us_va_suta_rate', state_code='VA')
+ code
+ result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_va_suta_wage_base', rate='us_va_suta_rate', state_code='VA')
+
+
+
+
+
+
+
+ EE: US VA Virginia State Income Tax Withholding
+ EE_US_VA_SIT
+ python
+ result, _ = va_virginia_state_income_withholding(payslip, categories, worked_days, inputs)
+ code
+ result, result_rate = va_virginia_state_income_withholding(payslip, categories, worked_days, inputs)
+
+
+
+
+
\ No newline at end of file
diff --git a/l10n_us_hr_payroll/data/state/wa_washington.xml b/l10n_us_hr_payroll/data/state/wa_washington.xml
new file mode 100644
index 00000000..52e0b736
--- /dev/null
+++ b/l10n_us_hr_payroll/data/state/wa_washington.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+ US WA Washington SUTA Wage Base
+ us_wa_suta_wage_base
+ 49800.0
+
+
+
+ US WA Washington SUTA Wage Base
+ us_wa_suta_wage_base
+ 52700.00
+
+
+
+
+
+
+ US WA Washington FML Wage Base
+ us_wa_fml_wage_base
+ 132900.00
+
+
+
+ US WA Washington FML Wage Base
+ us_wa_fml_wage_base
+ 137700.00
+
+
+
+
+
+
+
+ US WA Washington SUTA Rate
+ us_wa_suta_rate
+ 1.18
+
+
+
+ US WA Washington SUTA Rate
+ us_wa_suta_rate
+ 1.0
+
+
+
+
+
+
+ US WA Washington FML Rate (Total)
+ us_wa_fml_rate
+ 0.4
+
+
+
+ US WA Washington FML Rate (Total)
+ us_wa_fml_rate
+ 0.4
+
+
+
+
+
+
+ US WA Washington FML Rate (Employee)
+ us_wa_fml_rate_ee
+ 66.33
+
+
+
+ US WA Washington FML Rate (Employee)
+ us_wa_fml_rate_ee
+ 66.33
+
+
+
+
+
+
+ US WA Washington FML Rate (Employer)
+ us_wa_fml_rate_er
+ 33.67
+
+
+
+ US WA Washington FML Rate (Employer)
+ us_wa_fml_rate_er
+ 33.67
+
+
+
+
+
+
+ US Washington - Employment Security Department (Unemployment)
+
+
+
+ US Washington - Employment Security Department (Unemployment)
+
+
+
+
+ US Washington - Department of Labor & Industries
+
+
+
+ US Washington - Department of Labor & Industries
+
+
+
+
+ US Washington - Employment Security Department (PFML)
+
+
+
+ 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.dict.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_er_code'))
+ code
+
+hours = worked_days.WORK100.number_of_hours
+rate = payslip.dict.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.dict.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.dict.rule_parameter(payslip.contract_id.us_payroll_config_value('workers_comp_ee_code'))
+ code
+ result, result_rate = worked_days.WORK100.number_of_hours, -payslip.dict.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/migrations/12.0.2020.1.0/post-migration.py b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py
new file mode 100644
index 00000000..3f4499ba
--- /dev/null
+++ b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py
@@ -0,0 +1,85 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from odoo.addons.l10n_us_hr_payroll.migrations.data import FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020, \
+ XMLIDS_COPY_ACCOUNTING_2020
+from odoo.addons.l10n_us_hr_payroll.migrations.helper import field_exists, \
+ temp_field_exists, \
+ remove_temp_field, \
+ temp_field_values
+
+
+from odoo import SUPERUSER_ID
+from odoo.api import Environment
+
+
+import logging
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, installed_version):
+ fields_to_move = [f for f in FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 if temp_field_exists(cr, 'hr_contract', f)]
+ if not fields_to_move:
+ _logger.warn(' migration aborted because no temporary fields exist...')
+ return
+
+ env = Environment(cr, SUPERUSER_ID, {})
+ new_structure = env.ref('l10n_us_hr_payroll.structure_type_employee')
+
+ def get_state(code, cache={}):
+ country_key = 'US_COUNTRY'
+ if code in cache:
+ return cache[code]
+ if country_key not in cache:
+ cache[country_key] = env.ref('base.us')
+ us_country = cache[country_key]
+ us_state = env['res.country.state'].search([
+ ('country_id', '=', us_country.id),
+ ('code', '=', code),
+ ], limit=1)
+ cache[code] = us_state
+ return us_state
+
+ # We will assume all contracts without a struct (because we deleted it), or with one like US_xx_EMP, need config
+ contracts = env['hr.contract'].search([
+ '|',
+ ('struct_id', '=', False),
+ ('struct_id.code', '=like', 'US_%'),
+ ])
+ _logger.warn('Migrating Contracts: ' + str(contracts))
+ for contract in contracts:
+ _logger.warn('Migrating contract: ' + str(contract) + ' for employee: ' + str(contract.employee_id))
+ # Could we somehow detect the state off of the current/orphaned salary structure?
+ state_code = False
+ old_struct_code = contract.struct_id.code
+ if old_struct_code:
+ state_code = old_struct_code.split('_')[1]
+ temp_values = temp_field_values(cr, 'hr_contract', contract.id, fields_to_move)
+ # Resolve mapping to the new field names.
+ values = {FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020[k]: v for k, v in temp_values.items()}
+ values.update({
+ 'name': 'MIG: ' + str(contract.name),
+ 'employee_id': contract.employee_id.id,
+ 'state_id': get_state(state_code).id,
+ })
+ us_payroll_config = env['hr.contract.us_payroll_config'].create(values)
+ contract.write({
+ 'struct_id': new_structure.id,
+ 'us_payroll_config_id': us_payroll_config.id,
+ })
+
+ for field in fields_to_move:
+ remove_temp_field(cr, 'hr_contract', field)
+
+ # Some added rules should have the same accounting side effects of other migrated rules
+ # To ease the transition, we will copy the accounting fields from one to the other.
+ for source, destinations in XMLIDS_COPY_ACCOUNTING_2020.items():
+ source_rule = env.ref(source, raise_if_not_found=False)
+ if source_rule:
+ for destination in destinations:
+ destination_rule = env.ref(destination, raise_if_not_found=False)
+ if destination_rule:
+ _logger.warn('Mirgrating accounting from rule: ' + source + ' to rule: ' + destination)
+ destination_rule.write({
+ 'account_debit': source_rule.account_debit.id,
+ 'account_credit': source_rule.account_credit.id,
+ })
diff --git a/l10n_us_hr_payroll/migrations/12.0.2020.1.0/pre-migration.py b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/pre-migration.py
new file mode 100644
index 00000000..420181e4
--- /dev/null
+++ b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/pre-migration.py
@@ -0,0 +1,29 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from odoo.addons.l10n_us_hr_payroll.migrations.data import FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020, \
+ XMLIDS_TO_REMOVE_2020, \
+ XMLIDS_TO_RENAME_2020
+from odoo.addons.l10n_us_hr_payroll.migrations.helper import field_exists, \
+ temp_field_exists, \
+ make_temp_field, \
+ remove_xmlid, \
+ rename_xmlid
+
+
+def migrate(cr, installed_version):
+ # Add temporary columns for all hr_contract fields that move to hr_contract_us_payroll_config
+ fields_to_move = [f for f in FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 if field_exists(cr, 'hr_contract', f)]
+ # Prevent error if repeatedly running and already copied.
+ fields_to_move = [f for f in fields_to_move if not temp_field_exists(cr, 'hr_contract', f)]
+ for field in fields_to_move:
+ make_temp_field(cr, 'hr_contract', field)
+
+ # Need to migrate XMLIDs..
+ for xmlid in XMLIDS_TO_REMOVE_2020:
+ remove_xmlid(cr, xmlid)
+
+ for from_xmlid, to_xmlid in XMLIDS_TO_RENAME_2020.items():
+ rename_xmlid(cr, from_xmlid, to_xmlid)
+
+ # Need to remove views as they don't work anymore.
+ cr.execute("DELETE FROM ir_ui_view as v WHERE v.id in (SELECT t.res_id FROM ir_model_data as t WHERE t.model = 'ir.ui.view' and (t.module = 'l10n_us_hr_payroll' or t.module like 'l10n_us_%_hr_payroll'))")
diff --git a/l10n_us_hr_payroll/migrations/__init__.py b/l10n_us_hr_payroll/migrations/__init__.py
new file mode 100644
index 00000000..0358305d
--- /dev/null
+++ b/l10n_us_hr_payroll/migrations/__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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py
new file mode 100644
index 00000000..d6dd22b0
--- /dev/null
+++ b/l10n_us_hr_payroll/migrations/data.py
@@ -0,0 +1,191 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = {
+ # Federal
+ 'w4_allowances': 'fed_941_fit_w4_allowances',
+ 'w4_filing_status': 'fed_941_fit_w4_filing_status',
+ 'w4_is_nonresident_alien': 'fed_941_fit_w4_is_nonresident_alien',
+ 'w4_additional_withholding': 'fed_941_fit_w4_additional_withholding',
+ 'fica_exempt': 'fed_941_fica_exempt',
+ 'futa_type': 'fed_940_type',
+ # State
+ 'ga_g4_filing_status': 'ga_g4_sit_filing_status',
+ 'ga_g4_dependent_allowances': 'ga_g4_sit_dependent_allowances',
+ 'ga_g4_additional_allowances': 'ga_g4_sit_additional_allowances',
+ 'ga_g4_additional_wh': 'state_income_tax_additional_withholding',
+
+ 'ms_89_350_filing_status': 'ms_89_350_sit_filing_status',
+ 'ms_89_350_exemption': 'ms_89_350_sit_exemption_value',
+ 'ms_89_350_additional_withholding': 'state_income_tax_additional_withholding',
+
+ 'mt_mw4_additional_withholding': 'state_income_tax_additional_withholding',
+ 'mt_mw4_exemptions': 'mt_mw4_sit_exemptions',
+ 'mt_mw4_exempt': 'mt_mw4_sit_exempt',
+
+ 'oh_additional_withholding': 'state_income_tax_additional_withholding',
+ 'oh_income_allowances': 'oh_it4_sit_exemptions',
+
+ 'pa_additional_withholding': 'state_income_tax_additional_withholding',
+
+ 'va_va4_exemptions': 'va_va4_sit_exemptions',
+
+}
+
+XMLIDS_TO_REMOVE_2020 = [
+ # Federal
+ # Categories -- These are now all up in the EE FICA or ER FICA
+ 'l10n_us_hr_payroll.hr_payroll_fica_emp_m',
+ 'l10n_us_hr_payroll.hr_payroll_fica_emp_m_add',
+ 'l10n_us_hr_payroll.hr_payroll_fica_emp_m_add_wages',
+ 'l10n_us_hr_payroll.hr_payroll_fica_comp_m',
+ 'l10n_us_hr_payroll.hr_payroll_futa_wages',
+ 'l10n_us_hr_payroll.hr_payroll_fica_emp_m_wages',
+ 'l10n_us_hr_payroll.hr_payroll_fica_emp_ss_wages',
+ # Rules -- These are mainly Wage rules or were simplified to a single rule
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_emp_ss_wages_2018',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_emp_m_wages_2018',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_emp_m_add_wages_2018',
+ 'l10n_us_hr_payroll.hr_payroll_rules_futa_wages_2018',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fed_inc_withhold_2018_married',
+ # State
+ 'l10n_us_fl_hr_payroll.hr_payroll_fl_unemp_wages',
+ 'l10n_us_fl_hr_payroll.hr_payroll_fl_unemp',
+ 'l10n_us_fl_hr_payroll.hr_payroll_rules_fl_unemp_wages_2018',
+
+ 'l10n_us_ga_hr_payroll.hr_payroll_ga_unemp_wages',
+ 'l10n_us_ga_hr_payroll.hr_payroll_ga_unemp',
+ 'l10n_us_ga_hr_payroll.hr_payroll_ga_income_withhold',
+ 'l10n_us_ga_hr_payroll.hr_payroll_rules_ga_unemp_wages',
+
+ 'l10n_us_ms_hr_payroll.hr_payroll_ms_unemp_wages',
+ 'l10n_us_ms_hr_payroll.hr_payroll_ms_unemp',
+ 'l10n_us_ms_hr_payroll.hr_payroll_ms_income_withhold',
+ 'l10n_us_ms_hr_payroll.hr_payroll_rules_ms_unemp_wages',
+
+ 'l10n_us_mt_hr_payroll.hr_payroll_mt_unemp_wages',
+ 'l10n_us_mt_hr_payroll.hr_payroll_mt_unemp',
+ 'l10n_us_mt_hr_payroll.hr_payroll_mt_income_withhold',
+ 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_unemp_wages',
+
+ 'l10n_us_oh_hr_payroll.hr_payroll_oh_unemp_wages',
+ 'l10n_us_oh_hr_payroll.hr_payroll_oh_unemp',
+ 'l10n_us_oh_hr_payroll.hr_payroll_oh_income_withhold',
+ 'l10n_us_oh_hr_payroll.hr_payroll_rules_oh_unemp_wages_2018',
+
+ 'l10n_us_pa_hr_payroll.res_partner_pador_unemp_employee',
+ 'l10n_us_pa_hr_payroll.contrib_register_pador_unemp_employee',
+ 'l10n_us_pa_hr_payroll.hr_payroll_pa_unemp_wages',
+ 'l10n_us_pa_hr_payroll.hr_payroll_pa_unemp_employee',
+ 'l10n_us_pa_hr_payroll.hr_payroll_pa_unemp_company',
+ 'l10n_us_pa_hr_payroll.hr_payroll_pa_withhold',
+ 'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_unemp_wages_2018',
+ 'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_inc_withhold_add',
+
+ 'l10n_us_tx_hr_payroll.contrib_register_txdor',
+ 'l10n_us_tx_hr_payroll.hr_payroll_tx_unemp_wages',
+ 'l10n_us_tx_hr_payroll.hr_payroll_tx_unemp',
+ 'l10n_us_tx_hr_payroll.hr_payroll_tx_oa',
+ 'l10n_us_tx_hr_payroll.hr_payroll_tx_etia',
+ 'l10n_us_tx_hr_payroll.hr_payroll_rules_tx_unemp_wages_2018',
+
+ 'l10n_us_va_hr_payroll.hr_payroll_va_unemp_wages',
+ 'l10n_us_va_hr_payroll.hr_payroll_va_unemp',
+ 'l10n_us_va_hr_payroll.hr_payroll_va_income_withhold',
+ 'l10n_us_va_hr_payroll.hr_payroll_rules_va_unemp_wages_2018',
+
+ 'l10n_us_wa_hr_payroll.hr_payroll_wa_unemp_wages',
+ 'l10n_us_wa_hr_payroll.hr_payroll_wa_unemp',
+ 'l10n_us_wa_hr_payroll.hr_payroll_wa_lni',
+ 'l10n_us_wa_hr_payroll.hr_payroll_wa_lni_withhold',
+ 'l10n_us_wa_hr_payroll.hr_payroll_rules_wa_unemp_wages_2018',
+
+]
+
+XMLIDS_TO_RENAME_2020 = {
+ # Federal
+ 'l10n_us_hr_payroll.hr_payroll_futa': 'l10n_us_hr_payroll.hr_payroll_category_er_fed_940',
+ 'l10n_us_hr_payroll.hr_payroll_fica_emp_ss': 'l10n_us_hr_payroll.hr_payroll_category_ee_fed_941',
+ 'l10n_us_hr_payroll.hr_payroll_fed_income_withhold': 'l10n_us_hr_payroll.hr_payroll_category_ee_fed_941_fit',
+ 'l10n_us_hr_payroll.hr_payroll_fica_comp_ss': 'l10n_us_hr_payroll.hr_payroll_category_er_fed_941',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_emp_ss_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_fed_941_ss',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_emp_m_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_fed_941_m',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_emp_m_add_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_fed_941_m_add',
+ 'l10n_us_hr_payroll.hr_payroll_rules_futa_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_fed_940',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_comp_ss': 'l10n_us_hr_payroll.hr_payroll_rule_er_fed_941_ss',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fica_comp_m': 'l10n_us_hr_payroll.hr_payroll_rule_er_fed_941_m',
+ 'l10n_us_hr_payroll.hr_payroll_rules_fed_inc_withhold_2018_single': 'l10n_us_hr_payroll.hr_payroll_rule_ee_fed_941_fit',
+ # State
+ 'l10n_us_fl_hr_payroll.hr_payroll_rules_fl_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_fl_suta',
+ 'l10n_us_fl_hr_payroll.res_partner_fldor': 'l10n_us_hr_payroll.res_partner_us_fl_dor',
+ 'l10n_us_fl_hr_payroll.contrib_register_fldor': 'l10n_us_hr_payroll.contrib_register_us_fl_dor',
+
+ 'l10n_us_ga_hr_payroll.res_partner_ga_dol_unemp': 'l10n_us_hr_payroll.res_partner_us_ga_dor',
+ 'l10n_us_ga_hr_payroll.res_partner_ga_dor_withhold': 'l10n_us_hr_payroll.res_partner_us_ga_dor_sit',
+ 'l10n_us_ga_hr_payroll.contrib_register_ga_dol_unemp': 'l10n_us_hr_payroll.contrib_register_us_ga_dor',
+ 'l10n_us_ga_hr_payroll.contrib_register_ga_dor_withhold': 'l10n_us_hr_payroll.contrib_register_us_ga_dor_sit',
+ 'l10n_us_ga_hr_payroll.hr_payroll_rules_ga_unemp': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_ga_suta',
+ 'l10n_us_ga_hr_payroll.hr_payroll_rules_ga_inc_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_ga_sit',
+
+ 'l10n_us_ms_hr_payroll.res_partner_msdor_unemp': 'l10n_us_hr_payroll.res_partner_us_ms_dor',
+ 'l10n_us_ms_hr_payroll.res_partner_msdor_withhold': 'l10n_us_hr_payroll.res_partner_us_ms_dor_sit',
+ 'l10n_us_ms_hr_payroll.contrib_register_msdor_unemp': 'l10n_us_hr_payroll.contrib_register_us_ms_dor',
+ 'l10n_us_ms_hr_payroll.contrib_register_msdor_withhold': 'l10n_us_hr_payroll.contrib_register_us_ms_dor_sit',
+ 'l10n_us_ms_hr_payroll.hr_payroll_rules_ms_unemp': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_ms_suta',
+ 'l10n_us_ms_hr_payroll.hr_payroll_rules_ms_inc_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_ms_sit',
+
+ 'l10n_us_mt_hr_payroll.res_partner_mtdor_unemp': 'l10n_us_hr_payroll.res_partner_us_mt_dor',
+ 'l10n_us_mt_hr_payroll.res_partner_mtdor_withhold': 'l10n_us_hr_payroll.res_partner_us_mt_dor_sit',
+ 'l10n_us_mt_hr_payroll.contrib_register_mtdor_unemp': 'l10n_us_hr_payroll.contrib_register_us_mt_dor',
+ 'l10n_us_mt_hr_payroll.contrib_register_mtdor_withhold': 'l10n_us_hr_payroll.contrib_register_us_mt_dor_sit',
+ 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_unemp': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta',
+ 'l10n_us_mt_hr_payroll.hr_payroll_rules_mt_inc_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_mt_sit',
+
+ 'l10n_us_oh_hr_payroll.res_partner_ohdor_unemp': 'l10n_us_hr_payroll.res_partner_us_oh_dor',
+ 'l10n_us_oh_hr_payroll.res_partner_ohdor_withhold': 'l10n_us_hr_payroll.res_partner_us_oh_dor_sit',
+ 'l10n_us_oh_hr_payroll.res_partner_ohdor_unemp': 'l10n_us_hr_payroll.res_partner_us_oh_dor',
+ 'l10n_us_oh_hr_payroll.res_partner_ohdor_withhold': 'l10n_us_hr_payroll.res_partner_us_oh_dor_sit',
+ 'l10n_us_oh_hr_payroll.hr_payroll_rules_oh_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_oh_suta',
+ 'l10n_us_oh_hr_payroll.hr_payroll_rules_oh_inc_withhold_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_oh_sit',
+
+ 'l10n_us_pa_hr_payroll.res_partner_pador_unemp_company': 'l10n_us_hr_payroll.res_partner_us_pa_dor',
+ 'l10n_us_pa_hr_payroll.res_partner_pador_withhold': 'l10n_us_hr_payroll.res_partner_us_pa_dor_sit',
+ 'l10n_us_pa_hr_payroll.contrib_register_pador_unemp_company': 'l10n_us_hr_payroll.contrib_register_us_pa_dor',
+ 'l10n_us_pa_hr_payroll.contrib_register_pador_withhold': 'l10n_us_hr_payroll.contrib_register_us_pa_dor_sit',
+ 'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_unemp_employee_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_pa_suta',
+ 'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_unemp_company_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_pa_suta',
+ 'l10n_us_pa_hr_payroll.hr_payroll_rules_pa_inc_withhold_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_pa_sit',
+
+ 'l10n_us_tx_hr_payroll.res_partner_txdor': 'l10n_us_hr_payroll.res_partner_us_tx_dor',
+ 'l10n_us_tx_hr_payroll.contrib_register_txdor': 'l10n_us_hr_payroll.contrib_register_us_tx_dor',
+ 'l10n_us_tx_hr_payroll.hr_payroll_rules_tx_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_tx_suta',
+ 'l10n_us_tx_hr_payroll.hr_payroll_rules_tx_oa_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_tx_suta_oa',
+ 'l10n_us_tx_hr_payroll.hr_payroll_rules_tx_etia_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_tx_suta_etia',
+
+ 'l10n_us_va_hr_payroll.res_partner_vador_unemp': 'l10n_us_hr_payroll.res_partner_us_va_dor',
+ 'l10n_us_va_hr_payroll.res_partner_vador_withhold': 'l10n_us_hr_payroll.res_partner_us_va_dor_sit',
+ 'l10n_us_va_hr_payroll.contrib_register_vador_unemp': 'l10n_us_hr_payroll.contrib_register_us_va_dor',
+ 'l10n_us_va_hr_payroll.contrib_register_vador_withhold': 'l10n_us_hr_payroll.contrib_register_us_va_dor_sit',
+ 'l10n_us_va_hr_payroll.hr_payroll_rules_va_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_va_suta',
+ 'l10n_us_va_hr_payroll.hr_payroll_rules_va_inc_withhold_2018': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_va_sit',
+
+ 'l10n_us_wa_hr_payroll.res_partner_wador_unemp': 'l10n_us_hr_payroll.res_partner_us_wa_dor',
+ 'l10n_us_wa_hr_payroll.res_partner_wador_lni': 'l10n_us_hr_payroll.res_partner_us_wa_dor_lni',
+ 'l10n_us_wa_hr_payroll.contrib_register_wador_unemp': 'l10n_us_hr_payroll.contrib_register_us_wa_dor',
+ 'l10n_us_wa_hr_payroll.contrib_register_wador_lni': 'l10n_us_hr_payroll.contrib_register_us_wa_dor_lni',
+ 'l10n_us_wa_hr_payroll.hr_payroll_rules_wa_unemp_2018': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_wa_suta',
+ 'l10n_us_wa_hr_payroll.hr_payroll_rules_wa_lni_withhold': 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_wa_lni',
+ 'l10n_us_wa_hr_payroll.hr_payroll_rules_wa_lni': 'l10n_us_hr_payroll.hr_payroll_rule_er_us_wa_lni',
+
+}
+
+XMLIDS_COPY_ACCOUNTING_2020 = {
+ 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta': [
+ 'l10n_us_hr_payroll.hr_payroll_rule_er_us_mt_suta_aft',
+ ],
+ 'l10n_us_hr_payroll.hr_payroll_rule_er_us_wa_lni': [
+ 'l10n_us_hr_payroll.hr_payroll_rule_er_us_wa_fml',
+ ],
+ 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_wa_lni': [
+ 'l10n_us_hr_payroll.hr_payroll_rule_ee_us_wa_fml',
+ ],
+}
diff --git a/l10n_us_hr_payroll/migrations/helper.py b/l10n_us_hr_payroll/migrations/helper.py
new file mode 100644
index 00000000..f5a7a87d
--- /dev/null
+++ b/l10n_us_hr_payroll/migrations/helper.py
@@ -0,0 +1,63 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+TMP_PREFIX = 'tmp_'
+
+"""
+Fields
+"""
+
+
+def field_exists(cr, table_name, field_name):
+ cr.execute('SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name=%s and column_name=%s);', (table_name, field_name))
+ return cr.fetchone()[0]
+
+
+def temp_field_exists(cr, table_name, field_name):
+ tmp_field_name = TMP_PREFIX + field_name
+ cr.execute('SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name=%s and column_name=%s);', (table_name, tmp_field_name))
+ return cr.fetchone()[0]
+
+
+def make_temp_field(cr, table_name, field_name):
+ tmp_field_name = TMP_PREFIX + field_name
+ cr.execute('SELECT data_type FROM information_schema.columns WHERE table_name=%s and column_name=%s;', (table_name, field_name))
+ tmp_field_type = cr.fetchone()[0]
+ cr.execute('ALTER TABLE ' + table_name + ' ADD ' + tmp_field_name + ' ' + tmp_field_type)
+ cr.execute('UPDATE ' + table_name + ' SET ' + tmp_field_name + '=' + field_name)
+
+
+def remove_temp_field(cr, table_name, field_name):
+ tmp_field_name = TMP_PREFIX + field_name
+ cr.execute('ALTER TABLE ' + table_name + ' DROP COLUMN ' + tmp_field_name)
+
+
+def temp_field_values(cr, table_name, id, field_names):
+ tmp_field_names = [TMP_PREFIX + f for f in field_names]
+ if not tmp_field_names:
+ return {}
+ cr.execute('SELECT ' + ', '.join(tmp_field_names) + ' FROM ' + table_name + ' WHERE id=' + str(id))
+ values = cr.dictfetchone()
+ if not values:
+ return {}
+
+ def _remove_tmp_prefix(key):
+ if key.startswith(TMP_PREFIX):
+ return key[len(TMP_PREFIX):]
+ return key
+ return {_remove_tmp_prefix(k): v for k, v in values.items()}
+
+
+"""
+XMLIDs
+"""
+
+
+def remove_xmlid(cr, xmlid):
+ module, name = xmlid.split('.')
+ cr.execute('DELETE FROM ir_model_data WHERE module=%s and name=%s;', (module, name))
+
+
+def rename_xmlid(cr, from_xmlid, to_xmlid):
+ from_module, from_name = from_xmlid.split('.')
+ to_module, to_name = to_xmlid.split('.')
+ cr.execute('UPDATE ir_model_data SET module=%s, name=%s WHERE module=%s and name=%s;', (to_module, to_name, from_module, from_name))
diff --git a/l10n_us_hr_payroll/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py
index ff687165..c208ca19 100644
--- a/l10n_us_hr_payroll/models/__init__.py
+++ b/l10n_us_hr_payroll/models/__init__.py
@@ -1 +1,6 @@
-from . import l10n_us_hr_payroll
+# 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 hr_salary_rule
+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..ed475a75
--- /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.dict.contract_id.futa_type == payslip.dict.contract_id.FUTA_TYPE_EXEMPT:
+ # Exit early
+ return 0.0, 0.0
+ elif payslip.dict.contract_id.futa_type == payslip.dict.contract_id.FUTA_TYPE_BASIC:
+ result_rate = -payslip.dict.rule_parameter('fed_940_futa_rate_basic')
+ else:
+ result_rate = -payslip.dict.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.dict.contract_id.external_wages
+
+ wage_base = payslip.dict.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..7a0916ec
--- /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.dict.contract_id.us_payroll_config_value('fed_941_fica_exempt')
+ if exempt:
+ return 0.0, 0.0
+
+ # Determine Rate.
+ result_rate = -payslip.dict.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.dict.contract_id.external_wages
+
+ wage_base = payslip.dict.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.dict.contract_id.us_payroll_config_value('fed_941_fica_exempt')
+ if exempt:
+ return 0.0, 0.0
+
+ # Determine Rate.
+ result_rate = -payslip.dict.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.dict.contract_id.external_wages
+
+ wage_base = float(payslip.dict.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.dict.contract_id.us_payroll_config_value('fed_941_fica_exempt')
+ if exempt:
+ return 0.0, 0.0
+
+ # Determine Rate.
+ result_rate = -payslip.dict.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.dict.contract_id.external_wages
+
+ wage_start = payslip.dict.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.dict.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status')
+ if not filing_status:
+ return 0.0, 0.0
+
+ schedule_pay = payslip.dict.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.dict.contract_id.us_payroll_config_value('fed_941_fit_w4_is_nonresident_alien')
+ if is_nra:
+ nra_table = payslip.dict.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.dict.contract_id.us_payroll_config_value('fed_941_fit_w4_other_income')
+ #_logger.warn(' after other income: ' + str(wage_annual))
+
+ deductions = payslip.dict.contract_id.us_payroll_config_value('fed_941_fit_w4_deductions')
+ #_logger.warn('deductions from W4: ' + str(deductions))
+
+ higher_rate_type = payslip.dict.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.dict.rule_parameter('fed_941_fit_table_single')
+ elif filing_status == 'married':
+ tax_tables = payslip.dict.rule_parameter('fed_941_fit_table_married')
+ else:
+ # married_as_single for historic reasons
+ tax_tables = payslip.dict.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.dict.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.dict.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.dict.contract_id.us_payroll_config_value('fed_941_fit_w4_is_nonresident_alien')
+ if is_nra:
+ nra_table = payslip.dict.rule_parameter('fed_941_fit_nra_additional')
+ working_wage += nra_table[schedule_pay]
+
+ allowance_table = payslip.dict.rule_parameter('fed_941_fit_allowance')
+ allowances = payslip.dict.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.dict.rule_parameter('fed_941_fit_table_married')
+ else:
+ tax_table = payslip.dict.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.dict.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..ce3e6de9
--- /dev/null
+++ b/l10n_us_hr_payroll/models/hr_contract.py
@@ -0,0 +1,24 @@
+# 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 USHRContract(models.Model):
+ _inherit = 'hr.contract'
+
+ FUTA_TYPE_NORMAL = FUTA_TYPE_NORMAL
+ FUTA_TYPE_BASIC = FUTA_TYPE_BASIC
+ FUTA_TYPE_EXEMPT = FUTA_TYPE_EXEMPT
+
+ schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')])
+ 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..cdcb7208
--- /dev/null
+++ b/l10n_us_hr_payroll/models/hr_payslip.py
@@ -0,0 +1,235 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.tools.safe_eval import safe_eval
+from odoo.exceptions import UserError
+
+from .federal.fed_940 import er_us_940_futa
+from .federal.fed_941 import ee_us_941_fica_ss, \
+ ee_us_941_fica_m, \
+ ee_us_941_fica_m_add,\
+ er_us_941_fica_ss, \
+ er_us_941_fica_m, \
+ ee_us_941_fit
+from .state.general import general_state_unemployment, \
+ general_state_income_withholding, \
+ is_us_state
+from .state.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
+from .state.wa_washington import wa_washington_fml_er, \
+ wa_washington_fml_ee
+
+
+class HRPayslip(models.Model):
+ _inherit = 'hr.payslip'
+
+ # From IRS Publication 15-T or logically (annually, bi-monthly)
+ PAY_PERIODS_IN_YEAR = {
+ 'annually': 1,
+ 'semi-annually': 2,
+ 'quarterly': 4,
+ 'bi-monthly': 6,
+ 'monthly': 12,
+ 'semi-monthly': 24,
+ 'bi-weekly': 26,
+ 'weekly': 52,
+ 'daily': 260,
+ }
+
+ def _get_base_local_dict(self):
+ # back port for US Payroll
+ #res = super()._get_base_local_dict()
+ return {
+ 'er_us_940_futa': er_us_940_futa,
+ 'ee_us_941_fica_ss': ee_us_941_fica_ss,
+ 'ee_us_941_fica_m': ee_us_941_fica_m,
+ 'ee_us_941_fica_m_add': ee_us_941_fica_m_add,
+ 'er_us_941_fica_ss': er_us_941_fica_ss,
+ 'er_us_941_fica_m': er_us_941_fica_m,
+ 'ee_us_941_fit': ee_us_941_fit,
+ 'general_state_unemployment': general_state_unemployment,
+ 'general_state_income_withholding': general_state_income_withholding,
+ 'is_us_state': is_us_state,
+ '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,
+ 'wa_washington_fml_er': wa_washington_fml_er,
+ 'wa_washington_fml_ee': wa_washington_fml_ee,
+ }
+
+ 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)
+
+ @api.model
+ def _get_payslip_lines(self, contract_ids, payslip_id):
+ def _sum_salary_rule_category(localdict, category, amount):
+ if category.parent_id:
+ localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
+ localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
+ return localdict
+
+ class BrowsableObject(object):
+ def __init__(self, employee_id, dict, env):
+ self.employee_id = employee_id
+ self.dict = dict
+ self.env = env
+
+ def __getattr__(self, attr):
+ return attr in self.dict and self.dict.__getitem__(attr) or 0.0
+
+ class InputLine(BrowsableObject):
+ """a class that will be used into the python code, mainly for usability purposes"""
+ def sum(self, code, from_date, to_date=None):
+ if to_date is None:
+ to_date = fields.Date.today()
+ self.env.cr.execute("""
+ 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.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""",
+ (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 _sum(self, code, from_date, to_date=None):
+ if to_date is None:
+ to_date = fields.Date.today()
+ self.env.cr.execute("""
+ 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.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""",
+ (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 sum(self, code, from_date, to_date=None):
+ if to_date is None:
+ to_date = fields.Date.today()
+ self.env.cr.execute("""SELECT sum(case when hp.credit_note = False 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.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""",
+ (self.employee_id, from_date, to_date, code))
+ res = self.env.cr.fetchone()
+ return res and res[0] or 0.0
+
+ def sum_category(self, code, from_date, to_date=None):
+ # Hibou Backport
+ if to_date is None:
+ to_date = fields.Date.today()
+
+ self.env.cr.execute("""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.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id
+ AND rc.id = pl.category_id AND rc.code = %s""",
+ (self.employee_id, from_date, to_date, code))
+ res = self.env.cr.fetchone()
+ return res and res[0] or 0.0
+
+ #we keep a dict with the result because a value can be overwritten by another rule with the same code
+ result_dict = {}
+ rules_dict = {}
+ worked_days_dict = {}
+ inputs_dict = {}
+ blacklist = []
+ payslip = self.env['hr.payslip'].browse(payslip_id)
+ for worked_days_line in payslip.worked_days_line_ids:
+ worked_days_dict[worked_days_line.code] = worked_days_line
+ for input_line in payslip.input_line_ids:
+ inputs_dict[input_line.code] = input_line
+
+ categories = BrowsableObject(payslip.employee_id.id, {}, self.env)
+ inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env)
+ worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env)
+ payslips = Payslips(payslip.employee_id.id, payslip, self.env)
+ rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env)
+
+ baselocaldict = {'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, 'inputs': inputs}
+
+ # Hibou Backport
+ baselocaldict.update(self._get_base_local_dict())
+
+ #get the ids of the structures on the contracts and their parent id as well
+ contracts = self.env['hr.contract'].browse(contract_ids)
+ if len(contracts) == 1 and payslip.struct_id:
+ structure_ids = list(set(payslip.struct_id._get_parent_structure().ids))
+ else:
+ structure_ids = contracts.get_all_structures()
+ #get the rules of the structure and thier children
+ rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules()
+ #run the rules by sequence
+ sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
+ sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids)
+
+ for contract in contracts:
+ employee = contract.employee_id
+ localdict = dict(baselocaldict, employee=employee, contract=contract)
+ for rule in sorted_rules:
+ key = rule.code + '-' + str(contract.id)
+ localdict['result'] = None
+ localdict['result_qty'] = 1.0
+ localdict['result_rate'] = 100
+ #check if the rule can be applied
+ if rule._satisfy_condition(localdict) and rule.id not in blacklist:
+ #compute the amount of the rule
+ amount, qty, rate = rule._compute_rule(localdict)
+ #check if there is already a rule computed with that code
+ previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
+ #set/overwrite the amount computed for this rule in the localdict
+ tot_rule = amount * qty * rate / 100.0
+ localdict[rule.code] = tot_rule
+ rules_dict[rule.code] = rule
+ #sum the amount for its salary category
+ localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
+ #create/overwrite the rule in the temporary results
+ result_dict[key] = {
+ 'salary_rule_id': rule.id,
+ 'contract_id': contract.id,
+ 'name': rule.name,
+ 'code': rule.code,
+ 'category_id': rule.category_id.id,
+ 'sequence': rule.sequence,
+ 'appears_on_payslip': rule.appears_on_payslip,
+ 'condition_select': rule.condition_select,
+ 'condition_python': rule.condition_python,
+ 'condition_range': rule.condition_range,
+ 'condition_range_min': rule.condition_range_min,
+ 'condition_range_max': rule.condition_range_max,
+ 'amount_select': rule.amount_select,
+ 'amount_fix': rule.amount_fix,
+ 'amount_python_compute': rule.amount_python_compute,
+ 'amount_percentage': rule.amount_percentage,
+ 'amount_percentage_base': rule.amount_percentage_base,
+ 'register_id': rule.register_id.id,
+ 'amount': amount,
+ 'employee_id': contract.employee_id.id,
+ 'quantity': qty,
+ 'rate': rate,
+ }
+ else:
+ #blacklist this rule and its children
+ blacklist += [id for id, seq in rule._recursive_search_of_rules()]
+
+ return list(result_dict.values())
diff --git a/l10n_us_hr_payroll/models/hr_salary_rule.py b/l10n_us_hr_payroll/models/hr_salary_rule.py
new file mode 100644
index 00000000..f2d1dec8
--- /dev/null
+++ b/l10n_us_hr_payroll/models/hr_salary_rule.py
@@ -0,0 +1,35 @@
+from odoo import api, fields, models
+from odoo.tools.safe_eval import safe_eval
+from odoo.exceptions import UserError
+
+
+class HRSalaryRule(models.Model):
+ _inherit = 'hr.salary.rule'
+
+ @api.multi
+ def _compute_rule(self, localdict):
+ """
+ :param localdict: dictionary containing the environement in which to compute the rule
+ :return: returns a tuple build as the base/amount computed, the quantity and the rate
+ :rtype: (float, float, float)
+ """
+ self.ensure_one()
+ if self.amount_select == 'fix':
+ try:
+ return self.amount_fix, float(safe_eval(self.quantity, localdict)), 100.0
+ except:
+ raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code))
+ elif self.amount_select == 'percentage':
+ try:
+ return (float(safe_eval(self.amount_percentage_base, localdict)),
+ float(safe_eval(self.quantity, localdict)),
+ self.amount_percentage)
+ except:
+ raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (self.name, self.code))
+ else:
+ try:
+ safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True)
+ # Hibou Fix for setting 0.0 for result rate or result qty
+ return float(localdict['result']), localdict.get('result_qty', 1.0), localdict.get('result_rate', 100.0)
+ except:
+ raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code))
diff --git a/l10n_us_hr_payroll/models/l10n_us_hr_payroll.py b/l10n_us_hr_payroll/models/l10n_us_hr_payroll.py
deleted file mode 100755
index d7d4b4f6..00000000
--- a/l10n_us_hr_payroll/models/l10n_us_hr_payroll.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from odoo import models, fields, api
-
-
-class Payslip(models.Model):
- _inherit = 'hr.payslip'
-
- def get_futa_rate(self, contract):
- self.ensure_one()
- if contract.futa_type == USHrContract.FUTA_TYPE_EXEMPT:
- rate = self.get_rate('US_FUTA_EXEMPT')
- elif contract.futa_type == USHrContract.FUTA_TYPE_NORMAL:
- rate = self.get_rate('US_FUTA_NORMAL')
- else:
- rate = self.get_rate('US_FUTA_BASIC')
- return rate
-
-
-class USHrContract(models.Model):
- FUTA_TYPE_EXEMPT = 'exempt'
- FUTA_TYPE_BASIC = 'basic'
- FUTA_TYPE_NORMAL = 'normal'
-
- _inherit = 'hr.contract'
-
- schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')])
- w4_allowances = fields.Integer(string='Federal W4 Allowances', default=0)
- w4_filing_status = fields.Selection([
- ('', 'Exempt'),
- ('single', 'Single'),
- ('married', 'Married'),
- ('married_as_single', 'Married but at Single Rate'),
- ], string='Federal W4 Filing Status', default='single')
- w4_is_nonresident_alien = fields.Boolean(string="Federal W4 Is Nonresident Alien", default=False)
- w4_additional_withholding = fields.Float(string="Federal W4 Additional Withholding", default=0.0)
-
- external_wages = fields.Float(string='External Existing Wages', default=0.0)
-
- fica_exempt = fields.Boolean(string='FICA Exempt', help="Exempt from Social Security and "
- "Medicare e.g. F1 Student Visa")
- futa_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')
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/ga_georgia.py b/l10n_us_hr_payroll/models/state/ga_georgia.py
new file mode 100644
index 00000000..331e6646
--- /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.dict.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.dict.contract_id.schedule_pay
+ additional = payslip.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ dependent_allowances = payslip.dict.contract_id.us_payroll_config_value('ga_g4_sit_dependent_allowances')
+ additional_allowances = payslip.dict.contract_id.us_payroll_config_value('ga_g4_sit_additional_allowances')
+ dependent_allowance_rate = payslip.dict.rule_parameter('us_ga_sit_dependent_allowance_rate').get(schedule_pay)
+ personal_allowance = payslip.dict.rule_parameter('us_ga_sit_personal_allowance').get(ga_filing_status, {}).get(schedule_pay)
+ deduction = payslip.dict.rule_parameter('us_ga_sit_deduction').get(ga_filing_status, {}).get(schedule_pay)
+ withholding_rate = payslip.dict.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/state/general.py b/l10n_us_hr_payroll/models/state/general.py
new file mode 100644
index 00000000..af2e3931
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/general.py
@@ -0,0 +1,127 @@
+# 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 _state_applies(payslip, state_code):
+ return state_code == payslip.dict.contract_id.us_payroll_config_value('state_code')
+
+
+# Export for eval context
+is_us_state = _state_applies
+
+
+def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, rate=None):
+ """
+ Function parameters:
+ wage_base, wage_start, rate can either be strings (rule_parameters) or floats
+ :return: result, result_rate(wage, percent)
+ """
+
+ # Resolve parameters. On exception, return (probably missing a year, would rather not have exception)
+ if wage_base and isinstance(wage_base, str):
+ try:
+ wage_base = payslip.dict.rule_parameter(wage_base)
+ except (KeyError, UserError):
+ return 0.0, 0.0
+
+ if wage_start and isinstance(wage_start, str):
+ try:
+ wage_start = payslip.dict.rule_parameter(wage_start)
+ except (KeyError, UserError):
+ return 0.0, 0.0
+
+ if rate and isinstance(rate, str):
+ try:
+ rate = payslip.dict.rule_parameter(rate)
+ except (KeyError, UserError):
+ return 0.0, 0.0
+
+ if not rate:
+ return 0.0, 0.0
+ else:
+ # Rate assumed positive percentage!
+ rate = -rate
+
+ if wage_base:
+ remaining = wage_base - ytd_wage
+ if remaining < 0.0:
+ result = 0.0
+ elif remaining < wage:
+ result = remaining
+ else:
+ result = wage
+
+ # _logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate))
+ return result, rate
+ if wage_start:
+ if ytd_wage >= wage_start:
+ # _logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate))
+ return wage, rate
+ if ytd_wage + wage <= wage_start:
+ # _logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0))
+ return 0.0, 0.0
+ # _logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate))
+ return (wage - (wage_start - ytd_wage)), rate
+
+ # If the wage doesn't have a start or a base
+ # _logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate))
+ return wage, rate
+
+
+def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None):
+ """
+ Returns SUTA eligible wage and rate.
+ WAGE = GROSS - 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.dict.contract_id.futa_type in (payslip.dict.contract_id.FUTA_TYPE_EXEMPT, payslip.dict.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.dict.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 SIT eligible wage and rate.
+ WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
+
+ :return: result, result_rate (wage, percent)
+ """
+ if not _state_applies(payslip, state_code):
+ return 0.0, 0.0
+
+ if payslip.dict.contract_id.us_payroll_config_value('state_income_tax_exempt'):
+ 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.dict.contract_id.external_wages
+
+ wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
+ result, result_rate = _general_rate(payslip, wage, ytd_wage, wage_base=wage_base, wage_start=wage_start, rate=rate)
+ additional = payslip.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ if additional:
+ tax = result * (result_rate / 100.0)
+ tax -= additional # assumed result_rate is negative and that the 'additional' should increase it.
+ return result, ((tax / result) * 100.0)
+ return result, result_rate
diff --git a/l10n_us_hr_payroll/models/state/ms_mississippi.py b/l10n_us_hr_payroll/models/state/ms_mississippi.py
new file mode 100644
index 00000000..ab9fc178
--- /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.dict.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.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ exemptions = payslip.dict.contract_id.us_payroll_config_value('ms_89_350_sit_exemption_value')
+ standard_deduction = payslip.dict.rule_parameter('us_ms_sit_deduction').get(filing_status)
+ withholding_rate = payslip.dict.rule_parameter('us_ms_sit_rate')
+
+ wage_annual = wage * pay_periods
+ taxable_income = wage_annual - (exemptions + standard_deduction)
+ if taxable_income <= 0.01:
+ return wage, 0.0
+
+ withholding = 0.0
+ for row in withholding_rate:
+ wage_base, base, rate = row
+ if taxable_income >= wage_base:
+ withholding = base + ((taxable_income - wage_base) * rate)
+ break
+ withholding /= pay_periods
+ withholding = round(withholding)
+ withholding += round(additional)
+ return wage, -((withholding / wage) * 100.0)
diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py
new file mode 100644
index 00000000..b9fa0986
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/mt_montana.py
@@ -0,0 +1,42 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+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
+
+ :return: result, result_rate (wage, percent)
+ """
+ state_code = 'MT'
+ if not _state_applies(payslip, state_code):
+ return 0.0, 0.0
+
+ if payslip.dict.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.dict.contract_id.schedule_pay
+ additional = payslip.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ exemptions = payslip.dict.contract_id.us_payroll_config_value('mt_mw4_sit_exemptions')
+ exemption_rate = payslip.dict.rule_parameter('us_mt_sit_exemption_rate').get(schedule_pay)
+ withholding_rate = payslip.dict.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
+
+ adjusted_wage = wage - (exemption_rate * (exemptions or 0))
+ withholding = 0.0
+ if adjusted_wage > 0.0:
+ prior_wage_cap = 0.0
+ for row in withholding_rate:
+ wage_cap, base, rate = row
+ wage_cap = float(wage_cap) # e.g. 'inf'
+ if adjusted_wage < wage_cap:
+ withholding = round(base + ((rate / 100.0) * (adjusted_wage - prior_wage_cap)))
+ break
+ prior_wage_cap = wage_cap
+ withholding += additional
+ return wage, -((withholding / wage) * 100.0)
diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py
new file mode 100644
index 00000000..8ec52538
--- /dev/null
+++ b/l10n_us_hr_payroll/models/state/oh_ohio.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 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.dict.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.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ exemptions = payslip.dict.contract_id.us_payroll_config_value('oh_it4_sit_exemptions')
+ exemption_rate = payslip.dict.rule_parameter('us_oh_sit_exemption_rate')
+ withholding_rate = payslip.dict.rule_parameter('us_oh_sit_rate')
+ multiplier_rate = payslip.dict.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/state/va_virginia.py b/l10n_us_hr_payroll/models/state/va_virginia.py
new file mode 100644
index 00000000..881e80e6
--- /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.dict.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.dict.contract_id.us_payroll_config_value('state_income_tax_additional_withholding')
+ personal_exemptions = payslip.dict.contract_id.us_payroll_config_value('va_va4_sit_exemptions')
+ other_exemptions = payslip.dict.contract_id.us_payroll_config_value('va_va4_sit_other_exemptions')
+ personal_exemption_rate = payslip.dict.rule_parameter('us_va_sit_exemption_rate')
+ other_exemption_rate = payslip.dict.rule_parameter('us_va_sit_other_exemption_rate')
+ deduction = payslip.dict.rule_parameter('us_va_sit_deduction')
+ withholding_rate = payslip.dict.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/state/wa_washington.py b/l10n_us_hr_payroll/models/state/wa_washington.py
new file mode 100644
index 00000000..c608d2da
--- /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.dict.rule_parameter('us_wa_fml_rate')
+ rate *= payslip.dict.rule_parameter(inner_rate) / 100.0
+ return _general_rate(payslip, wage, ytd_wage, wage_base='us_wa_fml_wage_base', rate=rate)
+
+
+def wa_washington_fml_er(payslip, categories, worked_days, inputs):
+ return _wa_washington_fml(payslip, categories, worked_days, inputs, inner_rate='us_wa_fml_rate_er')
+
+
+def wa_washington_fml_ee(payslip, categories, worked_days, inputs):
+ return _wa_washington_fml(payslip, categories, worked_days, inputs, inner_rate='us_wa_fml_rate_ee')
diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py
new file mode 100644
index 00000000..5ff26656
--- /dev/null
+++ b/l10n_us_hr_payroll/models/us_payroll_config.py
@@ -0,0 +1,96 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+FUTA_TYPE_EXEMPT = 'exempt'
+FUTA_TYPE_BASIC = 'basic'
+FUTA_TYPE_NORMAL = 'normal'
+
+
+class HRContractUSPayrollConfig(models.Model):
+ _name = 'hr.contract.us_payroll_config'
+ _description = 'Contract US Payroll Forms'
+
+ name = fields.Char(string="Description")
+ employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
+ state_id = fields.Many2one('res.country.state', string="Applied State")
+ state_code = fields.Char(related='state_id.code')
+ state_income_tax_exempt = fields.Boolean(string='State Income Tax Exempt')
+ state_income_tax_additional_withholding = fields.Float(string='State Income Tax Additional Withholding')
+ workers_comp_ee_code = fields.Char(string='Workers\' Comp Code (Employee Withholding)',
+ help='Code for a Payroll Rate, used by some states or your own rules.')
+ workers_comp_er_code = fields.Char(string='Workers\' Comp Code (Employer Withholding)',
+ help='Code for a Payroll Rate, used by some states or your own rules.')
+
+ fed_940_type = fields.Selection([
+ (FUTA_TYPE_EXEMPT, 'Exempt (0%)'),
+ (FUTA_TYPE_NORMAL, 'Normal Net Rate (0.6%)'),
+ (FUTA_TYPE_BASIC, 'Basic Rate (6%)'),
+ ], string="Federal Unemployment Tax Type (FUTA)", default='normal')
+
+ fed_941_fica_exempt = fields.Boolean(string='FICA Exempt', help="Exempt from Social Security and "
+ "Medicare e.g. F1 Student Visa")
+
+ fed_941_fit_w4_filing_status = fields.Selection([
+ ('', 'Exempt'),
+ ('single', 'Single or Married filing separately'),
+ ('married', 'Married filing jointly'),
+ ('married_as_single', 'Head of Household'),
+ ], string='Federal W4 Filing Status [1(c)]', default='single')
+ fed_941_fit_w4_allowances = fields.Integer(string='Federal W4 Allowances (before 2020)')
+ fed_941_fit_w4_is_nonresident_alien = fields.Boolean(string='Federal W4 Is Nonresident Alien')
+ fed_941_fit_w4_multiple_jobs_higher = fields.Boolean(string='Federal W4 Multiple Jobs Higher [2(c)]',
+ help='Form W4 (2020+) 2(c) Checkbox. '
+ 'Uses Higher Withholding tables.')
+ fed_941_fit_w4_dependent_credit = fields.Float(string='Federal W4 Dependent Credit [3]',
+ help='Form W4 (2020+) Line 3')
+ fed_941_fit_w4_other_income = fields.Float(string='Federal W4 Other Income [4(a)]',
+ help='Form W4 (2020+) 4(a)')
+ fed_941_fit_w4_deductions = fields.Float(string='Federal W4 Deductions [4(b)]',
+ help='Form W4 (2020+) 4(b)')
+ fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]',
+ help='Form W4 (2020+) 4(c)')
+
+ 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.')
+
+ ms_89_350_sit_filing_status = fields.Selection([
+ ('', 'Exempt'),
+ ('single', 'Single'),
+ ('married', 'Married (spouse NOT employed)'),
+ ('married_dual', 'Married (spouse IS employed)'),
+ ('head_of_household', 'Head of Household'),
+ ], string='Mississippi 89-350 Filing Status', help='89-350 1. 2. 3. 8.')
+ ms_89_350_sit_exemption_value = fields.Float(string='Mississippi 89-350 Exemption Total',
+ help='89-350 Box 6 (including filing status amounts)')
+
+ mt_mw4_sit_exemptions = fields.Integer(string='Montana MW-4 Exemptions',
+ help='MW-4 Box G')
+ # Don't use the main state_income_tax_exempt because of special meaning and reporting
+ # Use additional withholding but name it on the form 'MW-4 Box H'
+ mt_mw4_sit_exempt = fields.Selection([
+ ('', 'Not Exempt'),
+ ('tribe', 'Registered Tribe'),
+ ('reserve', 'Reserve or National Guard'),
+ ('north_dakota', 'North Dakota'),
+ ('montana_for_marriage', 'Montana for Marriage'),
+ ], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2')
+
+ # Ohio will use generic SIT exempt and additional fields
+ oh_it4_sit_exemptions = fields.Integer(string='Ohio IT-4 Exemptions',
+ help='Line 4')
+
+ va_va4_sit_exemptions = fields.Integer(string='Virginia VA-4(P) Personal Exemptions',
+ help='VA-4(P) 1(a)')
+ va_va4_sit_other_exemptions = fields.Integer(string='Virginia VA-4(P) Age & Blindness Exemptions',
+ help='VA-4(P) 1(b)')
diff --git a/l10n_us_hr_payroll/security/ir.model.access.csv b/l10n_us_hr_payroll/security/ir.model.access.csv
new file mode 100644
index 00000000..67a8fa2a
--- /dev/null
+++ b/l10n_us_hr_payroll/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_hr_contract_us_payroll_config,hr.contract.us_payroll_config,model_hr_contract_us_payroll_config,hr_payroll.group_hr_payroll_manager,1,1,1,1
diff --git a/l10n_us_hr_payroll/static/description/icon.png b/l10n_us_hr_payroll/static/description/icon.png
new file mode 100644
index 00000000..2a58a381
Binary files /dev/null and b/l10n_us_hr_payroll/static/description/icon.png differ
diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py
index abe27c5a..3e4cb355 100755
--- a/l10n_us_hr_payroll/tests/__init__.py
+++ b/l10n_us_hr_payroll/tests/__init__.py
@@ -1,3 +1,32 @@
-from . import test_us_payslip
-from . import test_us_payslip_2018
+# 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
+
+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_ms_mississippi_payslip_2019
+from . import test_us_ms_mississippi_payslip_2020
+
+from . import test_us_mt_montana_payslip_2019
+from . import test_us_mt_montana_payslip_2020
+
+from . import test_us_oh_ohio_payslip_2019
+from . import test_us_oh_ohio_payslip_2020
+
+from . import test_us_pa_pennsylvania_payslip_2019
+from . import test_us_pa_pennsylvania_payslip_2020
+
+from . import test_us_tx_texas_payslip_2019
+from . import test_us_tx_texas_payslip_2020
+
+from . import test_us_va_virginia_payslip_2019
+from . import test_us_va_virginia_payslip_2020
+
+from . import test_us_wa_washington_payslip_2019
+from . import test_us_wa_washington_payslip_2020
diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py
new file mode 100755
index 00000000..338e1d44
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/common.py
@@ -0,0 +1,242 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from logging import getLogger
+from sys import float_info as sys_float_info
+from collections import defaultdict
+from datetime import timedelta
+
+from odoo.tests import common
+from odoo.tools.float_utils import float_round as odoo_float_round
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+
+def process_payslip(payslip):
+ try:
+ payslip.action_payslip_done()
+ except AttributeError:
+ # v9
+ payslip.process_sheet()
+
+
+class TestUsPayslip(common.TransactionCase):
+ debug = False
+ _logger = getLogger(__name__)
+
+ float_info = sys_float_info
+
+ def float_round(self, value, digits):
+ return odoo_float_round(value, digits)
+
+ _payroll_digits = -1
+
+ @property
+ def payroll_digits(self):
+ if self._payroll_digits == -1:
+ self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll')
+ return self._payroll_digits
+
+ def _log(self, message):
+ if self.debug:
+ self._logger.warn(message)
+
+ def _createEmployee(self):
+ return self.env['hr.employee'].create({
+ 'birthday': '1985-03-14',
+ 'country_id': self.ref('base.us'),
+ 'department_id': self.ref('hr.dep_rd'),
+ 'gender': 'male',
+ 'name': 'Jared'
+ })
+
+ def _createContract(self, employee, **kwargs):
+ if not 'schedule_pay' in kwargs:
+ kwargs['schedule_pay'] = 'monthly'
+ schedule_pay = kwargs['schedule_pay']
+ config_model = self.env['hr.contract.us_payroll_config']
+ contract_model = self.env['hr.contract']
+ config_values = {
+ 'name': 'Test Config Values',
+ 'employee_id': employee.id,
+ }
+ contract_values = {
+ 'name': 'Test Contract',
+ 'employee_id': employee.id,
+ }
+
+ # Backwards compatability with 'futa_type'
+ if 'futa_type' in kwargs:
+ kwargs['fed_940_type'] = kwargs['futa_type']
+
+ for key, val in kwargs.items():
+ # Assume any Odoo object is in a Many2one
+ if hasattr(val, 'id'):
+ val = val.id
+ found = False
+ if hasattr(contract_model, key):
+ contract_values[key] = val
+ found = True
+ if hasattr(config_model, key):
+ config_values[key] = val
+ found = True
+ if not found:
+ self._logger.warn('cannot locate attribute names "%s" on contract or payroll config' % (key, ))
+
+ # US Payroll Config Defaults Should be set on the Model
+ config = config_model.create(config_values)
+ contract_values['us_payroll_config_id'] = config.id
+
+ # Some Basic Defaults
+ if not contract_values.get('state'):
+ contract_values['state'] = 'open' # Running
+ if not contract_values.get('struct_id'):
+ contract_values['struct_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)
+
+ 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
+ })
+ if hasattr(slip, '_onchange_employee'):
+ slip._onchange_employee()
+ if hasattr(slip, 'onchange_employee'):
+ # Odoo 12
+ slip.onchange_employee()
+ if self.debug:
+ self._logger.warn(slip.read())
+ self._logger.warn(slip.contract_id.read())
+ return slip
+
+ def _getCategories(self, payslip):
+ categories = defaultdict(float)
+ for line in payslip.line_ids:
+ self._log(' line code: ' + str(line.code) +
+ ' category code: ' + line.category_id.code +
+ ' total: ' + str(line.total) +
+ ' rate: ' + str(line.rate) +
+ ' amount: ' + str(line.amount))
+ category_id = line.category_id
+ category_code = line.category_id.code
+ while category_code:
+ categories[category_code] += line.total
+ category_id = category_id.parent_id
+ category_code = category_id.code
+ return categories
+
+ def _getRules(self, payslip):
+ rules = defaultdict(float)
+ for line in payslip.line_ids:
+ rules[line.code] += line.total
+ return rules
+
+ def assertPayrollEqual(self, first, second):
+ self.assertAlmostEqual(first, second, self.payroll_digits)
+
+ def assertPayrollAlmostEqual(self, first, second):
+ self.assertAlmostEqual(first, second, self.payroll_digits-1)
+
+ def 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:
+ return cache[code]
+ if country_key not in cache:
+ cache[country_key] = self.env.ref('base.us')
+ us_country = cache[country_key]
+ us_state = self.env['res.country.state'].search([
+ ('country_id', '=', us_country.id),
+ ('code', '=', code),
+ ], limit=1)
+ cache[code] = us_state
+ return us_state
+
+ def _test_suta(self, category, state_code, rate, date, wage_base=None, **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(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)
+
+ # 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)
+ process_payslip(payslip)
+
+ # Second Payslip
+ payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60))
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ if wage_base:
+ remaining_unemp_wages = wage_base - wage
+ self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative
+ 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)
+ # 1 wage - external_wages
+ # 1 wage - current Payslip
+ contract.external_wages = wage
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ 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
new file mode 100755
index 00000000..419be377
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py
@@ -0,0 +1,84 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+
+class TestUsFlPayslip(TestUsPayslip):
+ ###
+ # 2019 Taxes and Rates
+ ###
+ FL_UNEMP_MAX_WAGE = 7000.0
+ FL_UNEMP = -2.7 / 100.0
+
+ def test_2019_taxes(self):
+ salary = 5000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('FL'))
+
+ self._log('2019 Florida tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.FL_UNEMP)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_fl_unemp_wages = self.FL_UNEMP_MAX_WAGE - salary if (self.FL_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 Florida tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_fl_unemp_wages * self.FL_UNEMP)
+
+ def test_2019_taxes_with_external(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ external_wages=external_wages,
+ state_id=self.get_us_state('FL'))
+
+ self._log('2019 Forida_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP)
+
+ def test_2019_taxes_with_state_exempt(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ external_wages=external_wages,
+ futa_type=USHRContract.FUTA_TYPE_BASIC,
+ state_id=self.get_us_state('FL'))
+
+ self._log('2019 Forida_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py
new file mode 100755
index 00000000..5952eb1f
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py
@@ -0,0 +1,16 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip
+
+
+class TestUsFlPayslip(TestUsPayslip):
+ ###
+ # 2020 Taxes and Rates
+ ###
+ FL_UNEMP_MAX_WAGE = 7000.0
+ FL_UNEMP = 2.7
+
+ def test_2020_taxes(self):
+ # Only has state unemployment
+ self._test_er_suta('FL', self.FL_UNEMP, date(2020, 1, 1), wage_base=self.FL_UNEMP_MAX_WAGE)
diff --git a/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py
new file mode 100755
index 00000000..b407a079
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py
@@ -0,0 +1,135 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsGAPayslip(TestUsPayslip):
+
+ # TAXES AND RATES
+ GA_UNEMP_MAX_WAGE = 9500.00
+ GA_UNEMP = -(2.70 / 100.0)
+
+ def test_taxes_weekly_single_with_additional_wh(self):
+ salary = 15000.00
+ schedule_pay = 'weekly'
+ allowances = 1
+ filing_status = 'single'
+ additional_wh = 12.50
+ # Hand Calculated Amount to Test
+ # Step 1 - Subtract standard deduction from wages. Std Deduct for single weekly is 88.50
+ # step1 = 15000.00 - 88.50 = 14911.5
+ # Step 2 - Subtract personal allowance from step1. Allowance for single weekly is 51.92
+ # step2 = step1 - 51.92 = 14859.58
+ # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 57.50
+ # step3 = 14859.58 - 57.50 = 14802.08
+ # Step 4 -Determine wh amount from tables
+ # step4 = 4.42 + ((5.75 / 100.00) * (14802.08 - 135.00))
+ # Add additional_wh
+ # wh = 847.7771 + 12.50 = 860.2771
+ wh = -860.28
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('GA'),
+ ga_g4_sit_dependent_allowances=allowances,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=filing_status,
+ state_income_tax_additional_withholding=additional_wh,
+ schedule_pay=schedule_pay)
+
+ self.assertEqual(contract.schedule_pay, 'weekly')
+
+ self._log('2019 Georgia tax first payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], self.GA_UNEMP_MAX_WAGE * self.GA_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ remaining_GA_UNEMP_wages = 0.0 # We already reached max unemployment wages.
+
+ self._log('2019 Georgia tax second payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_GA_UNEMP_wages * self.GA_UNEMP)
+
+
+ def test_taxes_monthly_head_of_household(self):
+ salary = 25000.00
+ schedule_pay = 'monthly'
+ allowances = 2
+ filing_status = 'head of household'
+ additional_wh = 15.00
+ # Hand Calculated Amount to Test
+ # Step 1 - Subtract standard deduction from wages. Std Deduct for head of household monthly is 383.50
+ # step1 = 25000.00 - 383.50 = 24616.5
+ # Step 2 - Subtract personal allowance from step1. Allowance for head of household monthly is 225.00
+ # step2 = 24616.5 - 225.00 = 24391.5
+ # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 250.00
+ # step3 = 24391.5 - (2 * 250.00) = 23891.5
+ # Step 4 - Determine wh amount from tables
+ # step4 = 28.33 + ((5.75 / 100.00) * (23891.5 - 833.00)) = 1354.19375
+ # Add additional_wh
+ # wh = 1354.19375 + 15.00 = 1369.19375
+ wh = -1369.19
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('GA'),
+ ga_g4_sit_dependent_allowances=allowances,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=filing_status,
+ state_income_tax_additional_withholding=additional_wh,
+ schedule_pay=schedule_pay)
+
+ self.assertEqual(contract.schedule_pay, 'monthly')
+
+ self._log('2019 Georgia tax first payslip monthly:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], self.GA_UNEMP_MAX_WAGE * self.GA_UNEMP)
+ self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh)
+
+ process_payslip(payslip)
+
+ remaining_GA_UNEMP_wages = 0.0 # We already reached max unemployment wages.
+
+ self._log('2019 Georgia tax second payslip weekly:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_GA_UNEMP_wages * self.GA_UNEMP)
+
+ def test_taxes_exempt(self):
+ salary = 25000.00
+ schedule_pay = 'monthly'
+ allowances = 2
+ filing_status = 'exempt'
+ additional_wh = 15.00
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('GA'),
+ ga_g4_sit_dependent_allowances=allowances,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=filing_status,
+ state_income_tax_additional_withholding=additional_wh,
+ schedule_pay=schedule_pay)
+
+ self._log('2019 Georgia tax first payslip exempt:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0), 0)
diff --git a/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py
new file mode 100755
index 00000000..6debc2ca
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py
@@ -0,0 +1,148 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsGAPayslip(TestUsPayslip):
+
+ # TAXES AND RATES
+ GA_UNEMP_MAX_WAGE = 9500.00
+ GA_UNEMP = 2.70
+
+ def _run_test_sit(self,
+ wage=0.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=0.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=0.0,
+ ga_g4_sit_dependent_allowances=0,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=None,
+ expected=0.0,
+ ):
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien,
+ fed_941_fit_w4_filing_status=filing_status,
+ fed_941_fit_w4_multiple_jobs_higher=False,
+ fed_941_fit_w4_dependent_credit=dependent_credit,
+ fed_941_fit_w4_other_income=other_income,
+ fed_941_fit_w4_deductions=deductions,
+ fed_941_fit_w4_additional_withholding=additional_withholding,
+ state_income_tax_exempt=state_income_tax_exempt,
+ state_income_tax_additional_withholding=state_income_tax_additional_withholding,
+ ga_g4_sit_dependent_allowances=ga_g4_sit_dependent_allowances,
+ ga_g4_sit_additional_allowances=ga_g4_sit_additional_allowances,
+ ga_g4_sit_filing_status=ga_g4_sit_filing_status,
+ state_id=self.get_us_state('GA'),
+ )
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ # Instead of PayrollEqual after initial first round of testing.
+ self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected)
+ return payslip
+
+ def test_taxes_weekly_single_with_additional_wh(self):
+ self._test_er_suta('GA', self.GA_UNEMP, date(2020, 1, 1), wage_base=self.GA_UNEMP_MAX_WAGE)
+ salary = 15000.00
+ schedule_pay = 'weekly'
+ allowances = 1
+ filing_status = 'single'
+ additional_wh = 12.50
+ # Hand Calculated Amount to Test
+ # Step 1 - Subtract standard deduction from wages. Std Deduct for single weekly is 88.50
+ # step1 = 15000.00 - 88.50 = 14911.5
+ # Step 2 - Subtract personal allowance from step1. Allowance for single weekly is 51.92
+ # step2 = step1 - 51.92 = 14859.58
+ # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 57.50
+ # step3 = 14859.58 - 57.50 = 14802.08
+ # Step 4 -Determine wh amount from tables
+ # step4 = 4.42 + ((5.75 / 100.00) * (14802.08 - 135.00))
+ # Add additional_wh
+ # wh = 847.7771 + 12.50 = 860.2771
+ wh = 860.28
+
+ self._run_test_sit(wage=salary,
+ schedule_pay=schedule_pay,
+ state_income_tax_additional_withholding=additional_wh,
+ ga_g4_sit_dependent_allowances=allowances,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=filing_status,
+ expected=wh,
+ )
+
+
+ def test_taxes_monthly_head_of_household(self):
+ salary = 25000.00
+ schedule_pay = 'monthly'
+ allowances = 2
+ filing_status = 'head of household'
+ additional_wh = 15.00
+ # Hand Calculated Amount to Test
+ # Step 1 - Subtract standard deduction from wages. Std Deduct for head of household monthly is 383.50
+ # step1 = 25000.00 - 383.50 = 24616.5
+ # Step 2 - Subtract personal allowance from step1. Allowance for head of household monthly is 225.00
+ # step2 = 24616.5 - 225.00 = 24391.5
+ # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 250.00
+ # step3 = 24391.5 - (2 * 250.00) = 23891.5
+ # Step 4 - Determine wh amount from tables
+ # step4 = 28.33 + ((5.75 / 100.00) * (23891.5 - 833.00)) = 1354.19375
+ # Add additional_wh
+ # wh = 1354.19375 + 15.00 = 1369.19375
+ wh = 1369.19
+
+ self._run_test_sit(wage=salary,
+ schedule_pay=schedule_pay,
+ state_income_tax_additional_withholding=additional_wh,
+ ga_g4_sit_dependent_allowances=allowances,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=filing_status,
+ expected=wh,
+ )
+
+ # additional from external calculator
+ self._run_test_sit(wage=425.0,
+ schedule_pay='weekly',
+ state_income_tax_additional_withholding=0.0,
+ ga_g4_sit_dependent_allowances=1,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status='married filing separate',
+ expected=11.45,
+ )
+
+ self._run_test_sit(wage=3000.0,
+ schedule_pay='quarterly',
+ state_income_tax_additional_withholding=0.0,
+ ga_g4_sit_dependent_allowances=1,
+ ga_g4_sit_additional_allowances=1,
+ ga_g4_sit_filing_status='single',
+ expected=0.0,
+ )
+
+ # TODO 'married filing joint, both spouses working' returns lower than calculator
+ # TODO 'married filing joint, one spouse working' returns lower than calculator
+
+ def test_taxes_exempt(self):
+ salary = 25000.00
+ schedule_pay = 'monthly'
+ allowances = 2
+ filing_status = 'exempt'
+ additional_wh = 15.00
+
+ self._run_test_sit(wage=salary,
+ schedule_pay=schedule_pay,
+ state_income_tax_additional_withholding=additional_wh,
+ ga_g4_sit_dependent_allowances=allowances,
+ ga_g4_sit_additional_allowances=0,
+ ga_g4_sit_filing_status=filing_status,
+ expected=0.0,
+ )
diff --git a/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py
new file mode 100755
index 00000000..e7ce35d0
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py
@@ -0,0 +1,94 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip
+
+
+class TestUsMsPayslip(TestUsPayslip):
+ # Calculations from https://www.dor.ms.gov/Documents/Computer%20Payroll%20Accounting%201-2-19.pdf
+ MS_UNEMP = -1.2 / 100.0
+
+ def test_2019_taxes_one(self):
+ salary = 1250.0
+ ms_89_350_exemption = 11000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='head_of_household',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 Mississippi tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.MS_UNEMP)
+
+ STDED = 3400.0 # Head of Household
+ AGP = salary * 24 # Semi-Monthly
+ TI = AGP - (ms_89_350_exemption + STDED)
+ self.assertPayrollEqual(TI, 15600.0)
+ TAX = ((TI - 10000) * 0.05) + 290 # Over 10,000
+ self.assertPayrollEqual(TAX, 570.0)
+
+ ms_withhold = round(TAX / 24) # Semi-Monthly
+ self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold)
+
+ def test_2019_taxes_one_exempt(self):
+ salary = 1250.0
+ ms_89_350_exemption = 11000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 Mississippi tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0)
+
+ def test_2019_taxes_additional(self):
+ salary = 1250.0
+ ms_89_350_exemption = 11000.0
+ additional = 40.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='head_of_household',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=additional,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 Mississippi tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.MS_UNEMP)
+
+ STDED = 3400.0 # Head of Household
+ AGP = salary * 24 # Semi-Monthly
+ TI = AGP - (ms_89_350_exemption + STDED)
+ self.assertPayrollEqual(TI, 15600.0)
+ TAX = ((TI - 10000) * 0.05) + 290 # Over 10,000
+ self.assertPayrollEqual(TAX, 570.0)
+
+ ms_withhold = round(TAX / 24) # Semi-Monthly
+ self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold + -additional)
diff --git a/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py
new file mode 100755
index 00000000..5942d7ad
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py
@@ -0,0 +1,120 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip
+
+
+class TestUsMsPayslip(TestUsPayslip):
+ # Calculations from https://www.dor.ms.gov/Documents/Computer%20Payroll%20Accounting%201-2-19.pdf
+ MS_UNEMP = 1.2
+ MS_UNEMP_MAX_WAGE = 14000.0
+
+ def test_2020_taxes_one(self):
+ self._test_er_suta('MS', self.MS_UNEMP, date(2020, 1, 1), wage_base=self.MS_UNEMP_MAX_WAGE)
+
+ salary = 1250.0
+ ms_89_350_exemption = 11000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='head_of_household',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2020 Mississippi tax single first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ STDED = 3400.0 # Head of Household
+ AGP = salary * 24 # Semi-Monthly
+ TI = AGP - (ms_89_350_exemption + STDED)
+ self.assertPayrollEqual(TI, 15600.0)
+ TAX = ((TI - 10000) * 0.05) + 260 # Over 10,000
+ self.assertPayrollEqual(TAX, 540.0)
+
+ ms_withhold = round(TAX / 24) # Semi-Monthly
+ self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold)
+
+ def test_2020_taxes_one_exempt(self):
+ salary = 1250.0
+ ms_89_350_exemption = 11000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=0.0,
+ schedule_pay='semi-monthly')
+
+ self._log('2020 Mississippi tax single first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0)
+
+ def test_2020_taxes_additional(self):
+ salary = 1250.0
+ ms_89_350_exemption = 11000.0
+ additional = 40.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='single',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=additional,
+ schedule_pay='monthly')
+
+ self._log('2020 Mississippi tax single first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ STDED = 2300.0 # Single
+ AGP = salary * 12 # Monthly
+ TI = AGP - (ms_89_350_exemption + STDED)
+ self.assertPayrollEqual(TI, 1700.0)
+ TAX = ((TI - 3000) * 0.03)
+ self.assertPayrollEqual(TAX, -39.0)
+
+ ms_withhold = round(TAX / 12) # Monthly
+ self.assertTrue(ms_withhold <= 0.0)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -40.0) # only additional
+
+ # Test with higher wage
+ salary = 1700.0
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MS'),
+ ms_89_350_sit_filing_status='single',
+ ms_89_350_sit_exemption_value=ms_89_350_exemption,
+ state_income_tax_additional_withholding=additional,
+ schedule_pay='monthly')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ STDED = 2300.0 # Single
+ AGP = salary * 12 # Monthly
+ TI = AGP - (ms_89_350_exemption + STDED)
+ self.assertPayrollEqual(TI, 7100.0)
+ TAX = ((TI - 5000) * 0.04) + 60.0
+ self.assertPayrollEqual(TAX, 144.0)
+
+ ms_withhold = round(TAX / 12) # Monthly
+ self.assertPayrollEqual(ms_withhold, 12.0)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -(ms_withhold + additional))
diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py
new file mode 100755
index 00000000..ff6e2daf
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py
@@ -0,0 +1,139 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsMtPayslip(TestUsPayslip):
+ # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705
+ MT_UNEMP = -1.18 / 100.0
+ MT_UNEMP_AFT = -0.13 / 100.0
+
+ def test_2019_taxes_one(self):
+ # Payroll Period Semi-Monthly example
+ salary = 550
+ mt_mw4_exemptions = 5
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MT'),
+ mt_mw4_sit_exemptions=mt_mw4_exemptions,
+ schedule_pay='semi-monthly')
+
+ self._log('2019 Montana tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * (self.MT_UNEMP + self.MT_UNEMP_AFT)) # New non-combined...
+
+ mt_taxable_income = salary - (79.0 * mt_mw4_exemptions)
+ mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0)))
+ self.assertPayrollEqual(mt_taxable_income, 155.0)
+ self.assertPayrollEqual(mt_withhold, 3.0)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold)
+
+ def test_2019_taxes_two(self):
+ # Payroll Period Bi-Weekly example
+ salary = 2950
+ mt_mw4_exemptions = 2
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MT'),
+ mt_mw4_sit_exemptions=mt_mw4_exemptions,
+ schedule_pay='bi-weekly')
+
+ self._log('2019 Montana tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2))
+
+ # Note!!
+ # The example calculation uses A = 16 but the actual table describes this as A = 18
+ mt_taxable_income = salary - (73.0 * mt_mw4_exemptions)
+ mt_withhold = round(18 + (0.06 * (mt_taxable_income - 577)))
+ self.assertPayrollEqual(mt_taxable_income, 2804.0)
+ self.assertPayrollEqual(mt_withhold, 152.0)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold)
+
+ def test_2019_taxes_three(self):
+ # Payroll Period Weekly example
+ salary = 135
+ mt_mw4_exemptions = 1
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MT'),
+ mt_mw4_sit_exemptions=mt_mw4_exemptions,
+ schedule_pay='weekly')
+
+ self._log('2019 Montana tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2))
+
+ mt_taxable_income = salary - (37.0 * mt_mw4_exemptions)
+ mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0)))
+ self.assertPayrollEqual(mt_taxable_income, 98.0)
+ self.assertPayrollEqual(mt_withhold, 2.0)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold)
+
+ def test_2019_taxes_three_exempt(self):
+ # Payroll Period Weekly example
+ salary = 135
+ mt_mw4_exemptions = 1
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MT'),
+ mt_mw4_sit_exemptions=mt_mw4_exemptions,
+ mt_mw4_sit_exempt='reserve',
+ schedule_pay='weekly')
+
+ self._log('2019 Montana tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0)
+
+ def test_2019_taxes_three_additional(self):
+ # Payroll Period Weekly example
+ salary = 135
+ mt_mw4_exemptions = 1
+ mt_mw4_additional_withholding = 20.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('MT'),
+ mt_mw4_sit_exemptions=mt_mw4_exemptions,
+ state_income_tax_additional_withholding=mt_mw4_additional_withholding,
+ schedule_pay='weekly')
+
+ self._log('2019 Montana tax single first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+
+ mt_taxable_income = salary - (37.0 * mt_mw4_exemptions)
+ mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0)))
+ self.assertPayrollEqual(mt_taxable_income, 98.0)
+ self.assertPayrollEqual(mt_withhold, 2.0)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold + -mt_mw4_additional_withholding)
diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py
new file mode 100755
index 00000000..ec861a0d
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py
@@ -0,0 +1,17 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsMtPayslip(TestUsPayslip):
+ # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705
+ MT_UNEMP_WAGE_MAX = 34100.0
+ MT_UNEMP = 1.18
+ MT_UNEMP_AFT = 0.13
+
+ def test_2020_taxes_one(self):
+ combined_rate = self.MT_UNEMP + self.MT_UNEMP_AFT # Combined for test as they both go to the same category and have the same cap
+ self._test_er_suta('MT', combined_rate, date(2020, 1, 1), wage_base=self.MT_UNEMP_WAGE_MAX)
+
+ # TODO Montana Incometax rates for 2020 when released
diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py
new file mode 100755
index 00000000..d1f65f05
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py
@@ -0,0 +1,96 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+
+class TestUsOhPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ OH_UNEMP_MAX_WAGE = 9500.0
+ OH_UNEMP = -2.7 / 100.0
+
+ def test_2019_taxes(self):
+ salary = 5000.0
+
+ # For formula here
+ # http://www.tax.ohio.gov/Portals/0/employer_withholding/August2015Rates/WTH_OptionalComputerFormula_073015.pdf
+ tw = salary * 12 # = 60000
+ wd = ((tw - 40000) * 0.035 + 900) / 12 * 1.075
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('OH'),
+ )
+
+ self._log('2019 Ohio tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.OH_UNEMP)
+ self.assertPayrollAlmostEqual(cats['EE_US_SIT'], -wd) # Off by 0.6 cents so it rounds off by a penny
+ #self.assertPayrollEqual(cats['EE_US_SIT'], -wd)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_oh_unemp_wages = self.OH_UNEMP_MAX_WAGE - salary if (self.OH_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 Ohio tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_oh_unemp_wages * self.OH_UNEMP)
+
+ def test_2019_taxes_with_external(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('OH'),
+ external_wages=external_wages,
+ )
+
+ self._log('2019 Ohio_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], (self.OH_UNEMP_MAX_WAGE - external_wages) * self.OH_UNEMP)
+
+ def test_2019_taxes_with_state_exempt(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('OH'),
+ external_wages=external_wages,
+ futa_type=USHRContract.FUTA_TYPE_BASIC)
+
+ self._log('2019 Ohio exempt tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ # FUTA_TYPE_BASIC
+ self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), salary * 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py
new file mode 100755
index 00000000..9026da92
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py
@@ -0,0 +1,108 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsOhPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ OH_UNEMP_MAX_WAGE = 9000.0
+ OH_UNEMP = 2.7
+
+ def test_2020_taxes(self):
+ self._test_er_suta('OH', self.OH_UNEMP, date(2020, 1, 1), wage_base=self.OH_UNEMP_MAX_WAGE)
+
+ def _run_test_sit(self,
+ wage=0.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=0.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=0.0,
+ oh_it4_sit_exemptions=0,
+ expected=0.0,
+ ):
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien,
+ fed_941_fit_w4_filing_status=filing_status,
+ fed_941_fit_w4_multiple_jobs_higher=False,
+ fed_941_fit_w4_dependent_credit=dependent_credit,
+ fed_941_fit_w4_other_income=other_income,
+ fed_941_fit_w4_deductions=deductions,
+ fed_941_fit_w4_additional_withholding=additional_withholding,
+ state_income_tax_exempt=state_income_tax_exempt,
+ state_income_tax_additional_withholding=state_income_tax_additional_withholding,
+ oh_it4_sit_exemptions=oh_it4_sit_exemptions,
+ state_id=self.get_us_state('OH'),
+ )
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ # Instead of PayrollEqual after initial first round of testing.
+ self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected)
+ return payslip
+
+ def test_2020_sit_1(self):
+ wage = 400.0
+ exemptions = 1
+ additional = 10.0
+ pay_periods = 12.0
+ annual_adjusted_wage = (wage * pay_periods) - (650.0 * exemptions)
+ self.assertPayrollEqual(4150.0, annual_adjusted_wage)
+ WD = ((annual_adjusted_wage * 0.005) / pay_periods) * 1.032
+ self.assertPayrollEqual(WD, 1.7845)
+ expected = WD + additional
+ self._run_test_sit(wage=wage,
+ schedule_pay='monthly',
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=additional,
+ oh_it4_sit_exemptions=exemptions,
+ expected=expected,
+ )
+
+ # the above agrees with online calculator to the penny 0.01
+ # below expected coming from calculator to 0.10
+ #
+ # semi-monthly
+ self._run_test_sit(wage=1200,
+ schedule_pay='semi-monthly',
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=20.0,
+ oh_it4_sit_exemptions=2,
+ expected=42.58,
+ )
+
+ # bi-weekly
+ self._run_test_sit(wage=3000,
+ schedule_pay='bi-weekly',
+ state_income_tax_exempt=False,
+ #state_income_tax_additional_withholding=0.0,
+ oh_it4_sit_exemptions=0,
+ expected=88.51,
+ )
+ # weekly
+ self._run_test_sit(wage=355,
+ schedule_pay='weekly',
+ state_income_tax_exempt=False,
+ # state_income_tax_additional_withholding=0.0,
+ oh_it4_sit_exemptions=1,
+ expected=4.87,
+ )
+
+ # Exempt!
+ self._run_test_sit(wage=355,
+ schedule_pay='weekly',
+ state_income_tax_exempt=True,
+ # state_income_tax_additional_withholding=0.0,
+ oh_it4_sit_exemptions=1,
+ expected=0.0,
+ )
diff --git a/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py
new file mode 100755
index 00000000..ce7e4fb4
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py
@@ -0,0 +1,33 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+
+class TestUsPAPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ PA_UNEMP_MAX_WAGE = 10000.0
+ ER_PA_UNEMP = -3.6890 / 100.0
+ EE_PA_UNEMP = -0.06 / 100.0
+ PA_INC_WITHHOLD = 3.07
+
+ def test_2019_taxes(self):
+ salary = 4166.67
+ wh = -127.92
+
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('PA'))
+
+ self._log('2019 Pennsylvania tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['EE_US_SUTA'], cats['GROSS'] * self.EE_PA_UNEMP)
+ self.assertPayrollEqual(cats['ER_US_SUTA'], cats['GROSS'] * self.ER_PA_UNEMP)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
diff --git a/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py
new file mode 100755
index 00000000..3dd3fd27
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py
@@ -0,0 +1,43 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip
+
+
+class TestUsPAPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ PA_UNEMP_MAX_WAGE = 10000.0
+ ER_PA_UNEMP = 3.6890
+ EE_PA_UNEMP = 0.06
+ PA_INC_WITHHOLD = 3.07
+
+ def test_2020_taxes(self):
+ self._test_er_suta('PA', self.ER_PA_UNEMP, date(2020, 1, 1), wage_base=self.PA_UNEMP_MAX_WAGE)
+ self._test_ee_suta('PA', self.EE_PA_UNEMP, date(2020, 1, 1))
+
+ salary = 4166.67
+ wh = -127.92
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('PA'))
+
+ self._log('2019 Pennsylvania tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh)
+
+ # Test Additional
+ contract.us_payroll_config_id.state_income_tax_additional_withholding = 100.0
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_SIT'], wh - 100.0)
+
+ # Test Exempt
+ contract.us_payroll_config_id.state_income_tax_exempt = True
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_payslip.py b/l10n_us_hr_payroll/tests/test_us_payslip.py
deleted file mode 100755
index e25642fc..00000000
--- a/l10n_us_hr_payroll/tests/test_us_payslip.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from logging import getLogger
-from sys import float_info as sys_float_info
-
-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.l10n_us_hr_payroll import USHrContract
-
-
-def process_payslip(payslip):
- try:
- #v9
- payslip.process_sheet()
- except AttributeError:
- payslip.action_payslip_done()
-
-
-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, salary,
- schedule_pay='monthly',
- w4_allowances=0,
- w4_filing_status='single',
- w4_is_nonresident_alien=False,
- w4_additional_withholding=0.0,
- external_wages=0.0,
- struct_id=False,
- futa_type=USHrContract.FUTA_TYPE_NORMAL,
- ):
- if not struct_id:
- struct_id = self.ref('l10n_us_hr_payroll.hr_payroll_salary_structure_us_employee')
-
- values = {
- 'date_start': '2016-01-01',
- 'date_end': '2030-12-31',
- 'name': 'Contract for Jared 2016',
- 'wage': salary,
- 'type_id': self.ref('hr_contract.hr_contract_type_emp'),
- 'employee_id': employee.id,
- 'struct_id': struct_id,
- 'resource_calendar_id': self.ref('resource.resource_calendar_std'),
- 'schedule_pay': schedule_pay,
- 'w4_allowances': w4_allowances,
- 'w4_filing_status': w4_filing_status,
- 'w4_is_nonresident_alien': w4_is_nonresident_alien,
- 'w4_additional_withholding': w4_additional_withholding,
- 'external_wages': external_wages,
- 'futa_type': futa_type,
- 'state': 'open', # if not "Running" then no automatic selection when Payslip is created
- }
- try:
- values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id
- except KeyError:
- pass
-
- return self.env['hr.contract'].create(values)
-
- def _createPayslip(self, employee, date_from, date_to):
- return self.env['hr.payslip'].create({
- 'employee_id': employee.id,
- 'date_from': date_from,
- 'date_to': date_to
- })
-
- def _getCategories(self, payslip):
- detail_lines = payslip.details_by_salary_rule_category
- categories = {}
- for line in detail_lines:
- 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))
- if line.category_id.code not in categories:
- categories[line.category_id.code] = line.total
- else:
- categories[line.category_id.code] += line.total
-
- return categories
-
- def assertPayrollEqual(self, first, second):
- self.assertAlmostEqual(first, second, self.payroll_digits)
-
- def test_semi_monthly(self):
- salary = 80000.0
- employee = self._createEmployee()
- contract = self._createContract(employee, salary, schedule_pay='semi-monthly')
- payslip = self._createPayslip(employee, '2016-01-01', '2016-01-14')
-
- payslip.compute_sheet()
diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2018.py b/l10n_us_hr_payroll/tests/test_us_payslip_2018.py
deleted file mode 100755
index 3bfffda1..00000000
--- a/l10n_us_hr_payroll/tests/test_us_payslip_2018.py
+++ /dev/null
@@ -1,368 +0,0 @@
-from .test_us_payslip import TestUsPayslip, process_payslip
-
-from odoo.addons.l10n_us_hr_payroll.models.l10n_us_hr_payroll import USHrContract
-
-from sys import float_info
-
-
-class TestUsPayslip2018(TestUsPayslip):
- # FUTA Constants
- FUTA_RATE_NORMAL = 0.6
- FUTA_RATE_BASIC = 6.0
- FUTA_RATE_EXEMPT = 0.0
-
- # Wage caps
- FICA_SS_MAX_WAGE = 128400.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
-
- ###
- # 2018 Taxes and Rates
- ###
-
- def test_2018_taxes(self):
- # salary is high so that second payslip runs over max
- # social security salary
- salary = 80000.0
-
- employee = self._createEmployee()
-
- self._createContract(employee, salary)
-
- self._log('2017 tax last slip')
- payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31')
- payslip.compute_sheet()
- process_payslip(payslip)
-
- self._log('2018 tax first payslip:')
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
- self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
-
- process_payslip(payslip)
-
- # Make a new payslip, this one will have maximums for FICA Social Security Wages
-
- 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('2018 tax second payslip:')
- payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], remaining_ss_wages)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], remaining_m_wages)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], 0)
- self.assertPayrollEqual(cats['ER_US_FUTA'], 0)
-
- process_payslip(payslip)
-
- # Make a new payslip, this one will have reached Medicare Additional (employee only)
-
- self._log('2018 tax third payslip:')
- payslip = self._createPayslip(employee, '2018-03-01', '2018-03-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], self.FICA_M_ADD_START_WAGE - (salary * 2)) # aka 40k
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
-
- process_payslip(payslip)
-
- # Make a new payslip, this one will have all salary as Medicare Additional
-
- self._log('2018 tax fourth payslip:')
- payslip = self._createPayslip(employee, '2018-04-01', '2018-04-30')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
-
- process_payslip(payslip)
-
- def test_2018_fed_income_withholding_single(self):
- salary = 6000.00
- schedule_pay = 'monthly'
- w4_allowances = 3
- w4_allowance_amt = 345.80 * 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(-(371.12 + ((adjusted_salary - 3533) * 0.22)), self.payroll_digits)
-
- employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'single')
-
- self._log('2018 fed income single payslip: adjusted_salary: ' + str(adjusted_salary))
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
-
- def test_2018_fed_income_withholding_married_as_single(self):
- salary = 500.00
- schedule_pay = 'weekly'
- w4_allowances = 1
- w4_allowance_amt = 79.80 * w4_allowances
- adjusted_salary = salary - w4_allowance_amt # should be 420.50, but would work over a wide value for the rate
- ###
- # Single MONTHLY form Publication 15
- expected_withholding = self.float_round(-(18.30 + ((adjusted_salary - 254) * 0.12)), self.payroll_digits)
-
- employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'married_as_single')
-
- self._log('2018 fed income married_as_single payslip: adjusted_salary: ' + str(adjusted_salary))
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
-
- def test_2018_fed_income_withholding_married(self):
- salary = 14000.00
- schedule_pay = 'bi-weekly'
- w4_allowances = 2
- w4_allowance_amt = 159.60 * 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(-(2468.56 + ((adjusted_salary - 12560) * 0.32)), self.payroll_digits)
-
- employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'married')
-
- self._log('2018 fed income married payslip: adjusted_salary: ' + str(adjusted_salary))
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
-
- def test_2018_taxes_with_external(self):
-
- # social security salary
- salary = self.FICA_M_ADD_START_WAGE
- external_wages = 6000.0
-
- employee = self._createEmployee()
-
- self._createContract(employee, salary, external_wages=external_wages)
-
- self._log('2018 tax first payslip:')
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE - external_wages)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE - external_wages)
- self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
-
- def test_2018_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, salary, futa_type=USHrContract.FUTA_TYPE_BASIC)
-
- self._log('2018 tax first payslip:')
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
- self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * futa_rate)
-
- def test_2018_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, salary, futa_type=USHrContract.FUTA_TYPE_EXEMPT)
-
- self._log('2018 tax first payslip:')
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
-
- futa_wages = 0.0
- if 'WAGE_US_FUTA' in cats:
- futa_wages = cats['WAGE_US_FUTA']
- futa = 0.0
- if 'ER_US_FUTA' in cats:
- futa = cats['ER_US_FUTA']
- self.assertPayrollEqual(futa_wages, 0.0)
- self.assertPayrollEqual(futa, futa_wages * futa_rate)
-
- def test_2018_fed_income_withholding_nonresident_alien(self):
- salary = 3500.00
- schedule_pay = 'quarterly'
- w4_allowances = 1
- w4_allowance_amt = 1037.50 * w4_allowances
- nra_adjustment = 1962.50 # for quarterly
- adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4425
-
- ###
- # Single QUARTERLY form Publication 15
- expected_withholding = self.float_round(-(238.10 + ((adjusted_salary - 3306) * 0.12)), self.payroll_digits)
-
- employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'single',
- w4_is_nonresident_alien=True)
-
- self._log('2018 fed income single payslip nonresident alien: adjusted_salary: ' + str(adjusted_salary))
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
-
- def test_2018_fed_income_additional_withholding(self):
- salary = 50000.00
- schedule_pay = 'annually'
- w4_additional_withholding = 5000.0
- w4_allowances = 2
- w4_allowance_amt = 4150.00 * w4_allowances
- adjusted_salary = salary - w4_allowance_amt # 41700
-
- ###
- # Single ANNUAL form Publication 15
- expected_withholding = \
- self.float_round(-((1905.00 + ((adjusted_salary - 30600) * 0.12)) + w4_additional_withholding),
- self.payroll_digits)
-
- employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'married',
- w4_additional_withholding=w4_additional_withholding)
-
- self._log('2018 fed income married payslip additional withholding: adjusted_salary: ' + str(adjusted_salary))
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
-
- def test_2018_taxes_with_w4_exempt(self):
- salary = 6000.0
- schedule_pay = 'bi-weekly'
- w4_allowances = 0
- employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, '')
-
- self._log('2018 tax w4 exempt payslip:')
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- fed_inc_withhold = 0.0
- if 'EE_US_FED_INC_WITHHOLD' in cats:
- fed_inc_withhold = cats['EE_US_FED_INC_WITHHOLD']
- self.assertPayrollEqual(fed_inc_withhold, 0.0)
-
- def test_2018_taxes_with_fica_exempt(self):
- salary = 6000.0
- schedule_pay = 'bi-weekly'
- w4_allowances = 2
- employee = self._createEmployee()
- contract = self._createContract(employee, salary, schedule_pay, w4_allowances)
- contract.fica_exempt = True
-
- self._log('2018 tax w4 exempt payslip:')
- payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
-
- payslip.compute_sheet()
-
- cats = self._getCategories(payslip)
-
- ss_wages = cats.get('WAGE_US_FICA_SS', 0.0)
- med_wages = cats.get('WAGE_US_FICA_M', 0.0)
- ss = cats.get('EE_US_FICA_SS', 0.0)
- med = cats.get('EE_US_FICA_M', 0.0)
- self.assertPayrollEqual(ss_wages, 0.0)
- self.assertPayrollEqual(med_wages, 0.0)
- self.assertPayrollEqual(ss, 0.0)
- self.assertPayrollEqual(med, 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py
old mode 100755
new mode 100644
index 000c063b..46ab66af
--- a/l10n_us_hr_payroll/tests/test_us_payslip_2019.py
+++ b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py
@@ -1,6 +1,8 @@
-from .test_us_payslip import TestUsPayslip, process_payslip
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
-from odoo.addons.l10n_us_hr_payroll.models.l10n_us_hr_payroll import USHrContract
+from .common import TestUsPayslip, process_payslip
+
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
from sys import float_info
@@ -28,41 +30,44 @@ class TestUsPayslip2019(TestUsPayslip):
###
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()
- self._createContract(employee, salary)
+ 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)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
- self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
+ 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 maximums for FICA Social Security Wages
-
+ # 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
@@ -72,32 +77,25 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], remaining_ss_wages)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], remaining_m_wages)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], 0)
- self.assertPayrollEqual(cats['ER_US_FUTA'], 0)
+ 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(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], self.FICA_M_ADD_START_WAGE - (salary * 2)) # aka 40k
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
+ 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)
@@ -109,14 +107,15 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
+ 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
@@ -127,16 +126,18 @@ class TestUsPayslip2019(TestUsPayslip):
expected_withholding = self.float_round(-(378.52 + ((adjusted_salary - 3606) * 0.22)), self.payroll_digits)
employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'single')
+ 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_FED_INC_WITHHOLD'], expected_withholding)
+ self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_withholding_married_as_single(self):
salary = 500.00
@@ -145,20 +146,21 @@ class TestUsPayslip2019(TestUsPayslip):
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
###
- # Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(18.70 + ((adjusted_salary - 260) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'married_as_single')
+ 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_FED_INC_WITHHOLD'], expected_withholding)
+ self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_withholding_married(self):
salary = 14000.00
@@ -171,53 +173,52 @@ class TestUsPayslip2019(TestUsPayslip):
expected_withholding = self.float_round(-(2519.06 + ((adjusted_salary - 12817) * 0.32)), self.payroll_digits)
employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'married')
+ 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)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
+ # 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, salary, external_wages=external_wages)
+ 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)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE - external_wages)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE - external_wages)
- self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
+ 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):
+ self.debug = False
futa_rate = self.FUTA_RATE_BASIC / -100.0
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
- self._createContract(employee, salary, futa_type=USHrContract.FUTA_TYPE_BASIC)
+ 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')
@@ -225,52 +226,26 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
- self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
- self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * futa_rate)
+ 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, salary, futa_type=USHrContract.FUTA_TYPE_EXEMPT)
-
+ 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['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
- self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
- self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
- self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
- self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
- self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
- self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
-
- futa_wages = 0.0
- if 'WAGE_US_FUTA' in cats:
- futa_wages = cats['WAGE_US_FUTA']
- futa = 0.0
- if 'ER_US_FUTA' in cats:
- futa = cats['ER_US_FUTA']
- self.assertPayrollEqual(futa_wages, 0.0)
- self.assertPayrollEqual(futa, futa_wages * futa_rate)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
def test_2019_fed_income_withholding_nonresident_alien(self):
salary = 3500.00
@@ -278,24 +253,26 @@ class TestUsPayslip2019(TestUsPayslip):
w4_allowances = 1
w4_allowance_amt = 1050.0 * w4_allowances
nra_adjustment = 2000.0 # for quarterly
- adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4425
+ 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()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'single',
- w4_is_nonresident_alien=True)
+ 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()
-
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_additional_withholding(self):
salary = 50000.00
@@ -312,44 +289,45 @@ class TestUsPayslip2019(TestUsPayslip):
self.payroll_digits)
employee = self._createEmployee()
- self._createContract(employee, salary, schedule_pay, w4_allowances, 'married',
- w4_additional_withholding=w4_additional_withholding)
+ 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()
- cats = self._getCategories(payslip)
-
- self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
+ 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()
- self._createContract(employee, salary, schedule_pay, w4_allowances, '')
+ 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()
-
- cats = self._getCategories(payslip)
-
- fed_inc_withhold = 0.0
- if 'EE_US_FED_INC_WITHHOLD' in cats:
- fed_inc_withhold = cats['EE_US_FED_INC_WITHHOLD']
- self.assertPayrollEqual(fed_inc_withhold, 0.0)
+ 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'
- w4_allowances = 2
employee = self._createEmployee()
- contract = self._createContract(employee, salary, schedule_pay, w4_allowances)
- contract.fica_exempt = True
+ 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')
@@ -357,12 +335,5 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
-
- ss_wages = cats.get('WAGE_US_FICA_SS', 0.0)
- med_wages = cats.get('WAGE_US_FICA_M', 0.0)
- ss = cats.get('EE_US_FICA_SS', 0.0)
- med = cats.get('EE_US_FICA_M', 0.0)
- self.assertPayrollEqual(ss_wages, 0.0)
- self.assertPayrollEqual(med_wages, 0.0)
- self.assertPayrollEqual(ss, 0.0)
- self.assertPayrollEqual(med, 0.0)
+ self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_payslip_2020.py
new file mode 100644
index 00000000..c10a230b
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_payslip_2020.py
@@ -0,0 +1,302 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+from sys import float_info
+
+
+class TestUsPayslip2020(TestUsPayslip):
+ # FUTA Constants
+ FUTA_RATE_NORMAL = 0.6
+ FUTA_RATE_BASIC = 6.0
+ FUTA_RATE_EXEMPT = 0.0
+
+ # Wage caps
+ FICA_SS_MAX_WAGE = 137700.0
+ FICA_M_MAX_WAGE = float_info.max
+ FICA_M_ADD_START_WAGE = 200000.0
+ FUTA_MAX_WAGE = 7000.0
+
+ # Rates
+ FICA_SS = 6.2 / -100.0
+ FICA_M = 1.45 / -100.0
+ FUTA = FUTA_RATE_NORMAL / -100.0
+ FICA_M_ADD = 0.9 / -100.0
+
+ ###
+ # 2020 Taxes and Rates
+ ###
+
+ def test_2020_taxes(self):
+ self.debug = False
+ # salary is high so that second payslip runs over max
+ # social security salary
+ salary = 80000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee, wage=salary)
+ self._log(contract.read())
+
+ self._log('2019 tax last slip')
+ payslip = self._createPayslip(employee, '2019-12-01', '2019-12-31')
+ payslip.compute_sheet()
+ self._log(payslip.read())
+ process_payslip(payslip)
+
+ # Ensure amounts are there, they shouldn't be added in the next year...
+ cats = self._getCategories(payslip)
+ self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
+
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ # Employee
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
+ # Employer
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have reached Medicare Additional (employee only)
+ remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
+ remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
+
+ self._log('2020 tax second payslip:')
+ payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have reached Medicare Additional (employee only)
+ self._log('2020 tax third payslip:')
+ payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have all salary as Medicare Additional
+
+ self._log('2020 tax fourth payslip:')
+ payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD)
+
+ process_payslip(payslip)
+
+ def test_2020_taxes_with_external(self):
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ self._createContract(employee, wage=salary, external_wages=external_wages)
+
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD)
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA)
+
+ def test_2020_taxes_with_full_futa(self):
+ futa_rate = self.FUTA_RATE_BASIC / -100.0
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+
+ employee = self._createEmployee()
+
+ self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC)
+
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
+ self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD)
+ self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
+ self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate)
+
+ def test_2020_taxes_with_futa_exempt(self):
+ futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
+
+ # social security salary
+ salary = self.FICA_M_ADD_START_WAGE
+ employee = self._createEmployee()
+ self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT)
+ self._log('2020 tax first payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
+
+ def test_2020_taxes_with_fica_exempt(self):
+ salary = 6000.0
+ schedule_pay = 'bi-weekly'
+ employee = self._createEmployee()
+ contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay)
+ contract.us_payroll_config_id.fed_941_fica_exempt = True
+
+ self._log('2020 tax w4 exempt payslip:')
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
+ self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)
+
+ """
+ For Federal Income Tax Withholding, we are utilizing the calculations from the new IRS Excel calculator.
+ Given that you CAN round, we will round to compare even though we will calculate as close to the penny as possible
+ with the wage * computed_percent method.
+ """
+
+ def _run_test_fit(self,
+ wage=0.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=0.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ expected_standard=0.0,
+ expected_higher=0.0,
+ ):
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien,
+ fed_941_fit_w4_filing_status=filing_status,
+ fed_941_fit_w4_multiple_jobs_higher=False,
+ fed_941_fit_w4_dependent_credit=dependent_credit,
+ fed_941_fit_w4_other_income=other_income,
+ fed_941_fit_w4_deductions=deductions,
+ fed_941_fit_w4_additional_withholding=additional_withholding,
+ )
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_standard)
+
+ contract.us_payroll_config_id.fed_941_fit_w4_multiple_jobs_higher = True
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_higher)
+ return payslip
+
+ def test_2020_fed_income_withholding_single(self):
+ _ = self._run_test_fit(
+ wage=6000.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=100.0,
+ other_income=200.0,
+ deductions=300.0,
+ additional_withholding=400.0,
+ is_nonresident_alien=False,
+ expected_standard=1132.0,
+ expected_higher=1459.0,
+ )
+
+ def test_2020_fed_income_withholding_married_as_single(self):
+ # This is "Head of Household" though the field name is the same for historical reasons.
+ _ = self._run_test_fit(
+ wage=500.0,
+ schedule_pay='weekly',
+ filing_status='married_as_single',
+ dependent_credit=20.0,
+ other_income=30.0,
+ deductions=40.0,
+ additional_withholding=10.0,
+ is_nonresident_alien=False,
+ expected_standard=24.0,
+ expected_higher=45.0,
+ )
+
+ def test_2020_fed_income_withholding_married(self):
+ _ = self._run_test_fit(
+ wage=14000.00,
+ schedule_pay='bi-weekly',
+ filing_status='married',
+ dependent_credit=2500.0,
+ other_income=1200.0,
+ deductions=1000.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ expected_standard=2621.0,
+ expected_higher=3702.0,
+ )
+
+ def test_2020_fed_income_withholding_nonresident_alien(self):
+ # Monthly NRA additional wage is 1033.30
+ # Wage input on IRS Form entered as (3500+1033.30)=4533.30, not 3500.0
+ _ = self._run_test_fit(
+ wage=3500.00,
+ schedule_pay='monthly',
+ filing_status='married',
+ dependent_credit=340.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=True,
+ expected_standard=235.0,
+ expected_higher=391.0,
+ )
+
+ def test_2020_taxes_with_w4_exempt(self):
+ _ = self._run_test_fit(
+ wage=3500.00,
+ schedule_pay='monthly',
+ filing_status='', # Exempt
+ dependent_credit=340.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=True,
+ expected_standard=0.0,
+ expected_higher=0.0,
+ )
diff --git a/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py
new file mode 100755
index 00000000..15e657ae
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py
@@ -0,0 +1,100 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from .common import TestUsPayslip, process_payslip
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+class TestUsTXPayslip(TestUsPayslip):
+ ###
+ # 2019 Taxes and Rates
+ ###
+ TX_UNEMP_MAX_WAGE = 9000.0
+ TX_UNEMP = -2.7 / 100.0
+ TX_OA = 0.0
+ TX_ETIA = -0.1 / 100.0
+
+ def test_2019_taxes(self):
+ salary = 5000.0
+
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('TX'),
+ )
+
+ self._log('2019 Texas tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA'], salary * self.TX_UNEMP)
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA_OA'], salary * self.TX_OA)
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA_ETIA'], salary * self.TX_ETIA)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_tx_unemp_wages = self.TX_UNEMP_MAX_WAGE - salary if (self.TX_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 Texas tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA'], remaining_tx_unemp_wages * self.TX_UNEMP)
+
+ def test_2019_taxes_with_external(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('TX'),
+ external_wages=external_wages,
+ )
+
+ self._log('2019 Texas_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ expected_wage = self.TX_UNEMP_MAX_WAGE - external_wages
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA'], expected_wage * self.TX_UNEMP)
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA_OA'], expected_wage * self.TX_OA)
+ self.assertPayrollEqual(rules['ER_US_TX_SUTA_ETIA'], expected_wage * self.TX_ETIA)
+
+ def test_2019_taxes_with_state_exempt(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('TX'),
+ external_wages=external_wages,
+ futa_type=USHRContract.FUTA_TYPE_BASIC)
+
+ self._log('2019 Texas_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ rules = self._getRules(payslip)
+
+ self.assertPayrollEqual(rules.get('ER_US_TX_SUTA', 0.0), 0.0)
+ self.assertPayrollEqual(rules.get('ER_US_TX_SUTA_OA', 0.0), 0.0)
+ self.assertPayrollEqual(rules.get('ER_US_TX_SUTA_ETIA', 0.0), 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py
new file mode 100755
index 00000000..8dba312c
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py
@@ -0,0 +1,17 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip
+
+class TestUsTXPayslip(TestUsPayslip):
+ ###
+ # 2020 Taxes and Rates
+ ###
+ TX_UNEMP_MAX_WAGE = 9000.0
+ TX_UNEMP = 2.7
+ TX_OA = 0.0
+ TX_ETIA = 0.1
+
+ def test_2020_taxes(self):
+ combined_rate = self.TX_UNEMP + self.TX_OA + self.TX_ETIA
+ self._test_er_suta('TX', combined_rate, date(2020, 1, 1), wage_base=self.TX_UNEMP_MAX_WAGE)
diff --git a/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py
new file mode 100644
index 00000000..b8f14393
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py
@@ -0,0 +1,133 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip, process_payslip
+from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
+
+
+class TestUsVaPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ VA_UNEMP_MAX_WAGE = 8000.0
+ VA_UNEMP = 2.51
+ VA_SIT_DEDUCTION = 4500.0
+ VA_SIT_EXEMPTION = 930.0
+ VA_SIT_OTHER_EXEMPTION = 800.0
+
+ def test_2019_taxes(self):
+ salary = 5000.0
+
+ # For formula from https://www.tax.virginia.gov/withholding-calculator
+ """
+ Key
+ G = Gross Pay for Pay Period P = Pay periods per year
+ A = Annualized gross pay E1 = Personal and Dependent Exemptions
+ T = Annualized taxable income E2 = Age 65 and Over & Blind Exemptions
+ WH = Tax to be withheld for pay period W = Annualized tax to be withheld
+ G x P - [$3000+ (E1 x 930) + (E2 x 800)] = T
+ Calculate W as follows:
+ If T is: W is:
+ Not over $3,000 2% of T
+ Over But Not Over Then
+ $3,000 $5,000 $60 + (3% of excess over $3,000)
+ $5,000 $17,000 $120 + (5% of excess over $5,000)
+ $17,000 $720 + (5.75% of excess over $17,000)
+ W / P = WH
+ """
+ e1 = 2
+ e2 = 0
+ t = salary * 12 - (self.VA_SIT_DEDUCTION + (e1 * self.VA_SIT_EXEMPTION) + (e2 * self.VA_SIT_OTHER_EXEMPTION))
+
+ if t <= 3000:
+ w = 0.02 * t
+ elif t <= 5000:
+ w = 60 + (0.03 * (t - 3000))
+ elif t <= 17000:
+ w = 120 + (0.05 * (t - 5000))
+ else:
+ w = 720 + (0.0575 * (t - 17000))
+
+ wh = w / 12
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('VA'),
+ va_va4_sit_exemptions=e1,
+ va_va4_sit_other_exemptions=e2
+ )
+
+ # tax rates
+ va_unemp = self.VA_UNEMP / -100.0
+
+ self._log('2019 Virginia tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], salary * va_unemp)
+ self.assertPayrollEqual(cats['EE_US_SIT'], -wh)
+
+ process_payslip(payslip)
+
+ # Make a new payslip, this one will have maximums
+
+ remaining_va_unemp_wages = self.VA_UNEMP_MAX_WAGE - salary if (self.VA_UNEMP_MAX_WAGE - 2*salary < salary) \
+ else salary
+
+ self._log('2019 Virginia tax second payslip:')
+ payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_va_unemp_wages * va_unemp)
+
+ def test_2019_taxes_with_external(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('VA'),
+ external_wages=external_wages,
+ )
+
+ # tax rates
+ va_unemp = self.VA_UNEMP / -100.0
+
+ self._log('2019 Virginia_external tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+ self.assertPayrollEqual(cats['ER_US_SUTA'], (self.VA_UNEMP_MAX_WAGE - external_wages) * va_unemp)
+
+ def test_2019_taxes_with_state_exempt(self):
+ salary = 5000.0
+ external_wages = 6000.0
+
+ employee = self._createEmployee()
+
+ contract = self._createContract(employee,
+ wage=salary,
+ state_id=self.get_us_state('VA'),
+ external_wages=external_wages,
+ futa_type=USHRContract.FUTA_TYPE_BASIC)
+
+ # tax rates
+ self._log('2019 Virginia exempt tax first payslip:')
+ payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
+
+ payslip.compute_sheet()
+
+ cats = self._getCategories(payslip)
+
+ self.assertPayrollEqual(cats['ER_US_SUTA'], 0.0)
diff --git a/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py
new file mode 100644
index 00000000..012e4845
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py
@@ -0,0 +1,116 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from datetime import date
+from .common import TestUsPayslip
+
+
+class TestUsVaPayslip(TestUsPayslip):
+ ###
+ # Taxes and Rates
+ ###
+ VA_UNEMP_MAX_WAGE = 8000.0
+ VA_UNEMP = 2.51
+ VA_SIT_DEDUCTION = 4500.0
+ VA_SIT_EXEMPTION = 930.0
+ VA_SIT_OTHER_EXEMPTION = 800.0
+
+ def _run_test_sit(self,
+ wage=0.0,
+ schedule_pay='monthly',
+ filing_status='single',
+ dependent_credit=0.0,
+ other_income=0.0,
+ deductions=0.0,
+ additional_withholding=0.0,
+ is_nonresident_alien=False,
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=0.0,
+ va_va4_sit_exemptions=0,
+ va_va4_sit_other_exemptions=0,
+ expected=0.0,
+ ):
+ employee = self._createEmployee()
+ contract = self._createContract(employee,
+ wage=wage,
+ schedule_pay=schedule_pay,
+ fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien,
+ fed_941_fit_w4_filing_status=filing_status,
+ fed_941_fit_w4_multiple_jobs_higher=False,
+ fed_941_fit_w4_dependent_credit=dependent_credit,
+ fed_941_fit_w4_other_income=other_income,
+ fed_941_fit_w4_deductions=deductions,
+ fed_941_fit_w4_additional_withholding=additional_withholding,
+ state_income_tax_exempt=state_income_tax_exempt,
+ state_income_tax_additional_withholding=state_income_tax_additional_withholding,
+ va_va4_sit_exemptions=va_va4_sit_exemptions,
+ va_va4_sit_other_exemptions=va_va4_sit_other_exemptions,
+ state_id=self.get_us_state('VA'),
+ )
+ payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
+ payslip.compute_sheet()
+ cats = self._getCategories(payslip)
+ # Instead of PayrollEqual after initial first round of testing.
+ self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected)
+ return payslip
+
+ def test_2020_taxes(self):
+ self._test_er_suta('VA', self.VA_UNEMP, date(2020, 1, 1), wage_base=self.VA_UNEMP_MAX_WAGE)
+
+ salary = 5000.0
+
+ # For formula from https://www.tax.virginia.gov/withholding-calculator
+ e1 = 2
+ e2 = 0
+ t = salary * 12 - (self.VA_SIT_DEDUCTION + (e1 * self.VA_SIT_EXEMPTION) + (e2 * self.VA_SIT_OTHER_EXEMPTION))
+
+ if t <= 3000:
+ w = 0.02 * t
+ elif t <= 5000:
+ w = 60 + (0.03 * (t - 3000))
+ elif t <= 17000:
+ w = 120 + (0.05 * (t - 5000))
+ else:
+ w = 720 + (0.0575 * (t - 17000))
+
+ wh = w / 12
+
+ self._run_test_sit(wage=salary,
+ schedule_pay='monthly',
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=0.0,
+ va_va4_sit_exemptions=e1,
+ va_va4_sit_other_exemptions=e2,
+ expected=wh,)
+ self.assertPayrollEqual(wh, 235.57) # To test against calculator
+
+ # Below expected comes from the calculator linked above
+ self._run_test_sit(wage=450.0,
+ schedule_pay='weekly',
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=0.0,
+ va_va4_sit_exemptions=3,
+ va_va4_sit_other_exemptions=1,
+ expected=12.22,)
+ self._run_test_sit(wage=2500.0,
+ schedule_pay='bi-weekly',
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=0.0,
+ va_va4_sit_exemptions=1,
+ va_va4_sit_other_exemptions=0,
+ expected=121.84,)
+ self._run_test_sit(wage=10000.0,
+ schedule_pay='semi-monthly',
+ state_income_tax_exempt=False,
+ state_income_tax_additional_withholding=100.0,
+ va_va4_sit_exemptions=0,
+ va_va4_sit_other_exemptions=1,
+ expected=651.57,)
+
+ # Test exempt
+ self._run_test_sit(wage=2400.0,
+ schedule_pay='monthly',
+ state_income_tax_exempt=True,
+ state_income_tax_additional_withholding=0.0,
+ va_va4_sit_exemptions=1,
+ va_va4_sit_other_exemptions=1,
+ expected=0.0,)
diff --git a/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py
new file mode 100755
index 00000000..11cf6138
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py
@@ -0,0 +1,88 @@
+# 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.payroll.rate'].create({
+ 'name': 'Test LNI EE',
+ 'code': 'test_lni_ee',
+ 'date_from': date(2019, 1, 1),
+ 'parameter_value': str(self.test_ee_lni * 100),
+ })
+ self.parameter_lni_er = self.env['hr.payroll.rate'].create({
+ 'name': 'Test LNI ER',
+ 'code': 'test_lni_er',
+ '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..9272eba0
--- /dev/null
+++ b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2020.py
@@ -0,0 +1,86 @@
+# 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.payroll.rate'].create({
+ 'name': 'Test LNI EE',
+ 'code': 'test_lni_ee',
+ 'date_from': date(2019, 1, 1),
+ 'parameter_value': str(self.test_ee_lni * 100),
+ })
+ self.parameter_lni_er = self.env['hr.payroll.rate'].create({
+ 'name': 'Test LNI ER',
+ 'code': 'test_lni_er',
+ 'date_from': date(2019, 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/hr_contract_views.xml b/l10n_us_hr_payroll/views/hr_contract_views.xml
new file mode 100644
index 00000000..6abe6e14
--- /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/l10n_us_hr_payroll_view.xml b/l10n_us_hr_payroll/views/l10n_us_hr_payroll_view.xml
deleted file mode 100755
index 257508bc..00000000
--- a/l10n_us_hr_payroll/views/l10n_us_hr_payroll_view.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
- hr.contract.form.inherit
- hr.contract
- 20
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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..247309b5
--- /dev/null
+++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml
@@ -0,0 +1,126 @@
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+ State Information and Extra
+
+
+
+
+
+
+ No additional fields.
+
+
+ Form G-4 - State Income Tax
+
+
+
+
+
+
+
+ Form 89-350 - State Income Tax
+
+
+
+
+
+ Form MT-4 - State Income Tax
+
+
+
+
+
+ Form IT-4 - State Income Tax
+
+
+
+
+
+
+
+
+
+ No additional fields.
+
+
+ Form VA-4/VA-4P - State Income Tax
+
+
+
+
+
+
+ No additional fields.
+ Ensure that your Employee and Employer workers' comp code fields are filled in for WA LNI withholding.
+
+
+
+
+
+
+
+
+ hr.contract.us_payroll_config.search
+ hr.contract.us_payroll_config
+
+
+
+
+
+
+
+
+
+
+ Employee Payroll Forms
+ hr.contract.us_payroll_config
+ tree,form
+
+
+ No Forms
+
+
+
+
+
+
diff --git a/l10n_us_mi_hr_payroll/__manifest__.py b/l10n_us_mi_hr_payroll/__manifest__.py
index 8411421f..4d453e71 100755
--- a/l10n_us_mi_hr_payroll/__manifest__.py
+++ b/l10n_us_mi_hr_payroll/__manifest__.py
@@ -24,5 +24,5 @@ USA::Michigan Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_mn_hr_payroll/__manifest__.py b/l10n_us_mn_hr_payroll/__manifest__.py
index 62583b61..37b784d1 100755
--- a/l10n_us_mn_hr_payroll/__manifest__.py
+++ b/l10n_us_mn_hr_payroll/__manifest__.py
@@ -23,5 +23,5 @@ USA - Minnesota Payroll Rules
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_mo_hr_payroll/__manifest__.py b/l10n_us_mo_hr_payroll/__manifest__.py
index 636d550e..061a58af 100755
--- a/l10n_us_mo_hr_payroll/__manifest__.py
+++ b/l10n_us_mo_hr_payroll/__manifest__.py
@@ -24,5 +24,5 @@ USA::Missouri Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_ms_hr_payroll/__manifest__.py b/l10n_us_ms_hr_payroll/__manifest__.py
index 8a370516..f5ad13a6 100755
--- a/l10n_us_ms_hr_payroll/__manifest__.py
+++ b/l10n_us_ms_hr_payroll/__manifest__.py
@@ -24,5 +24,5 @@ USA::Mississippi Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_mt_hr_payroll/__manifest__.py b/l10n_us_mt_hr_payroll/__manifest__.py
index 64883a44..3d8c7396 100755
--- a/l10n_us_mt_hr_payroll/__manifest__.py
+++ b/l10n_us_mt_hr_payroll/__manifest__.py
@@ -24,5 +24,5 @@ USA::Montana Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_nc_hr_payroll/__manifest__.py b/l10n_us_nc_hr_payroll/__manifest__.py
index 4960939c..26637bde 100755
--- a/l10n_us_nc_hr_payroll/__manifest__.py
+++ b/l10n_us_nc_hr_payroll/__manifest__.py
@@ -24,5 +24,5 @@ USA::North Carolina Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_nj_hr_payroll/__manifest__.py b/l10n_us_nj_hr_payroll/__manifest__.py
index d93a3b11..89584029 100755
--- a/l10n_us_nj_hr_payroll/__manifest__.py
+++ b/l10n_us_nj_hr_payroll/__manifest__.py
@@ -28,5 +28,5 @@ USA::New Jersey Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_oh_hr_payroll/__manifest__.py b/l10n_us_oh_hr_payroll/__manifest__.py
index ea602ecd..49fc01d2 100755
--- a/l10n_us_oh_hr_payroll/__manifest__.py
+++ b/l10n_us_oh_hr_payroll/__manifest__.py
@@ -25,5 +25,5 @@ USA::Ohio Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_pa_hr_payroll/__manifest__.py b/l10n_us_pa_hr_payroll/__manifest__.py
index 3f124969..0cec3566 100755
--- a/l10n_us_pa_hr_payroll/__manifest__.py
+++ b/l10n_us_pa_hr_payroll/__manifest__.py
@@ -25,5 +25,5 @@ USA::Pennsylvania Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_tx_hr_payroll/__manifest__.py b/l10n_us_tx_hr_payroll/__manifest__.py
index fbc60187..650444a3 100755
--- a/l10n_us_tx_hr_payroll/__manifest__.py
+++ b/l10n_us_tx_hr_payroll/__manifest__.py
@@ -25,5 +25,5 @@ USA::Texas Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}
diff --git a/l10n_us_wa_hr_payroll/__manifest__.py b/l10n_us_wa_hr_payroll/__manifest__.py
index 2fc72921..f20702be 100755
--- a/l10n_us_wa_hr_payroll/__manifest__.py
+++ b/l10n_us_wa_hr_payroll/__manifest__.py
@@ -25,5 +25,5 @@ USA::Washington Payroll Rules.
'data/rules.xml',
'data/final.xml',
],
- 'installable': True
+ 'installable': False
}