248 lines
9.6 KiB
Python
248 lines
9.6 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import csv
|
|
import io
|
|
|
|
from datetime import date
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import date_utils
|
|
|
|
|
|
class L10nUsForm941(models.Model):
|
|
_name = 'l10n.us.941'
|
|
_description = 'Form 941'
|
|
|
|
@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)
|
|
|
|
irs_payment_option = fields.Selection([
|
|
('efw', 'EFW'),
|
|
('eftps', 'EFTPS'),
|
|
('check_money_order', 'Check/Money Order'),
|
|
('credit_card', 'Credit Card'),
|
|
], required=True, string="IRS Payment Option", help="""
|
|
Select the payment method you will use to file your form:
|
|
- Electronic Funds Withdrawal (EFW)
|
|
- Electronic Federal Tax Payment System (EFTPS)
|
|
- Check or Money order
|
|
- Credit Card""")
|
|
tax_liability = fields.Selection([
|
|
("min_liability", "Min Tax Liability"),
|
|
("monthly", "Monthly"),
|
|
("semiweekly", "Semiweekly (Schedule B)"),
|
|
], required=True, string="Deposit Schedule and Tax Liability", help="This determines your required tax deposit schedule based on your business's employment tax liability amount.")
|
|
year = fields.Char(required=True, default=lambda self: fields.Date.today().year, string="Tax Year")
|
|
quarter = fields.Selection([
|
|
('1', 'Q1 (Jan-Mar)'),
|
|
('2', 'Q2 (Apr-Jun)'),
|
|
('3', 'Q3 (Jul-Sep)'),
|
|
('4', 'Q4 (Oct-Dec)'),
|
|
], required=True, default=lambda self: str(date_utils.get_quarter_number(fields.Date.today())), string="Quarter")
|
|
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('year', 'quarter')
|
|
def _compute_display_name(self):
|
|
for form_941 in self:
|
|
form_941.display_name = f"Form 941 - {form_941.year} {form_941.quarter}"
|
|
|
|
@api.depends('allowed_payslip_ids')
|
|
def _compute_payslips_ids(self):
|
|
for form_941 in self:
|
|
form_941.payslip_ids = form_941.allowed_payslip_ids
|
|
|
|
def _get_date_range(self):
|
|
self.ensure_one()
|
|
year = int(self.year)
|
|
month = int(self.quarter) * 3
|
|
return date_utils.get_quarter(date(year, month, 1))
|
|
|
|
def _get_allowed_payslips_domain(self):
|
|
self.ensure_one()
|
|
date_from, date_to = self._get_date_range()
|
|
return [
|
|
('state', 'in', ['validated', 'paid']),
|
|
('date_from', '>=', date_from),
|
|
('date_to', '<=', date_to),
|
|
('company_id', '=', self.company_id.id),
|
|
]
|
|
|
|
@api.depends('company_id', 'year', 'quarter')
|
|
def _compute_allowed_payslip_ids(self):
|
|
for form_941 in self:
|
|
allowed_payslips = self.env['hr.payslip'].search(form_941._get_allowed_payslips_domain())
|
|
form_941.allowed_payslip_ids = allowed_payslips
|
|
|
|
def _get_month_to_tax_liability(self):
|
|
month_from = self._get_date_range()[0].month
|
|
month_to_payslips = self.payslip_ids.grouped(lambda payslip: payslip.date_from.month)
|
|
tax_liability_codes = ["FIT", "SST", "COMPANYSOCIAL", "MEDICARE", "COMPANYMEDICARE", "MEDICAREADD"]
|
|
month_to_tax_liability = {}
|
|
|
|
for month, payslips in sorted(month_to_payslips.items()):
|
|
tax_liability_lines = payslips._get_line_values(tax_liability_codes, compute_sum=True)
|
|
month_to_tax_liability[month - month_from] = sum(abs(tax_liability_lines[code]["sum"]["total"]) for code in tax_liability_codes)
|
|
|
|
return month_to_tax_liability
|
|
|
|
def action_generate_csv(self):
|
|
self.ensure_one()
|
|
header = (
|
|
"Form Type*",
|
|
"Employer EIN*",
|
|
"Business name*",
|
|
"Business structure or classification*",
|
|
"Trade name (if any)",
|
|
"Is Foreign Address*",
|
|
"US Address 1*",
|
|
"US Address 2",
|
|
"US City/Town*",
|
|
"US State*",
|
|
"US ZIP code*",
|
|
"Foreign Address 1*",
|
|
"Foreign Address 2",
|
|
"Foreign City/Town*",
|
|
"Foreign State/Province*",
|
|
"Postal code*",
|
|
"Foreign Country*",
|
|
"Email address",
|
|
"Phone number",
|
|
"Signatory name*",
|
|
"Signatory title*",
|
|
"Daytime phone*",
|
|
"Tax Year*",
|
|
"Quarter*",
|
|
"1) Total number of employees",
|
|
"2) Total wages, tips, and other compensation",
|
|
"3) Total Federal Income Tax Withheld",
|
|
"5a) Taxable social security wages (Column1)",
|
|
"5b) Taxable social security tips (Column1)",
|
|
"5c) Taxable Medicare wages & tips (Column1)",
|
|
"5d) Wages & tips subject to Add Medi Tax withhold (Column1)",
|
|
"5f) Section 3121(q) Tax due on unreported tips",
|
|
"7) adjustment for fractions of cents from Employees share",
|
|
"8) adjustment for sick pay",
|
|
"9) adjustments for tips and group-term life insurance",
|
|
"11) Qualified Small Business payroll tax credit",
|
|
"13) Total deposits for this quarter",
|
|
"choose how to use your IRS Credit, If return has Overpayment",
|
|
"IRS Payment Option",
|
|
"(EFW)Account Type*",
|
|
"(EFW)US Bank Account Number*",
|
|
"(EFW)US Bank Routing Number*",
|
|
"(EFW)Taxpayer day time Phone Number*",
|
|
"Deposit Schedule & Tax Liability*",
|
|
"Tax liability Month 1",
|
|
"Tax liability Month 2",
|
|
"Tax liability Month 3",
|
|
"Is your business has closed or you stopped paying wages",
|
|
"Enter the final date you paid wages",
|
|
"Recordkeeper name",
|
|
"Recordkeeper Address 1*",
|
|
"Recordkeeper Address 2",
|
|
"Recordkeeper City/Town*",
|
|
"Recordkeeper State/Province*",
|
|
"Recordkeeper ZIP code/Postal code*",
|
|
"Is seasonal employer",
|
|
"Designee name",
|
|
"Designee phone",
|
|
"Designee PIN",
|
|
"94x Online Signature PIN",
|
|
"RA PIN",
|
|
"Taxpayer PIN (For ERO)",
|
|
)
|
|
|
|
company = self.company_id
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(header)
|
|
|
|
def total(code):
|
|
return abs(line_values[code]['sum']['total'])
|
|
|
|
line_values = self.payslip_ids._get_line_values([
|
|
"401K",
|
|
"COMPANYMEDICARE",
|
|
"COMPANYSOCIAL",
|
|
"FIT",
|
|
"MEDICARE",
|
|
"MEDICAREADD",
|
|
"SST",
|
|
"TAXABLE",
|
|
"TIPS",
|
|
], compute_sum=True)
|
|
month_to_tax_liability = self._get_month_to_tax_liability()
|
|
|
|
bank_account = self.company_id.partner_id.bank_ids[:1]
|
|
row = [
|
|
"Form 941",
|
|
company.vat,
|
|
company.name,
|
|
dict(company._fields['l10n_us_business_structure']._description_selection(self.env)).get(company.l10n_us_business_structure),
|
|
"",
|
|
"No",
|
|
company.street,
|
|
company.street2,
|
|
company.city,
|
|
company.state_id.code,
|
|
company.zip,
|
|
*[""] * 6,
|
|
company.email,
|
|
company.phone,
|
|
company.l10n_us_signatory_id.name,
|
|
company.l10n_us_signatory_id.job_title,
|
|
company.l10n_us_signatory_id.phone,
|
|
self.year,
|
|
dict(self._fields['quarter']._description_selection(self.env)).get(self.quarter),
|
|
len(self.payslip_ids.mapped("employee_id")),
|
|
total("TAXABLE"),
|
|
total("FIT"),
|
|
total("TAXABLE") + total("401K") - total("TIPS"),
|
|
total("TIPS"),
|
|
total("TAXABLE") + total("401K"),
|
|
total("TAXABLE") + total("401K") if total("MEDICAREADD") > 0 else 0,
|
|
*[""] * 5,
|
|
total("FIT") + total("SST") + total("COMPANYSOCIAL") + total("MEDICARE") + total("COMPANYMEDICARE") + total("MEDICAREADD"),
|
|
"",
|
|
dict(self._fields['irs_payment_option']._description_selection(self.env)).get(self.irs_payment_option),
|
|
dict(bank_account._fields['l10n_us_bank_account_type']._description_selection(self.env)).get(bank_account.l10n_us_bank_account_type),
|
|
bank_account.acc_number,
|
|
bank_account.clearing_number,
|
|
company.phone,
|
|
dict(self._fields['tax_liability']._description_selection(self.env)).get(self.tax_liability),
|
|
month_to_tax_liability.get(0),
|
|
month_to_tax_liability.get(1),
|
|
month_to_tax_liability.get(2),
|
|
"No",
|
|
*[""] * 2,
|
|
company.street,
|
|
company.street2,
|
|
company.city,
|
|
company.state_id.code,
|
|
company.zip,
|
|
"No",
|
|
*[""] * 6,
|
|
]
|
|
writer.writerow([col or "" for col in row]) # never write Falsy, should be ok to make 0 blank
|
|
|
|
self.csv_file = base64.b64encode(output.getvalue().encode())
|
|
self.csv_filename = f"form_941_{self.year}_{self.quarter}.csv"
|