250 lines
9.8 KiB
Python
250 lines
9.8 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
||
import base64
|
||
import csv
|
||
import io
|
||
|
||
from collections import defaultdict
|
||
from datetime import date
|
||
from dateutil.relativedelta import relativedelta
|
||
|
||
from odoo import api, fields, models, _
|
||
from odoo.exceptions import UserError
|
||
|
||
|
||
class L10nUsW2(models.Model):
|
||
_name = 'l10n.us.w2'
|
||
_description = 'W2 Form'
|
||
|
||
@api.model
|
||
def default_get(self, fields):
|
||
if self.env.company.country_id.code != "US":
|
||
raise UserError(_('You must be logged in a US company to use this feature'))
|
||
return super().default_get(fields)
|
||
|
||
date_start = fields.Date("Start Date", default=lambda s: fields.Date.today() + relativedelta(day=1, month=1))
|
||
date_end = fields.Date("End Date", compute='_compute_date_end', store=True, readonly=False)
|
||
company_id = fields.Many2one(
|
||
'res.company',
|
||
default=lambda self: self.env.company,
|
||
domain=lambda self: [('id', 'in', self.env.companies.ids)],
|
||
required=True)
|
||
allowed_payslip_ids = fields.Many2many('hr.payslip', compute='_compute_allowed_payslip_ids')
|
||
payslip_ids = fields.Many2many(
|
||
'hr.payslip',
|
||
compute='_compute_payslips_ids',
|
||
store=True,
|
||
readonly=False,
|
||
domain="[('id', 'in', allowed_payslip_ids)]")
|
||
csv_file = fields.Binary("CSV file", readonly=True)
|
||
csv_filename = fields.Char()
|
||
|
||
@api.depends('date_start')
|
||
def _compute_date_end(self):
|
||
for w2 in self:
|
||
if not w2.date_start:
|
||
continue
|
||
w2.date_end = date(w2.date_start.year, 12, 31)
|
||
|
||
@api.depends('date_end')
|
||
def _compute_display_name(self):
|
||
for w2 in self:
|
||
w2.display_name = f"W-2 Form - {w2.date_end.year}" if w2.date_end else "W-2 Form"
|
||
|
||
@api.depends('allowed_payslip_ids')
|
||
def _compute_payslips_ids(self):
|
||
for w2 in self:
|
||
w2.payslip_ids = w2.allowed_payslip_ids
|
||
|
||
def _get_allowed_payslips_domain(self):
|
||
self.ensure_one()
|
||
return [
|
||
('state', 'in', ['validated', 'paid']),
|
||
('date_from', '<=', self.date_end),
|
||
('date_to', '>=', self.date_start),
|
||
('company_id', '=', self.company_id.id),
|
||
]
|
||
|
||
@api.depends('company_id', 'date_start', 'date_end')
|
||
def _compute_allowed_payslip_ids(self):
|
||
for w2 in self:
|
||
if not w2.date_start or not w2.date_end or not w2.company_id:
|
||
w2.allowed_payslip_ids = [(5, 0, 0)]
|
||
else:
|
||
allowed_payslips = self.env['hr.payslip'].search(w2._get_allowed_payslips_domain())
|
||
w2.allowed_payslip_ids = allowed_payslips
|
||
|
||
def _get_box17_state_income_tax(self, line_values):
|
||
state_income_tax_codes = ['CAINCOMETAX', 'NYINCOMETAX', 'COINCOMETAX', 'ALINCOMETAX']
|
||
return abs(sum(line_values[code]['sum']['total'] for code in state_income_tax_codes))
|
||
|
||
def _get_box18_local_wage(self, state, line_values):
|
||
if state == 'CO':
|
||
return abs(line_values['TAXABLE']['sum']['total'])
|
||
return ""
|
||
|
||
def _get_box19_local_income_tax(self, state, line_values):
|
||
if state == 'CO':
|
||
colorado_local_income_tax_codes = ['COOPTAURORA', 'COOPTDENVER', 'COOPTGLENDALE', 'COOPTGREENWOODVILLAGE', 'COOPTSHERIDAN']
|
||
return abs(sum(line_values[code]['sum']['total'] for code in colorado_local_income_tax_codes))
|
||
return ""
|
||
|
||
def _get_box20_locality_name(self, version):
|
||
if (version.address_id.state_id.code == 'CO' and version.address_id.city in ('Aurora', 'Denver', 'Glendale', 'Greenwood Village', 'Sheridan')):
|
||
return version.address_id.city
|
||
return ""
|
||
|
||
def action_generate_csv(self):
|
||
self.ensure_one()
|
||
header = [
|
||
"Employer identification number (EIN)",
|
||
"Employer's name",
|
||
"Street address 1",
|
||
"Street address 2",
|
||
"City or town",
|
||
"State or province",
|
||
"Country",
|
||
"ZIP code",
|
||
"Telephone no.",
|
||
"Email",
|
||
"Employee’s social security number (SSN)",
|
||
"Employee's first name",
|
||
"Employee's middle initial",
|
||
"Employee's last name",
|
||
"Street address 1",
|
||
"Street address 2",
|
||
"City or town",
|
||
"State or province",
|
||
"Country",
|
||
"ZIP code",
|
||
"Telephone no.",
|
||
"Email",
|
||
"Control number",
|
||
"1 Wages, tips, other compensation",
|
||
"2 Federal income tax withheld",
|
||
"3 Social security wages",
|
||
"4 Social security tax withheld",
|
||
"5 Medicare wages and tips",
|
||
"6 Medicare tax withheld",
|
||
"7 Social security tips",
|
||
"8 Allocated tips",
|
||
"10 Dependent care benefits",
|
||
"11 Nonqualified plans",
|
||
"12a Code A",
|
||
"12a Amount A",
|
||
"12b Code B",
|
||
"12b Amount B",
|
||
"12c Code C",
|
||
"12c Amount C",
|
||
"12d Code D",
|
||
"12d Amount D",
|
||
"13 Statutory employee",
|
||
"13 Retirement plan",
|
||
"13 Third-party sick pay",
|
||
"14 Other A",
|
||
"14 Other amount A",
|
||
"14 Other B",
|
||
"14 Other amount B",
|
||
"14 Other C",
|
||
"14 Other amount C",
|
||
"File directly with state",
|
||
"15 State",
|
||
"15 Employer’s state ID number",
|
||
"16 State wages, tips, etc.",
|
||
"17 State income tax",
|
||
"18 Local wages, tips, etc.",
|
||
"19 Local income tax",
|
||
"20 Locality name",
|
||
"File directly with state",
|
||
"Kind of payer",
|
||
"Kind of employer",
|
||
"Terminating Business",
|
||
]
|
||
|
||
output = io.StringIO()
|
||
writer = csv.writer(output)
|
||
writer.writerow(header)
|
||
|
||
payslips_by_employee = defaultdict(lambda: self.env['hr.payslip'])
|
||
for payslip in self.payslip_ids:
|
||
payslips_by_employee[payslip.employee_id] += payslip
|
||
|
||
for employee, payslips in payslips_by_employee.items():
|
||
line_values = payslips._get_line_values([
|
||
'TAXABLE', 'FIT', '401K', 'TIPS', 'SST', 'MEDICARE', 'MEDICAREADD', 'ALLOCATEDTIPS',
|
||
'MEDICALFSADC', 'MEDICALHSA', 'ROTH401K',
|
||
'CAINCOMETAX', 'NYINCOMETAX', 'COINCOMETAX', 'ALINCOMETAX',
|
||
'CASDITAX', 'NYSDITAX',
|
||
'COOPTAURORA', 'COOPTDENVER', 'COOPTGLENDALE', 'COOPTGREENWOODVILLAGE', 'COOPTSHERIDAN',
|
||
], compute_sum=True)
|
||
|
||
version = employee._get_version(self.date_start)
|
||
|
||
writer.writerow([
|
||
self.company_id.vat or "",
|
||
self.company_id.name or "",
|
||
self.company_id.street or "",
|
||
self.company_id.street2 or "",
|
||
self.company_id.city or "",
|
||
self.company_id.state_id.code or "",
|
||
self.company_id.country_id.name or "",
|
||
self.company_id.zip or "",
|
||
self.company_id.phone or "",
|
||
self.company_id.email or "",
|
||
version.ssnid or "",
|
||
employee.name or "",
|
||
"",
|
||
employee.name or "",
|
||
version.private_street or "",
|
||
version.private_street2 or "",
|
||
version.private_city or "",
|
||
version.private_state_id.code or "",
|
||
version.private_country_id.name or "",
|
||
version.private_zip or "",
|
||
employee.private_phone or "",
|
||
employee.private_email or "",
|
||
"",
|
||
abs(line_values['TAXABLE']['sum']['total']),
|
||
abs(line_values['FIT']['sum']['total']),
|
||
abs(line_values['TAXABLE']['sum']['total'] - line_values['401K']['sum']['total'] - line_values['TIPS']['sum']['total']),
|
||
abs(line_values['SST']['sum']['total']),
|
||
abs(line_values['TAXABLE']['sum']['total'] - line_values['401K']['sum']['total']),
|
||
abs(line_values['MEDICARE']['sum']['total'] + line_values['MEDICAREADD']['sum']['total']),
|
||
abs(line_values['TIPS']['sum']['total']),
|
||
abs(line_values['ALLOCATEDTIPS']['sum']['total']),
|
||
abs(line_values['MEDICALFSADC']['sum']['total']),
|
||
"",
|
||
"D",
|
||
abs(line_values['401K']['sum']['total']),
|
||
"W",
|
||
abs(line_values['MEDICALHSA']['sum']['total']),
|
||
"AA",
|
||
abs(line_values['ROTH401K']['sum']['total']),
|
||
"",
|
||
"",
|
||
"TRUE" if version.l10n_us_statutory_employee else "FALSE",
|
||
"TRUE" if version.l10n_us_retirement_plan else "FALSE",
|
||
"TRUE" if version.l10n_us_third_party_sick_pay else "FALSE",
|
||
_("%s SDI Tax", version.address_id.state_id.code) if version.address_id.state_id.code in ['NY', 'CA'] else "",
|
||
abs(line_values['CASDITAX']['sum']['total'] + line_values['NYSDITAX']['sum']['total']),
|
||
"",
|
||
"",
|
||
"",
|
||
"",
|
||
"",
|
||
version.address_id.state_id.code or "",
|
||
self.company_id.company_registry or "",
|
||
abs(line_values['TAXABLE']['sum']['total']),
|
||
self._get_box17_state_income_tax(line_values),
|
||
self._get_box18_local_wage(version.address_id.state_id.code, line_values),
|
||
self._get_box19_local_income_tax(version.address_id.state_id.code, line_values),
|
||
self._get_box20_locality_name(version),
|
||
"",
|
||
"R",
|
||
"N",
|
||
"",
|
||
])
|
||
|
||
self.csv_file = base64.b64encode(output.getvalue().encode())
|
||
self.csv_filename = f"form_w2_{(self.date_end or date.today()).year}.csv"
|