""" PDF Invoice Generation Service """ import io from datetime import datetime from typing import Any from reportlab.lib import colors from reportlab.lib.pagesizes import letter from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import inch from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle def generate_invoice_pdf( invoice_number: str, client: dict[str, Any], time_entries: list[dict[str, Any]], hourly_rate: float = 100.0, company_name: str = "Your Company", company_address: str = "", company_email: str = "", company_phone: str = "", ) -> bytes: """ Generate PDF invoice from time entries Returns: PDF bytes """ buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=letter) elements = [] styles = getSampleStyleSheet() # Custom styles title_style = ParagraphStyle( "CustomTitle", parent=styles["Heading1"], fontSize=24, textColor=colors.HexColor("#1F2937"), spaceAfter=30, ) header_style = ParagraphStyle( "Header", parent=styles["Normal"], fontSize=10, textColor=colors.HexColor("#6B7280"), ) # Company Header elements.append(Paragraph(company_name, title_style)) if company_address: elements.append(Paragraph(company_address, header_style)) if company_email: elements.append(Paragraph(f"Email: {company_email}", header_style)) if company_phone: elements.append(Paragraph(f"Phone: {company_phone}", header_style)) elements.append(Spacer(1, 0.3 * inch)) # Invoice Title invoice_title = ParagraphStyle( "InvoiceTitle", parent=styles["Heading2"], fontSize=18, textColor=colors.HexColor("#4F46E5"), ) elements.append(Paragraph("INVOICE", invoice_title)) elements.append(Spacer(1, 0.2 * inch)) # Invoice Details invoice_date = datetime.now().strftime("%B %d, %Y") invoice_data = [ ["Invoice Number:", invoice_number], ["Invoice Date:", invoice_date], ["Client:", client.get("name", "N/A")], ] if client.get("company"): invoice_data.append(["Company:", client["company"]]) if client.get("email"): invoice_data.append(["Email:", client["email"]]) invoice_table = Table(invoice_data, colWidths=[2 * inch, 4 * inch]) invoice_table.setStyle( TableStyle( [ ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), ("FONTNAME", (1, 0), (1, -1), "Helvetica"), ("FONTSIZE", (0, 0), (-1, -1), 10), ("TEXTCOLOR", (0, 0), (0, -1), colors.HexColor("#374151")), ("TEXTCOLOR", (1, 0), (1, -1), colors.HexColor("#1F2937")), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("BOTTOMPADDING", (0, 0), (-1, -1), 8), ] ) ) elements.append(invoice_table) elements.append(Spacer(1, 0.4 * inch)) # Time Entries Table table_data = [["Date", "Description", "Hours", "Rate", "Amount"]] total_hours = 0 total_amount = 0 for entry in time_entries: hours = entry["duration_minutes"] / 60 rate = entry.get("hourly_rate", hourly_rate) amount = hours * rate total_hours += hours total_amount += amount date_str = entry["started_at"].strftime("%Y-%m-%d") if entry.get("started_at") else "N/A" desc = entry.get("description", "Work performed") table_data.append( [date_str, desc[:50] + "..." if len(desc) > 50 else desc, f"{hours:.2f}", f"${rate:.2f}", f"${amount:.2f}"] ) # Add totals row table_data.append(["", "Total", f"{total_hours:.2f}", "", f"${total_amount:.2f}"]) # Create table time_table = Table(table_data, colWidths=[1.2 * inch, 2.8 * inch, 0.8 * inch, 0.8 * inch, 1 * inch]) time_table.setStyle( TableStyle( [ # Header row ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#4F46E5")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, 0), 11), ("BOTTOMPADDING", (0, 0), (-1, 0), 12), # Data rows ("FONTNAME", (0, 1), (-1, -2), "Helvetica"), ("FONTSIZE", (0, 1), (-1, -2), 9), ("ROWBACKGROUNDS", (0, 1), (-1, -2), [colors.white, colors.HexColor("#F9FAFB")]), ("GRID", (0, 0), (-1, -2), 0.5, colors.HexColor("#E5E7EB")), # Total row ("BACKGROUND", (0, -1), (-1, -1), colors.HexColor("#F3F4F6")), ("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"), ("FONTSIZE", (0, -1), (-1, -1), 11), ("TOPPADDING", (0, -1), (-1, -1), 12), ("BOTTOMPADDING", (0, -1), (-1, -1), 12), # Alignment ("ALIGN", (2, 0), (-1, -1), "RIGHT"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ] ) ) elements.append(time_table) elements.append(Spacer(1, 0.5 * inch)) # Payment Terms terms_style = ParagraphStyle( "Terms", parent=styles["Normal"], fontSize=9, textColor=colors.HexColor("#6B7280"), ) elements.append(Paragraph("Payment Terms: Due within 30 days", terms_style)) elements.append(Spacer(1, 0.1 * inch)) elements.append(Paragraph("Thank you for your business!", terms_style)) # Build PDF doc.build(elements) pdf_bytes = buffer.getvalue() buffer.close() return pdf_bytes def generate_invoice_from_project( project_id: int, client: dict[str, Any], time_entries: list[dict[str, Any]], invoice_number: str = None ) -> bytes: """Generate invoice for a project""" if not invoice_number: invoice_number = f"INV-{datetime.now().strftime('%Y%m%d')}-{project_id}" return generate_invoice_pdf( invoice_number=invoice_number, client=client, time_entries=time_entries, company_name="Your OPC", company_address="123 Business St, City, State 12345", company_email="billing@youropc.com", company_phone="+1 (555) 123-4567", )