Files

305 lines
16 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, models, fields
from odoo.exceptions import UserError
DEFAULT_STATE_FILING_STATUS = {
'NY': 'ny_status_1',
'CA': 'ca_status_1',
'AL': 'al_status_1',
'CO': 'co_status_1',
'VT': 'vt_status_1',
'IL': 'il_status_1',
'AZ': 'az_status_4',
'DC': 'dc_status_1',
'NC': 'nc_status_1',
'VA': 'va_status_1',
'OR': 'or_status_1',
'ID': 'id_status_1',
}
class HrVersion(models.Model):
_inherit = "hr.version"
@api.model
def _get_selection_state_filing_status(self):
return [
('ca_status_1', 'CA: Single, Dual Income Married or Married with Multiple Employers'),
('ca_status_2', 'CA: Married: One Income'),
('ca_status_4', 'CA: Unmarried Head of Household'),
('ny_status_1', 'NY: Single or Head of Household'),
('ny_status_2', 'NY: Married (filing jointly)'),
('ny_status_3', 'NY: Married, but withhold at a higher single rate'),
('al_status_1', 'AL: 0: No Exemption Made (withhold at the highest rate)'),
('al_status_2', 'AL: S: Single'),
('al_status_3', 'AL: MS: Married filing Separately'),
('al_status_4', 'AL: M: Married'),
('al_status_5', 'AL: H: Head of Household'),
('co_status_1', 'CO: Single or Married filing Separately'),
('co_status_2', 'CO: Married filing Jointly or Qualifying Surviving Spouse'),
('co_status_3', 'CO: Head of Household'),
('vt_status_1', 'VT: Single'),
('vt_status_2', 'VT: Married/Civil Union Filing Jointly'),
('vt_status_3', 'VT: Married/Civil Union Filing Separately'),
('vt_status_4', 'VT: Married, but withhold at a higher single rate'),
('il_status_1', 'IL: General rate used for deductions'),
('az_status_1', 'AZ: Withhold wages at 0.5%'),
('az_status_2', 'AZ: Withhold wages at 1.0%'),
('az_status_3', 'AZ: Withhold wages at 1.5%'),
('az_status_4', 'AZ: Withhold wages at 2.0%'),
('az_status_5', 'AZ: Withhold wages at 2.5%'),
('az_status_6', 'AZ: Withhold wages at 3.0%'),
('az_status_7', 'AZ: Withhold wages at 3.5%'),
('dc_status_1', 'DC: Single'),
('dc_status_2', 'DC: Married/domestic partners filing jointly/qualifying widow(er) with dependent child'),
('dc_status_3', 'DC: Head of household'),
('dc_status_4', 'DC: Married filing separately'),
('dc_status_5', 'DC: Married/domestic partners filing separately on same return'),
('nc_status_1', 'NC: Single or Married Filing Separately'),
('nc_status_2', 'NC: Head of Household'),
('nc_status_3', 'NC: Married Filing Jointly or Surviving Spouse'),
('va_status_1', 'VA: Single'),
('va_status_2', 'VA: Married, Filing a Joint Return'),
('va_status_3', 'VA: Married, Filing a Separate Return'),
('or_status_1', 'OR: Single'),
('or_status_2', 'OR: Married'),
('id_status_1', 'ID: Single'),
('id_status_2', 'ID: Married'),
('id_status_3', 'ID: Married, but withhold at Single rate'),
]
l10n_us_old_w4 = fields.Boolean(
string="Filed in 2019 or Before",
groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help="Check only if W4 was filed before 2020.")
l10n_us_w4_step_2 = fields.Boolean(
string="Step 2(c): Multiple Jobs or Spouse Works",
groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help="Check if Step 2 (c) in employee's W4 form is selected.")
l10n_us_w4_step_3 = fields.Float(
string="Step 3: Dependents Amount",
groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help="The total amount in USD from Step 3 (Dependants and Other Credits) of the W4 form.")
l10n_us_w4_step_4a = fields.Float(
string="Step 4(a): Other Income",
groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help="The total amount in USD from Step 4(a) (Other Income) of the W4 form.")
l10n_us_w4_step_4b = fields.Float(
string="Step 4(b): Deductions",
groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help="The total amount in USD from Step 4(b) (Deductions) of the W4 form.")
l10n_us_w4_step_4c = fields.Float(
string="Step 4(c): Withholdings",
groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help="For 2020 W4 Form: The total amount in USD from Step 4(c) (Extra Withholdings). For 2019 W4 Form: line 6 (Additional amount to withheld).")
l10n_us_w4_allowances_count = fields.Integer(
string="Number of Claimed Regular Allowances",
tracking=True,
groups="hr_payroll.group_hr_payroll_user",
help="Total number of allowances claimed in employee's W4 form and State's withholdings certificate.")
l10n_us_w4_withholding_deduction_allowances = fields.Integer(
string="Withholding Allowances for estimated deductions",
tracking=True,
groups="hr_payroll.group_hr_payroll_user",
help="Number of additional withholding allowances from estimated deductions. Step 1(b) in DE 4 form (CA).")
l10n_us_filing_status = fields.Selection(
selection=[
('single', 'Single'),
('jointly', 'Married/RDP filing jointly'),
('separately', 'Married/RDP filing separately'),
('head', 'Head of household'),
('survivor', 'Qualifying surviving spouse/RDP with child')],
string="Federal Tax Filing Status",
default='single',
tracking=True,
groups="hr_payroll.group_hr_payroll_user",
help="Filing status used for Federal income tax calculation.")
l10n_us_state_filing_status = fields.Selection(
selection=_get_selection_state_filing_status,
string="State Tax Filing Status",
tracking=True,
compute="_compute_l10n_us_state_filing_status",
store=True,
readonly=False,
groups="hr_payroll.group_hr_payroll_user",
help="Filing status used for State income tax calculation.")
l10n_us_statutory_employee = fields.Boolean(
string="Statutory Employee",
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Employees that are exempt from income tax, but subject to FICA Taxes. If checked off it will appear in box 13 of the W2 Report.")
l10n_us_retirement_plan = fields.Boolean(
string="Retirement Plan",
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="""Employee was an "active participant" in an employer-sponsor retirement plan. If checked off it will appear in box 13 of the W2 Report.""")
l10n_us_third_party_sick_pay = fields.Boolean(
string="Third-Party Sick Pay",
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Employee received third-party sick pay benefits from a third party during the tax year. If checked off it will appear in box 13 of the W2 Report.")
l10n_us_state_withholding_allowance = fields.Float(
string="State Withholding Allowance",
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Add the allowance the employee wants to deduct from their State's withholding (usually an annual amount) "
"according to their State Withholding Certificate. If the employee didn't fill out a State Withholding "
"Certificate, leave it blank")
l10n_us_state_extra_withholding = fields.Float(
string="State Extra Withholding",
groups="hr_payroll.group_hr_payroll_user",
help="Extra amount the employee requests to be withheld per pay period according to their State Withholding "
"Certificate. If the employee didn't fill out a State Withholding Certificate, leave it blank.",
tracking=True)
l10n_us_pre_retirement_amount = fields.Float(
string="Retirement Plans: 401(k)", groups="hr_payroll.group_hr_payroll_user",
help="Pre-tax contributions to Retirement Plans: 401(k), could be either a percentage or a fixed amount per payslip.", tracking=True)
l10n_us_pre_retirement_type = fields.Selection(
selection=[
('percent', '%'),
('fixed', '$ / slip')],
string="Retirement Plans: 401(k) Type",
default='percent',
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_pre_retirement_matching_amount = fields.Float(
string="Retirement Plans: Matching Amount", groups="hr_payroll.group_hr_payroll_user",
help="Benefit Matching to Retirement Plans: Employer's 401(k) benefit matching, could be either a percentage or a fixed amount per payslip.", tracking=True)
l10n_us_pre_retirement_matching_type = fields.Selection(
selection=[
('percent', '%'),
('fixed', '$ / slip')],
string="Retirement Plans: Matching Type",
default='percent',
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_pre_retirement_matching_yearly_cap = fields.Float(
string='Retirement Plans: Matching Yearly Cap',
default=100,
help="Maximum amount, as percentage (%), of the employee's salary that will be matched by the Employer as benefit.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_health_benefits_medical = fields.Monetary(
string="Health Benefits: Medical",
help="Pre-tax amount (USD) deducted for Medical health benefits.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_health_benefits_dental = fields.Monetary(
string="Health Benefits: Dental",
help="Pre-tax amount (USD) deducted for Dental health benefits.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_health_benefits_vision = fields.Monetary(
string="Health Benefits: Vision",
help="Pre-tax amount (USD) deducted for Vision health benefits.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_health_benefits_fsa = fields.Monetary(
string="Health Benefits: FSA",
help="Pre-tax amount (USD) deducted for Flexible Spending Accounts (FSA).",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_health_benefits_fsadc = fields.Monetary(
string="Health Benefits: FSA Dependent Care",
help="Pre-tax amount (USD) deducted for Flexible Spending Accounts (FSA) for Dependent Care.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_health_benefits_hsa = fields.Monetary(
string="Health Benefits: HSA",
help="Pre-tax amount (USD) deducted for pre-tax Health Savings Account (HSA).",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_commuter_benefits = fields.Monetary(
string="Commuter Benefits",
help="Pre-tax amount (USD) deducted for Commuter benefits.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_post_roth_401k_amount = fields.Float(
string="ROTH 401(k)",
help="Post-tax contributions to Retirement plan: Roth 401(K), could be either a percentage or a fixed amount per payslip.",
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_post_roth_401k_type = fields.Selection(
selection=[
('percent', '%'),
('fixed', '$ / slip')],
string="ROTH 401(k) Type",
default='percent',
groups="hr_payroll.group_hr_payroll_user", tracking=True)
l10n_us_employee_state_code = fields.Char(
related='address_id.state_id.code',
readonly=True, groups="hr_payroll.group_hr_payroll_user")
l10n_us_worker_compensation_id = fields.Many2one(
'l10n.us.worker.compensation',
string="Worker Compensation", groups="hr_payroll.group_hr_payroll_user", tracking=True)
_l10n_us_pre_retirement_amount_is_percentage = models.Constraint(
"CHECK(l10n_us_pre_retirement_type IS NULL OR l10n_us_pre_retirement_type='fixed' OR (l10n_us_pre_retirement_type='percent' AND l10n_us_pre_retirement_amount >= 0 AND l10n_us_pre_retirement_amount <= 100))",
"The contribution rate must be a percentage between 0 and 100.",
)
_l10n_us_post_roth_401k_amount_is_percentage = models.Constraint(
"CHECK(l10n_us_post_roth_401k_type IS NULL OR l10n_us_post_roth_401k_type='fixed' OR (l10n_us_post_roth_401k_type='percent' AND l10n_us_post_roth_401k_amount >= 0 AND l10n_us_post_roth_401k_amount <= 100))",
"The contribution rate must be a percentage between 0 and 100.",
)
@api.constrains('l10n_us_state_filing_status', 'address_id', 'employee_id')
def _check_us_state_filling_status(self):
VALID_STATES = DEFAULT_STATE_FILING_STATUS.keys()
for version in self:
state_code = version.address_id.state_id.code
filing_status = version.sudo().l10n_us_state_filing_status
if not state_code or not version.employee_id:
continue
if state_code not in VALID_STATES and filing_status:
raise UserError(_('The employee state filing status should be empty for this working address state. (Work Address State: %s)', version.address_id.state_id.name))
if state_code not in VALID_STATES:
continue
if not filing_status:
raise UserError(_('The employee state filing status is empty and should match the working address state. (Work Address State: %s)', version.address_id.state_id.name))
if not filing_status.startswith(state_code.lower()):
selection_description_values = {
e[0]: e[1] for e in self._fields['l10n_us_state_filing_status']._description_selection(self.env)}
raise UserError(_('The employee state filing status should match the working address state. (Filing Status: %(filing_status)s, Work Address State: %(address_state)s)', filing_status=selection_description_values[filing_status], address_state=version.address_id.state_id.name))
@api.depends('address_id.state_id')
def _compute_l10n_us_state_filing_status(self):
for employee in self:
state_code = employee.address_id.state_id.code
filing_status = employee.l10n_us_state_filing_status
# Clear filing status if state is invalid
if not state_code:
employee.l10n_us_state_filing_status = False
continue
# Set default filing status if current status is empty or doesn't match state
if not filing_status or state_code != filing_status.split('_')[0].upper():
employee.l10n_us_state_filing_status = DEFAULT_STATE_FILING_STATUS.get(
state_code, False
)
@api.constrains('ssnid')
def _check_ssnid(self):
super()._check_ssnid()
for version in self:
if version.company_id.country_id.code != "US":
continue
if version.ssnid and (len(version.ssnid) != 9 or not version.ssnid.isdigit()):
raise UserError(_('Social Security number (SSN) should be a nine-digit number.'))
@api.model
def _get_whitelist_fields_from_template(self):
whitelisted_fields = super()._get_whitelist_fields_from_template()
if self.env.company.country_id.code == "US":
whitelisted_fields += [
"l10n_us_commuter_benefits",
"l10n_us_health_benefits_dental",
"l10n_us_health_benefits_fsa",
"l10n_us_health_benefits_fsadc",
"l10n_us_health_benefits_hsa",
"l10n_us_health_benefits_medical",
"l10n_us_health_benefits_vision",
"l10n_us_post_roth_401k_amount",
"l10n_us_post_roth_401k_type",
"l10n_us_pre_retirement_amount",
"l10n_us_pre_retirement_matching_amount",
"l10n_us_pre_retirement_matching_type",
"l10n_us_pre_retirement_matching_yearly_cap",
"l10n_us_pre_retirement_type",
"l10n_us_worker_compensation_id",
]
return whitelisted_fields