Initial commit
This commit is contained in:
704
fusion_payroll/models/hr_payroll_t4a.py
Normal file
704
fusion_payroll/models/hr_payroll_t4a.py
Normal file
@@ -0,0 +1,704 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import os
|
||||
import io
|
||||
from datetime import date
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import tools
|
||||
|
||||
|
||||
class HrT4ASummary(models.Model):
|
||||
"""T4A Summary - One per company per tax year"""
|
||||
_name = 'hr.t4a.summary'
|
||||
_description = 'T4A Summary'
|
||||
_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 T4A Summary
|
||||
position_model = self.env['pdf.field.position']
|
||||
return position_model.get_coordinates_dict('T4A Summary')
|
||||
|
||||
STATE_SELECTION = [
|
||||
('draft', 'Draft'),
|
||||
('generated', 'Generated'),
|
||||
('filed', 'Filed'),
|
||||
]
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
compute='_compute_name',
|
||||
store=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
related='company_id.currency_id',
|
||||
)
|
||||
tax_year = fields.Integer(
|
||||
string='Tax Year',
|
||||
required=True,
|
||||
default=lambda self: date.today().year - 1,
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=STATE_SELECTION,
|
||||
string='Status',
|
||||
default='draft',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# === CRA Information ===
|
||||
cra_business_number = fields.Char(
|
||||
string='CRA Business Number',
|
||||
compute='_compute_cra_business_number',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_cra_business_number(self):
|
||||
"""Get CRA business number from payroll settings."""
|
||||
for summary in self:
|
||||
if summary.company_id:
|
||||
settings = self.env['payroll.config.settings'].get_settings(summary.company_id.id)
|
||||
summary.cra_business_number = settings.get_cra_payroll_account_number() or summary.company_id.vat or ''
|
||||
else:
|
||||
summary.cra_business_number = ''
|
||||
|
||||
# === Slip Count ===
|
||||
slip_count = fields.Integer(
|
||||
string='Total T4A Slips',
|
||||
compute='_compute_totals',
|
||||
store=True,
|
||||
)
|
||||
slip_ids = fields.One2many(
|
||||
'hr.t4a.slip',
|
||||
'summary_id',
|
||||
string='T4A Slips',
|
||||
)
|
||||
|
||||
# === Summary Totals ===
|
||||
total_box_016 = fields.Monetary(
|
||||
string='Total Box 016 (Pension)',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_totals',
|
||||
store=True,
|
||||
)
|
||||
total_box_018 = fields.Monetary(
|
||||
string='Total Box 018 (Lump-Sum)',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_totals',
|
||||
store=True,
|
||||
)
|
||||
total_box_020 = fields.Monetary(
|
||||
string='Total Box 020 (Commissions)',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_totals',
|
||||
store=True,
|
||||
)
|
||||
total_box_024 = fields.Monetary(
|
||||
string='Total Box 024 (Annuities)',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_totals',
|
||||
store=True,
|
||||
)
|
||||
total_box_048 = fields.Monetary(
|
||||
string='Total Box 048 (Fees)',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_totals',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# === Contact Information ===
|
||||
contact_name = fields.Char(
|
||||
string='Contact Person',
|
||||
default=lambda self: self.env.user.name,
|
||||
)
|
||||
contact_phone = fields.Char(
|
||||
string='Telephone',
|
||||
)
|
||||
|
||||
# === Filing Information ===
|
||||
filing_date = fields.Date(
|
||||
string='Filing Date',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
@api.depends('tax_year', 'company_id')
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
rec.name = f"T4A Summary {rec.tax_year} - {rec.company_id.name}"
|
||||
|
||||
@api.depends('slip_ids')
|
||||
def _compute_totals(self):
|
||||
for rec in self:
|
||||
slips = rec.slip_ids
|
||||
rec.slip_count = len(slips)
|
||||
rec.total_box_016 = sum(slips.mapped('box_016_pension'))
|
||||
rec.total_box_018 = sum(slips.mapped('box_018_lump_sum'))
|
||||
rec.total_box_020 = sum(slips.mapped('box_020_commissions'))
|
||||
rec.total_box_024 = sum(slips.mapped('box_024_annuities'))
|
||||
rec.total_box_048 = sum(slips.mapped('box_048_fees'))
|
||||
|
||||
def action_mark_filed(self):
|
||||
"""Mark T4A Summary as filed"""
|
||||
self.ensure_one()
|
||||
self.write({
|
||||
'state': 'filed',
|
||||
'filing_date': date.today(),
|
||||
})
|
||||
|
||||
|
||||
class HrT4ASlip(models.Model):
|
||||
"""T4A Slip - One per recipient per tax year"""
|
||||
_name = 'hr.t4a.slip'
|
||||
_description = 'T4A Slip'
|
||||
_order = 'recipient_name'
|
||||
|
||||
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 T4A
|
||||
position_model = self.env['pdf.field.position']
|
||||
return position_model.get_coordinates_dict('T4A')
|
||||
|
||||
def _overlay_text_on_pdf(self, template_path, field_mapping):
|
||||
"""Overlay text on a flattened PDF using reportlab
|
||||
Returns base64-encoded PDF data
|
||||
"""
|
||||
try:
|
||||
from reportlab.pdfgen import canvas
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
except ImportError as e:
|
||||
raise UserError(f'Required library not available: {str(e)}\nPlease install reportlab and PyPDF2.')
|
||||
|
||||
# Get text coordinates
|
||||
text_coords = self._get_pdf_text_coordinates()
|
||||
if not text_coords:
|
||||
raise UserError(
|
||||
'Text coordinates not configured for T4A template. '
|
||||
'Please configure PDF field positions in Payroll → Configuration → PDF Field Positions.'
|
||||
)
|
||||
|
||||
# Read the template PDF
|
||||
with open(template_path, 'rb') as template_file:
|
||||
template_reader = PdfReader(template_file)
|
||||
if not template_reader.pages:
|
||||
raise UserError('Template PDF has no pages')
|
||||
|
||||
# Get first page dimensions
|
||||
first_page = template_reader.pages[0]
|
||||
page_width = float(first_page.mediabox.width)
|
||||
page_height = float(first_page.mediabox.height)
|
||||
|
||||
# Create overlay PDF with text
|
||||
overlay_buffer = io.BytesIO()
|
||||
can = canvas.Canvas(overlay_buffer, pagesize=(page_width, page_height))
|
||||
|
||||
# Draw text for each field
|
||||
for field_name, value in field_mapping.items():
|
||||
if field_name in text_coords and value:
|
||||
coord_data = text_coords[field_name]
|
||||
# Handle both old format (x, y, font_size) and new format (x, y, font_size, font_name)
|
||||
if len(coord_data) == 4:
|
||||
x, y, font_size, font_name = coord_data
|
||||
elif len(coord_data) == 3:
|
||||
x, y, font_size = coord_data
|
||||
font_name = 'Helvetica' # Default font
|
||||
else:
|
||||
continue # Skip invalid coordinate data
|
||||
|
||||
can.setFont(font_name, font_size)
|
||||
can.drawString(x, y, str(value))
|
||||
|
||||
can.save()
|
||||
overlay_buffer.seek(0)
|
||||
|
||||
# Merge overlay with template
|
||||
with open(template_path, 'rb') as template_file:
|
||||
template_reader = PdfReader(template_file)
|
||||
overlay_reader = PdfReader(overlay_buffer)
|
||||
|
||||
writer = PdfWriter()
|
||||
for i, page in enumerate(template_reader.pages):
|
||||
if i < len(overlay_reader.pages):
|
||||
page.merge_page(overlay_reader.pages[i])
|
||||
writer.add_page(page)
|
||||
|
||||
# Write to bytes
|
||||
output_buffer = io.BytesIO()
|
||||
writer.write(output_buffer)
|
||||
return base64.b64encode(output_buffer.getvalue())
|
||||
|
||||
summary_id = fields.Many2one(
|
||||
'hr.t4a.summary',
|
||||
string='T4A Summary',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
related='summary_id.company_id',
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
related='summary_id.currency_id',
|
||||
)
|
||||
tax_year = fields.Integer(
|
||||
related='summary_id.tax_year',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# === Recipient Information ===
|
||||
recipient_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Recipient',
|
||||
help='Recipient partner (individual or business)',
|
||||
)
|
||||
recipient_name = fields.Char(
|
||||
string='Recipient Name',
|
||||
required=True,
|
||||
help='Last name, first name and initials',
|
||||
)
|
||||
recipient_address = fields.Text(
|
||||
string='Recipient Address',
|
||||
help='Full address including province and postal code',
|
||||
)
|
||||
recipient_sin = fields.Char(
|
||||
string='SIN (Box 12)',
|
||||
help='Social Insurance Number (9 digits)',
|
||||
)
|
||||
recipient_account_number = fields.Char(
|
||||
string='Account Number (Box 13)',
|
||||
help='Business Number if recipient is a business',
|
||||
)
|
||||
|
||||
# === Income Boxes ===
|
||||
box_016_pension = fields.Monetary(
|
||||
string='Box 016: Pension or Superannuation',
|
||||
currency_field='currency_id',
|
||||
)
|
||||
box_018_lump_sum = fields.Monetary(
|
||||
string='Box 018: Lump-Sum Payments',
|
||||
currency_field='currency_id',
|
||||
)
|
||||
box_020_commissions = fields.Monetary(
|
||||
string='Box 020: Self-Employed Commissions',
|
||||
currency_field='currency_id',
|
||||
)
|
||||
box_024_annuities = fields.Monetary(
|
||||
string='Box 024: Annuities',
|
||||
currency_field='currency_id',
|
||||
)
|
||||
box_048_fees = fields.Monetary(
|
||||
string='Box 048: Fees for Services',
|
||||
currency_field='currency_id',
|
||||
)
|
||||
|
||||
# === Other Information (028-197) ===
|
||||
other_info_ids = fields.One2many(
|
||||
'hr.t4a.other.info',
|
||||
'slip_id',
|
||||
string='Other Information',
|
||||
help='Other information boxes (028-197)',
|
||||
)
|
||||
|
||||
# === PDF Generation ===
|
||||
filled_pdf = fields.Binary(
|
||||
string='Filled PDF',
|
||||
attachment=True,
|
||||
)
|
||||
filled_pdf_filename = fields.Char(
|
||||
string='PDF Filename',
|
||||
)
|
||||
|
||||
@api.onchange('recipient_id')
|
||||
def _onchange_recipient_id(self):
|
||||
"""Auto-fill recipient information from partner"""
|
||||
if self.recipient_id:
|
||||
# Format name: Last name, First name
|
||||
name_parts = self.recipient_id.name.split(',') if ',' in self.recipient_id.name else self.recipient_id.name.split()
|
||||
if len(name_parts) >= 2:
|
||||
self.recipient_name = f"{name_parts[-1].strip()}, {' '.join(name_parts[:-1]).strip()}"
|
||||
else:
|
||||
self.recipient_name = self.recipient_id.name
|
||||
|
||||
# Build address
|
||||
address_parts = []
|
||||
if self.recipient_id.street:
|
||||
address_parts.append(self.recipient_id.street)
|
||||
if self.recipient_id.street2:
|
||||
address_parts.append(self.recipient_id.street2)
|
||||
if self.recipient_id.city:
|
||||
city_line = self.recipient_id.city
|
||||
if self.recipient_id.state_id:
|
||||
city_line += f", {self.recipient_id.state_id.code}"
|
||||
if self.recipient_id.zip:
|
||||
city_line += f" {self.recipient_id.zip}"
|
||||
address_parts.append(city_line)
|
||||
self.recipient_address = '\n'.join(address_parts)
|
||||
|
||||
# Get SIN if available (might be stored in a custom field)
|
||||
if hasattr(self.recipient_id, 'sin_number'):
|
||||
self.recipient_sin = self.recipient_id.sin_number
|
||||
|
||||
def action_fill_pdf(self):
|
||||
"""Fill the T4A PDF form with data from this slip"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
# Try to import pdfrw (preferred) or PyPDF2
|
||||
try:
|
||||
from pdfrw import PdfReader, PdfWriter
|
||||
use_pdfrw = True
|
||||
except ImportError:
|
||||
try:
|
||||
import PyPDF2
|
||||
use_pdfrw = False
|
||||
except ImportError:
|
||||
raise UserError(
|
||||
'PDF library not found. Please install pdfrw or PyPDF2:\n'
|
||||
'pip install pdfrw\n'
|
||||
'or\n'
|
||||
'pip install PyPDF2'
|
||||
)
|
||||
|
||||
# Get PDF template path - try multiple locations
|
||||
# 1. Try in static/pdf/ folder (recommended location)
|
||||
module_path = os.path.dirname(os.path.dirname(__file__))
|
||||
template_path = os.path.join(module_path, 'static', 'pdf', 't4a-fill-25e.pdf')
|
||||
|
||||
# 2. Try in module root directory (fallback)
|
||||
if not os.path.exists(template_path):
|
||||
template_path = os.path.join(module_path, 't4a-fill-25e.pdf')
|
||||
|
||||
# 3. Try using tools.file_path (Odoo 19)
|
||||
if not os.path.exists(template_path):
|
||||
try:
|
||||
template_path = tools.file_path('fusion_payroll/static/pdf/t4a-fill-25e.pdf')
|
||||
except:
|
||||
pass
|
||||
|
||||
# 4. Final fallback - root directory
|
||||
if not os.path.exists(template_path):
|
||||
try:
|
||||
template_path = tools.file_path('fusion_payroll/t4a-fill-25e.pdf')
|
||||
except:
|
||||
pass
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
raise UserError(
|
||||
'T4A PDF template not found. Please ensure t4a-fill-25e.pdf is in one of these locations:\n'
|
||||
f'1. {os.path.join(module_path, "static", "pdf", "t4a-fill-25e.pdf")} (recommended)\n'
|
||||
f'2. {os.path.join(module_path, "t4a-fill-25e.pdf")} (module root)\n\n'
|
||||
'The system will automatically fill the PDF with data from this T4A slip when you click "Fill PDF".'
|
||||
)
|
||||
|
||||
# Get field mapping
|
||||
field_mapping = self._get_pdf_field_mapping()
|
||||
|
||||
# Check if we should use text overlay (for flattened PDFs)
|
||||
text_coords = self._get_pdf_text_coordinates()
|
||||
if text_coords:
|
||||
# Use text overlay method for flattened PDF
|
||||
pdf_data = self._overlay_text_on_pdf(template_path, field_mapping)
|
||||
elif use_pdfrw:
|
||||
# Use pdfrw to fill PDF
|
||||
from pdfrw import PdfDict
|
||||
template = PdfReader(template_path)
|
||||
|
||||
# Fill form fields
|
||||
if hasattr(template.Root, 'AcroForm') and template.Root.AcroForm:
|
||||
if hasattr(template.Root.AcroForm, 'Fields') and template.Root.AcroForm.Fields:
|
||||
for field in template.Root.AcroForm.Fields:
|
||||
# Get field name (can be in /T or /TU)
|
||||
field_name = None
|
||||
if hasattr(field, 'T'):
|
||||
field_name = str(field.T).strip('()')
|
||||
elif hasattr(field, 'TU'):
|
||||
field_name = str(field.TU).strip('()')
|
||||
|
||||
if field_name and field_name in field_mapping:
|
||||
value = field_mapping[field_name]
|
||||
if value is not None and value != '':
|
||||
# Set field value
|
||||
field.V = str(value)
|
||||
# Make sure field is not read-only
|
||||
if hasattr(field, 'Ff'):
|
||||
field.Ff = 0 # Remove read-only flag
|
||||
|
||||
# Write filled PDF to temporary file
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
|
||||
tmp_path = tmp_file.name
|
||||
|
||||
writer = PdfWriter()
|
||||
writer.write(template, tmp_path)
|
||||
|
||||
# Read filled PDF
|
||||
with open(tmp_path, 'rb') as f:
|
||||
pdf_data = base64.b64encode(f.read())
|
||||
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
# Use PyPDF2 (fallback)
|
||||
with open(template_path, 'rb') as template_file:
|
||||
reader = PyPDF2.PdfReader(template_file)
|
||||
writer = PyPDF2.PdfWriter()
|
||||
|
||||
# Copy pages
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
# Fill form fields
|
||||
field_mapping = self._get_pdf_field_mapping()
|
||||
if reader.get_form_text_fields():
|
||||
writer.update_page_form_field_values(writer.pages[0], field_mapping)
|
||||
|
||||
# Write to bytes
|
||||
output_buffer = io.BytesIO()
|
||||
writer.write(output_buffer)
|
||||
pdf_data = base64.b64encode(output_buffer.getvalue())
|
||||
|
||||
# Generate filename
|
||||
recipient_safe = self.recipient_name.replace(' ', '_').replace(',', '')[:30]
|
||||
filename = f'T4A_{self.tax_year}_{recipient_safe}.pdf'
|
||||
|
||||
# Save filled PDF
|
||||
self.write({
|
||||
'filled_pdf': pdf_data,
|
||||
'filled_pdf_filename': filename,
|
||||
})
|
||||
|
||||
# Post to chatter
|
||||
self.message_post(
|
||||
body=f'T4A PDF generated: <strong>{filename}</strong>',
|
||||
attachment_ids=[(0, 0, {
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': pdf_data,
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})],
|
||||
)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'PDF Generated',
|
||||
'message': f'T4A PDF filled and saved: {filename}',
|
||||
'type': 'success',
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise UserError(f'Error filling PDF: {str(e)}')
|
||||
|
||||
def action_extract_pdf_fields(self):
|
||||
"""Helper method to extract PDF form field names (for debugging)"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
from pdfrw import PdfReader
|
||||
except ImportError:
|
||||
raise UserError('pdfrw library not installed. Install with: pip install pdfrw')
|
||||
|
||||
# Get PDF template path - try multiple locations
|
||||
module_path = os.path.dirname(os.path.dirname(__file__))
|
||||
template_path = os.path.join(module_path, 'static', 'pdf', 't4a-fill-25e.pdf')
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
template_path = os.path.join(module_path, 't4a-fill-25e.pdf')
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
try:
|
||||
template_path = tools.file_path('fusion_payroll/static/pdf/t4a-fill-25e.pdf')
|
||||
except:
|
||||
template_path = None
|
||||
|
||||
if not template_path or not os.path.exists(template_path):
|
||||
raise UserError('T4A PDF template not found. Please ensure t4a-fill-25e.pdf is in static/pdf/ or module root.')
|
||||
|
||||
template = PdfReader(template_path)
|
||||
field_names = []
|
||||
|
||||
# Extract field names from all pages
|
||||
for page_num, page in enumerate(template.pages, 1):
|
||||
if hasattr(page, 'Annots') and page.Annots:
|
||||
for annot in page.Annots:
|
||||
if hasattr(annot, 'Subtype') and str(annot.Subtype) == '/Widget':
|
||||
if hasattr(annot, 'T'):
|
||||
field_name = str(annot.T).strip('()')
|
||||
field_names.append(f'Page {page_num}: {field_name}')
|
||||
|
||||
# Return as message
|
||||
if field_names:
|
||||
message = 'PDF Form Fields Found:\n\n' + '\n'.join(field_names[:50])
|
||||
if len(field_names) > 50:
|
||||
message += f'\n\n... and {len(field_names) - 50} more fields'
|
||||
else:
|
||||
message = 'No form fields found in PDF. The PDF may not be a fillable form, or field names are stored differently.'
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'PDF Fields',
|
||||
'message': message,
|
||||
'type': 'info',
|
||||
'sticky': True,
|
||||
}
|
||||
}
|
||||
|
||||
def _get_pdf_field_mapping(self):
|
||||
"""Map model fields to PDF form field names"""
|
||||
# This mapping may need to be adjusted based on actual PDF form field names
|
||||
# Common field name patterns for T4A forms:
|
||||
# You can use action_extract_pdf_fields() to see actual field names in the PDF
|
||||
mapping = {}
|
||||
|
||||
# Year
|
||||
mapping['Year'] = str(self.tax_year)
|
||||
mapping['year'] = str(self.tax_year)
|
||||
mapping['YEAR'] = str(self.tax_year)
|
||||
|
||||
# Payer information (from company)
|
||||
company = self.company_id
|
||||
if company:
|
||||
mapping['PayerName'] = company.name or ''
|
||||
mapping['PayerName1'] = company.name or ''
|
||||
if company.street:
|
||||
mapping['PayerAddress1'] = company.street
|
||||
if company.street2:
|
||||
mapping['PayerAddress2'] = company.street2
|
||||
if company.city:
|
||||
city_line = company.city
|
||||
if company.state_id:
|
||||
city_line += f", {company.state_id.code}"
|
||||
if company.zip:
|
||||
city_line += f" {company.zip}"
|
||||
mapping['PayerCity'] = city_line
|
||||
|
||||
# Payer account number
|
||||
settings = self.env['payroll.config.settings'].get_settings(company.id)
|
||||
account_num = settings.get_cra_payroll_account_number() or company.vat or ''
|
||||
mapping['PayerAccount'] = account_num
|
||||
mapping['Box54'] = account_num
|
||||
|
||||
# Recipient information
|
||||
if self.recipient_name:
|
||||
# Split name into last, first
|
||||
name_parts = self.recipient_name.split(',')
|
||||
if len(name_parts) >= 2:
|
||||
mapping['LastName'] = name_parts[0].strip()
|
||||
mapping['FirstName'] = name_parts[1].strip()
|
||||
else:
|
||||
# Try to split by space
|
||||
name_parts = self.recipient_name.split()
|
||||
if len(name_parts) >= 2:
|
||||
mapping['LastName'] = name_parts[-1]
|
||||
mapping['FirstName'] = ' '.join(name_parts[:-1])
|
||||
else:
|
||||
mapping['LastName'] = self.recipient_name
|
||||
|
||||
if self.recipient_address:
|
||||
addr_lines = self.recipient_address.split('\n')
|
||||
for i, line in enumerate(addr_lines[:3], 1):
|
||||
mapping[f'RecipientAddress{i}'] = line
|
||||
|
||||
if self.recipient_sin:
|
||||
mapping['SIN'] = self.recipient_sin.replace('-', '').replace(' ', '')
|
||||
mapping['Box12'] = self.recipient_sin.replace('-', '').replace(' ', '')
|
||||
|
||||
if self.recipient_account_number:
|
||||
mapping['Box13'] = self.recipient_account_number
|
||||
|
||||
# Income boxes
|
||||
if self.box_016_pension:
|
||||
mapping['Box016'] = f"{self.box_016_pension:.2f}"
|
||||
mapping['016'] = f"{self.box_016_pension:.2f}"
|
||||
|
||||
if self.box_018_lump_sum:
|
||||
mapping['Box018'] = f"{self.box_018_lump_sum:.2f}"
|
||||
mapping['018'] = f"{self.box_018_lump_sum:.2f}"
|
||||
|
||||
if self.box_020_commissions:
|
||||
mapping['Box020'] = f"{self.box_020_commissions:.2f}"
|
||||
mapping['020'] = f"{self.box_020_commissions:.2f}"
|
||||
|
||||
if self.box_024_annuities:
|
||||
mapping['Box024'] = f"{self.box_024_annuities:.2f}"
|
||||
mapping['024'] = f"{self.box_024_annuities:.2f}"
|
||||
|
||||
if self.box_048_fees:
|
||||
mapping['Box048'] = f"{self.box_048_fees:.2f}"
|
||||
mapping['048'] = f"{self.box_048_fees:.2f}"
|
||||
|
||||
# Other information boxes
|
||||
for other_info in self.other_info_ids:
|
||||
box_num = str(other_info.box_number).zfill(3)
|
||||
mapping[f'Box{box_num}'] = f"{other_info.amount:.2f}"
|
||||
mapping[box_num] = f"{other_info.amount:.2f}"
|
||||
|
||||
return mapping
|
||||
|
||||
def action_download_pdf(self):
|
||||
"""Download the filled PDF"""
|
||||
self.ensure_one()
|
||||
if not self.filled_pdf:
|
||||
raise UserError('No PDF has been generated yet. Please click "Fill PDF" first.')
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/hr.t4a.slip/{self.id}/filled_pdf/{self.filled_pdf_filename}?download=true',
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
|
||||
class HrT4AOtherInfo(models.Model):
|
||||
"""T4A Other Information (Boxes 028-197)"""
|
||||
_name = 'hr.t4a.other.info'
|
||||
_description = 'T4A Other Information'
|
||||
_order = 'box_number'
|
||||
|
||||
slip_id = fields.Many2one(
|
||||
'hr.t4a.slip',
|
||||
string='T4A Slip',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
box_number = fields.Integer(
|
||||
string='Box Number',
|
||||
required=True,
|
||||
help='Box number (028-197)',
|
||||
)
|
||||
currency_id_slip = fields.Many2one(
|
||||
related='slip_id.currency_id',
|
||||
string='Currency',
|
||||
)
|
||||
amount = fields.Monetary(
|
||||
string='Amount',
|
||||
currency_field='currency_id_slip',
|
||||
required=True,
|
||||
)
|
||||
description = fields.Char(
|
||||
string='Description',
|
||||
help='Description of this income type',
|
||||
)
|
||||
Reference in New Issue
Block a user