# 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