From e377ea224a9235dd0b65e1857598eec490dea818 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 7 Jan 2020 07:52:41 -0800 Subject: [PATCH] IMP `l10n_us_hr_payroll` Add Generic SUTA Category and method, add FL Florida (unemployment, no income tax) --- l10n_us_hr_payroll/__manifest__.py | 1 + l10n_us_hr_payroll/data/base.xml | 12 +++ l10n_us_hr_payroll/data/state/fl_florida.xml | 63 ++++++++++++++ l10n_us_hr_payroll/models/hr_payslip.py | 2 + l10n_us_hr_payroll/models/state/__init__.py | 1 + l10n_us_hr_payroll/models/state/general.py | 85 +++++++++++++++++++ .../models/us_payroll_config.py | 1 + l10n_us_hr_payroll/tests/__init__.py | 3 + l10n_us_hr_payroll/tests/common.py | 18 ++++ .../tests/test_us_fl_florida_payslip_2019.py | 82 ++++++++++++++++++ .../tests/test_us_fl_florida_payslip_2020.py | 82 ++++++++++++++++++ .../views/us_payroll_config_views.xml | 4 + 12 files changed, 354 insertions(+) create mode 100644 l10n_us_hr_payroll/data/state/fl_florida.xml create mode 100644 l10n_us_hr_payroll/models/state/__init__.py create mode 100644 l10n_us_hr_payroll/models/state/general.py create mode 100755 l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py create mode 100755 l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py diff --git a/l10n_us_hr_payroll/__manifest__.py b/l10n_us_hr_payroll/__manifest__.py index a748aae6..01fb7563 100644 --- a/l10n_us_hr_payroll/__manifest__.py +++ b/l10n_us_hr_payroll/__manifest__.py @@ -25,6 +25,7 @@ United States of America - Payroll Rules. 'data/federal/fed_941_fica_rules.xml', 'data/federal/fed_941_fit_parameters.xml', 'data/federal/fed_941_fit_rules.xml', + 'data/state/fl_florida.xml', 'views/hr_contract_views.xml', 'views/us_payroll_config_views.xml', ], diff --git a/l10n_us_hr_payroll/data/base.xml b/l10n_us_hr_payroll/data/base.xml index 6838f283..a614f313 100644 --- a/l10n_us_hr_payroll/data/base.xml +++ b/l10n_us_hr_payroll/data/base.xml @@ -17,4 +17,16 @@ ]"/> + + + EE: State Unemployment SUTA + EE_US_SUTA + + + + ER: State Unemployment SUTA + ER_US_SUTA + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/data/state/fl_florida.xml b/l10n_us_hr_payroll/data/state/fl_florida.xml new file mode 100644 index 00000000..8002a2ee --- /dev/null +++ b/l10n_us_hr_payroll/data/state/fl_florida.xml @@ -0,0 +1,63 @@ + + + + + US FL Florida SUTA Wage Base + us_fl_suta_wage_base + + + + + 7000.00 + + + + + 7000.00 + + + + + + + + US FL Florida SUTA Rate + us_fl_suta_rate + + + + + 2.7 + + + + + 2.7 + + + + + + + + US Florida - Department of Revenue + + + + + + + + + + ER: US FL Florida State Unemployment (RT-6) + ER_US_FL_SUTA + python + result, _ = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_fl_suta_wage_base', rate='us_fl_suta_rate', state_code='FL') + code + result, result_rate = general_state_unemployment(payslip, categories, worked_days, inputs, wage_base='us_fl_suta_wage_base', rate='us_fl_suta_rate', state_code='FL') + + + + + \ No newline at end of file diff --git a/l10n_us_hr_payroll/models/hr_payslip.py b/l10n_us_hr_payroll/models/hr_payslip.py index 92a1e0fd..2865ee56 100644 --- a/l10n_us_hr_payroll/models/hr_payslip.py +++ b/l10n_us_hr_payroll/models/hr_payslip.py @@ -9,6 +9,7 @@ from .federal.fed_941 import ee_us_941_fica_ss, \ er_us_941_fica_ss, \ er_us_941_fica_m, \ ee_us_941_fit +from .state.general import general_state_unemployment class HRPayslip(models.Model): @@ -37,6 +38,7 @@ class HRPayslip(models.Model): 'er_us_941_fica_ss': er_us_941_fica_ss, 'er_us_941_fica_m': er_us_941_fica_m, 'ee_us_941_fit': ee_us_941_fit, + 'general_state_unemployment': general_state_unemployment, }) return res diff --git a/l10n_us_hr_payroll/models/state/__init__.py b/l10n_us_hr_payroll/models/state/__init__.py new file mode 100644 index 00000000..0358305d --- /dev/null +++ b/l10n_us_hr_payroll/models/state/__init__.py @@ -0,0 +1 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. diff --git a/l10n_us_hr_payroll/models/state/general.py b/l10n_us_hr_payroll/models/state/general.py new file mode 100644 index 00000000..f576510d --- /dev/null +++ b/l10n_us_hr_payroll/models/state/general.py @@ -0,0 +1,85 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. +from odoo.exceptions import UserError + +# import logging +# _logger = logging.getLogger(__name__) + + +def general_state_unemployment(payslip, categories, worked_days, inputs, wage_base=None, wage_start=None, rate=None, state_code=None): + """ + Returns SUTA eligible wage and rate. + WAGE = GROSS - WAGE_US_940_FUTA_EXEMPT + + The contract's `futa_type` determines if SUTA should be collected. + + Function parameters: + wage_base, wage_start, rate can either be strings (rule_parameters) or floats + + :return: result, result_rate (wage, percent) + """ + + if state_code != payslip.contract_id.us_payroll_config_value('state_code'): + return 0.0, 0.0 + + # Determine Eligible. + if payslip.contract_id.futa_type in (payslip.contract_id.FUTA_TYPE_EXEMPT, payslip.contract_id.FUTA_TYPE_BASIC): + return 0.0, 0.0 + + # Resolve parameters. On exception, return (probably missing a year, would rather not have exception) + if wage_base and isinstance(wage_base, str): + try: + wage_base = payslip.rule_parameter(wage_base) + except (KeyError, UserError): + return 0.0, 0.0 + + if wage_start and isinstance(wage_start, str): + try: + wage_start = payslip.rule_parameter(wage_start) + except (KeyError, UserError): + return 0.0, 0.0 + + if rate and isinstance(rate, str): + try: + rate = payslip.rule_parameter(rate) + except (KeyError, UserError): + return 0.0, 0.0 + + if not rate: + return 0.0, 0.0 + else: + # Rate assumed positive percentage! + rate = -rate + + # Determine Wage + year = payslip.dict.get_year() + ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year + 1) + '-01-01') + ytd_wage -= payslip.sum_category('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year + 1) + '-01-01') + ytd_wage += payslip.contract_id.external_wages + + wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT + #_logger.warn('ytd_wage: ' + str(ytd_wage) + ' wage: ' + str(wage)) + + if wage_base: + remaining = wage_base - ytd_wage + if remaining < 0.0: + result = 0.0 + elif remaining < wage: + result = remaining + else: + result = wage + + #_logger.warn(' wage_base method result: ' + str(result) + ' rate: ' + str(rate)) + return result, rate + if wage_start: + if ytd_wage >= wage_start: + #_logger.warn(' wage_start 1 method result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate + if ytd_wage + wage <= wage_start: + #_logger.warn(' wage_start 2 method result: ' + str(0.0) + ' rate: ' + str(0.0)) + return 0.0, 0.0 + #_logger.warn(' wage_start 3 method result: ' + str((wage - (wage_start - ytd_wage))) + ' rate: ' + str(rate)) + return (wage - (wage_start - ytd_wage)), rate + + # If the wage doesn't have a start or a base + #_logger.warn(' basic result: ' + str(wage) + ' rate: ' + str(rate)) + return wage, rate diff --git a/l10n_us_hr_payroll/models/us_payroll_config.py b/l10n_us_hr_payroll/models/us_payroll_config.py index ed0e6d73..d87ebcac 100644 --- a/l10n_us_hr_payroll/models/us_payroll_config.py +++ b/l10n_us_hr_payroll/models/us_payroll_config.py @@ -14,6 +14,7 @@ class HRContractUSPayrollConfig(models.Model): name = fields.Char(string="Description") employee_id = fields.Many2one('hr.employee', string="Employee", required=True) state_id = fields.Many2one('res.country.state', string="Applied State") + state_code = fields.Char(related='state_id.code') fed_940_type = fields.Selection([ (FUTA_TYPE_EXEMPT, 'Exempt (0%)'), diff --git a/l10n_us_hr_payroll/tests/__init__.py b/l10n_us_hr_payroll/tests/__init__.py index 23702419..10545ad0 100755 --- a/l10n_us_hr_payroll/tests/__init__.py +++ b/l10n_us_hr_payroll/tests/__init__.py @@ -3,3 +3,6 @@ from . import common from . import test_us_payslip_2019 from . import test_us_payslip_2020 + +from . import test_us_fl_florida_payslip_2019 +from . import test_us_fl_florida_payslip_2020 diff --git a/l10n_us_hr_payroll/tests/common.py b/l10n_us_hr_payroll/tests/common.py index fc84b0fe..57dd216f 100755 --- a/l10n_us_hr_payroll/tests/common.py +++ b/l10n_us_hr_payroll/tests/common.py @@ -61,6 +61,10 @@ class TestUsPayslip(common.TransactionCase): 'employee_id': employee.id, } + # Backwards compatability with 'futa_type' + if 'futa_type' in kwargs: + kwargs['fed_940_type'] = kwargs['futa_type'] + for key, val in kwargs.items(): # Assume any Odoo object is in a Many2one if hasattr(val, 'id'): @@ -148,3 +152,17 @@ class TestUsPayslip(common.TransactionCase): payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14') payslip.compute_sheet() + + def get_us_state(self, code, cache={}): + country_key = 'US_COUNTRY' + if code in cache: + return cache[code] + if country_key not in cache: + cache[country_key] = self.env.ref('base.us') + us_country = cache[country_key] + us_state = self.env['res.country.state'].search([ + ('country_id', '=', us_country.id), + ('code', '=', code), + ], limit=1) + cache[code] = us_state + return us_state diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py new file mode 100755 index 00000000..981e9ce0 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2019.py @@ -0,0 +1,82 @@ +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsFlPayslip(TestUsPayslip): + ### + # 2019 Taxes and Rates + ### + FL_UNEMP_MAX_WAGE = 7000.0 + FL_UNEMP = -2.7 / 100.0 + + def test_2019_taxes(self): + salary = 5000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('FL')) + + self._log('2019 Florida tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.FL_UNEMP) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_fl_unemp_wages = self.FL_UNEMP_MAX_WAGE - salary if (self.FL_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2019 Florida tax second payslip:') + payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_fl_unemp_wages * self.FL_UNEMP) + + def test_2019_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + state_id=self.get_us_state('FL')) + + self._log('2019 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP) + + def test_2019_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC, + state_id=self.get_us_state('FL')) + + self._log('2019 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py new file mode 100755 index 00000000..b38e77d6 --- /dev/null +++ b/l10n_us_hr_payroll/tests/test_us_fl_florida_payslip_2020.py @@ -0,0 +1,82 @@ +from .common import TestUsPayslip, process_payslip +from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract + + +class TestUsFlPayslip(TestUsPayslip): + ### + # 2020 Taxes and Rates + ### + FL_UNEMP_MAX_WAGE = 7000.0 + FL_UNEMP = -2.7 / 100.0 + + def test_2020_taxes(self): + salary = 5000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_us_state('FL')) + + self._log('2020 Florida tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], salary * self.FL_UNEMP) + + process_payslip(payslip) + + # Make a new payslip, this one will have maximums + + remaining_fl_unemp_wages = self.FL_UNEMP_MAX_WAGE - salary if (self.FL_UNEMP_MAX_WAGE - 2*salary < salary) \ + else salary + + self._log('2020 Florida tax second payslip:') + payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], remaining_fl_unemp_wages * self.FL_UNEMP) + + def test_2020_taxes_with_external(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + state_id=self.get_us_state('FL')) + + self._log('2020 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats['ER_US_SUTA'], (self.FL_UNEMP_MAX_WAGE - external_wages) * self.FL_UNEMP) + + def test_2020_taxes_with_state_exempt(self): + salary = 5000.0 + external_wages = 6000.0 + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + external_wages=external_wages, + futa_type=USHRContract.FUTA_TYPE_BASIC, + state_id=self.get_us_state('FL')) + + self._log('2020 Forida_external tax first payslip:') + payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31') + + payslip.compute_sheet() + + cats = self._getCategories(payslip) + + self.assertPayrollEqual(cats.get('ER_US_SUTA', 0.0), 0.0) diff --git a/l10n_us_hr_payroll/views/us_payroll_config_views.xml b/l10n_us_hr_payroll/views/us_payroll_config_views.xml index dfe0e301..e7ae0338 100644 --- a/l10n_us_hr_payroll/views/us_payroll_config_views.xml +++ b/l10n_us_hr_payroll/views/us_payroll_config_views.xml @@ -40,6 +40,10 @@ + + +

No additional fields.

+