Automated Action c8aed27755 Create SaaS invoicing application with FastAPI and SQLite
- Set up project structure with modular organization
- Implement database models for users, organizations, clients, invoices
- Create Alembic migration scripts for database setup
- Implement JWT-based authentication and authorization
- Create API endpoints for users, organizations, clients, invoices
- Add PDF generation for invoices using ReportLab
- Add comprehensive documentation in README
2025-06-06 11:21:11 +00:00

273 lines
8.6 KiB
Python

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