import io from reportlab.lib.pagesizes import letter from reportlab.lib import colors from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from sqlalchemy.orm import Session from app.models.invoice import Invoice def generate_invoice_pdf(db: Session, invoice: Invoice) -> bytes: """ Generate a PDF document for an invoice. Args: db: Database session invoice: Invoice model instance Returns: Bytes containing the PDF content """ # Create a BytesIO buffer to receive the PDF data buffer = io.BytesIO() # Create the PDF document using ReportLab doc = SimpleDocTemplate( buffer, pagesize=letter, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=72 ) # Container for PDF elements elements = [] # Get styles styles = getSampleStyleSheet() header_style = styles["Heading1"] subheader_style = styles["Heading2"] normal_style = styles["Normal"] # Custom styles label_style = ParagraphStyle( "label", parent=normal_style, fontName="Helvetica-Bold", fontSize=10, ) # Add invoice header elements.append(Paragraph(f"INVOICE #{invoice.invoice_number}", header_style)) elements.append(Spacer(1, 0.25 * inch)) # Add organization and client information org = invoice.organization client = invoice.client # Date information date_data = [ [Paragraph("Issue Date:", label_style), Paragraph(invoice.issue_date.strftime("%Y-%m-%d"), normal_style)], [Paragraph("Due Date:", label_style), Paragraph(invoice.due_date.strftime("%Y-%m-%d"), normal_style)], [Paragraph("Status:", label_style), Paragraph(invoice.status.value.upper(), normal_style)], ] date_table = Table(date_data, colWidths=[1.5 * inch, 2 * inch]) date_table.setStyle(TableStyle([ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("ALIGN", (0, 0), (0, -1), "RIGHT"), ("TOPPADDING", (0, 0), (-1, -1), 1), ("BOTTOMPADDING", (0, 0), (-1, -1), 1), ])) # Organization information org_data = [ [Paragraph("From:", label_style)], [Paragraph(org.name, normal_style)], ] if org.address: org_data.append([Paragraph(org.address, normal_style)]) city_state_zip = [] if org.city: city_state_zip.append(org.city) if org.state: city_state_zip.append(org.state) if org.postal_code: city_state_zip.append(org.postal_code) if city_state_zip: org_data.append([Paragraph(", ".join(city_state_zip), normal_style)]) if org.country: org_data.append([Paragraph(org.country, normal_style)]) if org.phone: org_data.append([Paragraph(f"Phone: {org.phone}", normal_style)]) if org.email: org_data.append([Paragraph(f"Email: {org.email}", normal_style)]) if org.tax_id: org_data.append([Paragraph(f"Tax ID: {org.tax_id}", normal_style)]) org_table = Table(org_data, colWidths=[3 * inch]) org_table.setStyle(TableStyle([ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 1), ("BOTTOMPADDING", (0, 0), (-1, -1), 1), ])) # Client information client_data = [ [Paragraph("Bill To:", label_style)], [Paragraph(client.name, normal_style)], ] if client.contact_name: client_data.append([Paragraph(f"Attn: {client.contact_name}", normal_style)]) if client.address: client_data.append([Paragraph(client.address, normal_style)]) client_city_state_zip = [] if client.city: client_city_state_zip.append(client.city) if client.state: client_city_state_zip.append(client.state) if client.postal_code: client_city_state_zip.append(client.postal_code) if client_city_state_zip: client_data.append([Paragraph(", ".join(client_city_state_zip), normal_style)]) if client.country: client_data.append([Paragraph(client.country, normal_style)]) if client.email: client_data.append([Paragraph(f"Email: {client.email}", normal_style)]) if client.phone: client_data.append([Paragraph(f"Phone: {client.phone}", normal_style)]) client_table = Table(client_data, colWidths=[3 * inch]) client_table.setStyle(TableStyle([ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 1), ("BOTTOMPADDING", (0, 0), (-1, -1), 1), ])) # Combine the top tables top_data = [[date_table, "", ""]] top_table = Table(top_data, colWidths=[2.5 * inch, 0.5 * inch, 3 * inch]) elements.append(top_table) elements.append(Spacer(1, 0.25 * inch)) # Combine organization and client tables address_data = [[org_table, client_table]] address_table = Table(address_data, colWidths=[3 * inch, 3 * inch]) address_table.setStyle(TableStyle([ ("VALIGN", (0, 0), (-1, -1), "TOP"), ])) elements.append(address_table) elements.append(Spacer(1, 0.5 * inch)) # Invoice items elements.append(Paragraph("Invoice Items", subheader_style)) elements.append(Spacer(1, 0.25 * inch)) # Create data for the items table items_data = [ [ Paragraph("Description", label_style), Paragraph("Quantity", label_style), Paragraph("Unit Price", label_style), Paragraph("Amount", label_style), ] ] for item in invoice.items: items_data.append([ Paragraph(item.description, normal_style), Paragraph(f"{item.quantity:.2f}", normal_style), Paragraph(f"${item.unit_price:.2f}", normal_style), Paragraph(f"${item.amount:.2f}", normal_style), ]) # Create the items table items_table = Table(items_data, colWidths=[3.5 * inch, 1 * inch, 1 * inch, 1 * inch]) items_table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("ALIGN", (1, 0), (-1, -1), "RIGHT"), ("GRID", (0, 0), (-1, -1), 0.5, colors.grey), ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ])) elements.append(items_table) elements.append(Spacer(1, 0.25 * inch)) # Summary table (subtotal, tax, discount, total) summary_data = [] if invoice.subtotal: summary_data.append([ "", Paragraph("Subtotal:", label_style), Paragraph(f"${invoice.subtotal:.2f}", normal_style), ]) if invoice.tax_rate: summary_data.append([ "", Paragraph(f"Tax ({invoice.tax_rate:.2f}%):", label_style), Paragraph(f"${invoice.tax_amount:.2f}", normal_style), ]) if invoice.discount: summary_data.append([ "", Paragraph("Discount:", label_style), Paragraph(f"${invoice.discount:.2f}", normal_style), ]) # Always include total summary_data.append([ "", Paragraph("Total:", ParagraphStyle( "total", parent=label_style, fontSize=12, )), Paragraph(f"${invoice.total:.2f}", ParagraphStyle( "total_amount", parent=normal_style, fontSize=12, fontName="Helvetica-Bold", )), ]) summary_table = Table(summary_data, colWidths=[3.5 * inch, 1.5 * inch, 1.5 * inch]) summary_table.setStyle(TableStyle([ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("ALIGN", (1, 0), (1, -1), "RIGHT"), ("ALIGN", (2, 0), (2, -1), "RIGHT"), ("TOPPADDING", (0, 0), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 3), ("LINEABOVE", (1, -1), (2, -1), 1, colors.black), ])) elements.append(summary_table) elements.append(Spacer(1, 0.5 * inch)) # Notes if invoice.notes: elements.append(Paragraph("Notes:", label_style)) elements.append(Paragraph(invoice.notes, normal_style)) elements.append(Spacer(1, 0.25 * inch)) # Terms if invoice.terms: elements.append(Paragraph("Terms and Conditions:", label_style)) elements.append(Paragraph(invoice.terms, normal_style)) # Build the PDF doc.build(elements) # Get the PDF content from the buffer pdf_content = buffer.getvalue() buffer.close() return pdf_content