Spaces:
Sleeping
Sleeping
| 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 |