This commit is contained in:
gsinghpal
2026-04-07 20:49:21 -04:00
parent 3cc93b8783
commit 4fde4c7bd1
25 changed files with 1253 additions and 900 deletions

View File

@@ -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',