NEW l10n_us_hr_payroll Initiall commit for Odoo 13 (rewrite) and 2020 Federal Rates (including new W4)

This commit is contained in:
Jared Kipe
2020-01-05 14:07:26 -08:00
parent 2acd13bd98
commit 5c7c9e6397
25 changed files with 2174 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,35 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'United States of America - Payroll',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '13.0.2020.0.0',
'category': 'Payroll Localization',
'depends': [
'hr_payroll',
'hr_contract_reports',
],
'description': """
United States of America - Payroll Rules.
=========================================
""",
'data': [
'security/ir.model.access.csv',
'data/base.xml',
'data/integration_rules.xml',
'data/federal/fed_940_futa_parameters.xml',
'data/federal/fed_940_futa_rules.xml',
'data/federal/fed_941_fica_parameters.xml',
'data/federal/fed_941_fica_rules.xml',
'data/federal/fed_941_fit_parameters.xml',
'data/federal/fed_941_fit_rules.xml',
'views/hr_contract_views.xml',
'views/us_payroll_config_views.xml',
],
'demo': [
],
'auto_install': False,
'license': 'OPL-1',
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="structure_type_employee" model="hr.payroll.structure.type">
<field name="name">USA Employee</field>
<field name="default_resource_calendar_id" ref="resource.resource_calendar_std"/>
<field name="country_id" ref="base.us"/>
</record>
<record id="hr_payroll_structure" model="hr.payroll.structure">
<field name="name">USA Employee Standard</field>
<field name="country_id" ref="base.us"/>
<field name="type_id" ref="l10n_us_hr_payroll.structure_type_employee"/>
<field name="regular_pay" eval="True"/>
<field name="unpaid_work_entry_type_ids" eval="[
(4, ref('hr_payroll.work_entry_type_unpaid_leave')),
]"/>
</record>
</odoo>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Wage Base -->
<record id="rule_parameter_940_futa_wage_base" model="hr.rule.parameter">
<field name="name">Federal 940 FUTA Wage Base</field>
<field name="code">fed_940_futa_wage_base</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_940_futa_wage_base_2016" model="hr.rule.parameter.value">
<field name="parameter_value">7000.00</field>
<field name="rule_parameter_id" ref="rule_parameter_940_futa_wage_base"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_940_futa_rate_basic" model="hr.rule.parameter">
<field name="name">Federal 940 FUTA Rate Basic</field>
<field name="code">fed_940_futa_rate_basic</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_940_futa_rate_basic_2016" model="hr.rule.parameter.value">
<field name="parameter_value">6.0</field>
<field name="rule_parameter_id" ref="rule_parameter_940_futa_rate_basic"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<record id="rule_parameter_940_futa_rate_normal" model="hr.rule.parameter">
<field name="name">Federal 940 FUTA Rate Normal</field>
<field name="code">fed_940_futa_rate_normal</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_940_futa_rate_normal_2016" model="hr.rule.parameter.value">
<field name="parameter_value">0.6</field>
<field name="rule_parameter_id" ref="rule_parameter_940_futa_rate_normal"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
</odoo>

View File

@@ -0,0 +1,34 @@
<?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="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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_eftps_940"/>
<field name="appears_on_payslip" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Social Security -->
<!-- Wage Base -->
<record id="rule_parameter_941_fica_ss_wage_base" model="hr.rule.parameter">
<field name="name">Federal 941 FICA Social Security Wage Base</field>
<field name="code">fed_941_fica_ss_wage_base</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fica_ss_wage_base_2016" model="hr.rule.parameter.value">
<field name="parameter_value">128400.0</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_ss_wage_base"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fica_ss_wage_base_2019" model="hr.rule.parameter.value">
<field name="parameter_value">132900.0</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_ss_wage_base"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fica_ss_wage_base_2020" model="hr.rule.parameter.value">
<field name="parameter_value">137700.0</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_ss_wage_base"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_941_fica_ss_rate" model="hr.rule.parameter">
<field name="name">Federal 941 FICA Rate</field>
<field name="code">fed_941_fica_ss_rate</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fica_ss_rate_2016" model="hr.rule.parameter.value">
<field name="parameter_value">6.2</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_ss_rate"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Medicare -->
<!-- Wage Base -->
<record id="rule_parameter_941_fica_m_wage_base" model="hr.rule.parameter">
<field name="name">Federal 941 FICA Medicare Wage Base</field>
<field name="code">fed_941_fica_m_wage_base</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fica_m_wage_base_2016" model="hr.rule.parameter.value">
<field name="parameter_value">"inf"</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_m_wage_base"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_941_fica_m_rate" model="hr.rule.parameter">
<field name="name">Federal 941 FICA Rate</field>
<field name="code">fed_941_fica_m_rate</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fica_m_rate_2016" model="hr.rule.parameter.value">
<field name="parameter_value">1.45</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_m_rate"/>
<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" model="hr.rule.parameter">
<field name="name">Federal 941 FICA Medicare Additional Wage Start</field>
<field name="code">fed_941_fica_m_add_wage_start</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fica_m_add_wage_start_2016" model="hr.rule.parameter.value">
<field name="parameter_value">200000.0</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_m_add_wage_start"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
<!-- Rate -->
<record id="rule_parameter_941_fica_m_add_rate" model="hr.rule.parameter">
<field name="name">Federal 941 FICA Medicare Additional Rate</field>
<field name="code">fed_941_fica_m_add_rate</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fica_m_add_rate_2016" model="hr.rule.parameter.value">
<field name="parameter_value">0.9</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fica_m_add_rate"/>
<field name="date_from" eval="datetime(2016, 1, 1).date()"/>
</record>
</odoo>

View File

@@ -0,0 +1,100 @@
<?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="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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_eftps_941"/>
<field name="appears_on_payslip" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,504 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Shared -->
<record id="rule_parameter_941_fit_allowance" model="hr.rule.parameter">
<field name="name">Federal 941 FIT Allowance</field>
<field name="code">fed_941_fit_allowance</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fit_allowance_2018" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_allowance"/>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_allowance_2019" model="hr.rule.parameter.value">
<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="rule_parameter_id" ref="rule_parameter_941_fit_allowance"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_allowance_2020" model="hr.rule.parameter.value">
<!-- Warning, major change to allowance in 2020 -->
<field name="parameter_value">4300.0</field>
<field name="rule_parameter_id" ref="rule_parameter_941_fit_allowance"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_nra_additional" model="hr.rule.parameter">
<field name="name">Federal 941 FIT NRA Additional</field>
<field name="code">fed_941_fit_nra_additional</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fit_nra_additional_2018" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_nra_additional"/>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_nra_additional_2019" model="hr.rule.parameter.value">
<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="rule_parameter_id" ref="rule_parameter_941_fit_nra_additional"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_nra_additional_2020" model="hr.rule.parameter.value">
<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="rule_parameter_id" ref="rule_parameter_941_fit_nra_additional"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<!-- Single and Married Single Rate -->
<record id="rule_parameter_941_fit_table_single" model="hr.rule.parameter">
<field name="name">Federal 941 FIT Table Single</field>
<field name="code">fed_941_fit_table_single</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fit_table_single_2018" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_single"/>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_single_2019" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_single"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_single_2020" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_single"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<!-- Married -->
<record id="rule_parameter_941_fit_table_married" model="hr.rule.parameter">
<field name="name">Federal 941 FIT Table Married</field>
<field name="code">fed_941_fit_table_married</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fit_table_married_2018" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_married"/>
<field name="date_from" eval="datetime(2018, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_married_2019" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_married"/>
<field name="date_from" eval="datetime(2019, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_married_2020" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_married"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
<record id="rule_parameter_941_fit_table_hh" model="hr.rule.parameter">
<field name="name">Federal 941 FIT Table Head of Household</field>
<field name="code">fed_941_fit_table_hh</field>
<field name="country_id" ref="base.us"/>
</record>
<record id="rule_parameter_941_fit_table_hh_2020" model="hr.rule.parameter.value">
<!-- 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="rule_parameter_id" ref="rule_parameter_941_fit_table_hh"/>
<field name="date_from" eval="datetime(2020, 1, 1).date()"/>
</record>
</odoo>

View File

@@ -0,0 +1,29 @@
<?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="struct_id" ref="hr_payroll_structure"/>
<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="partner_id" ref="res_partner_eftps_941"/>
<field name="appears_on_payslip" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,29 @@
<?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"/>
<field name="struct_id" ref="l10n_us_hr_payroll.hr_payroll_structure"/>
</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"/>
<field name="struct_id" ref="l10n_us_hr_payroll.hr_payroll_structure"/>
</record>
</odoo>

View File

@@ -0,0 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import hr_contract
from . import hr_payslip
from . import us_payroll_config

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.contract_id.futa_type == payslip.contract_id.FUTA_TYPE_EXEMPT:
# Exit early
return 0.0, 0.0
elif payslip.contract_id.futa_type == payslip.contract_id.FUTA_TYPE_BASIC:
result_rate = -payslip.rule_parameter('fed_940_futa_rate_basic')
else:
result_rate = -payslip.rule_parameter('fed_940_futa_rate_normal')
# Determine Wage
year = payslip.dict.get_year()
ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage -= payslip.sum_category('WAGE_US_940_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage += payslip.contract_id.external_wages
wage_base = payslip.rule_parameter('fed_940_futa_wage_base')
remaining = wage_base - ytd_wage
wage = categories.GROSS - categories.WAGE_US_940_FUTA_EXEMPT
if remaining < 0.0:
result = 0.0
elif remaining < wage:
result = remaining
else:
result = wage
return result, result_rate

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.contract_id.us_payroll_config_value('fed_941_fica_exempt')
if exempt:
return 0.0, 0.0
# Determine Rate.
result_rate = -payslip.rule_parameter('fed_941_fica_ss_rate')
# Determine Wage
year = payslip.dict.get_year()
ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage -= payslip.sum_category('WAGE_US_941_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage += payslip.contract_id.external_wages
wage_base = payslip.rule_parameter('fed_941_fica_ss_wage_base')
remaining = wage_base - ytd_wage
wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
if remaining < 0.0:
result = 0.0
elif remaining < wage:
result = remaining
else:
result = wage
return result, result_rate
er_us_941_fica_ss = ee_us_941_fica_ss
def ee_us_941_fica_m(payslip, categories, worked_days, inputs):
"""
Returns FICA Medicare eligible wage and rate.
WAGE = GROSS - WAGE_US_941_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
if exempt:
return 0.0, 0.0
# Determine Rate.
result_rate = -payslip.rule_parameter('fed_941_fica_m_rate')
# Determine Wage
year = payslip.dict.get_year()
ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage -= payslip.sum_category('WAGE_US_941_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage += payslip.contract_id.external_wages
wage_base = float(payslip.rule_parameter('fed_941_fica_m_wage_base')) # inf
remaining = wage_base - ytd_wage
wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
if remaining < 0.0:
result = 0.0
elif remaining < wage:
result = remaining
else:
result = wage
return result, result_rate
er_us_941_fica_m = ee_us_941_fica_m
def ee_us_941_fica_m_add(payslip, categories, worked_days, inputs):
"""
Returns FICA Medicare Additional eligible wage and rate.
Note that this wage is not capped like the above rules.
WAGE = GROSS - WAGE_FICA_EXEMPT
:return: result, result_rate (wage, percent)
"""
exempt = payslip.contract_id.us_payroll_config_value('fed_941_fica_exempt')
if exempt:
return 0.0, 0.0
# Determine Rate.
result_rate = -payslip.rule_parameter('fed_941_fica_m_add_rate')
# Determine Wage
year = payslip.dict.get_year()
ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage -= payslip.sum_category('WAGE_US_941_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
ytd_wage += payslip.contract_id.external_wages
wage_start = payslip.rule_parameter('fed_941_fica_m_add_wage_start')
existing_wage = ytd_wage - wage_start
wage = categories.GROSS - categories.WAGE_US_941_FICA_EXEMPT
if existing_wage >= 0.0:
result = wage
elif wage + existing_wage > 0.0:
result = wage + existing_wage
else:
result = 0.0
return result, result_rate
# Federal Income Tax
def ee_us_941_fit(payslip, categories, worked_days, inputs):
"""
Returns Wage and rate that is computed given the amount to withhold.
WAGE = GROSS - WAGE_US_941_FIT_EXEMPT
:return: result, result_rate (wage, percent)
"""
filing_status = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_filing_status')
if not filing_status:
return 0.0, 0.0
schedule_pay = payslip.contract_id.schedule_pay
wage = categories.GROSS - categories.WAGE_US_941_FIT_EXEMPT
#_logger.warn('initial gross wage: ' + str(wage))
year = payslip.dict.get_year()
if year >= 2020:
# Large changes in Federal Income Tax in 2020 and the W4
# We will assume that your W4 is the 2020 version
# Steps are from IRS Publication 15-T
#
# Step 1
working_wage = wage
is_nra = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_is_nonresident_alien')
if is_nra:
nra_table = payslip.rule_parameter('fed_941_fit_nra_additional')
working_wage += nra_table.get(schedule_pay, 0.0)
#_logger.warn(' is_nrm after wage: ' + str(working_wage))
pay_periods = payslip.dict.get_pay_periods_in_year()
wage_annual = pay_periods * working_wage
#_logger.warn('annual wage: ' + str(wage_annual))
wage_annual += payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_other_income')
#_logger.warn(' after other income: ' + str(wage_annual))
deductions = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_deductions')
#_logger.warn('deductions from W4: ' + str(deductions))
higher_rate_type = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_multiple_jobs_higher')
if not higher_rate_type:
deductions += 12900.0 if filing_status == 'married' else 8600.0
#_logger.warn(' deductions after standard deduction: ' + str(deductions))
adjusted_wage_annual = wage_annual - deductions
if adjusted_wage_annual < 0.0:
adjusted_wage_annual = 0.0
#_logger.warn('adusted annual wage: ' + str(adjusted_wage_annual))
# Step 2
if filing_status == 'single':
tax_tables = payslip.rule_parameter('fed_941_fit_table_single')
elif filing_status == 'married':
tax_tables = payslip.rule_parameter('fed_941_fit_table_married')
else:
# married_as_single for historic reasons
tax_tables = payslip.rule_parameter('fed_941_fit_table_hh')
if higher_rate_type:
tax_table = tax_tables['higher']
else:
tax_table = tax_tables['standard']
selected_row = None
for row in tax_table:
if row[0] <= adjusted_wage_annual:
selected_row = row
else:
# First row where wage is higher than adjusted_wage_annual
break
wage_threshold, base_withholding_amount, marginal_rate = selected_row
#_logger.warn(' selected row: ' + str(selected_row))
working_wage = adjusted_wage_annual - wage_threshold
tentative_withholding_amount = (working_wage * marginal_rate) + base_withholding_amount
tentative_withholding_amount = tentative_withholding_amount / pay_periods
#_logger.warn('tenative withholding amount: ' + str(tentative_withholding_amount))
# Step 3
dependent_credit = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_dependent_credit')
dependent_credit = dependent_credit / pay_periods
#_logger.warn('dependent credit (per period): ' + str(dependent_credit))
tentative_withholding_amount -= dependent_credit
if tentative_withholding_amount < 0.0:
tentative_withholding_amount = 0.0
# Step 4
withholding_amount = tentative_withholding_amount + payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_additional_withholding')
#_logger.warn('final withholding amount: ' + str(withholding_amount))
# Ideally we would set the 'taxable wage' as the result and compute the percentage tax.
# This is off by 1 penny across our tests, but I feel like it is worth it for the added reporting.
# - Jared Kipe 2019 during Odoo 13.0 rewrite.
#
# return -withholding_amount, 100.0
return wage, -(withholding_amount / wage * 100.0)
else:
working_wage = wage
is_nra = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_is_nonresident_alien')
if is_nra:
nra_table = payslip.rule_parameter('fed_941_fit_nra_additional')
working_wage += nra_table[schedule_pay]
allowance_table = payslip.rule_parameter('fed_941_fit_allowance')
allowances = payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_allowances')
working_wage -= allowance_table[schedule_pay] * allowances
tax = 0.0
last_limit = 0.0
if filing_status == 'married':
tax_table = payslip.rule_parameter('fed_941_fit_table_married')
else:
tax_table = payslip.rule_parameter('fed_941_fit_table_single')
for row in tax_table[schedule_pay]:
limit, base, percent = row
limit = float(limit) # 'inf'
if working_wage <= limit:
tax = base + ((working_wage - last_limit) * (percent / 100.0))
break
last_limit = limit
tax += payslip.contract_id.us_payroll_config_value('fed_941_fit_w4_additional_withholding')
# Ideally we would set the 'taxable wage' as the result and compute the percentage tax.
# This is off by 1 penny across our tests, but I feel like it is worth it for the added reporting.
# - Jared Kipe 2019 during Odoo 13.0 rewrite.
#
# return -tax, 100.0
return wage, -(tax / wage * 100.0)

View File

@@ -0,0 +1,28 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
from .us_payroll_config import FUTA_TYPE_NORMAL, \
FUTA_TYPE_BASIC, \
FUTA_TYPE_EXEMPT
class HrPayrollStructure(models.Model):
_inherit = 'hr.payroll.structure'
schedule_pay = fields.Selection(selection_add=[('semi-monthly', 'Semi-monthly')])
class USHRContract(models.Model):
_inherit = 'hr.contract'
FUTA_TYPE_NORMAL = FUTA_TYPE_NORMAL
FUTA_TYPE_BASIC = FUTA_TYPE_BASIC
FUTA_TYPE_EXEMPT = FUTA_TYPE_EXEMPT
us_payroll_config_id = fields.Many2one('hr.contract.us_payroll_config', 'Payroll Forms')
external_wages = fields.Float(string='External Existing Wages')
# Simplified fields for easier rules, state code will exempt based on contract's futa_type
futa_type = fields.Selection(related='us_payroll_config_id.fed_940_type')
def us_payroll_config_value(self, name):
return self.us_payroll_config_id[name]

View File

@@ -0,0 +1,48 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
from .federal.fed_940 import er_us_940_futa
from .federal.fed_941 import ee_us_941_fica_ss, \
ee_us_941_fica_m, \
ee_us_941_fica_m_add,\
er_us_941_fica_ss, \
er_us_941_fica_m, \
ee_us_941_fit
class HRPayslip(models.Model):
_inherit = 'hr.payslip'
# From IRS Publication 15-T or logically (annually, bi-monthly)
PAY_PERIODS_IN_YEAR = {
'annually': 1,
'semi-annually': 2,
'quarterly': 4,
'bi-monthly': 6,
'monthly': 12,
'semi-monthly': 24,
'bi-weekly': 26,
'weekly': 52,
'daily': 260,
}
def _get_base_local_dict(self):
res = super()._get_base_local_dict()
res.update({
'er_us_940_futa': er_us_940_futa,
'ee_us_941_fica_ss': ee_us_941_fica_ss,
'ee_us_941_fica_m': ee_us_941_fica_m,
'ee_us_941_fica_m_add': ee_us_941_fica_m_add,
'er_us_941_fica_ss': er_us_941_fica_ss,
'er_us_941_fica_m': er_us_941_fica_m,
'ee_us_941_fit': ee_us_941_fit,
})
return res
def get_year(self):
# Helper method to get the year (normalized between Odoo Versions)
return self.date_to.year
def get_pay_periods_in_year(self):
return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0)

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

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

View File

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

View File

@@ -0,0 +1,338 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from .common import TestUsPayslip, process_payslip
from odoo.addons.l10n_us_hr_payroll.models.hr_contract import USHRContract
from sys import float_info
class TestUsPayslip2019(TestUsPayslip):
# FUTA Constants
FUTA_RATE_NORMAL = 0.6
FUTA_RATE_BASIC = 6.0
FUTA_RATE_EXEMPT = 0.0
# Wage caps
FICA_SS_MAX_WAGE = 132900.0
FICA_M_MAX_WAGE = float_info.max
FICA_M_ADD_START_WAGE = 200000.0
FUTA_MAX_WAGE = 7000.0
# Rates
FICA_SS = 6.2 / -100.0
FICA_M = 1.45 / -100.0
FUTA = FUTA_RATE_NORMAL / -100.0
FICA_M_ADD = 0.9 / -100.0
###
# 2019 Taxes and Rates
###
def test_2019_taxes(self):
self.debug = False
# salary is high so that second payslip runs over max
# social security salary
salary = 80000.0
employee = self._createEmployee()
contract = self._createContract(employee, wage=salary)
self._log(contract.read())
self._log('2018 tax last slip')
payslip = self._createPayslip(employee, '2018-12-01', '2018-12-31')
payslip.compute_sheet()
self._log(payslip.read())
process_payslip(payslip)
# Ensure amounts are there, they shouldn't be added in the next year...
cats = self._getCategories(payslip)
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
# Employee
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], cats['BASIC'] * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], cats['BASIC'] * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
# Employer
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertTrue(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * self.FUTA)
process_payslip(payslip)
# Make a new payslip, this one will have reached Medicare Additional (employee only)
remaining_ss_wages = self.FICA_SS_MAX_WAGE - salary if (self.FICA_SS_MAX_WAGE - 2 * salary < salary) else salary
remaining_m_wages = self.FICA_M_MAX_WAGE - salary if (self.FICA_M_MAX_WAGE - 2 * salary < salary) else salary
self._log('2019 tax second payslip:')
payslip = self._createPayslip(employee, '2019-02-01', '2019-02-28')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], remaining_ss_wages * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], remaining_m_wages * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0)
self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
process_payslip(payslip)
# Make a new payslip, this one will have reached Medicare Additional (employee only)
self._log('2019 tax third payslip:')
payslip = self._createPayslip(employee, '2019-03-01', '2019-03-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], (self.FICA_M_ADD_START_WAGE - (salary * 2)) * self.FICA_M_ADD) # aka 40k
process_payslip(payslip)
# Make a new payslip, this one will have all salary as Medicare Additional
self._log('2019 tax fourth payslip:')
payslip = self._createPayslip(employee, '2019-04-01', '2019-04-30')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], salary * self.FICA_M_ADD)
process_payslip(payslip)
def test_2019_fed_income_withholding_single(self):
self.debug = False
salary = 6000.00
schedule_pay = 'monthly'
w4_allowances = 3
w4_allowance_amt = 350.00 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 4962.60, but would work over a wide value for the rate
###
# Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(378.52 + ((adjusted_salary - 3606) * 0.22)), self.payroll_digits)
employee = self._createEmployee()
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='single',
fed_941_fit_w4_allowances=w4_allowances
)
self._log('2019 fed income single payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_withholding_married_as_single(self):
salary = 500.00
schedule_pay = 'weekly'
w4_allowances = 1
w4_allowance_amt = 80.80 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 420.50, but would work over a wide value for the rate
###
expected_withholding = self.float_round(-(18.70 + ((adjusted_salary - 260) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='married_as_single',
fed_941_fit_w4_allowances=w4_allowances,
)
self._log('2019 fed income married_as_single payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_withholding_married(self):
salary = 14000.00
schedule_pay = 'bi-weekly'
w4_allowances = 2
w4_allowance_amt = 161.50 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # should be 13680.80, but would work over a wide value for the rate
###
# Single MONTHLY form Publication 15
expected_withholding = self.float_round(-(2519.06 + ((adjusted_salary - 12817) * 0.32)), self.payroll_digits)
employee = self._createEmployee()
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='married',
fed_941_fit_w4_allowances=w4_allowances
)
self._log('2019 fed income married payslip: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
# This is off by 1 penny given our new reporting of adjusted wage * computed percentage
#self.assertPayrollEqual(cats['EE_US_941_FIT'], expected_withholding)
self.assertTrue(abs(cats['EE_US_941_FIT'] - expected_withholding) < 0.011)
def test_2019_taxes_with_external(self):
# social security salary
salary = self.FICA_M_ADD_START_WAGE
external_wages = 6000.0
employee = self._createEmployee()
self._createContract(employee, wage=salary, external_wages=external_wages)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], (self.FICA_SS_MAX_WAGE - external_wages) * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], external_wages * self.FICA_M_ADD)
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertPayrollEqual(cats['ER_US_940_FUTA'], (self.FUTA_MAX_WAGE - external_wages) * self.FUTA)
def test_2019_taxes_with_full_futa(self):
futa_rate = self.FUTA_RATE_BASIC / -100.0
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_BASIC)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FICA_SS'], self.FICA_SS_MAX_WAGE * self.FICA_SS)
self.assertPayrollEqual(rules['EE_US_941_FICA_M'], salary * self.FICA_M)
self.assertPayrollEqual(rules['EE_US_941_FICA_M_ADD'], 0.0 * self.FICA_M_ADD)
self.assertPayrollEqual(rules['ER_US_941_FICA_SS'], rules['EE_US_941_FICA_SS'])
self.assertPayrollEqual(rules['ER_US_941_FICA_M'], rules['EE_US_941_FICA_M'])
self.assertPayrollEqual(cats['ER_US_940_FUTA'], self.FUTA_MAX_WAGE * futa_rate)
def test_2019_taxes_with_futa_exempt(self):
futa_rate = self.FUTA_RATE_EXEMPT / -100.0 # because of exemption
# social security salary
salary = self.FICA_M_ADD_START_WAGE
employee = self._createEmployee()
self._createContract(employee, wage=salary, fed_940_type=USHRContract.FUTA_TYPE_EXEMPT)
self._log('2019 tax first payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['ER_US_940_FUTA'], 0.0)
def test_2019_fed_income_withholding_nonresident_alien(self):
salary = 3500.00
schedule_pay = 'quarterly'
w4_allowances = 1
w4_allowance_amt = 1050.0 * w4_allowances
nra_adjustment = 2000.0 # for quarterly
adjusted_salary = salary - w4_allowance_amt + nra_adjustment # 4450
###
# Single QUARTERLY form Publication 15
expected_withholding = self.float_round(-(242.50 + ((adjusted_salary - 3375) * 0.12)), self.payroll_digits)
employee = self._createEmployee()
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_allowances=w4_allowances,
fed_941_fit_w4_is_nonresident_alien=True,
)
self._log('2019 fed income single payslip nonresident alien: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
def test_2019_fed_income_additional_withholding(self):
salary = 50000.00
schedule_pay = 'annually'
w4_additional_withholding = 5000.0
w4_allowances = 2
w4_allowance_amt = 4200.0 * w4_allowances
adjusted_salary = salary - w4_allowance_amt # 41700
###
# Single ANNUAL form Publication 15
expected_withholding = \
self.float_round(-((1940.00 + ((adjusted_salary - 31200) * 0.12)) + w4_additional_withholding),
self.payroll_digits)
employee = self._createEmployee()
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_filing_status='married',
fed_941_fit_w4_allowances=w4_allowances,
fed_941_fit_w4_additional_withholding=w4_additional_withholding,
)
self._log('2019 fed income married payslip additional withholding: adjusted_salary: ' + str(adjusted_salary))
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FIT'], expected_withholding)
def test_2019_taxes_with_w4_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
w4_allowances = 0
employee = self._createEmployee()
contract = self._createContract(employee,
wage=salary,
schedule_pay=schedule_pay,
fed_941_fit_w4_allowances=w4_allowances,
fed_941_fit_w4_filing_status='',
)
self._log('2019 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
rules = self._getRules(payslip)
self.assertPayrollEqual(rules['EE_US_941_FIT'], 0.0)
def test_2019_taxes_with_fica_exempt(self):
salary = 6000.0
schedule_pay = 'bi-weekly'
employee = self._createEmployee()
contract = self._createContract(employee, wage=salary, schedule_pay=schedule_pay)
contract.us_payroll_config_id.fed_941_fica_exempt = True
self._log('2019 tax w4 exempt payslip:')
payslip = self._createPayslip(employee, '2019-01-01', '2019-01-31')
payslip.compute_sheet()
cats = self._getCategories(payslip)
self.assertPayrollEqual(cats['EE_US_941_FICA'], 0.0)
self.assertPayrollEqual(cats['ER_US_941_FICA'], 0.0)

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='structure_type_id']" position="after">
<field name="us_payroll_config_id"
domain="[('employee_id', '=', employee_id)]"
attrs="{'invisible': [('structure_type_id', '!=', %(l10n_us_hr_payroll.structure_type_employee)s)],
'required': [('structure_type_id', '=', %(l10n_us_hr_payroll.structure_type_employee)s)]}"
context="{'default_employee_id': employee_id}"/>
</xpath>
</field>
</record>
</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="50" parent="hr_payroll.menu_hr_payroll_employees_root"/>
</odoo>