MIG l10n_us_hr_payroll Major refactor and Federal 2020 Rules (new W4 Form)

This commit is contained in:
Jared Kipe
2020-01-05 19:07:55 -08:00
parent b62e2f57b0
commit 2785672a8e
35 changed files with 2069 additions and 1933 deletions

View File

@@ -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)

View File

@@ -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',

View File

@@ -27,6 +27,7 @@
<field name="name"/>
<field name="code"/>
<field name="rate"/>
<field name="parameter_value"/>
</group>
<group>
<field name="date_from"/>

View File

@@ -1 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models

View File

@@ -1,10 +1,9 @@
{
'name': 'USA - Payroll',
'author': 'Hibou Corp. <hello@hibou.io>',
'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',
}

81
l10n_us_hr_payroll/data/base.xml Executable file → Normal file
View File

@@ -1,83 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<!-- CONTRIBUTION REGISTERS -->
<record id="res_partner_eftps_941" model="res.partner">
<field name="name">EFTPS - Form 941</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="res_partner_eftps_940" model="res.partner">
<field name="name">EFTPS - Form 940</field>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
</record>
<record id="contrib_register_eftps_941" model="hr.contribution.register">
<field name="name">EFTPS - 941 (FICA + Federal Witholding)</field>
<field name="note">Electronic Federal Tax Payment System - Form 941</field>
<field name="partner_id" ref="res_partner_eftps_941"/>
</record>
<record id="contrib_register_eftps_940" model="hr.contribution.register">
<field name="name">EFTPS - 940 (FUTA)</field>
<field name="note">Electronic Federal Tax Payment System - Form 940</field>
<field name="partner_id" ref="res_partner_eftps_940"/>
</record>
<!-- HR SALARY RULE CATEGORIES-->
<record id="hr_payroll_fica_emp_ss_wages" model="hr.salary.rule.category">
<field name="name">Wage: US FICA Social Security</field>
<field name="code">WAGE_US_FICA_SS</field>
</record>
<record id="hr_payroll_fica_emp_m_wages" model="hr.salary.rule.category">
<field name="name">Wage: US FICA Medicare</field>
<field name="code">WAGE_US_FICA_M</field>
</record>
<record id="hr_payroll_fica_emp_m_add_wages" model="hr.salary.rule.category">
<field name="name">Wage: US FICA Medicare Additional</field>
<field name="code">WAGE_US_FICA_M_ADD</field>
</record>
<record id="hr_payroll_futa_wages" model="hr.salary.rule.category">
<field name="name">Wage: US FUTA Federal Unemployment</field>
<field name="code">WAGE_US_FUTA</field>
</record>
<record id="hr_payroll_fica_emp_ss" model="hr.salary.rule.category">
<field name="name">EE: US FICA Social Security</field>
<field name="code">EE_US_FICA_SS</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_fica_emp_m" model="hr.salary.rule.category">
<field name="name">EE: US FICA Medicare</field>
<field name="code">EE_US_FICA_M</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_fica_emp_m_add" model="hr.salary.rule.category">
<field name="name">EE: US FICA Medicare Additional</field>
<field name="code">EE_US_FICA_M_ADD</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_fed_income_withhold" model="hr.salary.rule.category">
<field name="name">EE: US Federal Income Tax Withholding</field>
<field name="code">EE_US_FED_INC_WITHHOLD</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_fica_comp_ss" model="hr.salary.rule.category">
<field name="name">ER: US FICA Social Security</field>
<field name="code">ER_US_FICA_SS</field>
<field name="parent_id" ref="hr_payroll.COMP"/>
</record>
<record id="hr_payroll_fica_comp_m" model="hr.salary.rule.category">
<field name="name">ER: US FICA Medicare</field>
<field name="code">ER_US_FICA_M</field>
<field name="parent_id" ref="hr_payroll.COMP"/>
</record>
<record id="hr_payroll_futa" model="hr.salary.rule.category">
<field name="name">ER: US FUTA Federal Unemployment</field>
<field name="code">ER_US_FUTA</field>
<field name="parent_id" ref="hr_payroll.COMP"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Wage Base -->
<record id="rule_parameter_940_futa_wage_base_2016" model="hr.payroll.rate">
<field name="name">Federal 940 FUTA Wage Base</field>
<field name="code">fed_940_futa_wage_base</field>
<field name="parameter_value">7000.00</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_940_futa_rate_basic_2016" model="hr.payroll.rate">
<field name="name">Federal 940 FUTA Rate Basic</field>
<field name="code">fed_940_futa_rate_basic</field>
<field name="parameter_value">6.0</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<record id="rule_parameter_940_futa_rate_normal_2016" model="hr.payroll.rate">
<field name="name">Federal 940 FUTA Rate Normal</field>
<field name="code">fed_940_futa_rate_normal</field>
<field name="parameter_value">0.6</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
</odoo>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="res_partner_eftps_940" model="res.partner">
<field name="name">US Federal 940 - EFTPS</field>
</record>
<record id="contrib_register_eftps_940" model="hr.contribution.register">
<field name="name">EFTPS - 940 (FUTA)</field>
<field name="note">Electronic Federal Tax Payment System - Form 940</field>
<field name="partner_id" ref="res_partner_eftps_940"/>
</record>
<record id="hr_payroll_category_er_fed_940" model="hr.salary.rule.category">
<field name="name">ER: Federal 940 FUTA</field>
<field name="code">ER_US_940_FUTA</field>
<field name="parent_id" ref="hr_payroll.COMP"/>
</record>
<!-- Category to increase when reducing wage for Unemployment Insurance/Tax -->
<record id="hr_payroll_category_wage_fed_940_futa_exempt" model="hr.salary.rule.category">
<field name="name">WAGE: Federal 940 FUTA Exempt</field>
<field name="code">WAGE_US_940_FUTA_EXEMPT</field>
</record>
<record id="hr_payroll_rule_er_fed_940" model="hr.salary.rule">
<field name="sequence" eval="440"/>
<field name="category_id" ref="hr_payroll_category_er_fed_940"/>
<field name="name">ER: US FUTA Federal Unemployment</field>
<field name="code">ER_US_940_FUTA</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = er_us_940_futa(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = er_us_940_futa(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_940"/>
<field name="appears_on_payslip" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Social Security -->
<!-- Wage Base -->
<record id="rule_parameter_941_fica_ss_wage_base_2016" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Social Security Wage Base</field>
<field name="code">fed_941_fica_ss_wage_base</field>
<field name="parameter_value">128400.0</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fica_ss_wage_base_2019" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Social Security Wage Base</field>
<field name="code">fed_941_fica_ss_wage_base</field>
<field name="parameter_value">132900.0</field>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fica_ss_wage_base_2020" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Social Security Wage Base</field>
<field name="code">fed_941_fica_ss_wage_base</field>
<field name="parameter_value">137700.0</field>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_941_fica_ss_rate_2016" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Rate</field>
<field name="code">fed_941_fica_ss_rate</field>
<field name="parameter_value">6.2</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Medicare -->
<!-- Wage Base -->
<record id="rule_parameter_941_fica_m_wage_base_2016" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Medicare Wage Base</field>
<field name="code">fed_941_fica_m_wage_base</field>
<field name="parameter_value">"inf"</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_941_fica_m_rate_2016" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Rate</field>
<field name="code">fed_941_fica_m_rate</field>
<field name="parameter_value">1.45</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Medicare Additional -->
<!-- Wage Base -->
<record id="rule_parameter_941_fica_m_add_wage_start_2016" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Medicare Additional Wage Start</field>
<field name="code">fed_941_fica_m_add_wage_start</field>
<field name="parameter_value">200000.0</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_941_fica_m_add_rate_2016" model="hr.payroll.rate">
<field name="name">Federal 941 FICA Medicare Additional Rate</field>
<field name="code">fed_941_fica_m_add_rate</field>
<field name="parameter_value">0.9</field>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
</odoo>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="res_partner_eftps_941" model="res.partner">
<field name="name">US Federal 941 - EFTPS</field>
</record>
<record id="contrib_register_eftps_941" model="hr.contribution.register">
<field name="name">EFTPS - 941 (FICA + Federal Witholding)</field>
<field name="note">Electronic Federal Tax Payment System - Form 941</field>
<field name="partner_id" ref="res_partner_eftps_941"/>
</record>
<record id="hr_payroll_category_ee_fed_941" model="hr.salary.rule.category">
<field name="name">EE: Federal 941 FICA</field>
<field name="code">EE_US_941_FICA</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_category_er_fed_941" model="hr.salary.rule.category">
<field name="name">ER: Federal 941 FICA</field>
<field name="code">ER_US_941_FICA</field>
<field name="parent_id" ref="hr_payroll.COMP"/>
</record>
<!-- Category to increase when reducing wage for FICA -->
<record id="hr_payroll_category_wage_fed_941_fica_exempt" model="hr.salary.rule.category">
<field name="name">WAGE: Federal 941 FICA Exempt</field>
<field name="code">WAGE_US_941_FICA_EXEMPT</field>
</record>
<!-- Social Security -->
<record id="hr_payroll_rule_ee_fed_941_ss" model="hr.salary.rule">
<field name="sequence" eval="190"/>
<field name="category_id" ref="hr_payroll_category_ee_fed_941"/>
<field name="name">EE: US FICA Social Security</field>
<field name="code">EE_US_941_FICA_SS</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = ee_us_941_fica_ss(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = ee_us_941_fica_ss(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_941"/>
<field name="appears_on_payslip" eval="True"/>
</record>
<record id="hr_payroll_rule_er_fed_941_ss" model="hr.salary.rule">
<field name="sequence" eval="440"/>
<field name="category_id" ref="hr_payroll_category_er_fed_941"/>
<field name="name">ER: US FICA Social Security</field>
<field name="code">ER_US_941_FICA_SS</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = er_us_941_fica_ss(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = er_us_941_fica_ss(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_941"/>
<field name="appears_on_payslip" eval="False"/>
</record>
<!-- Medicare -->
<record id="hr_payroll_rule_ee_fed_941_m" model="hr.salary.rule">
<field name="sequence" eval="190"/>
<field name="category_id" ref="hr_payroll_category_ee_fed_941"/>
<field name="name">EE: US FICA Medicare</field>
<field name="code">EE_US_941_FICA_M</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = ee_us_941_fica_m(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = ee_us_941_fica_m(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_941"/>
<field name="appears_on_payslip" eval="True"/>
</record>
<record id="hr_payroll_rule_er_fed_941_m" model="hr.salary.rule">
<field name="sequence" eval="440"/>
<field name="category_id" ref="hr_payroll_category_er_fed_941"/>
<field name="name">ER: US FICA Medicare</field>
<field name="code">ER_US_941_FICA_M</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = er_us_941_fica_m(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = er_us_941_fica_m(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_941"/>
<field name="appears_on_payslip" eval="False"/>
</record>
<!-- Medicare Additional -->
<record id="hr_payroll_rule_ee_fed_941_m_add" model="hr.salary.rule">
<field name="sequence" eval="190"/>
<field name="category_id" ref="hr_payroll_category_ee_fed_941"/>
<field name="name">EE: US FICA Medicare Additional</field>
<field name="code">EE_US_941_FICA_M_ADD</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = ee_us_941_fica_m_add(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = ee_us_941_fica_m_add(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_941"/>
<field name="appears_on_payslip" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,492 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Shared -->
<record id="rule_parameter_941_fit_allowance_2018" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Allowance</field>
<field name="code">fed_941_fit_allowance</field>
<!-- Bogus 2018 -->
<field name="parameter_value">{
'weekly': 80.80,
'bi-weekly': 161.50,
'semi-monthly': 175.00,
'monthly': 350.00,
'quarterly': 1050.00,
'semi-annually': 2100.00,
'annually': 4200.00,
}</field>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_allowance_2019" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Allowance</field>
<field name="code">fed_941_fit_allowance</field>
<field name="parameter_value">{
'weekly': 80.80,
'bi-weekly': 161.50,
'semi-monthly': 175.00,
'monthly': 350.00,
'quarterly': 1050.00,
'semi-annually': 2100.00,
'annually': 4200.00,
}</field>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_allowance_2020" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Allowance</field>
<field name="code">fed_941_fit_allowance</field>
<!-- Warning, major change to allowance in 2020 -->
<field name="parameter_value">4300.0</field>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_nra_additional_2018" model="hr.payroll.rate">
<field name="name">Federal 941 FIT NRA Additional</field>
<field name="code">fed_941_fit_nra_additional</field>
<!-- Bogus from 2018 -->
<field name="parameter_value">{
'weekly': 153.80,
'bi-weekly': 307.70,
'semi-monthly': 333.30,
'monthly': 666.70,
'quarterly': 2000.00,
'semi-annually': 4000.00,
'annually': 8000.00,
}</field>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_nra_additional_2019" model="hr.payroll.rate">
<field name="name">Federal 941 FIT NRA Additional</field>
<field name="code">fed_941_fit_nra_additional</field>
<field name="parameter_value">{
'weekly': 153.80,
'bi-weekly': 307.70,
'semi-monthly': 333.30,
'monthly': 666.70,
'quarterly': 2000.00,
'semi-annually': 4000.00,
'annually': 8000.00,
}</field>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_nra_additional_2020" model="hr.payroll.rate">
<field name="name">Federal 941 FIT NRA Additional</field>
<field name="code">fed_941_fit_nra_additional</field>
<field name="parameter_value">{
'weekly': 238.50,
'bi-weekly': 476.90,
'semi-monthly': 516.70,
'monthly': 1033.30,
'quarterly': 3100.00,
'semi-annually': 6200.00,
'annually': 12400.00,
}</field>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<!-- Single and Married Single Rate -->
<record id="rule_parameter_941_fit_table_single_2018" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Single</field>
<field name="code">fed_941_fit_table_single</field>
<!-- Bogus 2018 -->
<!-- But-not-over, $, % -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_single_2019" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Single</field>
<field name="code">fed_941_fit_table_single</field>
<!-- But-not-over, $, % -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_single_2020" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Single</field>
<field name="code">fed_941_fit_table_single</field>
<!-- Major changes in 2020 -->
<!-- Wage Threshold, Base Withholding Amount, Marginal Rate Over Threshold -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<!-- Married -->
<record id="rule_parameter_941_fit_table_married_2018" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Married</field>
<field name="code">fed_941_fit_table_married</field>
<!-- Bogus 2018 -->
<!-- But-not-over, $, % -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_married_2019" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Married</field>
<field name="code">fed_941_fit_table_married</field>
<!-- But-not-over, $, % -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_married_2020" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Married</field>
<field name="code">fed_941_fit_table_married</field>
<!-- Major changes in 2020 -->
<!-- Wage Threshold, Base Withholding Amount, Marginal Rate Over Threshold -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_hh_2020" model="hr.payroll.rate">
<field name="name">Federal 941 FIT Table Head of Household</field>
<field name="code">fed_941_fit_table_hh</field>
<!-- Major changes in 2020 -->
<!-- Wage Threshold, Base Withholding Amount, Marginal Rate Over Threshold -->
<field name="parameter_value">{
'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),
],
}</field>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</odoo>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Category to increase when reducing wage for Federal Income Tax (e.g. 401k) -->
<record id="hr_payroll_category_wage_fed_941_fit_exempt" model="hr.salary.rule.category">
<field name="name">WAGE: Federal 941 Income Tax Exempt</field>
<field name="code">WAGE_US_941_FIT_EXEMPT</field>
</record>
<record id="hr_payroll_category_ee_fed_941_fit" model="hr.salary.rule.category">
<field name="name">EE: Federal 941 Income Tax Withholding</field>
<field name="code">EE_US_941_FIT</field>
<field name="parent_id" ref="hr_payroll.DED"/>
</record>
<record id="hr_payroll_rule_ee_fed_941_fit" model="hr.salary.rule">
<field name="sequence" eval="190"/>
<field name="category_id" ref="hr_payroll_category_ee_fed_941_fit"/>
<field name="name">EE: US Federal Income Tax Withholding</field>
<field name="code">EE_US_941_FIT</field>
<field name="condition_select">python</field>
<field name="condition_python">result, _ = ee_us_941_fit(payslip, categories, worked_days, inputs)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result, result_rate = ee_us_941_fit(payslip, categories, worked_days, inputs)</field>
<field name="register_id" ref="contrib_register_eftps_941"/>
<field name="appears_on_payslip" eval="True"/>
</record>
</odoo>

27
l10n_us_hr_payroll/data/final.xml Executable file → Normal file
View File

@@ -1,29 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- HR PAYROLL STRUCTURE -->
<record id="hr_payroll_salary_structure_us_employee" model="hr.payroll.structure">
<record id="structure_type_employee" model="hr.payroll.structure">
<field name="code">US_EMP</field>
<field name="name">USA Employee</field>
<field eval="[(6, 0, [
ref('hr_payroll_rules_fica_comp_ss'),
ref('hr_payroll_rules_fica_comp_m'),
ref('hr_payroll_rule_er_fed_940'),
ref('hr_payroll_rules_fica_emp_ss_wages_2018'),
ref('hr_payroll_rules_fica_emp_m_wages_2018'),
ref('hr_payroll_rules_fica_emp_m_add_wages_2018'),
ref('hr_payroll_rules_fica_emp_ss_2018'),
ref('hr_payroll_rules_fica_emp_m_2018'),
ref('hr_payroll_rules_fica_emp_m_add_2018'),
ref('hr_payroll_rules_futa_wages_2018'),
ref('hr_payroll_rules_futa_2018'),
ref('hr_payroll_rules_fed_inc_withhold_2018_single'),
ref('hr_payroll_rules_fed_inc_withhold_2018_married'),
ref('hr_payroll_rule_ee_fed_941_ss'),
ref('hr_payroll_rule_er_fed_941_ss'),
ref('hr_payroll_rule_ee_fed_941_m'),
ref('hr_payroll_rule_er_fed_941_m'),
ref('hr_payroll_rule_ee_fed_941_m_add'),
ref('hr_payroll_rule_ee_fed_941_fit'),
ref('hr_salary_rule_commission'),
ref('hr_salary_rule_gamification'),
])]" name="rule_ids"/>
<field name="company_id" ref="base.main_company"/>
<field name="parent_id" ref="hr_payroll.structure_base"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Commissions from hr_payroll_commission -->
<record id="hr_salary_rule_commission" model="hr.salary.rule">
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.COMMISSION.amount > 0.0 if inputs.COMMISSION else False</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = inputs.COMMISSION.amount if inputs.COMMISSION else 0</field>
<field name="code">BASIC_COM</field>
<field name="category_id" ref="hr_payroll.BASIC"/>
<field name="name">Commissions</field>
<field name="sequence" eval="20"/>
</record>
<!-- Badges from hr_payroll_gamification -->
<record id="hr_salary_rule_gamification" model="hr.salary.rule">
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.BADGES.amount > 0.0 if inputs.BADGES else False</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = inputs.BADGES.amount if inputs.BADGES else 0</field>
<field name="code">BASIC_BADGES</field>
<field name="category_id" ref="hr_payroll.BASIC"/>
<field name="name">Badges</field>
<field name="sequence" eval="20"/>
</record>
</odoo>

View File

@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- FUTA -->
<record id="hr_payroll_rates_futa_exempt" model="hr.payroll.rate">
<field name="name">US FUTA Exempt</field>
<field name="code">US_FUTA_EXEMPT</field>
<field name="rate">0.0</field>
<field name="date_from">2016-01-01</field>
<field name="wage_limit_year" eval="7000"/>
</record>
<record id="hr_payroll_rates_futa_normal" model="hr.payroll.rate">
<field name="name">US FUTA Normal</field>
<field name="code">US_FUTA_NORMAL</field>
<field name="rate">0.6</field>
<field name="date_from">2016-01-01</field>
<field name="wage_limit_year" eval="7000"/>
</record>
<record id="hr_payroll_rates_futa_basic" model="hr.payroll.rate">
<field name="name">US FUTA Basic</field>
<field name="code">US_FUTA_BASIC</field>
<field name="rate">6.0</field>
<field name="date_from">2016-01-01</field>
<field name="wage_limit_year" eval="7000"/>
</record>
<!-- FICA -->
<!-- Social Security -->
<record id="hr_payroll_rates_fica_ss_old" model="hr.payroll.rate">
<field name="name">US FICA Social Security</field>
<field name="code">US_FICA_SS</field>
<field name="rate">6.2</field>
<field name="date_from">2016-01-01</field>
<field name="date_to">2017-12-31</field>
<field name="wage_limit_year" eval="128400.0"/>
</record>
<record id="hr_payroll_rates_fica_ss_2018" model="hr.payroll.rate">
<field name="name">US FICA Social Security</field>
<field name="code">US_FICA_SS</field>
<field name="rate">6.2</field>
<field name="date_from">2018-01-01</field>
<field name="date_to">2018-12-31</field>
<field name="wage_limit_year" eval="128400.0"/>
</record>
<record id="hr_payroll_rates_fica_ss_2019" model="hr.payroll.rate">
<field name="name">US FICA Social Security</field>
<field name="code">US_FICA_SS</field>
<field name="rate">6.2</field>
<field name="date_from">2019-01-01</field>
<field name="date_to">2019-12-31</field>
<field name="wage_limit_year" eval="132900.0"/>
</record>
<!-- Medicare -->
<record id="hr_payroll_rates_fica_m" model="hr.payroll.rate">
<field name="name">US FICA Medicare</field>
<field name="code">US_FICA_M</field>
<field name="rate">1.45</field>
<field name="date_from">2016-01-01</field>
</record>
<!-- Medicare Additional -->
<record id="hr_payroll_rates_fica_m_add" model="hr.payroll.rate">
<field name="name">US FICA Medicare Additional</field>
<field name="code">US_FICA_M_ADD</field>
<field name="rate">0.9</field>
<field name="date_from">2016-01-01</field>
<field name="wage_limit_year">200000.0</field>
</record>
</odoo>

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -0,0 +1 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.

View File

@@ -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

View File

@@ -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)

View File

@@ -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]

View File

@@ -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())

View File

@@ -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')

View File

@@ -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)')

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -1,3 +1,5 @@
from . import test_us_payslip
from . import test_us_payslip_2018
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import common
from . import test_us_payslip_2019
from . import test_us_payslip_2020

View File

@@ -0,0 +1,155 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from logging import getLogger
from sys import float_info as sys_float_info
from collections import defaultdict
from odoo.tests import common
from odoo.tools.float_utils import float_round as odoo_float_round
def process_payslip(payslip):
try:
payslip.action_payslip_done()
except AttributeError:
# v9
payslip.process_sheet()
class TestUsPayslip(common.TransactionCase):
debug = False
_logger = getLogger(__name__)
float_info = sys_float_info
def float_round(self, value, digits):
return odoo_float_round(value, digits)
_payroll_digits = -1
@property
def payroll_digits(self):
if self._payroll_digits == -1:
self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll')
return self._payroll_digits
def _log(self, message):
if self.debug:
self._logger.warn(message)
def _createEmployee(self):
return self.env['hr.employee'].create({
'birthday': '1985-03-14',
'country_id': self.ref('base.us'),
'department_id': self.ref('hr.dep_rd'),
'gender': 'male',
'name': 'Jared'
})
def _createContract(self, employee, **kwargs):
if not 'schedule_pay' in kwargs:
kwargs['schedule_pay'] = 'monthly'
schedule_pay = kwargs['schedule_pay']
config_model = self.env['hr.contract.us_payroll_config']
contract_model = self.env['hr.contract']
config_values = {
'name': 'Test Config Values',
'employee_id': employee.id,
}
contract_values = {
'name': 'Test Contract',
'employee_id': employee.id,
}
for key, val in kwargs.items():
# Assume any Odoo object is in a Many2one
if hasattr(val, 'id'):
val = val.id
found = False
if hasattr(contract_model, key):
contract_values[key] = val
found = True
if hasattr(config_model, key):
config_values[key] = val
found = True
if not found:
self._logger.warn('cannot locate attribute names "%s" on contract or payroll config' % (key, ))
# US Payroll Config Defaults Should be set on the Model
config = config_model.create(config_values)
contract_values['us_payroll_config_id'] = config.id
# Some Basic Defaults
if not contract_values.get('state'):
contract_values['state'] = 'open' # Running
if not contract_values.get('struct_id'):
contract_values['struct_id'] = self.ref('l10n_us_hr_payroll.structure_type_employee')
if not contract_values.get('date_start'):
contract_values['date_start'] = '2016-01-01'
if not contract_values.get('date_end'):
contract_values['date_end'] = '2030-12-31'
if not contract_values.get('resource_calendar_id'):
contract_values['resource_calendar_id'] = self.ref('resource.resource_calendar_std')
# Compatibility with earlier Odoo versions
if not contract_values.get('journal_id') and hasattr(contract_model, 'journal_id'):
try:
contract_values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id
except KeyError:
# Accounting not installed
pass
contract = contract_model.create(contract_values)
return contract
def _createPayslip(self, employee, date_from, date_to):
slip = self.env['hr.payslip'].create({
'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to),
'employee_id': employee.id,
'date_from': date_from,
'date_to': date_to
})
if hasattr(slip, '_onchange_employee'):
slip._onchange_employee()
if hasattr(slip, 'onchange_employee'):
# Odoo 12
slip.onchange_employee()
if self.debug:
self._logger.warn(slip.read())
self._logger.warn(slip.contract_id.read())
return slip
def _getCategories(self, payslip):
categories = defaultdict(float)
for line in payslip.line_ids:
self._log(' line code: ' + str(line.code) +
' category code: ' + line.category_id.code +
' total: ' + str(line.total) +
' rate: ' + str(line.rate) +
' amount: ' + str(line.amount))
category_id = line.category_id
category_code = line.category_id.code
while category_code:
categories[category_code] += line.total
category_id = category_id.parent_id
category_code = category_id.code
return categories
def _getRules(self, payslip):
rules = defaultdict(float)
for line in payslip.line_ids:
rules[line.code] += line.total
return rules
def assertPayrollEqual(self, first, second):
self.assertAlmostEqual(first, second, self.payroll_digits)
def test_semi_monthly(self):
salary = 80000.0
employee = self._createEmployee()
# so the schedule_pay is now on the Structure...
contract = self._createContract(employee, wage=salary, schedule_pay='semi-monthly')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-14')
payslip.compute_sheet()

View File

@@ -1,117 +0,0 @@
from logging import getLogger
from sys import float_info as sys_float_info
from odoo.tests import common
from odoo.tools.float_utils import float_round as odoo_float_round
from odoo.addons.l10n_us_hr_payroll.models.l10n_us_hr_payroll import USHrContract
def process_payslip(payslip):
try:
#v9
payslip.process_sheet()
except AttributeError:
payslip.action_payslip_done()
class TestUsPayslip(common.TransactionCase):
debug = False
_logger = getLogger(__name__)
float_info = sys_float_info
def float_round(self, value, digits):
return odoo_float_round(value, digits)
_payroll_digits = -1
@property
def payroll_digits(self):
if self._payroll_digits == -1:
self._payroll_digits = self.env['decimal.precision'].precision_get('Payroll')
return self._payroll_digits
def _log(self, message):
if self.debug:
self._logger.warn(message)
def _createEmployee(self):
return self.env['hr.employee'].create({
'birthday': '1985-03-14',
'country_id': self.ref('base.us'),
'department_id': self.ref('hr.dep_rd'),
'gender': 'male',
'name': 'Jared'
})
def _createContract(self, employee, salary,
schedule_pay='monthly',
w4_allowances=0,
w4_filing_status='single',
w4_is_nonresident_alien=False,
w4_additional_withholding=0.0,
external_wages=0.0,
struct_id=False,
futa_type=USHrContract.FUTA_TYPE_NORMAL,
):
if not struct_id:
struct_id = self.ref('l10n_us_hr_payroll.hr_payroll_salary_structure_us_employee')
values = {
'date_start': '2016-01-01',
'date_end': '2030-12-31',
'name': 'Contract for Jared 2016',
'wage': salary,
'type_id': self.ref('hr_contract.hr_contract_type_emp'),
'employee_id': employee.id,
'struct_id': struct_id,
'resource_calendar_id': self.ref('resource.resource_calendar_std'),
'schedule_pay': schedule_pay,
'w4_allowances': w4_allowances,
'w4_filing_status': w4_filing_status,
'w4_is_nonresident_alien': w4_is_nonresident_alien,
'w4_additional_withholding': w4_additional_withholding,
'external_wages': external_wages,
'futa_type': futa_type,
'state': 'open', # if not "Running" then no automatic selection when Payslip is created
}
try:
values['journal_id'] = self.env['account.journal'].search([('type', '=', 'general')], limit=1).id
except KeyError:
pass
return self.env['hr.contract'].create(values)
def _createPayslip(self, employee, date_from, date_to):
return self.env['hr.payslip'].create({
'employee_id': employee.id,
'date_from': date_from,
'date_to': date_to
})
def _getCategories(self, payslip):
detail_lines = payslip.details_by_salary_rule_category
categories = {}
for line in detail_lines:
self._log(' line code: ' + str(line.code) +
' category code: ' + line.category_id.code +
' total: ' + str(line.total) +
' rate: ' + str(line.rate) +
' amount: ' + str(line.amount))
if line.category_id.code not in categories:
categories[line.category_id.code] = line.total
else:
categories[line.category_id.code] += line.total
return categories
def assertPayrollEqual(self, first, second):
self.assertAlmostEqual(first, second, self.payroll_digits)
def test_semi_monthly(self):
salary = 80000.0
employee = self._createEmployee()
contract = self._createContract(employee, salary, schedule_pay='semi-monthly')
payslip = self._createPayslip(employee, '2016-01-01', '2016-01-14')
payslip.compute_sheet()

View File

@@ -1,368 +0,0 @@
from .test_us_payslip import TestUsPayslip, process_payslip
from odoo.addons.l10n_us_hr_payroll.models.l10n_us_hr_payroll import USHrContract
from sys import float_info
class TestUsPayslip2018(TestUsPayslip):
# FUTA Constants
FUTA_RATE_NORMAL = 0.6
FUTA_RATE_BASIC = 6.0
FUTA_RATE_EXEMPT = 0.0
# Wage caps
FICA_SS_MAX_WAGE = 128400.0
FICA_M_MAX_WAGE = float_info.max
FICA_M_ADD_START_WAGE = 200000.0
FUTA_MAX_WAGE = 7000.0
# Rates
FICA_SS = 6.2 / -100.0
FICA_M = 1.45 / -100.0
FUTA = FUTA_RATE_NORMAL / -100.0
FICA_M_ADD = 0.9 / -100.0
###
# 2018 Taxes and Rates
###
def test_2018_taxes(self):
# salary is high so that second payslip runs over max
# social security salary
salary = 80000.0
employee = self._createEmployee()
self._createContract(employee, salary)
self._log('2017 tax last slip')
payslip = self._createPayslip(employee, '2017-12-01', '2017-12-31')
payslip.compute_sheet()
process_payslip(payslip)
self._log('2018 tax first payslip:')
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
process_payslip(payslip)
# Make a new payslip, this one will have maximums for FICA Social Security Wages
remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
self._log('2018 tax second payslip:')
payslip = self._createPayslip(employee, '2018-02-01', '2018-02-28')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], remaining_ss_wages)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], remaining_m_wages)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], 0)
self.assertPayrollEqual(cats['ER_US_FUTA'], 0)
process_payslip(payslip)
# Make a new payslip, this one will have reached Medicare Additional (employee only)
self._log('2018 tax third payslip:')
payslip = self._createPayslip(employee, '2018-03-01', '2018-03-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], self.FICA_M_ADD_START_WAGE - (salary * 2)) # aka 40k
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
process_payslip(payslip)
# Make a new payslip, this one will have all salary as Medicare Additional
self._log('2018 tax fourth payslip:')
payslip = self._createPayslip(employee, '2018-04-01', '2018-04-30')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
process_payslip(payslip)
def test_2018_fed_income_withholding_single(self):
salary = 6000.00
schedule_pay = 'monthly'
w4_allowances = 3
w4_allowance_amt = 345.80 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 4962.60, but would work over a wide value for the rate
###
# Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(371.12 + ((adjusted_salary - 3533) * 0.22)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'single')
self._log('2018 fed income single payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
def test_2018_fed_income_withholding_married_as_single(self):
salary = 500.00
schedule_pay = 'weekly'
w4_allowances = 1
w4_allowance_amt = 79.80 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 420.50, but would work over a wide value for the rate
###
# Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(18.30 + ((adjusted_salary - 254) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'married_as_single')
self._log('2018 fed income married_as_single payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
def test_2018_fed_income_withholding_married(self):
salary = 14000.00
schedule_pay = 'bi-weekly'
w4_allowances = 2
w4_allowance_amt = 159.60 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 13680.80, but would work over a wide value for the rate
###
# Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(2468.56 + ((adjusted_salary - 12560) * 0.32)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'married')
self._log('2018 fed income married payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
def test_2018_taxes_with_external(self):
# social security salary
salary = self.FICA_M_ADD_START_WAGE
external_wages = 6000.0
employee = self._createEmployee()
self._createContract(employee, salary, external_wages=external_wages)
self._log('2018 tax first payslip:')
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE - external_wages)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE - external_wages)
self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
def test_2018_taxes_with_full_futa(self):
futa_rate = self.FUTA_RATE_BASIC / -100.0
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, salary, futa_type=USHrContract.FUTA_TYPE_BASIC)
self._log('2018 tax first payslip:')
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * futa_rate)
def test_2018_taxes_with_futa_exempt(self):
futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, salary, futa_type=USHrContract.FUTA_TYPE_EXEMPT)
self._log('2018 tax first payslip:')
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
futa_wages = 0.0
if 'WAGE_US_FUTA' in cats:
futa_wages = cats['WAGE_US_FUTA']
futa = 0.0
if 'ER_US_FUTA' in cats:
futa = cats['ER_US_FUTA']
self.assertPayrollEqual(futa_wages, 0.0)
self.assertPayrollEqual(futa, futa_wages * futa_rate)
def test_2018_fed_income_withholding_nonresident_alien(self):
salary = 3500.00
schedule_pay = 'quarterly'
w4_allowances = 1
w4_allowance_amt = 1037.50 * w4_allowances
nra_adjustment = 1962.50 # for quarterly
adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4425
###
# Single QUARTERLY form Publication 15
expected_withholding = self.float_round(-(238.10 + ((adjusted_salary - 3306) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'single',
w4_is_nonresident_alien=True)
self._log('2018 fed income single payslip nonresident alien: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
def test_2018_fed_income_additional_withholding(self):
salary = 50000.00
schedule_pay = 'annually'
w4_additional_withholding = 5000.0
w4_allowances = 2
w4_allowance_amt = 4150.00 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # 41700
###
# Single ANNUAL form Publication 15
expected_withholding = \
self.float_round(-((1905.00 + ((adjusted_salary - 30600) * 0.12)) + w4_additional_withholding),
self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'married',
w4_additional_withholding=w4_additional_withholding)
self._log('2018 fed income married payslip additional withholding: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
def test_2018_taxes_with_w4_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
w4_allowances = 0
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, '')
self._log('2018 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
fed_inc_withhold = 0.0
if 'EE_US_FED_INC_WITHHOLD' in cats:
fed_inc_withhold = cats['EE_US_FED_INC_WITHHOLD']
self.assertPayrollEqual(fed_inc_withhold, 0.0)
def test_2018_taxes_with_fica_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
w4_allowances = 2
employee = self._createEmployee()
contract = self._createContract(employee, salary, schedule_pay, w4_allowances)
contract.fica_exempt = True
self._log('2018 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2018-01-01', '2018-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
ss_wages = cats.get('WAGE_US_FICA_SS', 0.0)
med_wages = cats.get('WAGE_US_FICA_M', 0.0)
ss = cats.get('EE_US_FICA_SS', 0.0)
med = cats.get('EE_US_FICA_M', 0.0)
self.assertPayrollEqual(ss_wages, 0.0)
self.assertPayrollEqual(med_wages, 0.0)
self.assertPayrollEqual(ss, 0.0)
self.assertPayrollEqual(med, 0.0)

239
l10n_us_hr_payroll/tests/test_us_payslip_2019.py Executable file → Normal file
View File

@@ -1,6 +1,8 @@
from .test_us_payslip import TestUsPayslip, process_payslip
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.addons.l10n_us_hr_payroll.models.l10n_us_hr_payroll import USHrContract
from .common import TestUsPayslip, process_payslip
from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
from sys import float_info
@@ -28,41 +30,44 @@ class TestUsPayslip2019(TestUsPayslip):
###
def test_2019_taxes(self):
self.debug = False
# salary is high so that second payslip runs over max
# social security salary
salary = 80000.0
employee = self._createEmployee()
self._createContract(employee, salary)
contract = self._createContract(employee, wage=salary)
self._log(contract.read())
self._log('2018 tax last slip')
payslip = self._createPayslip(employee, '2018-12-01', '2018-12-31')
payslip.compute_sheet()
self._log(payslip.read())
process_payslip(payslip)
# Ensure amounts are there, they shouldn't be added in the next year...
cats = self._getCategories(payslip)
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
rules = self._getRules(payslip)
# Employee
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
# Employer
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
process_payslip(payslip)
# Make a new payslip, this one will have maximums for FICA Social Security Wages
# Make a new payslip, this one will have reached Medicare Additional (employee only)
remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
@@ -72,32 +77,25 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], remaining_ss_wages)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], remaining_m_wages)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], 0)
self.assertPayrollEqual(cats['ER_US_FUTA'], 0)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
process_payslip(payslip)
# Make a new payslip, this one will have reached Medicare Additional (employee only)
self._log('2019 tax third payslip:')
payslip = self._createPayslip(employee, '2019-03-01', '2019-03-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], self.FICA_M_ADD_START_WAGE - (salary * 2)) # aka 40k
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k
process_payslip(payslip)
@@ -109,14 +107,15 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD)
process_payslip(payslip)
def test_2019_fed_income_withholding_single(self):
self.debug = False
salary = 6000.00
schedule_pay = 'monthly'
w4_allowances = 3
@@ -127,16 +126,18 @@ class TestUsPayslip2019(TestUsPayslip):
expected_withholding = self.float_round(-(378.52 + ((adjusted_salary - 3606) * 0.22)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'single')
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='single',
fed_941_fit_w4_allowances=w4_allowances
)
self._log('2019 fed income single payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_withholding_married_as_single(self):
salary = 500.00
@@ -145,20 +146,21 @@ class TestUsPayslip2019(TestUsPayslip):
w4_allowance_amt = 80.80 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 420.50, but would work over a wide value for the rate
###
# Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(18.70 + ((adjusted_salary - 260) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'married_as_single')
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='married_as_single',
fed_941_fit_w4_allowances=w4_allowances,
)
self._log('2019 fed income married_as_single payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_withholding_married(self):
salary = 14000.00
@@ -171,53 +173,52 @@ class TestUsPayslip2019(TestUsPayslip):
expected_withholding = self.float_round(-(2519.06 + ((adjusted_salary - 12817) * 0.32)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'married')
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='married',
fed_941_fit_w4_allowances=w4_allowances
)
self._log('2019 fed income married payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
# This is off by 1 penny given our new reporting of adjusted wage * computed percentage
#self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
self.assertTrue(abs(cats['EE_US_941_FIT'] - expected_withholding) < 0.011)
def test_2019_taxes_with_external(self):
# social security salary
salary = self.FICA_M_ADD_START_WAGE
external_wages = 6000.0
employee = self._createEmployee()
self._createContract(employee, salary, external_wages=external_wages)
self._createContract(employee, wage=salary, external_wages=external_wages)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE - external_wages)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE - external_wages)
self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * self.FUTA)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD)
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA)
def test_2019_taxes_with_full_futa(self):
self.debug = True
futa_rate = self.FUTA_RATE_BASIC / -100.0
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, salary, futa_type=USHrContract.FUTA_TYPE_BASIC)
self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
@@ -225,52 +226,26 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
self.assertPayrollEqual(cats['WAGE_US_FUTA'], self.FUTA_MAX_WAGE)
self.assertPayrollEqual(cats['ER_US_FUTA'], cats['WAGE_US_FUTA'] * futa_rate)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD)
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate)
def test_2019_taxes_with_futa_exempt(self):
futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, salary, futa_type=USHrContract.FUTA_TYPE_EXEMPT)
self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['WAGE_US_FICA_SS'], self.FICA_SS_MAX_WAGE)
self.assertPayrollEqual(cats['EE_US_FICA_SS'], cats['WAGE_US_FICA_SS'] * self.FICA_SS)
self.assertPayrollEqual(cats['WAGE_US_FICA_M'], salary)
self.assertPayrollEqual(cats['EE_US_FICA_M'], cats['WAGE_US_FICA_M'] * self.FICA_M)
self.assertPayrollEqual(cats['WAGE_US_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['EE_US_FICA_M_ADD'], cats['WAGE_US_FICA_M_ADD'] * self.FICA_M_ADD)
self.assertPayrollEqual(cats['ER_US_FICA_SS'], cats['EE_US_FICA_SS'])
self.assertPayrollEqual(cats['ER_US_FICA_M'], cats['EE_US_FICA_M'])
futa_wages = 0.0
if 'WAGE_US_FUTA' in cats:
futa_wages = cats['WAGE_US_FUTA']
futa = 0.0
if 'ER_US_FUTA' in cats:
futa = cats['ER_US_FUTA']
self.assertPayrollEqual(futa_wages, 0.0)
self.assertPayrollEqual(futa, futa_wages * futa_rate)
self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
def test_2019_fed_income_withholding_nonresident_alien(self):
salary = 3500.00
@@ -278,24 +253,26 @@ class TestUsPayslip2019(TestUsPayslip):
w4_allowances = 1
w4_allowance_amt = 1050.0 * w4_allowances
nra_adjustment = 2000.0 # for quarterly
adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4425
adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4450
###
# Single QUARTERLY form Publication 15
expected_withholding = self.float_round(-(242.50 + ((adjusted_salary - 3375) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'single',
w4_is_nonresident_alien=True)
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_allowances=w4_allowances,
fed_941_fit_w4_is_nonresident_alien=True,
)
self._log('2019 fed income single payslip nonresident alien: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_additional_withholding(self):
salary = 50000.00
@@ -312,44 +289,45 @@ class TestUsPayslip2019(TestUsPayslip):
self.payroll_digits)
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, 'married',
w4_additional_withholding=w4_additional_withholding)
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='married',
fed_941_fit_w4_allowances=w4_allowances,
fed_941_fit_w4_additional_withholding=w4_additional_withholding,
)
self._log('2019 fed income married payslip additional withholding: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_FED_INC_WITHHOLD'], expected_withholding)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
def test_2019_taxes_with_w4_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
w4_allowances = 0
employee = self._createEmployee()
self._createContract(employee, salary, schedule_pay, w4_allowances, '')
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_allowances=w4_allowances,
fed_941_fit_w4_filing_status='',
)
self._log('2019 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
fed_inc_withhold = 0.0
if 'EE_US_FED_INC_WITHHOLD' in cats:
fed_inc_withhold = cats['EE_US_FED_INC_WITHHOLD']
self.assertPayrollEqual(fed_inc_withhold, 0.0)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FIT'], 0.0)
def test_2019_taxes_with_fica_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
w4_allowances = 2
employee = self._createEmployee()
contract = self._createContract(employee, salary, schedule_pay, w4_allowances)
contract.fica_exempt = True
contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay)
contract.us_payroll_config_id.fed_941_fica_exempt = True
self._log('2019 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
@@ -357,12 +335,5 @@ class TestUsPayslip2019(TestUsPayslip):
payslip.compute_sheet()
cats = self._getCategories(payslip)
ss_wages = cats.get('WAGE_US_FICA_SS', 0.0)
med_wages = cats.get('WAGE_US_FICA_M', 0.0)
ss = cats.get('EE_US_FICA_SS', 0.0)
med = cats.get('EE_US_FICA_M', 0.0)
self.assertPayrollEqual(ss_wages, 0.0)
self.assertPayrollEqual(med_wages, 0.0)
self.assertPayrollEqual(ss, 0.0)
self.assertPayrollEqual(med, 0.0)
self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)

View File

@@ -0,0 +1,302 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from .common import TestUsPayslip, process_payslip
from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
from sys import float_info
class TestUsPayslip2020(TestUsPayslip):
# FUTA Constants
FUTA_RATE_NORMAL = 0.6
FUTA_RATE_BASIC = 6.0
FUTA_RATE_EXEMPT = 0.0
# Wage caps
FICA_SS_MAX_WAGE = 137700.0
FICA_M_MAX_WAGE = float_info.max
FICA_M_ADD_START_WAGE = 200000.0
FUTA_MAX_WAGE = 7000.0
# Rates
FICA_SS = 6.2 / -100.0
FICA_M = 1.45 / -100.0
FUTA = FUTA_RATE_NORMAL / -100.0
FICA_M_ADD = 0.9 / -100.0
###
# 2020 Taxes and Rates
###
def test_2020_taxes(self):
self.debug = False
# salary is high so that second payslip runs over max
# social security salary
salary = 80000.0
employee = self._createEmployee()
contract = self._createContract(employee, wage=salary)
self._log(contract.read())
self._log('2019 tax last slip')
payslip = self._createPayslip(employee, '2019-12-01', '2019-12-31')
payslip.compute_sheet()
self._log(payslip.read())
process_payslip(payslip)
# Ensure amounts are there, they shouldn't be added in the next year...
cats = self._getCategories(payslip)
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
self._log('2020 tax first payslip:')
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
# Employee
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
# Employer
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
process_payslip(payslip)
# Make a new payslip, this one will have reached Medicare Additional (employee only)
remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
self._log('2020 tax second payslip:')
payslip = self._createPayslip(employee, '2020-02-01', '2020-02-28')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
process_payslip(payslip)
# Make a new payslip, this one will have reached Medicare Additional (employee only)
self._log('2020 tax third payslip:')
payslip = self._createPayslip(employee, '2020-03-01', '2020-03-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k
process_payslip(payslip)
# Make a new payslip, this one will have all salary as Medicare Additional
self._log('2020 tax fourth payslip:')
payslip = self._createPayslip(employee, '2020-04-01', '2020-04-30')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD)
process_payslip(payslip)
def test_2020_taxes_with_external(self):
# social security salary
salary = self.FICA_M_ADD_START_WAGE
external_wages = 6000.0
employee = self._createEmployee()
self._createContract(employee, wage=salary, external_wages=external_wages)
self._log('2020 tax first payslip:')
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD)
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA)
def test_2020_taxes_with_full_futa(self):
futa_rate = self.FUTA_RATE_BASIC / -100.0
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC)
self._log('2020 tax first payslip:')
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD)
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate)
def test_2020_taxes_with_futa_exempt(self):
futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT)
self._log('2020 tax first payslip:')
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
def test_2020_taxes_with_fica_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
employee = self._createEmployee()
contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay)
contract.us_payroll_config_id.fed_941_fica_exempt = True
self._log('2020 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)
"""
For Federal Income Tax Withholding, we are utilizing the calculations from the new IRS Excel calculator.
Given that you CAN round, we will round to compare even though we will calculate as close to the penny as possible
with the wage * computed_percent method.
"""
def _run_test_fit(self,
wage=0.0,
schedule_pay='monthly',
filing_status='single',
dependent_credit=0.0,
other_income=0.0,
deductions=0.0,
additional_withholding=0.0,
is_nonresident_alien=False,
expected_standard=0.0,
expected_higher=0.0,
):
employee = self._createEmployee()
contract = self._createContract(employee,
wage=wage,
schedule_pay=schedule_pay,
fed_941_fit_w4_is_nonresident_alien=is_nonresident_alien,
fed_941_fit_w4_filing_status=filing_status,
fed_941_fit_w4_multiple_jobs_higher=False,
fed_941_fit_w4_dependent_credit=dependent_credit,
fed_941_fit_w4_other_income=other_income,
fed_941_fit_w4_deductions=deductions,
fed_941_fit_w4_additional_withholding=additional_withholding,
)
payslip = self._createPayslip(employee, '2020-01-01', '2020-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_standard)
contract.us_payroll_config_id.fed_941_fit_w4_multiple_jobs_higher = True
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(round(cats.get('EE_US_941_FIT', 0.0)), -expected_higher)
return payslip
def test_2020_fed_income_withholding_single(self):
_ = self._run_test_fit(
wage=6000.0,
schedule_pay='monthly',
filing_status='single',
dependent_credit=100.0,
other_income=200.0,
deductions=300.0,
additional_withholding=400.0,
is_nonresident_alien=False,
expected_standard=1132.0,
expected_higher=1459.0,
)
def test_2020_fed_income_withholding_married_as_single(self):
# This is "Head of Household" though the field name is the same for historical reasons.
_ = self._run_test_fit(
wage=500.0,
schedule_pay='weekly',
filing_status='married_as_single',
dependent_credit=20.0,
other_income=30.0,
deductions=40.0,
additional_withholding=10.0,
is_nonresident_alien=False,
expected_standard=24.0,
expected_higher=45.0,
)
def test_2020_fed_income_withholding_married(self):
_ = self._run_test_fit(
wage=14000.00,
schedule_pay='bi-weekly',
filing_status='married',
dependent_credit=2500.0,
other_income=1200.0,
deductions=1000.0,
additional_withholding=0.0,
is_nonresident_alien=False,
expected_standard=2621.0,
expected_higher=3702.0,
)
def test_2020_fed_income_withholding_nonresident_alien(self):
# Monthly NRA additional wage is 1033.30
# Wage input on IRS Form entered as (3500+1033.30)=4533.30, not 3500.0
_ = self._run_test_fit(
wage=3500.00,
schedule_pay='monthly',
filing_status='married',
dependent_credit=340.0,
other_income=0.0,
deductions=0.0,
additional_withholding=0.0,
is_nonresident_alien=True,
expected_standard=235.0,
expected_higher=391.0,
)
def test_2020_taxes_with_w4_exempt(self):
_ = self._run_test_fit(
wage=3500.00,
schedule_pay='monthly',
filing_status='', # Exempt
dependent_credit=340.0,
other_income=0.0,
deductions=0.0,
additional_withholding=0.0,
is_nonresident_alien=True,
expected_standard=0.0,
expected_higher=0.0,
)

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_contract_view_form_inherit_l10n_us" model="ir.ui.view">
<field name="name">hr.contract.form.inherit</field>
<field name="model">hr.contract</field>
<field name="inherit_id" ref="hr_payroll.hr_contract_form_inherit"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='struct_id']" position="after">
<field name="us_payroll_config_id"
domain="[('employee_id', '=', employee_id)]"
attrs="{'invisible': [('struct_id', '!=', %(l10n_us_hr_payroll.structure_type_employee)s)],
'required': [('struct_id', '=', %(l10n_us_hr_payroll.structure_type_employee)s)]}"
context="{'default_employee_id': employee_id}"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="hr_contract_form_l10n_us_inherit" model="ir.ui.view">
<field name="name">hr.contract.form.inherit</field>
<field name="model">hr.contract</field>
<field name="priority">20</field>
<field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//page[@name='other']" position="before">
<page string="US Federal Filing">
<group>
<group string="W4" name="w4">
<field name="w4_filing_status" string="Filing Status"/>
<field name="w4_allowances" string="Allowances"/>
<field name="w4_additional_withholding" string="Additional Withholding"/>
<field name="w4_is_nonresident_alien" string="Non-resident Alien"/>
</group>
<group string="Other" name="other">
<field name="external_wages" string="External YTD Wages"/>
<field name="futa_type" string="Unemployment Tax Type (FUTA)"/>
<field name="fica_exempt"/>
</group>
</group>
</page>
<page string="US State Filing">
<group name="state_filing"/>
</page>
</xpath>
</data>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="us_payroll_config_tree" model="ir.ui.view">
<field name="name">hr.contract.us_payroll_config.tree</field>
<field name="model">hr.contract.us_payroll_config</field>
<field name="arch" type="xml">
<tree string="Employee Payroll Forms">
<field name="employee_id"/>
<field name="name"/>
<field name="state_id"/>
<field name="create_date"/>
<field name="write_date"/>
</tree>
</field>
</record>
<record id="us_payroll_config_form" model="ir.ui.view">
<field name="name">hr.contract.us_payroll_config.form</field>
<field name="model">hr.contract.us_payroll_config</field>
<field name="arch" type="xml">
<form string="Employee Payroll Forms">
<sheet>
<group name="General">
<field name="employee_id"/>
<field name="name"/>
</group>
<group>
<group name="federal" string="Federal">
<field name="state_id" domain="[('country_id', '=', %(base.us)s)]" options="{'no_create': True}"/>
<p colspan="2"><h3>Form 940 - Federal Unemployment</h3></p>
<field name="fed_940_type" string="Federal Unemployment Rate"/>
<p colspan="2"><h3>Form 941 / W4 - Federal Income Tax</h3></p>
<field name="fed_941_fica_exempt" string="FICA Exempt"/>
<field name="fed_941_fit_w4_filing_status" string="Filing Status"/>
<field name="fed_941_fit_w4_allowances" string="Allowances (Old W4)"/>
<field name="fed_941_fit_w4_is_nonresident_alien" string="Is Nonresident Alien"/>
<field name="fed_941_fit_w4_multiple_jobs_higher" string="Multiple Jobs Checked"/>
<field name="fed_941_fit_w4_dependent_credit" string="Dependent Credit"/>
<field name="fed_941_fit_w4_other_income" string="Other Income"/>
<field name="fed_941_fit_w4_deductions" string="Deductions"/>
<field name="fed_941_fit_w4_additional_withholding" string="Additional Withholding"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="us_payroll_config_search" model="ir.ui.view">
<field name="name">hr.contract.us_payroll_config.search</field>
<field name="model">hr.contract.us_payroll_config</field>
<field name="arch" type="xml">
<search string="Employee Payroll Forms Search">
<field name="employee_id"/>
<field name="name"/>
<field name="state_id"/>
</search>
</field>
</record>
<record id="us_payroll_config_action_main" model="ir.actions.act_window">
<field name="name">Employee Payroll Forms</field>
<field name="res_model">hr.contract.us_payroll_config</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p>
No Forms
</p>
</field>
</record>
<menuitem id="us_payroll_config_menu_main" name="Payroll Forms"
action="us_payroll_config_action_main"
sequence="10" parent="hr.menu_hr_root"/>
</odoo>