
Features: - User authentication with JWT - Client management with CRUD operations - Invoice generation and management - SQLite database with Alembic migrations - Detailed project documentation
184 lines
5.6 KiB
Python
184 lines
5.6 KiB
Python
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
from weasyprint import HTML
|
|
|
|
from app.core.config import settings
|
|
from app.core.logging import app_logger
|
|
from app.models.invoice import Invoice, InvoiceItem
|
|
from app.models.client import Client
|
|
from app.models.user import User
|
|
|
|
|
|
def generate_invoice_pdf(
|
|
invoice: Invoice,
|
|
items: List[InvoiceItem],
|
|
client: Client,
|
|
user: User,
|
|
) -> Path:
|
|
"""
|
|
Generate a PDF for an invoice and save it to the storage directory
|
|
"""
|
|
try:
|
|
# Create a unique filename
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
filename = f"invoice_{invoice.id}_{timestamp}.pdf"
|
|
file_path = settings.INVOICE_STORAGE_DIR / filename
|
|
|
|
# Make sure the directory exists
|
|
settings.INVOICE_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Generate HTML content for the invoice
|
|
html_content = _generate_invoice_html(invoice, items, client, user)
|
|
|
|
# Convert HTML to PDF
|
|
HTML(string=html_content).write_pdf(file_path)
|
|
|
|
app_logger.info(f"Generated PDF invoice: {file_path}")
|
|
return file_path
|
|
|
|
except Exception as e:
|
|
app_logger.error(f"Error generating PDF invoice: {str(e)}")
|
|
raise
|
|
|
|
|
|
def _generate_invoice_html(
|
|
invoice: Invoice,
|
|
items: List[InvoiceItem],
|
|
client: Client,
|
|
user: User,
|
|
) -> str:
|
|
"""
|
|
Generate HTML content for an invoice
|
|
"""
|
|
# Calculate totals
|
|
subtotal = sum(item.unit_price * item.quantity for item in items)
|
|
total = subtotal # Add tax calculations if needed
|
|
|
|
# Format dates
|
|
issued_date = invoice.issued_date.strftime("%Y-%m-%d") if invoice.issued_date else ""
|
|
due_date = invoice.due_date.strftime("%Y-%m-%d") if invoice.due_date else ""
|
|
|
|
# Basic HTML template for the invoice
|
|
return f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Invoice #{invoice.invoice_number}</title>
|
|
<style>
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
color: #333;
|
|
}}
|
|
.invoice-header {{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 30px;
|
|
}}
|
|
.invoice-title {{
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #2c3e50;
|
|
}}
|
|
.company-details, .client-details {{
|
|
margin-bottom: 20px;
|
|
}}
|
|
.invoice-meta {{
|
|
margin-bottom: 30px;
|
|
border: 1px solid #eee;
|
|
padding: 10px;
|
|
background-color: #f9f9f9;
|
|
}}
|
|
table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}}
|
|
th, td {{
|
|
padding: 10px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ddd;
|
|
}}
|
|
th {{
|
|
background-color: #f2f2f2;
|
|
}}
|
|
.totals {{
|
|
margin-top: 20px;
|
|
text-align: right;
|
|
}}
|
|
.total {{
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}}
|
|
.status {{
|
|
padding: 5px 10px;
|
|
border-radius: 3px;
|
|
display: inline-block;
|
|
color: white;
|
|
font-weight: bold;
|
|
}}
|
|
.draft {{ background-color: #f39c12; }}
|
|
.sent {{ background-color: #3498db; }}
|
|
.paid {{ background-color: #2ecc71; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="invoice-header">
|
|
<div>
|
|
<div class="invoice-title">INVOICE</div>
|
|
<div>#{invoice.invoice_number}</div>
|
|
</div>
|
|
<div>
|
|
<div class="status {invoice.status.lower()}">{invoice.status.upper()}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="company-details">
|
|
<h3>From:</h3>
|
|
<div>{user.full_name}</div>
|
|
<div>{user.email}</div>
|
|
<!-- Add more user/company details as needed -->
|
|
</div>
|
|
|
|
<div class="client-details">
|
|
<h3>To:</h3>
|
|
<div>{client.name}</div>
|
|
<div>{client.company_name or ""}</div>
|
|
<div>{client.email}</div>
|
|
<div>{client.address or ""}</div>
|
|
</div>
|
|
|
|
<div class="invoice-meta">
|
|
<div><strong>Invoice Date:</strong> {issued_date}</div>
|
|
<div><strong>Due Date:</strong> {due_date}</div>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Description</th>
|
|
<th>Quantity</th>
|
|
<th>Unit Price</th>
|
|
<th>Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{"".join(f"<tr><td>{item.description}</td><td>{item.quantity}</td><td>${item.unit_price:.2f}</td><td>${item.quantity * item.unit_price:.2f}</td></tr>" for item in items)}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="totals">
|
|
<div><strong>Subtotal:</strong> ${subtotal:.2f}</div>
|
|
<!-- Add tax calculations if needed -->
|
|
<div class="total"><strong>Total:</strong> ${total:.2f}</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 40px; font-size: 12px; color: #777; text-align: center;">
|
|
Thank you for your business!
|
|
</div>
|
|
</body>
|
|
</html>
|
|
""" |