From 2785672a8e5477f37118a0629993c5d7ad98cc2e Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sun, 5 Jan 2020 19:07:55 -0800 Subject: [PATCH 01/12] MIG `l10n_us_hr_payroll` Major refactor and Federal 2020 Rules (new W4 Form) --- hr_payroll_rate/models/payroll.py | 27 +- hr_payroll_rate/tests/test_payroll_rate.py | 9 + hr_payroll_rate/views/payroll_views.xml | 1 + l10n_us_hr_payroll/__init__.py | 2 + l10n_us_hr_payroll/__manifest__.py | 19 +- l10n_us_hr_payroll/data/base.xml | 83 +- .../data/federal/fed_940_futa_parameters.xml | 26 + .../data/federal/fed_940_futa_rules.xml | 39 + .../data/federal/fed_941_fica_parameters.xml | 66 + .../data/federal/fed_941_fica_rules.xml | 101 ++ .../data/federal/fed_941_fit_parameters.xml | 492 ++++++++ .../data/federal/fed_941_fit_rules.xml | 28 + l10n_us_hr_payroll/data/final.xml | 29 +- l10n_us_hr_payroll/data/integration_rules.xml | 27 + l10n_us_hr_payroll/data/rates.xml | 68 -- l10n_us_hr_payroll/data/rules.xml | 1058 ----------------- l10n_us_hr_payroll/models/__init__.py | 6 +- l10n_us_hr_payroll/models/federal/__init__.py | 1 + l10n_us_hr_payroll/models/federal/fed_940.py | 37 + l10n_us_hr_payroll/models/federal/fed_941.py | 239 ++++ l10n_us_hr_payroll/models/hr_contract.py | 24 + l10n_us_hr_payroll/models/hr_payslip.py | 213 ++++ .../models/l10n_us_hr_payroll.py | 44 - .../models/us_payroll_config.py | 45 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 8776 bytes l10n_us_hr_payroll/tests/__init__.py | 6 +- l10n_us_hr_payroll/tests/common.py | 155 +++ l10n_us_hr_payroll/tests/test_us_payslip.py | 117 -- .../tests/test_us_payslip_2018.py | 368 ------ .../tests/test_us_payslip_2019.py | 239 ++-- .../tests/test_us_payslip_2020.py | 302 +++++ .../views/hr_contract_views.xml | 19 + .../views/l10n_us_hr_payroll_view.xml | 35 - .../views/us_payroll_config_views.xml | 75 ++ 35 files changed, 2069 insertions(+), 1933 deletions(-) mode change 100755 => 100644 l10n_us_hr_payroll/data/base.xml create mode 100644 l10n_us_hr_payroll/data/federal/fed_940_futa_parameters.xml create mode 100644 l10n_us_hr_payroll/data/federal/fed_940_futa_rules.xml create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fica_parameters.xml create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fica_rules.xml create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fit_parameters.xml create mode 100644 l10n_us_hr_payroll/data/federal/fed_941_fit_rules.xml mode change 100755 => 100644 l10n_us_hr_payroll/data/final.xml create mode 100644 l10n_us_hr_payroll/data/integration_rules.xml delete mode 100644 l10n_us_hr_payroll/data/rates.xml delete mode 100755 l10n_us_hr_payroll/data/rules.xml create mode 100644 l10n_us_hr_payroll/models/federal/__init__.py create mode 100644 l10n_us_hr_payroll/models/federal/fed_940.py create mode 100644 l10n_us_hr_payroll/models/federal/fed_941.py create mode 100644 l10n_us_hr_payroll/models/hr_contract.py create mode 100644 l10n_us_hr_payroll/models/hr_payslip.py delete mode 100755 l10n_us_hr_payroll/models/l10n_us_hr_payroll.py create mode 100644 l10n_us_hr_payroll/models/us_payroll_config.py create mode 100644 l10n_us_hr_payroll/security/ir.model.access.csv create mode 100644 l10n_us_hr_payroll/static/description/icon.png create mode 100755 l10n_us_hr_payroll/tests/common.py delete mode 100755 l10n_us_hr_payroll/tests/test_us_payslip.py delete mode 100755 l10n_us_hr_payroll/tests/test_us_payslip_2018.py mode change 100755 => 100644 l10n_us_hr_payroll/tests/test_us_payslip_2019.py create mode 100644 l10n_us_hr_payroll/tests/test_us_payslip_2020.py create mode 100644 l10n_us_hr_payroll/views/hr_contract_views.xml delete mode 100755 l10n_us_hr_payroll/views/l10n_us_hr_payroll_view.xml create mode 100644 l10n_us_hr_payroll/views/us_payroll_config_views.xml 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_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..e32bde9c 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,19 @@ 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/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..75cafa13 --- a/l10n_us_hr_payroll/data/base.xml +++ b/l10n_us_hr_payroll/data/base.xml @@ -1,83 +1,4 @@ - + - - - - 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: 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: US FICA Medicare - ER_US_FICA_M - - - - 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..b2f700cc --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -1,29 +1,26 @@ - - + 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/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py index ff687165..c6d607ff 100644 --- a/l10n_us_hr_payroll/models/__init__.py +++ b/l10n_us_hr_payroll/models/__init__.py @@ -1 +1,5 @@ -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 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..5a335a65 --- /dev/null +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -0,0 +1,213 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + +from .federal.fed_940 import er_us_940_futa +from .federal.fed_941 import ee_us_941_fica_ss, \ + ee_us_941_fica_m, \ + ee_us_941_fica_m_add,\ + er_us_941_fica_ss, \ + er_us_941_fica_m, \ + ee_us_941_fit + + +class HRPayslip(models.Model): + _inherit = 'hr.payslip' + + # From IRS Publication 15-T or logically (annually, bi-monthly) + PAY_PERIODS_IN_YEAR = { + 'annually': 1, + 'semi-annually': 2, + 'quarterly': 4, + 'bi-monthly': 6, + 'monthly': 12, + 'semi-monthly': 24, + 'bi-weekly': 26, + 'weekly': 52, + 'daily': 260, + } + + def _get_base_local_dict(self): + # 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, + } + + 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/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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py new file mode 100644 index 00000000..ed0e6d73 --- /dev/null +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -0,0 +1,45 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + +FUTA_TYPE_EXEMPT = 'exempt' +FUTA_TYPE_BASIC = 'basic' +FUTA_TYPE_NORMAL = 'normal' + + +class HRContractUSPayrollConfig(models.Model): + _name = 'hr.contract.us_payroll_config' + _description = 'Contract US Payroll Forms' + + name = fields.Char(string="Description") + employee_id = fields.Many2one('hr.employee', string="Employee", required=True) + state_id = fields.Many2one('res.country.state', string="Applied State") + + fed_940_type = fields.Selection([ + (FUTA_TYPE_EXEMPT, 'Exempt (0%)'), + (FUTA_TYPE_NORMAL, 'Normal Net Rate (0.6%)'), + (FUTA_TYPE_BASIC, 'Basic Rate (6%)'), + ], string="Federal Unemployment Tax Type (FUTA)", default='normal') + + fed_941_fica_exempt = fields.Boolean(string='FICA Exempt', help="Exempt from Social Security and " + "Medicare e.g. F1 Student Visa") + + fed_941_fit_w4_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single or Married filing separately'), + ('married', 'Married filing jointly'), + ('married_as_single', 'Head of Household'), + ], string='Federal W4 Filing Status [1(c)]', default='single') + fed_941_fit_w4_allowances = fields.Integer(string='Federal W4 Allowances (before 2020)') + fed_941_fit_w4_is_nonresident_alien = fields.Boolean(string='Federal W4 Is Nonresident Alien') + fed_941_fit_w4_multiple_jobs_higher = fields.Boolean(string='Federal W4 Multiple Jobs Higher [2(c)]', + help='Form W4 (2020+) 2(c) Checkbox. ' + 'Uses Higher Withholding tables.') + fed_941_fit_w4_dependent_credit = fields.Float(string='Federal W4 Dependent Credit [3]', + help='Form W4 (2020+) Line 3') + fed_941_fit_w4_other_income = fields.Float(string='Federal W4 Other Income [4(a)]', + help='Form W4 (2020+) 4(a)') + fed_941_fit_w4_deductions = fields.Float(string='Federal W4 Deductions [4(b)]', + help='Form W4 (2020+) 4(b)') + fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]', + help='Form W4 (2020+) 4(c)') diff --git a/l10n_us_hr_payroll/security/ir.model.access.csv b/l10n_us_hr_payroll/security/ir.model.access.csv new file mode 100644 index 00000000..67a8fa2a --- /dev/null +++ b/l10n_us_hr_payroll/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_contract_us_payroll_config,hr.contract.us_payroll_config,model_hr_contract_us_payroll_config,hr_payroll.group_hr_payroll_manager,1,1,1,1 diff --git a/l10n_us_hr_payroll/static/description/icon.png b/l10n_us_hr_payroll/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2a58a38132bd0b9a86324caa86d0cc6fc145fd4c GIT binary patch literal 8776 zcmai4XH-+$wx)Nb3DQeYst|e!z1K(=klqOd2qg3>M3AafrFRgJ04g9IK|p%%(gXyg z_bR-2&bjy8JMMk&{n*J~Ykuq7-<)&JxyBx2N9jCPy+cGtgn@x^M_o-x5B;z6`y#+Y z|HhLaR$yS@yn`4RBaFdXAX|4=K5IL78+$%qR~XukfgvU93$wO$u}84j*gHbpqyc-4 ztpFB?oixBm1k4YHDcUb6YyB9(l06^PW{vt;}9R6l^ga73|IuO9$7N7thKk#4S_P!7YW#fO!v4y~_`J7?E z-_!qrwe~{0{g>{4WBw-^jrb4t|7Xd+ib2QtzoUJvVgI294F-e%Z@8=Lzo~&El)cf> z`fncm;{k93KbSpG&mQjX}~IcVEaSi@8E&dU9BDMwd~y-5l+$oaej1ie+T|= zlM%%3KP|t>pkBXIX6Ob%KVI5LYx7{p^3y`?uNTznRf-1)<3y4t`432z#_%egO$y0Rdiq5d#5n z5Wg@;n4gy)eehcy9oSCAndK0u0urL4yn=iJ7#N%h zUI{}yO1k&DW$_Z2)D`2pF|1hn8?wAGmzB zYMh_-%9AejE`s%`T$=@=lEG# zoW>{R(K#H!mO$7$d&NuT-E=xv)0=ZO_49sG1Qu0IyJB>4M!Ri0WqGl~Q7WuFF$zeO zL*TRJDZfl_sx5vEh0iW)XPg)dlc3nG-5ZdU58v4#42><~wFgj~t!LGiqp$kG+Un5z z?j0<>oUBc;LJDtKf=GH9zG#Osndq{&o^gaE+V>dRO@1x-pp;I+KA(?LJ1P8G4(r?CkME}LzMVl)(7*9^I8pyQyq29DMkz%{&-;YjrC;hes z^Vc1xn1j_~W(V-1x{80ru=ljdvJt29;|iYMLF zUG(p(;&?Sv-%ZTVKAr!0vU3&SQ*p5(Q`5jZL*WbA%nTeV9jn}{xSTKB`(-#$8GKUC zWBjY_wkxS$k=!vjrzIHWLR@*vy+$RUW4_YYLw=db%|v`?rZ613NQC1Rky?|~e^0$I zMomr^r!T-%_ul88QsMY5F@Xh!qLPBBf)&ED;yZGjcKqXuU5GL{Zqq~Da-zN~(|xI) zo=xFw4stow0Llumb^PN838{fK{^o)@`wz@3;;s+&n!@WAtEgFV)YONEFTOU49UYu4 zs&P8?y9^E(ksfd`w}#tG^WKPXZSKV%)oU;GfHUxdO7$p+=mM{Qi5({RxU=WFusl>C zaBB?~3AvW^86Kg?s6?)-?>A`*LI~8>Gy<>k?$hK9=0b?7osbHpz1lf4$UK)fsfwEL zWQ~c{wgfJ2kr<^ixkuCr8fwKcfm@%m7$hWAzYY^bV%_5rVOj{r=<|k|y`1#Ley*1? zkKe8C5M2njpG~(#G_AN@0xt7)m-ND{POyU~w&*;!ei%0G4aQxc)YS@kLh(Y^nk%lC zyWLiL)>V9+u7V3WqLA2uKW|Ji&M#&6a?MbAZ;8&ny^o(mnGRUqaGr?ZH`#jwlI@lpyW;1iW<522u4|0WE-$c}@3V;=x>tXx~7MXi=Ra43H z(7feOOaufpbl5=^3?NY4)+ZL6?Ddhw39WkPWq&4AvRZdRkwAOz!V_V`&dltUbH~=O z9TVU0qvP_Qf8rMwa|!jru^=B4V~NyZKv(~3Ev7P4&z`s(bCaGHi_*G8!E zSR}{8D;0JDl92bCJ5U>YOZuTb`i>K+!ehb~w_h0e@mfj!sr-l(o0-B7Prvtlh}^6x ze|<``@rh~1j27u$9C*Gk5DPAn5EmqDi?-kSJ(N0Mm5GlX&@EMKUuN-ft0#G|9dvPT z#*7eY)sZf<8#L#y()~kRAREJK=BLGO$dt(SBEFC3VJUVp=DlY&8#ws>1S?CtRM?f2 zVH2ck=K3ZhQySd*3Y<4@L}jw_N?DO)VTjr!NW@Pf$;lUJh+Q@uolGWY<}>Y8vQWr^M+clXt+oB-1B`GK#R2KXXU z>e>O6IU!4Y+2@@Ei~jq5dOcFhPA?W4_VC)}vu-cNUcz8@6nWggGwiv@NPjUp71I7S zfr~6`LUI0jbFU?dS*kXYS*G@Ny;63B@teiQ5aN`wPNSBCHlfaggi#lyQVXESBd9eK>JM;lO{@%Y#4qS}#N60}ER)Lk^T-C=5XjDWU~Kv+ zwaYV|FAex+ukPKfLAa?X^9UB|?HS!0z2CIngv_PA(ZEhLpMJCq921^t)qG3~SMwvIbKyv->5+tVh#sfYYL_IP!Owh0r)L*rKE; zZ2zmUBx$CSMii>_o7a*3MtW~8BfbDHeyCT6v{}d+hHT*VLy9y2#?ArW0{kjv8w5*i z2IgP|DqimLzP6R*dwG5R%3TO6V~ThEaM`WpcG@Bgu6`pW^0V;+^1U!g(_<$!f(^Q@ zh=8xFJktSYa`~J@*v%ElaLgOmt6=CHCLv>m03ZF&9|;hU%=DpASVWM z!-P>W9aKvoZDjm_4(AjBE<$gHBD`OzulQAuA4|mCU&Ys06iDj8Eu({mrLa1%o1rp~y3CU`rs7(@SMsVT#hwbv=wW)aV+7BRZ1k382hFYm- zwfq_VE8;X!Rc8Tq0jQNcJEC#b<6o?fbaKu@|L50KB?}9_Xzh@ zTQBY^$Y%j>e`#}3a}yXYFU(CIZS76B<^0-Vi)G5d#4LZ}eQ<%l`yG9RtbOJf!`7gjT|xJJxDmDdyCR=g%EbCOr?-C1$z7~4fB&H>BH8ZHo@urv z$9ARX0%f0mSJIoA^{q>CdCLW>`OA@N38p|#=F$d2#d)@0OG#HW5&~O|4bMC$R!g%A z#-WkvknrO^W}2*ghgtMVNykW9sn>~am?*Iyy#X!0#d`$edx=Yk?B{;CEDs4hZVtVC z6^7T|TnAf+Jswi9$fx7%(th|Fy04ocypqRfh`WG6zy%x`xdJ?MD0#1G9;r!ZGvvyA z<#Q*HkxZ7@>|hV4<*rk79?*-C5AtHu{bs3&ZUdL3Kc4!VRp=6wqC!XJrNJwOgMsw1 z$*AKFxi5YUA$q!18FW=oGe$J1uzE=HcVB#>@mMgK_Ld$^;oj7^(|z;;kK}2)ybUI{ zGJzSF0OS2WFApiwsU%;$LEyk+O<7kC9)(5$@W! zyt0oGiXV~TocBxfex>6yWIr%3O?+fj-7IwF5K%TtHav0>63T+8JalPYaq@rj}g0Gvg>gX(@gU)(Ps*|D0=qm&P7%851t;Os}!FZCFC~? zLc;o~QX9Bqa(mz;!y2ZUZK{j#kyrS+C_0wolAQc&=G)j92);(lV(>2>)LYmuywygx zAjO6s^mwfNyA5e_+{8o8Upgt`9&AIcb-`L(v_ia@Mo)&1$ZEr3W|r7U*~j@VYCB%B z;d+-2(JKI{A)f#)q781nvB9615oC{uX>j-wUzbC`caCn=xYXBTV!l!q$0hRQ4>}C9 zJB(16D0Si}FLeUM=PSg67(cm!eXIH3-v41r9v_p)lUI1OVLSE~ZlY8g_G(F4@l%$E zrCR`{1-w5*JrvuH>iMqmYCgZK(%P3&zuMmD_T;=PKIth%@EZT!Y?dm=dpoO-@|oBO zmT2g*f4K9+~&V_q8k?0;t zkD^ezP?!8~vuZW0v5dlG6vyf6RXXh3>c=U~##5H@bECeZQQ1lvTDdz?vC=Y%BDwl; z%2n8xO_@3^TEZ!8iClw8T;X^>iYE*fYspJmDWUj#>y5D1gCNdqN5f-wMIJ^FUTAQ4 z+(t*=udhcd`btdK5|;<17qpe*vtPE$WCVkDlAmV4PCCsED@~Nf{JwvnO%J)D=$%8g zyF_RAuml}`yMH&y6Z)P$lB?zm%*$1tX&4;i#I3#6V*-lL1Q>9#Ia7ajJS%V@N?r^Q zI+YXiG$C~mNB+8Q5z8o;LS9zOP2ESL$S=QjKX1q<70KeboaY#M37(igezoJ`xScN2 zkJ>GnrBN<(R3-OK*Wjhj8>i?fGx@@vRQ2A`f;a_AezZhbj0Nhk&oZEAbL=B`T)j7! zAwb`*^j`&LGdG{>c3n%xE} zg1)Xybs<-$)eQO{Yc%O9rnX39aU{pX3(w~Ud9f@0)AHfO&g zS=ddj!{n(K1u?F!;j`qDwPXo#fj+ZIOAk&|cl0`G^>(Lj@}=F1cb)F_KI61v^H`gs z7^3d>$aK(*v3Fq;iQ$H$kXLg0C>?T?EIk#^rrM+H6uTi{=pxt%jpg#4h4#WI`7qFhgqebk+{YKirM;Fp5&UE^c4f)REn#Q6sEf*M0jGD#`!31HWrV1_XX@u=a8x75W435G34ZJW7)Yo=%52Go5wiY}BmIX2u zBg4@(I$3Go==&Ucfk!jCg~&w6(fEe&-qX5RaNkpOUeX4rgB*F5p4PUeP8iTBUt=Ui=zh$t{xuYcE=<|JLY;%`8mt~73>wnle8d*3!H7jq!= z*%48>#fu98&GD=pg*HS>zv`{Wl?f)n!?C=lVu2#cX?1x;7(O>Is^lvAr1(>oNn6ht z^y@n#sy4;%xlCe7w6GWjJnvy!gV2lMWp-F!Ov+&bLfP5mjhBuL&F)vo&nT`QEo(Q=rTnNnyB=y+N z-UYLU16$YWP!;}P4uTuLBk;I5+NrGFDuC^^ zmzZrM(@GS{j2&~I~>HL!=h6jlgFsg4X5I@ zE-UW*97T%+7%xz-H&8K}Ne*M4m&=TKiwkBgZD?VAkSXrs`0?W5pi}PsJGP`JfVhYH zO@2=8p4ZU?@~%fu6<+QsRrtJi0%@;raP=P0I6P3Rc7A^~jl)4no9gog5`WGyIo2si z*~3t(Kse{|O6~*uxv|-LxzOE#hV~tStS77<&lKu=QEV$(4+M|vIw8^Ln)O81kZ=)FLK1*N-p3fcjS(;p00eRw@c=%IA?JmaM8C(h3(8JUOyjbJbkQ zmonAD!p?QjFAbhfGL=ssnQ9bQA?Rqn1r$11wzPX~^|6<_7`V3*F+>gvU6H5IMQ@F^ zIwt?o&|4}euH=9<8k(6x-!t2zMm!qGcy9Y zOEXtQRrB0d{YS_q8c~W}wm8|6FF+acZ`kidJ#xwZ7RN}W2dt^ecIk!`JL1|I5y;XIWv)0qq^Lf*gV?-wb(`62~f=D-CM;_5_}KI?*?_y zLhpI|nC5S&NKghJz6FE8kpHwP&Yf;BePq|8l2&Y5qW;BwfIPRUs8z)_Yt^X@> z?F&6r{tRDSJ%xc*Qv|=85%DREd6Fvp(|~{l51!&rx|#u(>eoIL(*ek@2V6FrSQ_<2 zsn}%^_esAiEBGGZZBt3V$#}9{E0$BEoa;dBq+i+u$|!9z!O0?}vh(wGI z&nyZnOWpLb(V8qC9hPNW%^FYyK|#35!8JcxgPq!tpbXT8Dm%=SPxj0$J%6cIbh%bE zD`?NF-6eKOM5Ssd$BSHPLWw>@K1*W|OOsl-07G3$g3;eUhi3ul+HRJowHtVVY{eQ^ zq7Fi|Qx{7Ut}gE5o3SJ>xSBFrwR(GY3EzF$n%5qa!aW9uP;4?3;kI;55nD@aa1;-- z|Db5|-K(u=eTUU*EiP>+6SMN9gH-xR6My$jDwy3UQ+BX{Y*UN8&--l}^>g_4;%t0} z7N7$dFsjgVk6@cl-uRRT_Wd>YHQtKwGi^-2omJsy_f>(Ps#ZQd7?ogm)GK`d+;3_O zB0Sl8nODZ0F#O$>cql0|T|Gfu{O;O9Pq(r@XjRCW!d}lYP)s7BxIbH)KR+)O9$FR(xyk$-{gU7~l>A>ir z>4W1he6KvIrzaX+w-WJ4_&$2KVy>px#qU!#UUR-Xk~5^T86k^6k>AA?`1qljt)VzC zYsH__ij0Vkra?tfSok(5Vk)ePQ9<$){pulQB^x|sqSrsw;4?Sb(YA>0XJJFkhmBwT zy4V$`Hg=rtb1TE+YuORziW*UnnRSLKv#Abmv)c@k29*#W`ab_zlgY8_uR_0^hgCTU zc6|x0?w$+4j%zfVE_~NqZui;>G>SPDwRcs(S&N`ygA^V9&?lrWJciC%L-w4>)WExD zQ*0SPf!F<)-IkbO{ixrsj%2vy?C+I=KVe%xHC$3{Ydv+FYgpAyjUm{cuNA?*b~N*8 z3_eaKk@-Mcgjhx&w%%&?hxat794JgOaYs%K#{g*qNrO~9KQv=@v+5E%i4r2STq|+Nlv!RvvqFv_~7S`(a8j=nSoZ=qJm&4AVpd`pY!{p zFsY$4@tmy%p1f5}WcD<8|3XnUSz(jM_b?A#7nJuGKGxw?&P4Y#%Cd!$MZX`6iIB0b zF!i%s%igkekI=Vf%8m?%M(?ix`!~ATTY1ZV$bR?l4ybcA7tC37Sq>}s$gy`53&8fT zB#B(G^jr7Z6RCU}lyd)0b1mpb@D;^F{dYJyvY&*`w{P}J>%X@y`;Ik}-VQy?fmjk( zYUC#C!5oGRD>&}R63OvAgoEKkb(3aikB5@Bq_u08#4(XN!v<#`p(eMIw-Q8LpFIqb zuweszf`pNK_C`AUYN`{#qN9rm*wDf4+N_%`Og#b*U2=cwXL=gAIh@$C&a4DD)DH@> z5pqY-YjP+1J*d&rdasGiRbGn+G)ZFbLQHB!Oh!rH8vP_`z=C>s7AIsyPCAu=sJCCp z(r-az$!0%-o&g>wIZP$hvrPQ#dAA5hl4&yM5Ai4fey-w%?uXs`Zum9HaO~~%c4tb; z4$$quVe~a^Jf$umPbt?1EOPTh8pt^^tYE7RN{WKrlngC8y?lJ^|NPJ&Nb&Pgt(c|C zfWI(e?2;=GOFPMFVhn;+eqw-Erl+xG!*XC|dPQ8L=eY~~tS0B*8U2GDey7W7@x>fj zk9yx-+)4|0n~ca5m#gWsVF2@Mp^UpOS-ga{pKb$ERDBl6n|2QwEm|0a&zN^e=RXD? zG7OoVH^orI5ZqJn)zhF1>95{jJcsNMd#YgHk<(AlFWP7pqe}MeFPEt<$2X_Uqo3l) z+B7hRycqZG+i|zQy#( X4vqDeFsl6h0f@TtV + + + + hr.contract.form.inherit + hr.contract + + + + + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/views/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..5b6d0dea --- /dev/null +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -0,0 +1,75 @@ + + + + hr.contract.us_payroll_config.tree + hr.contract.us_payroll_config + + + + + + + + + + + + + hr.contract.us_payroll_config.form + hr.contract.us_payroll_config + +
+ + + + + + + + +

Form 940 - Federal Unemployment

+ +

Form 941 / W4 - Federal Income Tax

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

+ No Forms +

+
+
+ + +
From a0810a196c68e5c823060b9cfd2158ea3d2279ba Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 6 Jan 2020 16:33:27 -0800 Subject: [PATCH 02/12] IMP `l10n_us_hr_payroll` Initial Migration script for Federal. Deprecate states in 2020 --- l10n_us_ca_hr_payroll/__manifest__.py | 2 +- l10n_us_fl_hr_payroll/__manifest__.py | 2 +- .../12.0.2020.1.0/post-migration.py | 52 +++++++++++++++++ .../migrations/12.0.2020.1.0/pre-migration.py | 29 ++++++++++ l10n_us_hr_payroll/migrations/__init__.py | 1 + l10n_us_hr_payroll/migrations/data.py | 47 +++++++++++++++ l10n_us_hr_payroll/migrations/helper.py | 58 +++++++++++++++++++ l10n_us_mi_hr_payroll/__manifest__.py | 2 +- l10n_us_mn_hr_payroll/__manifest__.py | 2 +- l10n_us_mo_hr_payroll/__manifest__.py | 2 +- l10n_us_ms_hr_payroll/__manifest__.py | 2 +- l10n_us_mt_hr_payroll/__manifest__.py | 2 +- l10n_us_nc_hr_payroll/__manifest__.py | 2 +- l10n_us_nj_hr_payroll/__manifest__.py | 2 +- l10n_us_oh_hr_payroll/__manifest__.py | 2 +- l10n_us_pa_hr_payroll/__manifest__.py | 2 +- l10n_us_tx_hr_payroll/__manifest__.py | 2 +- l10n_us_wa_hr_payroll/__manifest__.py | 2 +- 18 files changed, 200 insertions(+), 13 deletions(-) create mode 100644 l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py create mode 100644 l10n_us_hr_payroll/migrations/12.0.2020.1.0/pre-migration.py create mode 100644 l10n_us_hr_payroll/migrations/__init__.py create mode 100644 l10n_us_hr_payroll/migrations/data.py create mode 100644 l10n_us_hr_payroll/migrations/helper.py 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/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..752751db --- /dev/null +++ b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py @@ -0,0 +1,52 @@ +# 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 +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') + + # 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? + old_struct_code = contract.struct_id.code + 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, + }) + 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) 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..25bb93d5 --- /dev/null +++ b/l10n_us_hr_payroll/migrations/data.py @@ -0,0 +1,47 @@ +# 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', + +} + +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', + +] + +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', + +} diff --git a/l10n_us_hr_payroll/migrations/helper.py b/l10n_us_hr_payroll/migrations/helper.py new file mode 100644 index 00000000..b41cf304 --- /dev/null +++ b/l10n_us_hr_payroll/migrations/helper.py @@ -0,0 +1,58 @@ +# 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 {} + return {k.lstrip(TMP_PREFIX): 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_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 } From bad14c52af9550ee6477dd50039380ce123dc055 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 7 Jan 2020 12:08:23 -0800 Subject: [PATCH 03/12] IMP `l10n_us_hr_payroll` Port `l10n_us_fl_hr_payroll` FL Florida including migration. --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/base.xml | 12 +++ l10n_us_hr_payroll/data/final.xml | 2 + l10n_us_hr_payroll/data/state/fl_florida.xml | 61 +++++++++++++ .../12.0.2020.1.0/post-migration.py | 19 +++++ l10n_us_hr_payroll/migrations/data.py | 10 ++- l10n_us_hr_payroll/models/hr_payslip.py | 2 + l10n_us_hr_payroll/models/state/__init__.py | 1 + l10n_us_hr_payroll/models/state/general.py | 85 +++++++++++++++++++ .../models/us_payroll_config.py | 1 + l10n_us_hr_payroll/tests/__init__.py | 3 + l10n_us_hr_payroll/tests/common.py | 75 ++++++++++++++++ .../tests/test_us_fl_florida_payslip_2019.py | 82 ++++++++++++++++++ .../tests/test_us_fl_florida_payslip_2020.py | 14 +++ .../views/us_payroll_config_views.xml | 4 + 15 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 l10n_us_hr_payroll/data/state/fl_florida.xml create mode 100644 l10n_us_hr_payroll/models/state/__init__.py create mode 100644 l10n_us_hr_payroll/models/state/general.py create mode 100755 l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index e32bde9c..2b656521 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -28,6 +28,7 @@ USA Payroll Rules. 'data/federal/fed_941_fica_rules.xml', 'data/federal/fed_941_fit_parameters.xml', 'data/federal/fed_941_fit_rules.xml', + 'data/state/fl_florida.xml', 'data/final.xml', 'views/hr_contract_views.xml', 'views/us_payroll_config_views.xml', diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml index 75cafa13..db3e707e 100644 --- a/l10n_us_hr_payroll/data/base.xml +++ b/l10n_us_hr_payroll/data/base.xml @@ -1,4 +1,16 @@ + + + EE: State Unemployment SUTA + EE_US_SUTA + + + + ER: State Unemployment SUTA + ER_US_SUTA + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index b2f700cc..2ab128c0 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -16,6 +16,8 @@ ref('hr_payroll_rule_ee_fed_941_fit'), + ref('hr_payroll_rule_er_us_fl_suta'), + ref('hr_salary_rule_commission'), ref('hr_salary_rule_gamification'), ])]" name="rule_ids"/> 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/migrations/12.0.2020.1.0/post-migration.py b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py index 752751db..24c8fd0d 100644 --- 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 @@ -24,6 +24,21 @@ def migrate(cr, installed_version): 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: + _logger.warn(' return from cache: ' + str(cache[code])) + 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([ '|', @@ -34,13 +49,17 @@ def migrate(cr, installed_version): 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({ diff --git a/l10n_us_hr_payroll/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index 25bb93d5..6d87366e 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -27,7 +27,10 @@ XMLIDS_TO_REMOVE_2020 = [ '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', ] XMLIDS_TO_RENAME_2020 = { @@ -43,5 +46,8 @@ XMLIDS_TO_RENAME_2020 = { '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', } diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 5a335a65..ed37490d 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -9,6 +9,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ er_us_941_fica_ss, \ er_us_941_fica_m, \ ee_us_941_fit +from .state.general import general_state_unemployment class HRPayslip(models.Model): @@ -38,6 +39,7 @@ class HRPayslip(models.Model): 'er_us_941_fica_ss': er_us_941_fica_ss, 'er_us_941_fica_m': er_us_941_fica_m, 'ee_us_941_fit': ee_us_941_fit, + 'general_state_unemployment': general_state_unemployment, } def get_year(self): diff --git a/l10n_us_hr_payroll/models/state/__init__.py b/l10n_us_hr_payroll/models/state/__init__.py new file mode 100644 index 00000000..0358305d --- /dev/null +++ b/l10n_us_hr_payroll/models/state/__init__.py @@ -0,0 +1 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py new file mode 100644 index 00000000..88acba56 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/general.py @@ -0,0 +1,85 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. +from odoo.exceptions import UserError + +# import logging +# _logger = logging.getLogger(__name__) + + +def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): + """ + Returns SUTA eligible wage and rate. + WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT + + The contract's `futa_type` determines if SUTA should be collected. + + Function parameters: + wage_base, wage_start, rate can either be strings (rule_parameters) or floats + + :return: result, result_rate (wage, percent) + """ + + if state_code != payslip.dict.contract_id.us_payroll_config_value('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 + + # 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 + + # 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 + #_logger.warn('ytd_wage: ' + str(ytd_wage) + ' wage: ' + str(wage)) + + if wage_base: + remaining = wage_base - ytd_wage + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + #_logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) + return result, rate + if wage_start: + if ytd_wage >= wage_start: + #_logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate + if ytd_wage + wage <= wage_start: + #_logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) + return 0.0, 0.0 + #_logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) + return (wage - (wage_start - ytd_wage)), rate + + # If the wage doesn't have a start or a base + #_logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index ed0e6d73..d87ebcac 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -14,6 +14,7 @@ class HRContractUSPayrollConfig(models.Model): name = fields.Char(string="Description") employee_id = fields.Many2one('hr.employee', string="Employee", required=True) state_id = fields.Many2one('res.country.state', string="Applied State") + state_code = fields.Char(related='state_id.code') fed_940_type = fields.Selection([ (FUTA_TYPE_EXEMPT, 'Exempt (0%)'), diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 23702419..10545ad0 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -3,3 +3,6 @@ from . import common from . import test_us_payslip_2019 from . import test_us_payslip_2020 + +from . import test_us_fl_florida_payslip_2019 +from . import test_us_fl_florida_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py index f6989e67..78a3d1f5 100755 --- a/l10n_us_hr_payroll/tests/common.py +++ b/l10n_us_hr_payroll/tests/common.py @@ -3,9 +3,11 @@ from logging import getLogger from sys import float_info as sys_float_info from collections import defaultdict +from datetime import timedelta from odoo.tests import common from odoo.tools.float_utils import float_round as odoo_float_round +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract def process_payslip(payslip): @@ -61,6 +63,10 @@ class TestUsPayslip(common.TransactionCase): 'employee_id': employee.id, } + # Backwards compatability with 'futa_type' + if 'futa_type' in kwargs: + kwargs['fed_940_type'] = kwargs['futa_type'] + for key, val in kwargs.items(): # Assume any Odoo object is in a Many2one if hasattr(val, 'id'): @@ -153,3 +159,72 @@ class TestUsPayslip(common.TransactionCase): payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14') payslip.compute_sheet() + + def get_us_state(self, code, cache={}): + country_key = 'US_COUNTRY' + if code in cache: + return cache[code] + if country_key not in cache: + cache[country_key] = self.env.ref('base.us') + us_country = cache[country_key] + us_state = self.env['res.country.state'].search([ + ('country_id', '=', us_country.id), + ('code', '=', code), + ], limit=1) + cache[code] = us_state + return us_state + + def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract): + if wage_base: + # Slightly larger than 1/2 the wage_base + wage = round(wage_base / 2.0) + 100.0 + self.assertTrue((2 * wage) > wage_base, 'Granularity of wage_base too low.') + else: + wage = 1000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + state_id=self.get_us_state(state_code), + **extra_contract) + + rate = -rate / 100.0 # Assumed passed as percent positive + + # Tests + payslip = self._createPayslip(employee, date, date + timedelta(days=30)) + + # Test exemptions + contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_EXEMPT + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) + + contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_BASIC + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) + + # Test Normal + contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_NORMAL + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), wage * rate) + + if wage_base: + process_payslip(payslip) + + remaining_unemp_wages = wage_base - wage + self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative + payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60)) + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), remaining_unemp_wages * rate) + + # As if they were paid once already, so the first "two payslips" would remove all of the tax obligation + # 1 wage - Payslip (confirmed) + # 1 wage - external_wages + # 1 wage - current Payslip + contract.external_wages = wage + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py new file mode 100755 index 00000000..981e9ce0 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py @@ -0,0 +1,82 @@ +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsFlPayslip(TestUsPayslip): + ### + # 2019 Taxes and Rates + ### + FL_UNEMP_MAX_WAGE = 7000.0 + FL_UNEMP = -2.7 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('FL')) + + self._log('2019 Florida tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.FL_UNEMP) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_fl_unemp_wages = self.FL_UNEMP_MAX_WAGE - salary if (self.FL_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Florida tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_fl_unemp_wages * self.FL_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + state_id=self.get_us_state('FL')) + + self._log('2019 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC, + state_id=self.get_us_state('FL')) + + self._log('2019 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py new file mode 100755 index 00000000..b32c1030 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py @@ -0,0 +1,14 @@ +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/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index 5b6d0dea..232f27ab 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -40,6 +40,10 @@
+ + +

No additional fields.

+
From 6e0cc024c6e69d8366714a149a11b7454c194015 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 7 Jan 2020 16:07:40 -0800 Subject: [PATCH 04/12] IMP `l10n_us_hr_payroll` Port `l10n_us_pa_hr_payroll` PA Pennsylvania including migration. --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/base.xml | 7 + l10n_us_hr_payroll/data/final.xml | 3 + .../data/state/pa_pennsylvania.xml | 126 ++++++++++++++++++ .../12.0.2020.1.0/post-migration.py | 1 - l10n_us_hr_payroll/migrations/data.py | 18 ++- l10n_us_hr_payroll/models/hr_payslip.py | 4 +- l10n_us_hr_payroll/models/state/general.py | 96 +++++++++---- .../models/us_payroll_config.py | 2 + l10n_us_hr_payroll/tests/__init__.py | 2 + l10n_us_hr_payroll/tests/common.py | 31 +++-- .../tests/test_us_fl_florida_payslip_2019.py | 2 + .../tests/test_us_fl_florida_payslip_2020.py | 2 + .../test_us_pa_pennsylvania_payslip_2019.py | 33 +++++ .../test_us_pa_pennsylvania_payslip_2020.py | 43 ++++++ .../tests/test_us_payslip_2019.py | 2 +- .../views/us_payroll_config_views.xml | 4 + 17 files changed, 333 insertions(+), 44 deletions(-) create mode 100644 l10n_us_hr_payroll/data/state/pa_pennsylvania.xml create mode 100755 l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 2b656521..fb47d741 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -29,6 +29,7 @@ USA Payroll Rules. 'data/federal/fed_941_fit_parameters.xml', 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', + 'data/state/pa_pennsylvania.xml', 'data/final.xml', 'views/hr_contract_views.xml', 'views/us_payroll_config_views.xml', diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml index db3e707e..7327729f 100644 --- a/l10n_us_hr_payroll/data/base.xml +++ b/l10n_us_hr_payroll/data/base.xml @@ -13,4 +13,11 @@ + + + EE: State Income Tax Withholding + EE_US_SIT + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index 2ab128c0..d266884a 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -17,6 +17,9 @@ ref('hr_payroll_rule_ee_fed_941_fit'), ref('hr_payroll_rule_er_us_fl_suta'), + ref('hr_payroll_rule_er_us_pa_suta'), + ref('hr_payroll_rule_ee_us_pa_suta'), + ref('hr_payroll_rule_ee_us_pa_sit'), ref('hr_salary_rule_commission'), ref('hr_salary_rule_gamification'), 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/migrations/12.0.2020.1.0/post-migration.py b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py index 24c8fd0d..95984afb 100644 --- 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 @@ -27,7 +27,6 @@ def migrate(cr, installed_version): def get_state(code, cache={}): country_key = 'US_COUNTRY' if code in cache: - _logger.warn(' return from cache: ' + str(cache[code])) return cache[code] if country_key not in cache: cache[country_key] = env.ref('base.us') diff --git a/l10n_us_hr_payroll/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index 6d87366e..99534b57 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -8,7 +8,8 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { 'w4_additional_withholding': 'fed_941_fit_w4_additional_withholding', 'fica_exempt': 'fed_941_fica_exempt', 'futa_type': 'fed_940_type', - + # State + 'pa_additional_withholding': 'state_income_tax_additional_withholding', } XMLIDS_TO_REMOVE_2020 = [ @@ -31,6 +32,14 @@ XMLIDS_TO_REMOVE_2020 = [ '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_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', ] XMLIDS_TO_RENAME_2020 = { @@ -50,4 +59,11 @@ XMLIDS_TO_RENAME_2020 = { '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_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', } diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index ed37490d..90476930 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -9,7 +9,8 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ er_us_941_fica_ss, \ er_us_941_fica_m, \ ee_us_941_fit -from .state.general import general_state_unemployment +from .state.general import general_state_unemployment, \ + general_state_income_withholding class HRPayslip(models.Model): @@ -40,6 +41,7 @@ class HRPayslip(models.Model): 'er_us_941_fica_m': er_us_941_fica_m, 'ee_us_941_fit': ee_us_941_fit, 'general_state_unemployment': general_state_unemployment, + 'general_state_income_withholding': general_state_income_withholding, } def get_year(self): diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py index 88acba56..fd259f0e 100644 --- a/l10n_us_hr_payroll/models/state/general.py +++ b/l10n_us_hr_payroll/models/state/general.py @@ -5,26 +5,17 @@ from odoo.exceptions import UserError # _logger = logging.getLogger(__name__) -def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): +def _state_applies(payslip, state_code): + return state_code == payslip.dict.contract_id.us_payroll_config_value('state_code') + + +def _general_rate(payslip, wage, ytd_wage, wage_base=None, wage_start=None, rate=None): """ - Returns SUTA eligible wage and rate. - WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT - - The contract's `futa_type` determines if SUTA should be collected. - Function parameters: wage_base, wage_start, rate can either be strings (rule_parameters) or floats - - :return: result, result_rate (wage, percent) + :return: result, result_rate(wage, percent) """ - if state_code != payslip.dict.contract_id.us_payroll_config_value('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 - # Resolve parameters. On exception, return (probably missing a year, would rather not have exception) if wage_base and isinstance(wage_base, str): try: @@ -50,15 +41,6 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba # Rate assumed positive percentage! rate = -rate - # Determine Wage - year = payslip.dict.get_year() - ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year + 1) + '-01-01') - ytd_wage -= payslip.sum_category('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01') - ytd_wage += payslip.dict.contract_id.external_wages - - wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT - #_logger.warn('ytd_wage: ' + str(ytd_wage) + ' wage: ' + str(wage)) - if wage_base: remaining = wage_base - ytd_wage if remaining < 0.0: @@ -68,18 +50,74 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba else: result = wage - #_logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) + # _logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) return result, rate if wage_start: if ytd_wage >= wage_start: - #_logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) + # _logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) return wage, rate if ytd_wage + wage <= wage_start: - #_logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) + # _logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) return 0.0, 0.0 - #_logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) + # _logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) return (wage - (wage_start - ytd_wage)), rate # If the wage doesn't have a start or a base - #_logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) + # _logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) return wage, rate + + +def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): + """ + Returns SUTA eligible wage and rate. + WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT + + The contract's `futa_type` determines if SUTA should be collected. + + :return: result, result_rate(wage, percent) + """ + + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + # Determine Eligible. + if payslip.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 SUTA 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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index d87ebcac..e75b5210 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -15,6 +15,8 @@ class HRContractUSPayrollConfig(models.Model): employee_id = fields.Many2one('hr.employee', string="Employee", required=True) state_id = fields.Many2one('res.country.state', string="Applied State") state_code = fields.Char(related='state_id.code') + state_income_tax_exempt = fields.Boolean(string='State Income Tax Exempt') + state_income_tax_additional_withholding = fields.Float(string='State Income Tax Additional Withholding') fed_940_type = fields.Selection([ (FUTA_TYPE_EXEMPT, 'Exempt (0%)'), diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 10545ad0..57f8e5e1 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -6,3 +6,5 @@ from . import test_us_payslip_2020 from . import test_us_fl_florida_payslip_2019 from . import test_us_fl_florida_payslip_2020 +from . import test_us_pa_pennsylvania_payslip_2019 +from . import test_us_pa_pennsylvania_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py index 78a3d1f5..042302bb 100755 --- a/l10n_us_hr_payroll/tests/common.py +++ b/l10n_us_hr_payroll/tests/common.py @@ -174,7 +174,7 @@ class TestUsPayslip(common.TransactionCase): cache[code] = us_state return us_state - def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract): + def _test_suta(self, category, state_code, rate, date, wage_base=None, **extra_contract): if wage_base: # Slightly larger than 1/2 the wage_base wage = round(wage_base / 2.0) + 100.0 @@ -197,28 +197,29 @@ class TestUsPayslip(common.TransactionCase): contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_EXEMPT payslip.compute_sheet() cats = self._getCategories(payslip) - self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) + self.assertPayrollEqual(cats.get(category, 0.0), 0.0) contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_BASIC payslip.compute_sheet() cats = self._getCategories(payslip) - self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) + self.assertPayrollEqual(cats.get(category, 0.0), 0.0) # Test Normal contract.us_payroll_config_id.fed_940_type = USHRContract.FUTA_TYPE_NORMAL payslip.compute_sheet() cats = self._getCategories(payslip) - self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), wage * rate) + self.assertPayrollEqual(cats.get(category, 0.0), wage * rate) + process_payslip(payslip) + + # Second Payslip + payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60)) + payslip.compute_sheet() + cats = self._getCategories(payslip) if wage_base: - process_payslip(payslip) - remaining_unemp_wages = wage_base - wage self.assertTrue((remaining_unemp_wages * rate) <= 0.01) # less than 0.01 because rate is negative - payslip = self._createPayslip(employee, date + timedelta(days=31), date + timedelta(days=60)) - payslip.compute_sheet() - cats = self._getCategories(payslip) - self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), remaining_unemp_wages * rate) + self.assertPayrollEqual(cats.get(category, 0.0), remaining_unemp_wages * rate) # As if they were paid once already, so the first "two payslips" would remove all of the tax obligation # 1 wage - Payslip (confirmed) @@ -227,4 +228,12 @@ class TestUsPayslip(common.TransactionCase): contract.external_wages = wage payslip.compute_sheet() cats = self._getCategories(payslip) - self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) + self.assertPayrollEqual(cats.get(category, 0.0), 0.0) + else: + self.assertPayrollEqual(cats.get(category, 0.0), wage * rate) + + def _test_er_suta(self, state_code, rate, date, wage_base=None, **extra_contract): + self._test_suta('ER_US_SUTA', state_code, rate, date, wage_base=wage_base, **extra_contract) + + def _test_ee_suta(self, state_code, rate, date, wage_base=None, **extra_contract): + self._test_suta('EE_US_SUTA', state_code, rate, date, wage_base=wage_base, **extra_contract) diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py index 981e9ce0..419be377 100755 --- a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from .common import TestUsPayslip, process_payslip from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py index b32c1030..5952eb1f 100755 --- a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from datetime import date from .common import TestUsPayslip diff --git a/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py new file mode 100755 index 00000000..ce7e4fb4 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2019.py @@ -0,0 +1,33 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsPAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + PA_UNEMP_MAX_WAGE = 10000.0 + ER_PA_UNEMP = -3.6890 / 100.0 + EE_PA_UNEMP = -0.06 / 100.0 + PA_INC_WITHHOLD = 3.07 + + def test_2019_taxes(self): + salary = 4166.67 + wh = -127.92 + + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('PA')) + + self._log('2019 Pennsylvania tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['EE_US_SUTA'], cats['GROSS'] * self.EE_PA_UNEMP) + self.assertPayrollEqual(cats['ER_US_SUTA'], cats['GROSS'] * self.ER_PA_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) diff --git a/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py new file mode 100755 index 00000000..3dd3fd27 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_pa_pennsylvania_payslip_2020.py @@ -0,0 +1,43 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsPAPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + PA_UNEMP_MAX_WAGE = 10000.0 + ER_PA_UNEMP = 3.6890 + EE_PA_UNEMP = 0.06 + PA_INC_WITHHOLD = 3.07 + + def test_2020_taxes(self): + self._test_er_suta('PA', self.ER_PA_UNEMP, date(2020, 1, 1), wage_base=self.PA_UNEMP_MAX_WAGE) + self._test_ee_suta('PA', self.EE_PA_UNEMP, date(2020, 1, 1)) + + salary = 4166.67 + wh = -127.92 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('PA')) + + self._log('2019 Pennsylvania tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + # Test Additional + contract.us_payroll_config_id.state_income_tax_additional_withholding = 100.0 + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['EE_US_SIT'], wh - 100.0) + + # Test Exempt + contract.us_payroll_config_id.state_income_tax_exempt = True + payslip.compute_sheet() + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py index e9be35cd..46ab66af 100644 --- a/l10n_us_hr_payroll/tests/test_us_payslip_2019.py +++ b/l10n_us_hr_payroll/tests/test_us_payslip_2019.py @@ -211,7 +211,7 @@ class TestUsPayslip2019(TestUsPayslip): 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 = True + self.debug = False futa_rate = self.FUTA_RATE_BASIC / -100.0 # social security salary salary = self.FICA_M_ADD_START_WAGE diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index 232f27ab..4c6e183c 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -44,6 +44,10 @@

No additional fields.

+ + + + From fa57f71cfaa09e4616a6a13e6ddfbe913590d87e Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 8 Jan 2020 08:42:58 -0800 Subject: [PATCH 05/12] IMP `l10n_us_hr_payroll` Port `l10n_us_mt_hr_payroll` MT Montana including migration. --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 3 + l10n_us_hr_payroll/data/state/mt_montana.xml | 166 ++++++++++++++++++ .../12.0.2020.1.0/post-migration.py | 17 +- l10n_us_hr_payroll/migrations/data.py | 20 +++ l10n_us_hr_payroll/migrations/helper.py | 7 +- l10n_us_hr_payroll/models/hr_payslip.py | 2 + l10n_us_hr_payroll/models/state/general.py | 2 +- l10n_us_hr_payroll/models/state/mt_montana.py | 41 +++++ .../models/us_payroll_config.py | 12 ++ l10n_us_hr_payroll/tests/__init__.py | 2 + .../tests/test_us_mt_montana_payslip_2019.py | 139 +++++++++++++++ .../tests/test_us_mt_montana_payslip_2020.py | 17 ++ .../views/us_payroll_config_views.xml | 6 + 14 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 l10n_us_hr_payroll/data/state/mt_montana.xml create mode 100644 l10n_us_hr_payroll/models/state/mt_montana.py create mode 100755 l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index fb47d741..060f2657 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -29,6 +29,7 @@ USA Payroll Rules. 'data/federal/fed_941_fit_parameters.xml', 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', + 'data/state/mt_montana.xml', 'data/state/pa_pennsylvania.xml', 'data/final.xml', 'views/hr_contract_views.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index d266884a..d8756337 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -17,6 +17,9 @@ ref('hr_payroll_rule_ee_fed_941_fit'), ref('hr_payroll_rule_er_us_fl_suta'), + ref('hr_payroll_rule_er_us_mt_suta'), + ref('hr_payroll_rule_er_us_mt_suta_aft'), + ref('hr_payroll_rule_ee_us_mt_sit'), ref('hr_payroll_rule_er_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_sit'), 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..b757e11d --- /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_suta_sit_rate + { + 'weekly': [ + ( 135.00, 0.0, 1.80), + ( 288.00, 2.0, 4.40), + ( 2308.00, 9.0, 6.00), + ( 'inf', 130.0, 6.60), + ], + 'bi-weekly': [ + ( 269.00, 0.0, 1.80), + ( 577.00, 5.0, 4.40), + ( 4615.00, 18.0, 6.00), + ( 'inf', 261.0, 6.60), + ], + 'semi-monthly': [ + ( 292.00, 0.0, 1.80), + ( 625.00, 5.0, 4.40), + ( 5000.00, 20.0, 6.00), + ( 'inf', 282.0, 6.60), + ], + 'monthly': [ + ( 583.00, 0.0, 1.80), + ( 1250.00, 11.0, 4.40), + ( 10000.00, 40.0, 6.00), + ( 'inf', 565.0, 6.60), + ], + 'annually': [ + ( 7000.00, 0.0, 1.80), + ( 15000.00, 126.0, 4.40), + ( 120000.00, 478.0, 6.00), + ( 'inf', 6778.0, 6.60), + ], + } + + + + + + + US MT Montana SIT Exemption Rate Table + us_mt_suta_sit_exemption_rate + { + 'weekly': 37.0, + 'bi-weekly': 73.0, + 'semi-monthly': 79.0, + 'monthly': 158.0, + 'annually': 1900.0, + } + + + + + + + US Montana - Department of Labor & Industries + + + + US Montana - Department of 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/migrations/12.0.2020.1.0/post-migration.py b/l10n_us_hr_payroll/migrations/12.0.2020.1.0/post-migration.py index 95984afb..3f4499ba 100644 --- 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 @@ -1,6 +1,7 @@ # 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 +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, \ @@ -68,3 +69,17 @@ def migrate(cr, installed_version): 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/data.py b/l10n_us_hr_payroll/migrations/data.py index 99534b57..8a1a475c 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -9,6 +9,9 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { 'fica_exempt': 'fed_941_fica_exempt', 'futa_type': 'fed_940_type', # State + 'mt_mw4_additional_withholding': 'state_income_tax_additional_withholding', + 'mt_mw4_exemptions': 'mt_mw4_sit_exemptions', + 'mt_mw4_exempt': 'mt_mw4_sit_exempt', 'pa_additional_withholding': 'state_income_tax_additional_withholding', } @@ -32,6 +35,10 @@ XMLIDS_TO_REMOVE_2020 = [ '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_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_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', @@ -59,6 +66,12 @@ XMLIDS_TO_RENAME_2020 = { '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_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_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', @@ -66,4 +79,11 @@ XMLIDS_TO_RENAME_2020 = { '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', + +} + +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', + ], } diff --git a/l10n_us_hr_payroll/migrations/helper.py b/l10n_us_hr_payroll/migrations/helper.py index b41cf304..f5a7a87d 100644 --- a/l10n_us_hr_payroll/migrations/helper.py +++ b/l10n_us_hr_payroll/migrations/helper.py @@ -39,7 +39,12 @@ def temp_field_values(cr, table_name, id, field_names): values = cr.dictfetchone() if not values: return {} - return {k.lstrip(TMP_PREFIX): v for k, v in values.items()} + + 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()} """ diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 90476930..9104c69e 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -11,6 +11,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ ee_us_941_fit from .state.general import general_state_unemployment, \ general_state_income_withholding +from .state.mt_montana import mt_montana_state_income_withholding class HRPayslip(models.Model): @@ -42,6 +43,7 @@ class HRPayslip(models.Model): 'ee_us_941_fit': ee_us_941_fit, 'general_state_unemployment': general_state_unemployment, 'general_state_income_withholding': general_state_income_withholding, + 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, } def get_year(self): diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py index fd259f0e..0d11b054 100644 --- a/l10n_us_hr_payroll/models/state/general.py +++ b/l10n_us_hr_payroll/models/state/general.py @@ -96,7 +96,7 @@ def general_state_unemployment(payslip, categories, worked_days, inputs, wage_ba def general_state_income_withholding(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): """ - Returns SUTA eligible wage and rate. + Returns SIT eligible wage and rate. WAGE = GROSS - WAGE_US_941_FIT_EXEMPT :return: result, result_rate (wage, percent) diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py new file mode 100644 index 00000000..fd363301 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/mt_montana.py @@ -0,0 +1,41 @@ +from .general import _state_applies + + +def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs): + #, wage_base = None, wage_start = None, rate = None, state_code = None + """ + Returns SIT eligible wage and rate. + WAGE = GROSS - WAGE_US_941_FIT_EXEMPT + + :return: result, result_rate (wage, percent) + """ + state_code = 'MT' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + if payslip.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_suta_sit_exemption_rate').get(schedule_pay) + withholding_rate = payslip.dict.rule_parameter('us_mt_suta_sit_rate').get(schedule_pay) + if not exemption_rate or not withholding_rate or wage == 0.0: + return 0.0, 0.0 + + adjusted_wage = wage - (exemption_rate * (exemptions or 0)) + withholding = 0.0 + if adjusted_wage > 0.0: + prior_wage_cap = 0.0 + for row in withholding_rate: + wage_cap, base, rate = row + wage_cap = float(wage_cap) # e.g. 'inf' + if adjusted_wage < wage_cap: + withholding = round(base + ((rate / 100.0) * (adjusted_wage - prior_wage_cap))) + break + prior_wage_cap = wage_cap + withholding += additional + return wage, -((withholding / wage) * 100.0) diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index e75b5210..b90af562 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -46,3 +46,15 @@ class HRContractUSPayrollConfig(models.Model): help='Form W4 (2020+) 4(b)') fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]', help='Form W4 (2020+) 4(c)') + + mt_mw4_sit_exemptions = fields.Integer(string='Montana MW-4 Exemptions', + help='MW-4 Box G') + # Don't use the main state_income_tax_exempt because of special meaning and reporting + # Use additional withholding but name it on the form 'MW-4 Box H' + mt_mw4_sit_exempt = fields.Selection([ + ('', 'Not Exempt'), + ('tribe', 'Registered Tribe'), + ('reserve', 'Reserve or National Guard'), + ('north_dakota', 'North Dakota'), + ('montana_for_marriage', 'Montana for Marriage'), + ], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2') diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 57f8e5e1..cc0a4321 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -6,5 +6,7 @@ from . import test_us_payslip_2020 from . import test_us_fl_florida_payslip_2019 from . import test_us_fl_florida_payslip_2020 +from . import test_us_mt_montana_payslip_2019 +from . import test_us_mt_montana_payslip_2020 from . import test_us_pa_pennsylvania_payslip_2019 from . import test_us_pa_pennsylvania_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py new file mode 100755 index 00000000..ff6e2daf --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2019.py @@ -0,0 +1,139 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsMtPayslip(TestUsPayslip): + # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705 + MT_UNEMP = -1.18 / 100.0 + MT_UNEMP_AFT = -0.13 / 100.0 + + def test_2019_taxes_one(self): + # Payroll Period Semi-Monthly example + salary = 550 + mt_mw4_exemptions = 5 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='semi-monthly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * (self.MT_UNEMP + self.MT_UNEMP_AFT)) # New non-combined... + + mt_taxable_income = salary - (79.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 155.0) + self.assertPayrollEqual(mt_withhold, 3.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_two(self): + # Payroll Period Bi-Weekly example + salary = 2950 + mt_mw4_exemptions = 2 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='bi-weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2)) + + # Note!! + # The example calculation uses A = 16 but the actual table describes this as A = 18 + mt_taxable_income = salary - (73.0 * mt_mw4_exemptions) + mt_withhold = round(18 + (0.06 * (mt_taxable_income - 577))) + self.assertPayrollEqual(mt_taxable_income, 2804.0) + self.assertPayrollEqual(mt_withhold, 152.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_three(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], round(salary * (self.MT_UNEMP + self.MT_UNEMP_AFT), 2)) + + mt_taxable_income = salary - (37.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 98.0) + self.assertPayrollEqual(mt_withhold, 2.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold) + + def test_2019_taxes_three_exempt(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + mt_mw4_sit_exempt='reserve', + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2019_taxes_three_additional(self): + # Payroll Period Weekly example + salary = 135 + mt_mw4_exemptions = 1 + mt_mw4_additional_withholding = 20.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MT'), + mt_mw4_sit_exemptions=mt_mw4_exemptions, + state_income_tax_additional_withholding=mt_mw4_additional_withholding, + schedule_pay='weekly') + + self._log('2019 Montana tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + cats = self._getCategories(payslip) + + mt_taxable_income = salary - (37.0 * mt_mw4_exemptions) + mt_withhold = round(0 + (0.018 * (mt_taxable_income - 0))) + self.assertPayrollEqual(mt_taxable_income, 98.0) + self.assertPayrollEqual(mt_withhold, 2.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -mt_withhold + -mt_mw4_additional_withholding) diff --git a/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py new file mode 100755 index 00000000..ec861a0d --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_mt_montana_payslip_2020.py @@ -0,0 +1,17 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsMtPayslip(TestUsPayslip): + # Calculations from https://app.mt.gov/myrevenue/Endpoint/DownloadPdf?yearId=705 + MT_UNEMP_WAGE_MAX = 34100.0 + MT_UNEMP = 1.18 + MT_UNEMP_AFT = 0.13 + + def test_2020_taxes_one(self): + combined_rate = self.MT_UNEMP + self.MT_UNEMP_AFT # Combined for test as they both go to the same category and have the same cap + self._test_er_suta('MT', combined_rate, date(2020, 1, 1), wage_base=self.MT_UNEMP_WAGE_MAX) + + # TODO Montana Incometax rates for 2020 when released diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index 4c6e183c..323f1b92 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -44,6 +44,12 @@

No additional fields.

+ +

Form MT-4 - State Income Tax

+ + + +
From 3896bec5f6dc0d2a5a6197233ba5e0caddba0c01 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 8 Jan 2020 12:50:49 -0800 Subject: [PATCH 06/12] IMP `l10n_us_hr_payroll` Port `l10n_us_oh_hr_payroll` OH Ohio including migration. --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 5 + l10n_us_hr_payroll/data/state/mt_montana.xml | 4 +- l10n_us_hr_payroll/data/state/oh_ohio.xml | 150 ++++++++++++++++++ l10n_us_hr_payroll/migrations/data.py | 20 +++ l10n_us_hr_payroll/models/hr_payslip.py | 2 + l10n_us_hr_payroll/models/state/mt_montana.py | 5 +- l10n_us_hr_payroll/models/state/oh_ohio.py | 44 +++++ .../models/us_payroll_config.py | 4 + l10n_us_hr_payroll/tests/__init__.py | 5 + .../tests/test_us_oh_ohio_payslip_2019.py | 96 +++++++++++ .../tests/test_us_oh_ohio_payslip_2020.py | 108 +++++++++++++ .../views/us_payroll_config_views.xml | 6 + 13 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 l10n_us_hr_payroll/data/state/oh_ohio.xml create mode 100644 l10n_us_hr_payroll/models/state/oh_ohio.py create mode 100755 l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 060f2657..c10cc4e7 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -30,6 +30,7 @@ USA Payroll Rules. 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', 'data/state/mt_montana.xml', + 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', 'data/final.xml', 'views/hr_contract_views.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index d8756337..7eb30e83 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -17,9 +17,14 @@ ref('hr_payroll_rule_ee_fed_941_fit'), ref('hr_payroll_rule_er_us_fl_suta'), + ref('hr_payroll_rule_er_us_mt_suta'), ref('hr_payroll_rule_er_us_mt_suta_aft'), ref('hr_payroll_rule_ee_us_mt_sit'), + + ref('hr_payroll_rule_er_us_oh_suta'), + ref('hr_payroll_rule_ee_us_oh_sit'), + ref('hr_payroll_rule_er_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_sit'), diff --git a/l10n_us_hr_payroll/data/state/mt_montana.xml b/l10n_us_hr_payroll/data/state/mt_montana.xml index b757e11d..7cca142a 100644 --- a/l10n_us_hr_payroll/data/state/mt_montana.xml +++ b/l10n_us_hr_payroll/data/state/mt_montana.xml @@ -50,7 +50,7 @@ US MT Montana SIT Rate Table - us_mt_suta_sit_rate + us_mt_sit_rate { 'weekly': [ ( 135.00, 0.0, 1.80), @@ -90,7 +90,7 @@ US MT Montana SIT Exemption Rate Table - us_mt_suta_sit_exemption_rate + us_mt_sit_exemption_rate { 'weekly': 37.0, 'bi-weekly': 73.0, 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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index 8a1a475c..b5d7d15e 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -12,6 +12,10 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { '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', } @@ -35,10 +39,17 @@ XMLIDS_TO_REMOVE_2020 = [ '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_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', @@ -66,12 +77,21 @@ XMLIDS_TO_RENAME_2020 = { '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_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', diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 9104c69e..e2a885f7 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -12,6 +12,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ from .state.general import general_state_unemployment, \ general_state_income_withholding from .state.mt_montana import mt_montana_state_income_withholding +from .state.oh_ohio import oh_ohio_state_income_withholding class HRPayslip(models.Model): @@ -44,6 +45,7 @@ class HRPayslip(models.Model): 'general_state_unemployment': general_state_unemployment, 'general_state_income_withholding': general_state_income_withholding, 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, + 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, } def get_year(self): diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py index fd363301..3816b318 100644 --- a/l10n_us_hr_payroll/models/state/mt_montana.py +++ b/l10n_us_hr_payroll/models/state/mt_montana.py @@ -2,7 +2,6 @@ from .general import _state_applies def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs): - #, wage_base = None, wage_start = None, rate = None, state_code = None """ Returns SIT eligible wage and rate. WAGE = GROSS - WAGE_US_941_FIT_EXEMPT @@ -21,8 +20,8 @@ def mt_montana_state_income_withholding(payslip, categories, worked_days, inputs schedule_pay = payslip.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_suta_sit_exemption_rate').get(schedule_pay) - withholding_rate = payslip.dict.rule_parameter('us_mt_suta_sit_rate').get(schedule_pay) + 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 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..793d4900 --- /dev/null +++ b/l10n_us_hr_payroll/models/state/oh_ohio.py @@ -0,0 +1,44 @@ +from .general import _state_applies + + +def oh_ohio_state_income_withholding(payslip, categories, worked_days, inputs): + """ + Returns SIT eligible wage and rate. + WAGE = GROSS - WAGE_US_941_FIT_EXEMPT + + :return: result, result_rate (wage, percent) + """ + state_code = 'OH' + if not _state_applies(payslip, state_code): + return 0.0, 0.0 + + if payslip.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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index b90af562..e0d56a6c 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -58,3 +58,7 @@ class HRContractUSPayrollConfig(models.Model): ('north_dakota', 'North Dakota'), ('montana_for_marriage', 'Montana for Marriage'), ], string='Montana MW-4 Exempt from Withholding', help='MW-4 Section 2') + + # Ohio will use generic SIT exempt and additional fields + oh_it4_sit_exemptions = fields.Integer(string='Ohio IT-4 Exemptions', + help='Line 4') diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index cc0a4321..a2953520 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -6,7 +6,12 @@ from . import test_us_payslip_2020 from . import test_us_fl_florida_payslip_2019 from . import test_us_fl_florida_payslip_2020 + from . import test_us_mt_montana_payslip_2019 from . import test_us_mt_montana_payslip_2020 + +from . import test_us_oh_ohio_payslip_2019 +from . import test_us_oh_ohio_payslip_2020 + from . import test_us_pa_pennsylvania_payslip_2019 from . import test_us_pa_pennsylvania_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py new file mode 100755 index 00000000..bf38f4d5 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py @@ -0,0 +1,96 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsOhPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + OH_UNEMP_MAX_WAGE = 9500.0 + OH_UNEMP = -2.7 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + # For formula here + # http://www.tax.ohio.gov/Portals/0/employer_withholding/August2015Rates/WTH_OptionalComputerFormula_073015.pdf + tw = salary * 12 # = 60000 + wd = ((tw - 40000) * 0.035 + 900) / 12 * 1.075 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + ) + + self._log('2019 Ohio tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.OH_UNEMP) + self.assertAlmostEqual(cats['EE_US_SIT'], -wd, 1) # Off by 0.6 cents so it rounds off by a penny + #self.assertPayrollEqual(cats['EE_US_SIT'], -wd) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_oh_unemp_wages = self.OH_UNEMP_MAX_WAGE - salary if (self.OH_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Ohio tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_oh_unemp_wages * self.OH_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + external_wages=external_wages, + ) + + self._log('2019 Ohio_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], (self.OH_UNEMP_MAX_WAGE - external_wages) * self.OH_UNEMP) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('OH'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2019 Ohio exempt tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + # FUTA_TYPE_BASIC + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), salary * 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py new file mode 100755 index 00000000..04256afa --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py @@ -0,0 +1,108 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsOhPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + OH_UNEMP_MAX_WAGE = 9000.0 + OH_UNEMP = 2.7 + + def test_2020_taxes(self): + self._test_er_suta('OH', self.OH_UNEMP, date(2020, 1, 1), wage_base=self.OH_UNEMP_MAX_WAGE) + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=0, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + oh_it4_sit_exemptions=oh_it4_sit_exemptions, + state_id=self.get_us_state('OH'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected, 1) + return payslip + + def test_2020_sit_1(self): + wage = 400.0 + exemptions = 1 + additional = 10.0 + pay_periods = 12.0 + annual_adjusted_wage = (wage * pay_periods) - (650.0 * exemptions) + self.assertPayrollEqual(4150.0, annual_adjusted_wage) + WD = ((annual_adjusted_wage * 0.005) / pay_periods) * 1.032 + self.assertPayrollEqual(WD, 1.7845) + expected = WD + additional + self._run_test_sit(wage=wage, + schedule_pay='monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=additional, + oh_it4_sit_exemptions=exemptions, + expected=expected, + ) + + # the above agrees with online calculator to the penny 0.01 + # below expected coming from calculator to 0.10 + # + # semi-monthly + self._run_test_sit(wage=1200, + schedule_pay='semi-monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=20.0, + oh_it4_sit_exemptions=2, + expected=42.58, + ) + + # bi-weekly + self._run_test_sit(wage=3000, + schedule_pay='bi-weekly', + state_income_tax_exempt=False, + #state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=0, + expected=88.51, + ) + # weekly + self._run_test_sit(wage=355, + schedule_pay='weekly', + state_income_tax_exempt=False, + # state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=1, + expected=4.87, + ) + + # Exempt! + self._run_test_sit(wage=355, + schedule_pay='weekly', + state_income_tax_exempt=True, + # state_income_tax_additional_withholding=0.0, + oh_it4_sit_exemptions=1, + expected=0.0, + ) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index 323f1b92..ee993be7 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -50,6 +50,12 @@ + +

Form IT-4 - State Income Tax

+ + + +
From e2c73da4022e7bb9b729dc5687f60fa90f0c74d7 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 8 Jan 2020 18:56:49 -0800 Subject: [PATCH 07/12] IMP `l10n_us_hr_payroll` Port `l10n_us_wa_hr_payroll` WA Washington including migration + FML rules --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 6 + .../data/state/wa_washington.xml | 203 ++++++++++++++++++ l10n_us_hr_payroll/migrations/data.py | 21 ++ l10n_us_hr_payroll/models/hr_payslip.py | 8 +- l10n_us_hr_payroll/models/state/general.py | 4 + l10n_us_hr_payroll/models/state/mt_montana.py | 2 + l10n_us_hr_payroll/models/state/oh_ohio.py | 2 + .../models/state/wa_washington.py | 27 +++ .../models/us_payroll_config.py | 4 + l10n_us_hr_payroll/tests/__init__.py | 3 + l10n_us_hr_payroll/tests/common.py | 3 + .../tests/test_us_oh_ohio_payslip_2019.py | 2 +- .../tests/test_us_oh_ohio_payslip_2020.py | 2 +- .../test_us_wa_washington_payslip_2019.py | 88 ++++++++ .../test_us_wa_washington_payslip_2020.py | 86 ++++++++ .../views/us_payroll_config_views.xml | 9 +- 17 files changed, 467 insertions(+), 4 deletions(-) create mode 100644 l10n_us_hr_payroll/data/state/wa_washington.xml create mode 100644 l10n_us_hr_payroll/models/state/wa_washington.py create mode 100755 l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index c10cc4e7..f2d7f78d 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -32,6 +32,7 @@ USA Payroll Rules. 'data/state/mt_montana.xml', 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', + 'data/state/wa_washington.xml', 'data/final.xml', 'views/hr_contract_views.xml', 'views/us_payroll_config_views.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index 7eb30e83..6e855bd4 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -29,6 +29,12 @@ ref('hr_payroll_rule_ee_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_sit'), + ref('hr_payroll_rule_er_us_wa_suta'), + ref('hr_payroll_rule_er_us_wa_fml'), + ref('hr_payroll_rule_ee_us_wa_fml'), + ref('hr_payroll_rule_er_us_wa_lni'), + ref('hr_payroll_rule_ee_us_wa_lni'), + ref('hr_salary_rule_commission'), ref('hr_salary_rule_gamification'), ])]" name="rule_ids"/> 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/data.py b/l10n_us_hr_payroll/migrations/data.py index b5d7d15e..c15c3749 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -58,6 +58,13 @@ XMLIDS_TO_REMOVE_2020 = [ '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_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 = { @@ -100,10 +107,24 @@ XMLIDS_TO_RENAME_2020 = { '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_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/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index e2a885f7..5521b078 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -10,9 +10,12 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ er_us_941_fica_m, \ ee_us_941_fit from .state.general import general_state_unemployment, \ - general_state_income_withholding + general_state_income_withholding, \ + is_us_state from .state.mt_montana import mt_montana_state_income_withholding from .state.oh_ohio import oh_ohio_state_income_withholding +from .state.wa_washington import wa_washington_fml_er, \ + wa_washington_fml_ee class HRPayslip(models.Model): @@ -44,8 +47,11 @@ class HRPayslip(models.Model): 'ee_us_941_fit': ee_us_941_fit, 'general_state_unemployment': general_state_unemployment, 'general_state_income_withholding': general_state_income_withholding, + 'is_us_state': is_us_state, 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, + 'wa_washington_fml_er': wa_washington_fml_er, + 'wa_washington_fml_ee': wa_washington_fml_ee, } def get_year(self): diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py index 0d11b054..af2e3931 100644 --- a/l10n_us_hr_payroll/models/state/general.py +++ b/l10n_us_hr_payroll/models/state/general.py @@ -9,6 +9,10 @@ def _state_applies(payslip, state_code): return state_code == payslip.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: diff --git a/l10n_us_hr_payroll/models/state/mt_montana.py b/l10n_us_hr_payroll/models/state/mt_montana.py index 3816b318..b9fa0986 100644 --- a/l10n_us_hr_payroll/models/state/mt_montana.py +++ b/l10n_us_hr_payroll/models/state/mt_montana.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from .general import _state_applies diff --git a/l10n_us_hr_payroll/models/state/oh_ohio.py b/l10n_us_hr_payroll/models/state/oh_ohio.py index 793d4900..8ec52538 100644 --- a/l10n_us_hr_payroll/models/state/oh_ohio.py +++ b/l10n_us_hr_payroll/models/state/oh_ohio.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from .general import _state_applies diff --git a/l10n_us_hr_payroll/models/state/wa_washington.py b/l10n_us_hr_payroll/models/state/wa_washington.py new file mode 100644 index 00000000..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 index e0d56a6c..be1fa946 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -17,6 +17,10 @@ class HRContractUSPayrollConfig(models.Model): state_code = fields.Char(related='state_id.code') state_income_tax_exempt = fields.Boolean(string='State Income Tax Exempt') state_income_tax_additional_withholding = fields.Float(string='State Income Tax Additional Withholding') + workers_comp_ee_code = fields.Char(string='Workers\' Comp Code (Employee Withholding)', + help='Code for a 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%)'), diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index a2953520..7ffc630a 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -15,3 +15,6 @@ from . import test_us_oh_ohio_payslip_2020 from . import test_us_pa_pennsylvania_payslip_2019 from . import test_us_pa_pennsylvania_payslip_2020 + +from . import test_us_wa_washington_payslip_2019 +from . import test_us_wa_washington_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py index 042302bb..338e1d44 100755 --- a/l10n_us_hr_payroll/tests/common.py +++ b/l10n_us_hr_payroll/tests/common.py @@ -151,6 +151,9 @@ class TestUsPayslip(common.TransactionCase): def assertPayrollEqual(self, first, second): self.assertAlmostEqual(first, second, self.payroll_digits) + def assertPayrollAlmostEqual(self, first, second): + self.assertAlmostEqual(first, second, self.payroll_digits-1) + def test_semi_monthly(self): salary = 80000.0 employee = self._createEmployee() diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py index bf38f4d5..d1f65f05 100755 --- a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2019.py @@ -34,7 +34,7 @@ class TestUsOhPayslip(TestUsPayslip): cats = self._getCategories(payslip) self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.OH_UNEMP) - self.assertAlmostEqual(cats['EE_US_SIT'], -wd, 1) # Off by 0.6 cents so it rounds off by a penny + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], -wd) # Off by 0.6 cents so it rounds off by a penny #self.assertPayrollEqual(cats['EE_US_SIT'], -wd) process_payslip(payslip) diff --git a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py index 04256afa..9026da92 100755 --- a/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py +++ b/l10n_us_hr_payroll/tests/test_us_oh_ohio_payslip_2020.py @@ -48,7 +48,7 @@ class TestUsOhPayslip(TestUsPayslip): payslip.compute_sheet() cats = self._getCategories(payslip) # Instead of PayrollEqual after initial first round of testing. - self.assertAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected, 1) + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected) return payslip def test_2020_sit_1(self): diff --git a/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_wa_washington_payslip_2019.py new file mode 100755 index 00000000..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/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index ee993be7..f47e18fe 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -26,7 +26,6 @@ -

Form 940 - Federal Unemployment

Form 941 / W4 - Federal Income Tax

@@ -39,6 +38,10 @@ +

State Information and Extra

+ + +
@@ -60,6 +63,10 @@ + +

No additional fields.

+

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

+
From 6dca2d8a47afcffb8d70ec3a0c8fb79186726f06 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 8 Jan 2020 19:53:02 -0800 Subject: [PATCH 08/12] IMP `l10n_us_hr_payroll` Port `l10n_us_tx_hr_payroll` TX Texas including migration --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 4 + l10n_us_hr_payroll/data/state/tx_texas.xml | 117 ++++++++++++++++++ l10n_us_hr_payroll/migrations/data.py | 13 ++ l10n_us_hr_payroll/tests/__init__.py | 3 + .../tests/test_us_tx_texas_payslip_2019.py | 100 +++++++++++++++ .../tests/test_us_tx_texas_payslip_2020.py | 17 +++ .../views/us_payroll_config_views.xml | 3 + 8 files changed, 258 insertions(+) create mode 100644 l10n_us_hr_payroll/data/state/tx_texas.xml create mode 100755 l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index f2d7f78d..7c651472 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -32,6 +32,7 @@ USA Payroll Rules. 'data/state/mt_montana.xml', 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', + 'data/state/tx_texas.xml', 'data/state/wa_washington.xml', 'data/final.xml', 'views/hr_contract_views.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index 6e855bd4..146fb073 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -29,6 +29,10 @@ ref('hr_payroll_rule_ee_us_pa_suta'), ref('hr_payroll_rule_ee_us_pa_sit'), + ref('hr_payroll_rule_er_us_tx_suta'), + ref('hr_payroll_rule_er_us_tx_suta_oa'), + ref('hr_payroll_rule_er_us_tx_suta_etia'), + ref('hr_payroll_rule_er_us_wa_suta'), ref('hr_payroll_rule_er_us_wa_fml'), ref('hr_payroll_rule_ee_us_wa_fml'), 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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index c15c3749..02b90af2 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -59,6 +59,13 @@ XMLIDS_TO_REMOVE_2020 = [ '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_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', @@ -107,6 +114,12 @@ XMLIDS_TO_RENAME_2020 = { '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_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', diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 7ffc630a..90d6dfef 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -16,5 +16,8 @@ from . import test_us_oh_ohio_payslip_2020 from . import test_us_pa_pennsylvania_payslip_2019 from . import test_us_pa_pennsylvania_payslip_2020 +from . import test_us_tx_texas_payslip_2019 +from . import test_us_tx_texas_payslip_2020 + from . import test_us_wa_washington_payslip_2019 from . import test_us_wa_washington_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py new file mode 100755 index 00000000..15e657ae --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2019.py @@ -0,0 +1,100 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + +class TestUsTXPayslip(TestUsPayslip): + ### + # 2019 Taxes and Rates + ### + TX_UNEMP_MAX_WAGE = 9000.0 + TX_UNEMP = -2.7 / 100.0 + TX_OA = 0.0 + TX_ETIA = -0.1 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('TX'), + ) + + self._log('2019 Texas tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['ER_US_TX_SUTA'], salary * self.TX_UNEMP) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_OA'], salary * self.TX_OA) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_ETIA'], salary * self.TX_ETIA) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_tx_unemp_wages = self.TX_UNEMP_MAX_WAGE - salary if (self.TX_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Texas tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules['ER_US_TX_SUTA'], remaining_tx_unemp_wages * self.TX_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('TX'), + external_wages=external_wages, + ) + + self._log('2019 Texas_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + expected_wage = self.TX_UNEMP_MAX_WAGE - external_wages + self.assertPayrollEqual(rules['ER_US_TX_SUTA'], expected_wage * self.TX_UNEMP) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_OA'], expected_wage * self.TX_OA) + self.assertPayrollEqual(rules['ER_US_TX_SUTA_ETIA'], expected_wage * self.TX_ETIA) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('TX'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + self._log('2019 Texas_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + rules = self._getRules(payslip) + + self.assertPayrollEqual(rules.get('ER_US_TX_SUTA', 0.0), 0.0) + self.assertPayrollEqual(rules.get('ER_US_TX_SUTA_OA', 0.0), 0.0) + self.assertPayrollEqual(rules.get('ER_US_TX_SUTA_ETIA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py new file mode 100755 index 00000000..8dba312c --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_tx_texas_payslip_2020.py @@ -0,0 +1,17 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + +class TestUsTXPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + TX_UNEMP_MAX_WAGE = 9000.0 + TX_UNEMP = 2.7 + TX_OA = 0.0 + TX_ETIA = 0.1 + + def test_2020_taxes(self): + combined_rate = self.TX_UNEMP + self.TX_OA + self.TX_ETIA + self._test_er_suta('TX', combined_rate, date(2020, 1, 1), wage_base=self.TX_UNEMP_MAX_WAGE) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index f47e18fe..fefcc61e 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -63,6 +63,9 @@ + +

No additional fields.

+

No additional fields.

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

From ee16a9963e27efc2d663bc6af6b6e1826a7e9dde Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 8 Jan 2020 20:00:42 -0800 Subject: [PATCH 09/12] Add LICENSE_PROFESSIONAL from Hibou Suite 13 --- LICENSE_PROFESSIONAL | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 LICENSE_PROFESSIONAL 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. From 0919b2cb80195b1c01c1150835db4a5ca0cd21b5 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 9 Jan 2020 09:03:09 -0800 Subject: [PATCH 10/12] IMP `l10n_us_hr_payroll` Port `l10n_us_va_hr_payroll` VA Virginia including migration --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 3 + l10n_us_hr_payroll/data/state/va_virginia.xml | 124 ++++++++++++++++ l10n_us_hr_payroll/migrations/data.py | 15 ++ l10n_us_hr_payroll/models/hr_payslip.py | 2 + .../models/state/va_virginia.py | 43 ++++++ .../models/us_payroll_config.py | 5 + l10n_us_hr_payroll/tests/__init__.py | 3 + .../tests/test_us_va_virginia_payslip_2019.py | 133 ++++++++++++++++++ .../tests/test_us_va_virginia_payslip_2020.py | 116 +++++++++++++++ .../views/us_payroll_config_views.xml | 7 + 11 files changed, 452 insertions(+) create mode 100644 l10n_us_hr_payroll/data/state/va_virginia.xml create mode 100644 l10n_us_hr_payroll/models/state/va_virginia.py create mode 100644 l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py create mode 100644 l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 7c651472..6aef7df9 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -33,6 +33,7 @@ USA Payroll Rules. 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', 'data/state/tx_texas.xml', + 'data/state/va_virginia.xml', 'data/state/wa_washington.xml', 'data/final.xml', 'views/hr_contract_views.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index 146fb073..6d8ea0f4 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -33,6 +33,9 @@ ref('hr_payroll_rule_er_us_tx_suta_oa'), ref('hr_payroll_rule_er_us_tx_suta_etia'), + ref('hr_payroll_rule_er_us_va_suta'), + ref('hr_payroll_rule_ee_us_va_sit'), + ref('hr_payroll_rule_er_us_wa_suta'), ref('hr_payroll_rule_er_us_wa_fml'), ref('hr_payroll_rule_ee_us_wa_fml'), 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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index 02b90af2..0c426c2a 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -17,6 +17,9 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { '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 = [ @@ -66,6 +69,11 @@ XMLIDS_TO_REMOVE_2020 = [ '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', @@ -120,6 +128,13 @@ XMLIDS_TO_RENAME_2020 = { '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', diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 5521b078..9b7e2f78 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -14,6 +14,7 @@ from .state.general import general_state_unemployment, \ is_us_state from .state.mt_montana import mt_montana_state_income_withholding from .state.oh_ohio import oh_ohio_state_income_withholding +from .state.va_virginia import va_virginia_state_income_withholding from .state.wa_washington import wa_washington_fml_er, \ wa_washington_fml_ee @@ -50,6 +51,7 @@ class HRPayslip(models.Model): 'is_us_state': is_us_state, 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, + 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, 'wa_washington_fml_er': wa_washington_fml_er, 'wa_washington_fml_ee': wa_washington_fml_ee, } diff --git a/l10n_us_hr_payroll/models/state/va_virginia.py b/l10n_us_hr_payroll/models/state/va_virginia.py new file mode 100644 index 00000000..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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index be1fa946..befbb3b2 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -66,3 +66,8 @@ class HRContractUSPayrollConfig(models.Model): # Ohio will use generic SIT exempt and additional fields oh_it4_sit_exemptions = fields.Integer(string='Ohio IT-4 Exemptions', help='Line 4') + + va_va4_sit_exemptions = fields.Integer(string='Virginia VA-4(P) Personal Exemptions', + help='VA-4(P) 1(a)') + va_va4_sit_other_exemptions = fields.Integer(string='Virginia VA-4(P) Age & Blindness Exemptions', + help='VA-4(P) 1(b)') diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 90d6dfef..ef846086 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -19,5 +19,8 @@ from . import test_us_pa_pennsylvania_payslip_2020 from . import test_us_tx_texas_payslip_2019 from . import test_us_tx_texas_payslip_2020 +from . import test_us_va_virginia_payslip_2019 +from . import test_us_va_virginia_payslip_2020 + from . import test_us_wa_washington_payslip_2019 from . import test_us_wa_washington_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py new file mode 100644 index 00000000..b8f14393 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2019.py @@ -0,0 +1,133 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsVaPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + VA_UNEMP_MAX_WAGE = 8000.0 + VA_UNEMP = 2.51 + VA_SIT_DEDUCTION = 4500.0 + VA_SIT_EXEMPTION = 930.0 + VA_SIT_OTHER_EXEMPTION = 800.0 + + def test_2019_taxes(self): + salary = 5000.0 + + # For formula from https://www.tax.virginia.gov/withholding-calculator + """ + Key + G = Gross Pay for Pay Period P = Pay periods per year + A = Annualized gross pay E1 = Personal and Dependent Exemptions + T = Annualized taxable income E2 = Age 65 and Over & Blind Exemptions + WH = Tax to be withheld for pay period W = Annualized tax to be withheld + G x P - [$3000+ (E1 x 930) + (E2 x 800)] = T + Calculate W as follows: + If T is: W is: + Not over $3,000 2% of T + Over But Not Over Then + $3,000 $5,000 $60 + (3% of excess over $3,000) + $5,000 $17,000 $120 + (5% of excess over $5,000) + $17,000 $720 + (5.75% of excess over $17,000) + W / P = WH + """ + e1 = 2 + e2 = 0 + t = salary * 12 - (self.VA_SIT_DEDUCTION + (e1 * self.VA_SIT_EXEMPTION) + (e2 * self.VA_SIT_OTHER_EXEMPTION)) + + if t <= 3000: + w = 0.02 * t + elif t <= 5000: + w = 60 + (0.03 * (t - 3000)) + elif t <= 17000: + w = 120 + (0.05 * (t - 5000)) + else: + w = 720 + (0.0575 * (t - 17000)) + + wh = w / 12 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('VA'), + va_va4_sit_exemptions=e1, + va_va4_sit_other_exemptions=e2 + ) + + # tax rates + va_unemp = self.VA_UNEMP / -100.0 + + self._log('2019 Virginia tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * va_unemp) + self.assertPayrollEqual(cats['EE_US_SIT'], -wh) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_va_unemp_wages = self.VA_UNEMP_MAX_WAGE - salary if (self.VA_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Virginia tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_va_unemp_wages * va_unemp) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('VA'), + external_wages=external_wages, + ) + + # tax rates + va_unemp = self.VA_UNEMP / -100.0 + + self._log('2019 Virginia_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats['ER_US_SUTA'], (self.VA_UNEMP_MAX_WAGE - external_wages) * va_unemp) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('VA'), + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC) + + # tax rates + self._log('2019 Virginia exempt tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py new file mode 100644 index 00000000..012e4845 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_va_virginia_payslip_2020.py @@ -0,0 +1,116 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsVaPayslip(TestUsPayslip): + ### + # Taxes and Rates + ### + VA_UNEMP_MAX_WAGE = 8000.0 + VA_UNEMP = 2.51 + VA_SIT_DEDUCTION = 4500.0 + VA_SIT_EXEMPTION = 930.0 + VA_SIT_OTHER_EXEMPTION = 800.0 + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=0, + va_va4_sit_other_exemptions=0, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + va_va4_sit_exemptions=va_va4_sit_exemptions, + va_va4_sit_other_exemptions=va_va4_sit_other_exemptions, + state_id=self.get_us_state('VA'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected) + return payslip + + def test_2020_taxes(self): + self._test_er_suta('VA', self.VA_UNEMP, date(2020, 1, 1), wage_base=self.VA_UNEMP_MAX_WAGE) + + salary = 5000.0 + + # For formula from https://www.tax.virginia.gov/withholding-calculator + e1 = 2 + e2 = 0 + t = salary * 12 - (self.VA_SIT_DEDUCTION + (e1 * self.VA_SIT_EXEMPTION) + (e2 * self.VA_SIT_OTHER_EXEMPTION)) + + if t <= 3000: + w = 0.02 * t + elif t <= 5000: + w = 60 + (0.03 * (t - 3000)) + elif t <= 17000: + w = 120 + (0.05 * (t - 5000)) + else: + w = 720 + (0.0575 * (t - 17000)) + + wh = w / 12 + + self._run_test_sit(wage=salary, + schedule_pay='monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=e1, + va_va4_sit_other_exemptions=e2, + expected=wh,) + self.assertPayrollEqual(wh, 235.57) # To test against calculator + + # Below expected comes from the calculator linked above + self._run_test_sit(wage=450.0, + schedule_pay='weekly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=3, + va_va4_sit_other_exemptions=1, + expected=12.22,) + self._run_test_sit(wage=2500.0, + schedule_pay='bi-weekly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=1, + va_va4_sit_other_exemptions=0, + expected=121.84,) + self._run_test_sit(wage=10000.0, + schedule_pay='semi-monthly', + state_income_tax_exempt=False, + state_income_tax_additional_withholding=100.0, + va_va4_sit_exemptions=0, + va_va4_sit_other_exemptions=1, + expected=651.57,) + + # Test exempt + self._run_test_sit(wage=2400.0, + schedule_pay='monthly', + state_income_tax_exempt=True, + state_income_tax_additional_withholding=0.0, + va_va4_sit_exemptions=1, + va_va4_sit_other_exemptions=1, + expected=0.0,) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index fefcc61e..677ee061 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -66,6 +66,13 @@

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.

From 865080c9dad4180c200f2170755d9a1135e4c0b5 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 9 Jan 2020 11:28:15 -0800 Subject: [PATCH 11/12] IMP `l10n_us_hr_payroll` Port `l10n_us_ga_hr_payroll` GA Georgia including migration (FIX for core `result_rate = 0 == 100` bug) --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 3 + l10n_us_hr_payroll/data/state/ga_georgia.xml | 265 ++++++++++++++++++ l10n_us_hr_payroll/migrations/data.py | 17 ++ l10n_us_hr_payroll/models/__init__.py | 1 + l10n_us_hr_payroll/models/hr_payslip.py | 4 + l10n_us_hr_payroll/models/hr_salary_rule.py | 35 +++ l10n_us_hr_payroll/models/state/ga_georgia.py | 52 ++++ .../models/us_payroll_config.py | 13 + l10n_us_hr_payroll/tests/__init__.py | 3 + .../tests/test_us_ga_georgia_payslip_2019.py | 135 +++++++++ .../tests/test_us_ga_georgia_payslip_2020.py | 148 ++++++++++ .../views/us_payroll_config_views.xml | 8 + 13 files changed, 685 insertions(+) create mode 100644 l10n_us_hr_payroll/data/state/ga_georgia.xml create mode 100644 l10n_us_hr_payroll/models/hr_salary_rule.py create mode 100644 l10n_us_hr_payroll/models/state/ga_georgia.py create mode 100755 l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 6aef7df9..44fc3d44 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -29,6 +29,7 @@ USA Payroll Rules. 'data/federal/fed_941_fit_parameters.xml', 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', + 'data/state/ga_georgia.xml', 'data/state/mt_montana.xml', 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index 6d8ea0f4..0e04b999 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -18,6 +18,9 @@ ref('hr_payroll_rule_er_us_fl_suta'), + ref('hr_payroll_rule_er_us_ga_suta'), + ref('hr_payroll_rule_ee_us_ga_sit'), + ref('hr_payroll_rule_er_us_mt_suta'), ref('hr_payroll_rule_er_us_mt_suta_aft'), ref('hr_payroll_rule_ee_us_mt_sit'), 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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index 0c426c2a..b0668c65 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -9,6 +9,11 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { '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', + 'mt_mw4_additional_withholding': 'state_income_tax_additional_withholding', 'mt_mw4_exemptions': 'mt_mw4_sit_exemptions', 'mt_mw4_exempt': 'mt_mw4_sit_exempt', @@ -43,6 +48,11 @@ XMLIDS_TO_REMOVE_2020 = [ '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_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', @@ -100,6 +110,13 @@ XMLIDS_TO_RENAME_2020 = { '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_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', diff --git a/l10n_us_hr_payroll/models/__init__.py b/l10n_us_hr_payroll/models/__init__.py index c6d607ff..c208ca19 100644 --- a/l10n_us_hr_payroll/models/__init__.py +++ b/l10n_us_hr_payroll/models/__init__.py @@ -2,4 +2,5 @@ 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/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 9b7e2f78..8ea7333b 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -1,6 +1,8 @@ # 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, \ @@ -12,6 +14,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ from .state.general import general_state_unemployment, \ general_state_income_withholding, \ is_us_state +from .state.ga_georgia import ga_georgia_state_income_withholding from .state.mt_montana import mt_montana_state_income_withholding from .state.oh_ohio import oh_ohio_state_income_withholding from .state.va_virginia import va_virginia_state_income_withholding @@ -49,6 +52,7 @@ class HRPayslip(models.Model): 'general_state_unemployment': general_state_unemployment, 'general_state_income_withholding': general_state_income_withholding, 'is_us_state': is_us_state, + 'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding, 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, diff --git a/l10n_us_hr_payroll/models/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/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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index befbb3b2..bc262c84 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -51,6 +51,19 @@ class HRContractUSPayrollConfig(models.Model): fed_941_fit_w4_additional_withholding = fields.Float(string='Federal W4 Additional Withholding [4(c)]', help='Form W4 (2020+) 4(c)') + ga_g4_sit_filing_status = fields.Selection([ + ('exempt', 'Exempt'), + ('single', 'Single'), + ('married filing joint, both spouses working', 'Married Filing Joint, both spouses working'), + ('married filing joint, one spouse working', 'Married Filing Joint, one spouse working'), + ('married filing separate', 'Married Filing Separate'), + ('head of household', 'Head of Household'), + ], string='Georgia G-4 Filing Status', help='G-4 3.') + ga_g4_sit_dependent_allowances = fields.Integer(string='Georgia G-4 Dependent Allowances', + help='G-4 4.') + ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances', + help='G-4 5.') + mt_mw4_sit_exemptions = fields.Integer(string='Montana MW-4 Exemptions', help='MW-4 Box G') # Don't use the main state_income_tax_exempt because of special meaning and reporting diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index ef846086..43613969 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -7,6 +7,9 @@ from . import test_us_payslip_2020 from . import test_us_fl_florida_payslip_2019 from . import test_us_fl_florida_payslip_2020 +from . import test_us_ga_georgia_payslip_2019 +from . import test_us_ga_georgia_payslip_2020 + from . import test_us_mt_montana_payslip_2019 from . import test_us_mt_montana_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py new file mode 100755 index 00000000..b407a079 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2019.py @@ -0,0 +1,135 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip, process_payslip + + +class TestUsGAPayslip(TestUsPayslip): + + # TAXES AND RATES + GA_UNEMP_MAX_WAGE = 9500.00 + GA_UNEMP = -(2.70 / 100.0) + + def test_taxes_weekly_single_with_additional_wh(self): + salary = 15000.00 + schedule_pay = 'weekly' + allowances = 1 + filing_status = 'single' + additional_wh = 12.50 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for single weekly is 88.50 + # step1 = 15000.00 - 88.50 = 14911.5 + # Step 2 - Subtract personal allowance from step1. Allowance for single weekly is 51.92 + # step2 = step1 - 51.92 = 14859.58 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 57.50 + # step3 = 14859.58 - 57.50 = 14802.08 + # Step 4 -Determine wh amount from tables + # step4 = 4.42 + ((5.75 / 100.00) * (14802.08 - 135.00)) + # Add additional_wh + # wh = 847.7771 + 12.50 = 860.2771 + wh = -860.28 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('GA'), + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self.assertEqual(contract.schedule_pay, 'weekly') + + self._log('2019 Georgia tax first payslip weekly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], self.GA_UNEMP_MAX_WAGE * self.GA_UNEMP) + self.assertPayrollEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + remaining_GA_UNEMP_wages = 0.0 # We already reached max unemployment wages. + + self._log('2019 Georgia tax second payslip weekly:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_GA_UNEMP_wages * self.GA_UNEMP) + + + def test_taxes_monthly_head_of_household(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'head of household' + additional_wh = 15.00 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for head of household monthly is 383.50 + # step1 = 25000.00 - 383.50 = 24616.5 + # Step 2 - Subtract personal allowance from step1. Allowance for head of household monthly is 225.00 + # step2 = 24616.5 - 225.00 = 24391.5 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 250.00 + # step3 = 24391.5 - (2 * 250.00) = 23891.5 + # Step 4 - Determine wh amount from tables + # step4 = 28.33 + ((5.75 / 100.00) * (23891.5 - 833.00)) = 1354.19375 + # Add additional_wh + # wh = 1354.19375 + 15.00 = 1369.19375 + wh = -1369.19 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('GA'), + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self.assertEqual(contract.schedule_pay, 'monthly') + + self._log('2019 Georgia tax first payslip monthly:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], self.GA_UNEMP_MAX_WAGE * self.GA_UNEMP) + self.assertPayrollAlmostEqual(cats['EE_US_SIT'], wh) + + process_payslip(payslip) + + remaining_GA_UNEMP_wages = 0.0 # We already reached max unemployment wages. + + self._log('2019 Georgia tax second payslip weekly:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_GA_UNEMP_wages * self.GA_UNEMP) + + def test_taxes_exempt(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'exempt' + additional_wh = 15.00 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('GA'), + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + state_income_tax_additional_withholding=additional_wh, + schedule_pay=schedule_pay) + + self._log('2019 Georgia tax first payslip exempt:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('EE_US_SIT', 0), 0) diff --git a/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py new file mode 100755 index 00000000..6debc2ca --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ga_georgia_payslip_2020.py @@ -0,0 +1,148 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip, process_payslip + + +class TestUsGAPayslip(TestUsPayslip): + + # TAXES AND RATES + GA_UNEMP_MAX_WAGE = 9500.00 + GA_UNEMP = 2.70 + + def _run_test_sit(self, + wage=0.0, + schedule_pay='monthly', + filing_status='single', + dependent_credit=0.0, + other_income=0.0, + deductions=0.0, + additional_withholding=0.0, + is_nonresident_alien=False, + state_income_tax_exempt=False, + state_income_tax_additional_withholding=0.0, + ga_g4_sit_dependent_allowances=0, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=None, + expected=0.0, + ): + employee = self._createEmployee() + contract = self._createContract(employee, + wage=wage, + schedule_pay=schedule_pay, + fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien, + fed_941_fit_w4_filing_status=filing_status, + fed_941_fit_w4_multiple_jobs_higher=False, + fed_941_fit_w4_dependent_credit=dependent_credit, + fed_941_fit_w4_other_income=other_income, + fed_941_fit_w4_deductions=deductions, + fed_941_fit_w4_additional_withholding=additional_withholding, + state_income_tax_exempt=state_income_tax_exempt, + state_income_tax_additional_withholding=state_income_tax_additional_withholding, + ga_g4_sit_dependent_allowances=ga_g4_sit_dependent_allowances, + ga_g4_sit_additional_allowances=ga_g4_sit_additional_allowances, + ga_g4_sit_filing_status=ga_g4_sit_filing_status, + state_id=self.get_us_state('GA'), + ) + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + # Instead of PayrollEqual after initial first round of testing. + self.assertPayrollAlmostEqual(cats.get('EE_US_SIT', 0.0), -expected) + return payslip + + def test_taxes_weekly_single_with_additional_wh(self): + self._test_er_suta('GA', self.GA_UNEMP, date(2020, 1, 1), wage_base=self.GA_UNEMP_MAX_WAGE) + salary = 15000.00 + schedule_pay = 'weekly' + allowances = 1 + filing_status = 'single' + additional_wh = 12.50 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for single weekly is 88.50 + # step1 = 15000.00 - 88.50 = 14911.5 + # Step 2 - Subtract personal allowance from step1. Allowance for single weekly is 51.92 + # step2 = step1 - 51.92 = 14859.58 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 57.50 + # step3 = 14859.58 - 57.50 = 14802.08 + # Step 4 -Determine wh amount from tables + # step4 = 4.42 + ((5.75 / 100.00) * (14802.08 - 135.00)) + # Add additional_wh + # wh = 847.7771 + 12.50 = 860.2771 + wh = 860.28 + + self._run_test_sit(wage=salary, + schedule_pay=schedule_pay, + state_income_tax_additional_withholding=additional_wh, + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + expected=wh, + ) + + + def test_taxes_monthly_head_of_household(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'head of household' + additional_wh = 15.00 + # Hand Calculated Amount to Test + # Step 1 - Subtract standard deduction from wages. Std Deduct for head of household monthly is 383.50 + # step1 = 25000.00 - 383.50 = 24616.5 + # Step 2 - Subtract personal allowance from step1. Allowance for head of household monthly is 225.00 + # step2 = 24616.5 - 225.00 = 24391.5 + # Step 3 - Subtract amount for dependents. Weekly dependent allowance is 250.00 + # step3 = 24391.5 - (2 * 250.00) = 23891.5 + # Step 4 - Determine wh amount from tables + # step4 = 28.33 + ((5.75 / 100.00) * (23891.5 - 833.00)) = 1354.19375 + # Add additional_wh + # wh = 1354.19375 + 15.00 = 1369.19375 + wh = 1369.19 + + self._run_test_sit(wage=salary, + schedule_pay=schedule_pay, + state_income_tax_additional_withholding=additional_wh, + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + expected=wh, + ) + + # additional from external calculator + self._run_test_sit(wage=425.0, + schedule_pay='weekly', + state_income_tax_additional_withholding=0.0, + ga_g4_sit_dependent_allowances=1, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status='married filing separate', + expected=11.45, + ) + + self._run_test_sit(wage=3000.0, + schedule_pay='quarterly', + state_income_tax_additional_withholding=0.0, + ga_g4_sit_dependent_allowances=1, + ga_g4_sit_additional_allowances=1, + ga_g4_sit_filing_status='single', + expected=0.0, + ) + + # TODO 'married filing joint, both spouses working' returns lower than calculator + # TODO 'married filing joint, one spouse working' returns lower than calculator + + def test_taxes_exempt(self): + salary = 25000.00 + schedule_pay = 'monthly' + allowances = 2 + filing_status = 'exempt' + additional_wh = 15.00 + + self._run_test_sit(wage=salary, + schedule_pay=schedule_pay, + state_income_tax_additional_withholding=additional_wh, + ga_g4_sit_dependent_allowances=allowances, + ga_g4_sit_additional_allowances=0, + ga_g4_sit_filing_status=filing_status, + expected=0.0, + ) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index 677ee061..d7cc8a6e 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -47,6 +47,14 @@

No additional fields.

+ +

Form G-4 - State Income Tax

+ + + + + +

Form MT-4 - State Income Tax

From b2e610c746bb64356761bbeb11cb91cb72c38f70 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 9 Jan 2020 13:23:34 -0800 Subject: [PATCH 12/12] IMP `l10n_us_hr_payroll` Port `l10n_us_ms_hr_payroll` MS Mississippi including migration --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/final.xml | 3 + .../data/state/ms_mississippi.xml | 120 ++++++++++++++++++ l10n_us_hr_payroll/migrations/data.py | 16 +++ l10n_us_hr_payroll/models/hr_payslip.py | 2 + .../models/state/ms_mississippi.py | 46 +++++++ .../models/us_payroll_config.py | 10 ++ l10n_us_hr_payroll/tests/__init__.py | 3 + .../test_us_ms_mississippi_payslip_2019.py | 94 ++++++++++++++ .../test_us_ms_mississippi_payslip_2020.py | 120 ++++++++++++++++++ .../views/us_payroll_config_views.xml | 6 + 11 files changed, 421 insertions(+) create mode 100644 l10n_us_hr_payroll/data/state/ms_mississippi.xml create mode 100644 l10n_us_hr_payroll/models/state/ms_mississippi.py create mode 100755 l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index 44fc3d44..54791cef 100755 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -30,6 +30,7 @@ USA Payroll Rules. 'data/federal/fed_941_fit_rules.xml', 'data/state/fl_florida.xml', 'data/state/ga_georgia.xml', + 'data/state/ms_mississippi.xml', 'data/state/mt_montana.xml', 'data/state/oh_ohio.xml', 'data/state/pa_pennsylvania.xml', diff --git a/l10n_us_hr_payroll/data/final.xml b/l10n_us_hr_payroll/data/final.xml index 0e04b999..9bdeeef6 100644 --- a/l10n_us_hr_payroll/data/final.xml +++ b/l10n_us_hr_payroll/data/final.xml @@ -21,6 +21,9 @@ ref('hr_payroll_rule_er_us_ga_suta'), ref('hr_payroll_rule_ee_us_ga_sit'), + ref('hr_payroll_rule_er_us_ms_suta'), + ref('hr_payroll_rule_ee_us_ms_sit'), + ref('hr_payroll_rule_er_us_mt_suta'), ref('hr_payroll_rule_er_us_mt_suta_aft'), ref('hr_payroll_rule_ee_us_mt_sit'), 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/migrations/data.py b/l10n_us_hr_payroll/migrations/data.py index b0668c65..d6dd22b0 100644 --- a/l10n_us_hr_payroll/migrations/data.py +++ b/l10n_us_hr_payroll/migrations/data.py @@ -14,6 +14,10 @@ FIELDS_CONTRACT_TO_US_PAYROLL_FORMS_2020 = { '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', @@ -53,6 +57,11 @@ XMLIDS_TO_REMOVE_2020 = [ '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', @@ -117,6 +126,13 @@ XMLIDS_TO_RENAME_2020 = { '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', diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 8ea7333b..cdcb7208 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -15,6 +15,7 @@ from .state.general import general_state_unemployment, \ 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 @@ -53,6 +54,7 @@ class HRPayslip(models.Model): 'general_state_income_withholding': general_state_income_withholding, 'is_us_state': is_us_state, 'ga_georgia_state_income_withholding': ga_georgia_state_income_withholding, + 'ms_mississippi_state_income_withholding': ms_mississippi_state_income_withholding, 'mt_montana_state_income_withholding': mt_montana_state_income_withholding, 'oh_ohio_state_income_withholding': oh_ohio_state_income_withholding, 'va_virginia_state_income_withholding': va_virginia_state_income_withholding, diff --git a/l10n_us_hr_payroll/models/state/ms_mississippi.py b/l10n_us_hr_payroll/models/state/ms_mississippi.py new file mode 100644 index 00000000..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/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index bc262c84..5ff26656 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -64,6 +64,16 @@ class HRContractUSPayrollConfig(models.Model): ga_g4_sit_additional_allowances = fields.Integer(string='Georgia G-4 Additional Allowances', help='G-4 5.') + ms_89_350_sit_filing_status = fields.Selection([ + ('', 'Exempt'), + ('single', 'Single'), + ('married', 'Married (spouse NOT employed)'), + ('married_dual', 'Married (spouse IS employed)'), + ('head_of_household', 'Head of Household'), + ], string='Mississippi 89-350 Filing Status', help='89-350 1. 2. 3. 8.') + ms_89_350_sit_exemption_value = fields.Float(string='Mississippi 89-350 Exemption Total', + help='89-350 Box 6 (including filing status amounts)') + mt_mw4_sit_exemptions = fields.Integer(string='Montana MW-4 Exemptions', help='MW-4 Box G') # Don't use the main state_income_tax_exempt because of special meaning and reporting diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 43613969..3e4cb355 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -10,6 +10,9 @@ from . import test_us_fl_florida_payslip_2020 from . import test_us_ga_georgia_payslip_2019 from . import test_us_ga_georgia_payslip_2020 +from . import test_us_ms_mississippi_payslip_2019 +from . import test_us_ms_mississippi_payslip_2020 + from . import test_us_mt_montana_payslip_2019 from . import test_us_mt_montana_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py new file mode 100755 index 00000000..e7ce35d0 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2019.py @@ -0,0 +1,94 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestUsPayslip + + +class TestUsMsPayslip(TestUsPayslip): + # Calculations from https://www.dor.ms.gov/Documents/Computer%20Payroll%20Accounting%201-2-19.pdf + MS_UNEMP = -1.2 / 100.0 + + def test_2019_taxes_one(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='head_of_household', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2019 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.MS_UNEMP) + + STDED = 3400.0 # Head of Household + AGP = salary * 24 # Semi-Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 15600.0) + TAX = ((TI - 10000) * 0.05) + 290 # Over 10,000 + self.assertPayrollEqual(TAX, 570.0) + + ms_withhold = round(TAX / 24) # Semi-Monthly + self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold) + + def test_2019_taxes_one_exempt(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2019 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2019_taxes_additional(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + additional = 40.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='head_of_household', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=additional, + schedule_pay='semi-monthly') + + self._log('2019 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.MS_UNEMP) + + STDED = 3400.0 # Head of Household + AGP = salary * 24 # Semi-Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 15600.0) + TAX = ((TI - 10000) * 0.05) + 290 # Over 10,000 + self.assertPayrollEqual(TAX, 570.0) + + ms_withhold = round(TAX / 24) # Semi-Monthly + self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold + -additional) diff --git a/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py new file mode 100755 index 00000000..5942d7ad --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_ms_mississippi_payslip_2020.py @@ -0,0 +1,120 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from datetime import date +from .common import TestUsPayslip + + +class TestUsMsPayslip(TestUsPayslip): + # Calculations from https://www.dor.ms.gov/Documents/Computer%20Payroll%20Accounting%201-2-19.pdf + MS_UNEMP = 1.2 + MS_UNEMP_MAX_WAGE = 14000.0 + + def test_2020_taxes_one(self): + self._test_er_suta('MS', self.MS_UNEMP, date(2020, 1, 1), wage_base=self.MS_UNEMP_MAX_WAGE) + + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='head_of_household', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2020 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + STDED = 3400.0 # Head of Household + AGP = salary * 24 # Semi-Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 15600.0) + TAX = ((TI - 10000) * 0.05) + 260 # Over 10,000 + self.assertPayrollEqual(TAX, 540.0) + + ms_withhold = round(TAX / 24) # Semi-Monthly + self.assertPayrollEqual(cats['EE_US_SIT'], -ms_withhold) + + def test_2020_taxes_one_exempt(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=0.0, + schedule_pay='semi-monthly') + + self._log('2020 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + self.assertPayrollEqual(cats.get('EE_US_SIT', 0.0), 0.0) + + def test_2020_taxes_additional(self): + salary = 1250.0 + ms_89_350_exemption = 11000.0 + additional = 40.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='single', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=additional, + schedule_pay='monthly') + + self._log('2020 Mississippi tax single first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + STDED = 2300.0 # Single + AGP = salary * 12 # Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 1700.0) + TAX = ((TI - 3000) * 0.03) + self.assertPayrollEqual(TAX, -39.0) + + ms_withhold = round(TAX / 12) # Monthly + self.assertTrue(ms_withhold <= 0.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -40.0) # only additional + + # Test with higher wage + salary = 1700.0 + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('MS'), + ms_89_350_sit_filing_status='single', + ms_89_350_sit_exemption_value=ms_89_350_exemption, + state_income_tax_additional_withholding=additional, + schedule_pay='monthly') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + payslip.compute_sheet() + cats = self._getCategories(payslip) + + STDED = 2300.0 # Single + AGP = salary * 12 # Monthly + TI = AGP - (ms_89_350_exemption + STDED) + self.assertPayrollEqual(TI, 7100.0) + TAX = ((TI - 5000) * 0.04) + 60.0 + self.assertPayrollEqual(TAX, 144.0) + + ms_withhold = round(TAX / 12) # Monthly + self.assertPayrollEqual(ms_withhold, 12.0) + self.assertPayrollEqual(cats['EE_US_SIT'], -(ms_withhold + additional)) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index d7cc8a6e..247309b5 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -55,6 +55,12 @@
+ +

Form 89-350 - State Income Tax

+ + + +

Form MT-4 - State Income Tax