Finance_app / pdf_generator.py
ErhaTechnologies's picture
Update pdf_generator.py
fa5bd71 verified
Raw
History Blame Contribute Delete
14.9 kB
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from datetime import datetime
import os
class PDFGenerator:
def __init__(self, output_dir="pdfs"):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
def generate_invoice_pdf(self, invoice_data, client_data):
"""Generate PDF for invoice"""
filename = f"invoice_{invoice_data['invoice_number']}.pdf"
filepath = os.path.join(self.output_dir, filename)
# Margins: 0.75 inch
doc = SimpleDocTemplate(
filepath,
pagesize=letter,
rightMargin=0.75*inch,
leftMargin=0.75*inch,
topMargin=0.75*inch,
bottomMargin=0.75*inch
)
styles = getSampleStyleSheet()
story = []
# Custom styles
title_style = ParagraphStyle(
'InvoiceTitle',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=24,
leading=28,
textColor=colors.HexColor('#1E293B')
)
company_style = ParagraphStyle(
'CompanyHeader',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=16,
leading=20,
textColor=colors.HexColor('#6D28D9')
)
meta_label_style = ParagraphStyle(
'MetaLabel',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=10,
leading=14,
textColor=colors.HexColor('#64748B')
)
meta_val_style = ParagraphStyle(
'MetaVal',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=10,
leading=14,
textColor=colors.HexColor('#1E293B')
)
# Header Table: Company name on left, "INVOICE" on right
header_data = [
[
Paragraph("ERHA TECHNOLOGIES", company_style),
Paragraph("INVOICE", title_style)
]
]
header_table = Table(header_data, colWidths=[3.5 * inch, 3.5 * inch])
header_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('ALIGN', (0, 0), (0, 0), 'LEFT'),
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
]))
story.append(header_table)
story.append(Spacer(1, 0.1 * inch))
# Colored accent bar
accent_bar = Table([['']], colWidths=[7 * inch], rowHeights=[3])
accent_bar.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#6D28D9')),
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 0),
]))
story.append(accent_bar)
story.append(Spacer(1, 0.3 * inch))
# Invoice metadata grid
meta_data = [
[Paragraph("Invoice Number:", meta_label_style), Paragraph(invoice_data['invoice_number'], meta_val_style),
Paragraph("Issue Date:", meta_label_style), Paragraph(invoice_data['issue_date'].strftime('%b %d, %Y'), meta_val_style)],
[Paragraph("Client Name:", meta_label_style), Paragraph(client_data['name'], meta_val_style),
Paragraph("Due Date:", meta_label_style), Paragraph(invoice_data['due_date'].strftime('%b %d, %Y'), meta_val_style)],
[Paragraph("Client Email:", meta_label_style), Paragraph(client_data.get('email', 'N/A'), meta_val_style),
Paragraph("Payment Status:", meta_label_style), Paragraph(invoice_data['status'].upper(), ParagraphStyle('Status', parent=meta_val_style, fontName='Helvetica-Bold', textColor=colors.HexColor('#10B981') if invoice_data['status'] == 'Paid' else colors.HexColor('#F59E0B')))]
]
meta_table = Table(meta_data, colWidths=[1.3 * inch, 2.2 * inch, 1.3 * inch, 2.2 * inch])
meta_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
]))
story.append(meta_table)
story.append(Spacer(1, 0.4 * inch))
# Details Header
details_title = Paragraph("BILLING SUMMARY", ParagraphStyle('Sub', fontName='Helvetica-Bold', fontSize=12, leading=16, textColor=colors.HexColor('#475569')))
story.append(details_title)
story.append(Spacer(1, 0.1 * inch))
# Billing details table
bill_data = [
["Description", "Amount"],
[f"Services rendered for {client_data['name']}", f"PKR {invoice_data['amount']:,.2f}"],
["Total Due", f"PKR {invoice_data['amount']:,.2f}"]
]
bill_table = Table(bill_data, colWidths=[5 * inch, 2 * inch])
bill_table.setStyle(TableStyle([
# Header Row Styling
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#F8FAFC')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#475569')),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('ALIGN', (0, 0), (-1, 0), 'LEFT'),
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
('TOPPADDING', (0, 0), (-1, 0), 10),
# Line under header
('LINEBELOW', (0, 0), (-1, 0), 1.5, colors.HexColor('#E2E8F0')),
# Content Row Styling
('FONTNAME', (0, 1), (-1, 1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, 1), 10),
('TEXTCOLOR', (0, 1), (-1, 1), colors.HexColor('#1E293B')),
('ALIGN', (0, 1), (0, 1), 'LEFT'),
('ALIGN', (1, 1), (1, 1), 'RIGHT'),
('BOTTOMPADDING', (0, 1), (-1, 1), 12),
('TOPPADDING', (0, 1), (-1, 1), 12),
('LINEBELOW', (0, 1), (-1, 1), 0.5, colors.HexColor('#F1F5F9')),
# Total Row Styling
('FONTNAME', (0, 2), (-1, 2), 'Helvetica-Bold'),
('FONTSIZE', (0, 2), (-1, 2), 12),
('TEXTCOLOR', (0, 2), (-1, 2), colors.HexColor('#6D28D9')),
('ALIGN', (0, 2), (0, 2), 'LEFT'),
('ALIGN', (1, 2), (1, 2), 'RIGHT'),
('BOTTOMPADDING', (0, 2), (-1, 2), 12),
('TOPPADDING', (0, 2), (-1, 2), 12),
('LINEABOVE', (0, 2), (-1, 2), 1.5, colors.HexColor('#E2E8F0')),
]))
story.append(bill_table)
doc.build(story)
return filepath
def generate_payslip_pdf(self, payroll_data, employee_data):
"""Generate PDF for payslip"""
filename = f"payslip_{payroll_data['employee_name']}_{payroll_data['month']}_{payroll_data['year']}.pdf"
filepath = os.path.join(self.output_dir, filename)
# Margins: 0.75 inch
doc = SimpleDocTemplate(
filepath,
pagesize=letter,
rightMargin=0.75*inch,
leftMargin=0.75*inch,
topMargin=0.75*inch,
bottomMargin=0.75*inch
)
styles = getSampleStyleSheet()
story = []
# Custom styles
title_style = ParagraphStyle(
'PayslipTitle',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=24,
leading=28,
textColor=colors.HexColor('#1E293B')
)
company_style = ParagraphStyle(
'CompanyHeader',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=16,
leading=20,
textColor=colors.HexColor('#6D28D9')
)
meta_label_style = ParagraphStyle(
'MetaLabel',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=10,
leading=14,
textColor=colors.HexColor('#64748B')
)
meta_val_style = ParagraphStyle(
'MetaVal',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=10,
leading=14,
textColor=colors.HexColor('#1E293B')
)
# Header Table: Company name on left, "PAYSLIP" on right
header_data = [
[
Paragraph("ERHA TECHNOLOGIES", company_style),
Paragraph("PAYSLIP", title_style)
]
]
header_table = Table(header_data, colWidths=[3.5 * inch, 3.5 * inch])
header_table.setStyle(TableStyle([
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('ALIGN', (0, 0), (0, 0), 'LEFT'),
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
]))
story.append(header_table)
story.append(Spacer(1, 0.1 * inch))
# Colored accent bar
accent_bar = Table([['']], colWidths=[7 * inch], rowHeights=[3])
accent_bar.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#6D28D9')),
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 0),
]))
story.append(accent_bar)
story.append(Spacer(1, 0.3 * inch))
# Employee / Period metadata grid
payment_date_str = "N/A"
if payroll_data.get('payment_date'):
if isinstance(payroll_data['payment_date'], str):
try:
payment_date_str = datetime.strptime(payroll_data['payment_date'][:10], '%Y-%m-%d').strftime('%b %d, %Y')
except:
payment_date_str = payroll_data['payment_date']
elif hasattr(payroll_data['payment_date'], 'strftime'):
payment_date_str = payroll_data['payment_date'].strftime('%b %d, %Y')
meta_data = [
[Paragraph("Employee Name:", meta_label_style), Paragraph(payroll_data['employee_name'], meta_val_style),
Paragraph("Payroll Period:", meta_label_style), Paragraph(f"{payroll_data['month']} {payroll_data['year']}", meta_val_style)],
[Paragraph("Designation:", meta_label_style), Paragraph(employee_data.get('role', 'N/A'), meta_val_style),
Paragraph("Disbursement Date:", meta_label_style), Paragraph(payment_date_str, meta_val_style)],
[Paragraph("Payment Status:", meta_label_style), Paragraph("PAID" if payroll_data['is_paid'] else "PENDING", ParagraphStyle('Status', parent=meta_val_style, fontName='Helvetica-Bold', textColor=colors.HexColor('#10B981') if payroll_data['is_paid'] else colors.HexColor('#F59E0B'))),
Paragraph("Employer:", meta_label_style), Paragraph("Spaze", meta_val_style)]
]
meta_table = Table(meta_data, colWidths=[1.3 * inch, 2.2 * inch, 1.3 * inch, 2.2 * inch])
meta_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
]))
story.append(meta_table)
story.append(Spacer(1, 0.4 * inch))
# Details Header
details_title = Paragraph("EARNINGS & DEDUCTIONS", ParagraphStyle('Sub', fontName='Helvetica-Bold', fontSize=12, leading=16, textColor=colors.HexColor('#475569')))
story.append(details_title)
story.append(Spacer(1, 0.1 * inch))
# Salary breakdown list
salary_breakdown = [
["Description", "Amount"],
["Base Monthly Salary", f"PKR {payroll_data['base_salary']:,.2f}"]
]
# Add Bonus if exists
if payroll_data.get('bonus', 0.0) > 0.0:
salary_breakdown.append(["Performance Bonus", f"PKR {payroll_data['bonus']:,.2f}"])
# Add Deductions if exists
if payroll_data.get('deductions', 0.0) > 0.0:
salary_breakdown.append(["Salary Advance / Deductions", f"-PKR {payroll_data['deductions']:,.2f}"])
# Final Net Payout row
salary_breakdown.append(["Net Payout", f"PKR {payroll_data['net_salary']:,.2f}"])
# Build ReportLab TableStyle dynamic ranges based on count of rows
total_rows = len(salary_breakdown)
table_style_cmd = [
# Header Row Styling
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#F8FAFC')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#475569')),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('ALIGN', (0, 0), (-1, 0), 'LEFT'),
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
('BOTTOMPADDING', (0, 0), (-1, 0), 10),
('TOPPADDING', (0, 0), (-1, 0), 10),
('LINEBELOW', (0, 0), (-1, 0), 1.5, colors.HexColor('#E2E8F0')),
]
# Content Rows Styling
for i in range(1, total_rows - 1):
is_deduction = "-" in salary_breakdown[i][1]
row_color = colors.HexColor('#EF4444') if is_deduction else colors.HexColor('#1E293B')
table_style_cmd.extend([
('FONTNAME', (0, i), (-1, i), 'Helvetica'),
('FONTSIZE', (0, i), (-1, i), 10),
('TEXTCOLOR', (0, i), (0, i), colors.HexColor('#1E293B')),
('TEXTCOLOR', (1, i), (1, i), row_color),
('ALIGN', (0, i), (0, i), 'LEFT'),
('ALIGN', (1, i), (1, i), 'RIGHT'),
('BOTTOMPADDING', (0, i), (-1, i), 10),
('TOPPADDING', (0, i), (-1, i), 10),
('LINEBELOW', (0, i), (-1, i), 0.5, colors.HexColor('#F1F5F9')),
])
# Total Row Styling (last row)
last_idx = total_rows - 1
table_style_cmd.extend([
('FONTNAME', (0, last_idx), (-1, last_idx), 'Helvetica-Bold'),
('FONTSIZE', (0, last_idx), (-1, last_idx), 12),
('TEXTCOLOR', (0, last_idx), (-1, last_idx), colors.HexColor('#6D28D9')),
('ALIGN', (0, last_idx), (0, last_idx), 'LEFT'),
('ALIGN', (1, last_idx), (1, last_idx), 'RIGHT'),
('BOTTOMPADDING', (0, last_idx), (-1, last_idx), 12),
('TOPPADDING', (0, last_idx), (-1, last_idx), 12),
('LINEABOVE', (0, last_idx), (-1, last_idx), 1.5, colors.HexColor('#E2E8F0')),
])
bill_table = Table(salary_breakdown, colWidths=[5 * inch, 2 * inch])
bill_table.setStyle(TableStyle(table_style_cmd))
story.append(bill_table)
doc.build(story)
return filepath