changes
This commit is contained in:
@@ -4,6 +4,7 @@ import base64
|
||||
import os
|
||||
import io
|
||||
from datetime import date
|
||||
from lxml import etree
|
||||
from odoo import models, fields, api, tools
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
@@ -15,16 +16,6 @@ class HrT4Summary(models.Model):
|
||||
_order = 'tax_year desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
def _get_pdf_text_coordinates(self):
|
||||
"""Get text overlay coordinates for flattened PDF
|
||||
Returns dict mapping field names to (x, y, font_size, font_name) tuples
|
||||
Coordinates are in points (1/72 inch), origin at bottom-left
|
||||
Reads from pdf.field.position model based on template type
|
||||
"""
|
||||
# Query configured positions from database for T4 Summary
|
||||
position_model = self.env['pdf.field.position']
|
||||
return position_model.get_coordinates_dict('T4 Summary')
|
||||
|
||||
STATE_SELECTION = [
|
||||
('draft', 'Draft'),
|
||||
('generated', 'Generated'),
|
||||
@@ -205,6 +196,9 @@ class HrT4Summary(models.Model):
|
||||
xml_filename = fields.Char(
|
||||
string='XML Filename',
|
||||
)
|
||||
transmitter_bn = fields.Char(string='Business Number (BN)')
|
||||
transmitter_name = fields.Char(string='Transmitter Name')
|
||||
contact_email = fields.Char(string='Contact Email')
|
||||
|
||||
# === Box 74: SIN of Proprietor ===
|
||||
proprietor_sin = fields.Char(
|
||||
@@ -270,7 +264,7 @@ class HrT4Summary(models.Model):
|
||||
|
||||
payslips = self.env['hr.payslip'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('state', 'in', ['validated', 'paid']),
|
||||
('state', 'in', ['done', 'paid']),
|
||||
('date_from', '>=', year_start),
|
||||
('date_to', '<=', year_end),
|
||||
])
|
||||
@@ -305,6 +299,8 @@ class HrT4Summary(models.Model):
|
||||
box_40_allowances = 0
|
||||
box_42_commissions = 0
|
||||
box_44_union_dues = 0
|
||||
rrsp = 0
|
||||
union_dues = 0
|
||||
|
||||
for ps in emp_payslips:
|
||||
# Process each payslip line
|
||||
@@ -357,8 +353,12 @@ class HrT4Summary(models.Model):
|
||||
ei_ee += amount
|
||||
elif code == 'EI_ER':
|
||||
ei_er += amount
|
||||
elif code in ('FED_TAX', 'PROV_TAX'):
|
||||
elif code in ('FED_TAX', 'PROV_TAX', 'OHP'):
|
||||
income_tax += amount
|
||||
elif code == 'RRSP':
|
||||
rrsp += amount
|
||||
elif code == 'UNION_DUES':
|
||||
union_dues += amount
|
||||
|
||||
# Add GROSS to employment income (Box 14)
|
||||
# GROSS already includes all taxable income (salary, overtime, bonus, allowances, commissions, etc.)
|
||||
@@ -387,7 +387,8 @@ class HrT4Summary(models.Model):
|
||||
# New boxes
|
||||
'box_40_taxable_benefits': box_40_allowances,
|
||||
'box_42_commissions': box_42_commissions,
|
||||
'box_44_union_dues': box_44_union_dues,
|
||||
'box_44_union_dues': box_44_union_dues + union_dues,
|
||||
'box_20_rpp': rrsp,
|
||||
})
|
||||
|
||||
self.state = 'generated'
|
||||
@@ -410,6 +411,166 @@ class HrT4Summary(models.Model):
|
||||
'filing_date': date.today(),
|
||||
})
|
||||
|
||||
def action_export_xml(self):
|
||||
"""Generate CRA T4 XML file (T619 format) for electronic filing"""
|
||||
self.ensure_one()
|
||||
nsmap = {
|
||||
None: 'http://www.cra-arc.gc.ca/enov/ol/interfaces/efile/partnership/t4'
|
||||
}
|
||||
root = etree.Element('Submission', nsmap=nsmap)
|
||||
|
||||
# T619 header
|
||||
t619 = etree.SubElement(root, 'T619')
|
||||
self._add_xml_element(t619, 'sbmt_ref_id', 'T4-%s-%s' % (self.tax_year, self.id))
|
||||
self._add_xml_element(t619, 'rpt_tcd', 'O')
|
||||
bn = self.transmitter_bn or self.cra_business_number or ''
|
||||
self._add_xml_element(t619, 'trnmtr_nbr', 'MM' + bn[:7].ljust(7, '0') if bn else 'MM0000000')
|
||||
self._add_xml_element(t619, 'trnmtr_tcd', '4')
|
||||
self._add_xml_element(t619, 'summ_cnt', '1')
|
||||
self._add_xml_element(t619, 'lang_cd', 'E')
|
||||
|
||||
trnmtr_nm = etree.SubElement(t619, 'TRNMTR_NM')
|
||||
self._add_xml_element(trnmtr_nm, 'l1_nm', self.transmitter_name or self.company_id.name or '')
|
||||
|
||||
company = self.company_id
|
||||
trnmtr_addr = etree.SubElement(t619, 'TRNMTR_ADDR')
|
||||
if company.street:
|
||||
self._add_xml_element(trnmtr_addr, 'addr_l1_txt', company.street)
|
||||
if company.city:
|
||||
self._add_xml_element(trnmtr_addr, 'cty_nm', company.city)
|
||||
if company.state_id:
|
||||
self._add_xml_element(trnmtr_addr, 'prov_cd', company.state_id.code)
|
||||
if company.zip:
|
||||
self._add_xml_element(trnmtr_addr, 'pstl_cd', company.zip)
|
||||
self._add_xml_element(trnmtr_addr, 'cntry_cd', 'CAN')
|
||||
|
||||
if self.contact_name or self.contact_phone or self.contact_email:
|
||||
cntc = etree.SubElement(t619, 'CNTC')
|
||||
self._add_xml_element(cntc, 'cntc_nm', self.contact_name)
|
||||
if self.contact_phone:
|
||||
phone = ''.join(filter(str.isdigit, self.contact_phone))
|
||||
if len(phone) >= 10:
|
||||
self._add_xml_element(cntc, 'cntc_area_cd', phone[:3])
|
||||
self._add_xml_element(cntc, 'cntc_phn_nbr', phone[3:10])
|
||||
self._add_xml_element(cntc, 'cntc_email_area', self.contact_email)
|
||||
|
||||
# T4Return
|
||||
t4_return = etree.SubElement(root, 'T4Return')
|
||||
|
||||
# T4Summary
|
||||
t4_summary = etree.SubElement(t4_return, 'T4Summary')
|
||||
bn15 = (self.transmitter_bn or self.cra_business_number or '')[:15]
|
||||
self._add_xml_element(t4_summary, 'bn', bn15)
|
||||
self._add_xml_element(t4_summary, 'tx_yr', str(self.tax_year))
|
||||
self._add_xml_element(t4_summary, 'slp_cnt', str(self.slip_count))
|
||||
|
||||
payr_nm = etree.SubElement(t4_summary, 'PAYR_NM')
|
||||
self._add_xml_element(payr_nm, 'l1_nm', company.name or '')
|
||||
|
||||
payr_addr = etree.SubElement(t4_summary, 'PAYR_ADDR')
|
||||
if company.street:
|
||||
self._add_xml_element(payr_addr, 'addr_l1_txt', company.street)
|
||||
if company.city:
|
||||
self._add_xml_element(payr_addr, 'cty_nm', company.city)
|
||||
if company.state_id:
|
||||
self._add_xml_element(payr_addr, 'prov_cd', company.state_id.code)
|
||||
if company.zip:
|
||||
self._add_xml_element(payr_addr, 'pstl_cd', company.zip)
|
||||
self._add_xml_element(payr_addr, 'cntry_cd', 'CAN')
|
||||
|
||||
t4_tamt = etree.SubElement(t4_summary, 'T4_TAMT')
|
||||
self._add_xml_amount(t4_tamt, 'tot_empt_incm_amt', self.total_employment_income)
|
||||
self._add_xml_amount(t4_tamt, 'tot_empe_cpp_amt', self.total_cpp_employee)
|
||||
self._add_xml_amount(t4_tamt, 'tot_empe_eip_amt', self.total_ei_employee)
|
||||
self._add_xml_amount(t4_tamt, 'tot_itx_ddct_amt', self.total_income_tax)
|
||||
self._add_xml_amount(t4_tamt, 'tot_empr_cpp_amt', self.total_cpp_employer)
|
||||
self._add_xml_amount(t4_tamt, 'tot_empr_eip_amt', self.total_ei_employer)
|
||||
|
||||
# T4Slips
|
||||
for slip in self.slip_ids:
|
||||
slip_elem = etree.SubElement(t4_return, 'T4Slip')
|
||||
emp = slip.employee_id
|
||||
|
||||
empe_nm = etree.SubElement(slip_elem, 'EMPE_NM')
|
||||
name_parts = (emp.name or '').split(' ', 1)
|
||||
self._add_xml_element(empe_nm, 'snm', name_parts[-1] if len(name_parts) > 1 else emp.name or '')
|
||||
self._add_xml_element(empe_nm, 'gvn_nm', name_parts[0] if len(name_parts) > 1 else '')
|
||||
|
||||
if hasattr(emp, 'private_street') and (emp.private_street or emp.private_city):
|
||||
empe_addr = etree.SubElement(slip_elem, 'EMPE_ADDR')
|
||||
if emp.private_street:
|
||||
self._add_xml_element(empe_addr, 'addr_l1_txt', emp.private_street)
|
||||
if emp.private_city:
|
||||
self._add_xml_element(empe_addr, 'cty_nm', emp.private_city)
|
||||
if emp.private_state_id:
|
||||
self._add_xml_element(empe_addr, 'prov_cd', emp.private_state_id.code)
|
||||
if emp.private_zip:
|
||||
self._add_xml_element(empe_addr, 'pstl_cd', emp.private_zip)
|
||||
self._add_xml_element(empe_addr, 'cntry_cd', 'CAN')
|
||||
elif emp.home_street or emp.home_city:
|
||||
empe_addr = etree.SubElement(slip_elem, 'EMPE_ADDR')
|
||||
if emp.home_street:
|
||||
self._add_xml_element(empe_addr, 'addr_l1_txt', emp.home_street)
|
||||
if emp.home_city:
|
||||
self._add_xml_element(empe_addr, 'cty_nm', emp.home_city)
|
||||
if emp.home_province:
|
||||
self._add_xml_element(empe_addr, 'prov_cd', emp.home_province)
|
||||
if emp.home_postal_code:
|
||||
self._add_xml_element(empe_addr, 'pstl_cd', emp.home_postal_code)
|
||||
self._add_xml_element(empe_addr, 'cntry_cd', 'CAN')
|
||||
|
||||
self._add_xml_element(slip_elem, 'sin', slip.sin_number or '')
|
||||
self._add_xml_element(slip_elem, 'empe_nbr', str(emp.employee_number or emp.id))
|
||||
province = emp.home_province or ''
|
||||
self._add_xml_element(slip_elem, 'prov_cd', province)
|
||||
|
||||
t4_amt = etree.SubElement(slip_elem, 'T4_AMT')
|
||||
self._add_xml_amount(t4_amt, 'empt_incm_amt', slip.employment_income)
|
||||
self._add_xml_amount(t4_amt, 'cpp_cntrb_amt', slip.cpp_employee)
|
||||
self._add_xml_amount(t4_amt, 'empe_eip_amt', slip.ei_employee)
|
||||
self._add_xml_amount(t4_amt, 'itx_ddct_amt', slip.income_tax)
|
||||
self._add_xml_amount(t4_amt, 'ei_insu_earn_amt', slip.ei_insurable_earnings)
|
||||
self._add_xml_amount(t4_amt, 'cpp_qpp_pnsn_amt', slip.cpp_pensionable_earnings)
|
||||
self._add_xml_amount(t4_amt, 'unn_dues_amt', slip.box_44_union_dues)
|
||||
|
||||
xml_bytes = etree.tostring(root, xml_declaration=True, encoding='UTF-8', pretty_print=True)
|
||||
filename = 'T4_%s_%s.xml' % (self.tax_year, (company.name or 'Company').replace(' ', '_'))
|
||||
|
||||
self.write({
|
||||
'xml_file': base64.b64encode(xml_bytes),
|
||||
'xml_filename': filename,
|
||||
})
|
||||
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(xml_bytes),
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'mimetype': 'application/xml',
|
||||
})
|
||||
|
||||
self.message_post(
|
||||
body='T4 XML generated: <strong>%s</strong>' % filename,
|
||||
attachment_ids=[attachment.id],
|
||||
)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/web/content/%s?download=true' % attachment.id,
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
def _add_xml_element(self, parent, tag, value):
|
||||
if value:
|
||||
elem = etree.SubElement(parent, tag)
|
||||
elem.text = str(value)
|
||||
|
||||
def _add_xml_amount(self, parent, tag, amount):
|
||||
if amount:
|
||||
elem = etree.SubElement(parent, tag)
|
||||
elem.text = '%.2f' % amount
|
||||
|
||||
def _get_pdf_text_coordinates(self):
|
||||
"""Get text overlay coordinates for flattened PDF
|
||||
Returns dict mapping field names to (x, y, font_size, font_name) tuples
|
||||
@@ -1005,6 +1166,13 @@ class HrT4Slip(models.Model):
|
||||
currency_field='currency_id',
|
||||
)
|
||||
|
||||
# === Box 20: RPP/RRSP Contributions ===
|
||||
box_20_rpp = fields.Monetary(
|
||||
string='Box 20: RPP/RRSP',
|
||||
currency_field='currency_id',
|
||||
help='Registered Pension Plan or RRSP contributions',
|
||||
)
|
||||
|
||||
# === T4 Dental Benefits Code ===
|
||||
t4_dental_code = fields.Selection(
|
||||
related='employee_id.t4_dental_code',
|
||||
|
||||
Reference in New Issue
Block a user