mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
This release fixes multiple logical problems with Odoo's payroll, including the difference in behavior on payslips with 'recursive' category calculations needed for many.
322 lines
13 KiB
Python
322 lines
13 KiB
Python
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
|
|
|
# import logging
|
|
# _logger = logging.getLogger(__name__)
|
|
|
|
|
|
def fica_wage(payslip, categories):
|
|
"""
|
|
Returns FICA eligible wage for current Payslip (no wage_base, just by categories)
|
|
WAGE = GROSS - ALW_FICA_EXEMPT + DED_FICA_EXEMPT
|
|
:return: wage
|
|
"""
|
|
wage = categories.GROSS
|
|
|
|
|
|
less_exempt = categories.ALW_FICA_EXEMPT + \
|
|
categories.ALW_FIT_FICA_EXEMPT + \
|
|
categories.ALW_FIT_FICA_FUTA_EXEMPT + \
|
|
categories.ALW_FICA_FUTA_EXEMPT
|
|
|
|
plus_exempt = categories.DED_FICA_EXEMPT + \
|
|
categories.DED_FIT_FICA_EXEMPT + \
|
|
categories.DED_FIT_FICA_FUTA_EXEMPT + \
|
|
categories.DED_FICA_FUTA_EXEMPT
|
|
# _logger.info('fica wage GROSS: %0.2f less exempt ALW: %0.2f plus exempt DED: %0.2f' % (wage, less_exempt, plus_exempt))
|
|
return wage - less_exempt + plus_exempt
|
|
|
|
|
|
def fica_wage_ytd(payslip, categories):
|
|
"""
|
|
Returns Year to Date FICA eligible wages
|
|
WAGE = GROSS - ALW_FICA_EXEMPT + DED_FICA_EXEMPT
|
|
:return: wage
|
|
"""
|
|
year = payslip.dict.get_year()
|
|
ytd_wage = payslip.sum_category('GROSS', str(year) + '-01-01', str(year+1) + '-01-01')
|
|
|
|
less_exempt = payslip.sum_category('ALW_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('ALW_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('ALW_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('ALW_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
|
|
|
|
plus_exempt = payslip.sum_category('DED_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('DED_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('DED_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('DED_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
|
|
|
|
external_wages = payslip.dict.contract_id.external_wages
|
|
# _logger.info('fica ytd wage GROSS: %0.2f less exempt ALW: %0.2f plus exempt DED: %0.2f plus external: %0.2f' % (ytd_wage, less_exempt, plus_exempt, external_wages))
|
|
return ytd_wage - less_exempt + plus_exempt + external_wages
|
|
|
|
|
|
def ee_us_941_fica_ss(payslip, categories, worked_days, inputs):
|
|
"""
|
|
Returns FICA Social Security eligible wage and rate.
|
|
: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
|
|
wage = fica_wage(payslip, categories)
|
|
if not wage:
|
|
return 0.0, 0.0
|
|
|
|
ytd_wage = fica_wage_ytd(payslip, categories)
|
|
wage_base = payslip.dict.rule_parameter('fed_941_fica_ss_wage_base')
|
|
remaining = wage_base - ytd_wage
|
|
|
|
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.
|
|
: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
|
|
wage = fica_wage(payslip, categories)
|
|
if not wage:
|
|
return 0.0, 0.0
|
|
|
|
ytd_wage = fica_wage_ytd(payslip, categories)
|
|
wage_base = float(payslip.dict.rule_parameter('fed_941_fica_m_wage_base')) # inf
|
|
remaining = wage_base - ytd_wage
|
|
|
|
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.
|
|
: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
|
|
wage = fica_wage(payslip, categories)
|
|
if not wage:
|
|
return 0.0, 0.0
|
|
|
|
ytd_wage = fica_wage_ytd(payslip, categories)
|
|
wage_start = payslip.dict.rule_parameter('fed_941_fica_m_add_wage_start')
|
|
existing_wage = ytd_wage - wage_start
|
|
|
|
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
|
|
|
|
|
|
def fit_wage(payslip, categories):
|
|
"""
|
|
Returns FIT eligible wage for current Payslip (no wage_base, just by categories)
|
|
WAGE = GROSS - ALW_FIT_EXEMPT + DED_FIT_EXEMPT
|
|
:return: wage
|
|
"""
|
|
wage = categories.GROSS
|
|
|
|
wage -= categories.ALW_FIT_EXEMPT + \
|
|
categories.ALW_FIT_FICA_EXEMPT + \
|
|
categories.ALW_FIT_FICA_FUTA_EXEMPT + \
|
|
categories.ALW_FIT_FUTA_EXEMPT
|
|
|
|
wage += categories.DED_FIT_EXEMPT + \
|
|
categories.DED_FIT_FICA_EXEMPT + \
|
|
categories.DED_FIT_FICA_FUTA_EXEMPT + \
|
|
categories.DED_FIT_FUTA_EXEMPT
|
|
|
|
return wage
|
|
|
|
|
|
def fit_wage_ytd(payslip, categories):
|
|
"""
|
|
Returns Year to Date FIT eligible wages
|
|
WAGE = GROSS - ALW_FIT_EXEMPT + DED_FIT_EXEMPT
|
|
:return: 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('ALW_FIT_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('ALW_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('ALW_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('ALW_FIT_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
|
|
|
|
ytd_wage += payslip.sum_category('DED_FIT_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('DED_FIT_FICA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('DED_FIT_FICA_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01') + \
|
|
payslip.sum_category('DED_FIT_FUTA_EXEMPT', str(year) + '-01-01', str(year+1) + '-01-01')
|
|
|
|
ytd_wage += payslip.dict.contract_id.external_wages
|
|
return ytd_wage
|
|
|
|
|
|
# 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.
|
|
: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 = fit_wage(payslip, categories)
|
|
if not wage:
|
|
return 0.0, 0.0
|
|
|
|
#_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)
|