705 lines
26 KiB
Python
705 lines
26 KiB
Python
# -*- 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',
|
|
)
|