from io import BytesIO from reportlab.lib.pagesizes import letter from reportlab.lib.units import inch from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib import colors from app.models.invoice import Invoice class PDFGenerator: def generate_invoice_pdf(self, invoice: Invoice) -> BytesIO: buffer = BytesIO() doc = SimpleDocTemplate(buffer, pagesize=letter) styles = getSampleStyleSheet() story = [] title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=24, spaceAfter=30, alignment=TA_CENTER, ) header_style = ParagraphStyle( 'Header', parent=styles['Normal'], fontSize=12, spaceAfter=6, ) story.append(Paragraph("INVOICE", title_style)) story.append(Spacer(1, 20)) invoice_info = [ ["Invoice Number:", invoice.invoice_number], ["Issue Date:", invoice.issue_date.strftime("%Y-%m-%d")], ["Due Date:", invoice.due_date.strftime("%Y-%m-%d")], ["Status:", invoice.status.title()], ] invoice_table = Table(invoice_info, colWidths=[2*inch, 3*inch]) invoice_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ])) story.append(invoice_table) story.append(Spacer(1, 20)) if invoice.user.company_name: story.append(Paragraph(f"From: {invoice.user.company_name}", header_style)) story.append(Paragraph(f"Billed by: {invoice.user.full_name}", header_style)) story.append(Paragraph(f"Email: {invoice.user.email}", header_style)) story.append(Spacer(1, 10)) story.append(Paragraph("Bill To:", header_style)) story.append(Paragraph(f"Customer: {invoice.customer.name}", header_style)) story.append(Paragraph(f"Email: {invoice.customer.email}", header_style)) if invoice.customer.address: story.append(Paragraph(f"Address: {invoice.customer.address}", header_style)) if invoice.customer.city: story.append(Paragraph(f"City: {invoice.customer.city}", header_style)) if invoice.customer.country: story.append(Paragraph(f"Country: {invoice.customer.country}", header_style)) story.append(Spacer(1, 20)) if invoice.items: items_data = [["Description", "Quantity", "Unit Price", "Total"]] for item in invoice.items: items_data.append([ item.description, str(item.quantity), f"${item.unit_price:.2f}", f"${item.total_price:.2f}" ]) items_table = Table(items_data, colWidths=[3*inch, 1*inch, 1.5*inch, 1.5*inch]) items_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.grey), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('ALIGN', (1, 1), (-1, -1), 'RIGHT'), ('ALIGN', (0, 1), (0, -1), 'LEFT'), ])) story.append(items_table) story.append(Spacer(1, 20)) totals_data = [ ["Subtotal:", f"${invoice.subtotal:.2f}"], [f"Tax ({invoice.tax_rate}%):", f"${invoice.tax_amount:.2f}"], ["Total:", f"${invoice.total_amount:.2f}"], ] totals_table = Table(totals_data, colWidths=[4*inch, 2*inch]) totals_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'RIGHT'), ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ('LINEBELOW', (0, -1), (-1, -1), 2, colors.black), ])) story.append(totals_table) if invoice.notes: story.append(Spacer(1, 20)) story.append(Paragraph("Notes:", header_style)) story.append(Paragraph(invoice.notes, styles['Normal'])) doc.build(story) buffer.seek(0) return buffer pdf_generator = PDFGenerator()