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