""" PDF Invoice Generation Service """ from reportlab.lib.pagesizes import letter from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from reportlab.lib.enums import TA_RIGHT, TA_CENTER from datetime import datetime from typing import List, Dict, Any import io 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" )