232 lines
8.6 KiB
Python

from datetime import datetime
from pathlib import Path
from typing import Optional
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
from app.models.invoice import Invoice
class InvoiceGenerator:
"""Service for generating invoice PDFs."""
def __init__(self):
# Set up templates directory
self.templates_dir = Path(__file__).parent.parent / "templates"
self.templates_dir.mkdir(exist_ok=True)
# Set up output directory
self.output_dir = Path("/app/storage/invoices")
self.output_dir.mkdir(parents=True, exist_ok=True)
# Set up Jinja2 environment
self.env = Environment(
loader=FileSystemLoader(str(self.templates_dir)),
autoescape=True
)
def _ensure_template_exists(self):
"""Ensure the invoice template exists."""
template_path = self.templates_dir / "invoice.html"
if not template_path.exists():
# Create a simple invoice template
template_content = """
<!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 {
margin-bottom: 30px;
}
.invoice-header h1 {
color: #2c3e50;
}
.row {
display: flex;
margin-bottom: 20px;
}
.col {
flex: 1;
}
.invoice-details {
margin-bottom: 30px;
}
.invoice-details h3 {
margin-bottom: 10px;
color: #2c3e50;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f5f5f5;
}
.text-right {
text-align: right;
}
.totals {
margin-left: auto;
width: 300px;
}
.totals table {
margin-bottom: 0;
}
.footer {
margin-top: 50px;
text-align: center;
color: #7f8c8d;
font-size: 12px;
}
</style>
</head>
<body>
<div class="invoice-header">
<h1>INVOICE</h1>
</div>
<div class="row">
<div class="col">
<div class="invoice-details">
<h3>From</h3>
<p>{{ user.company_name or user.full_name }}</p>
<p>{{ user.address }}</p>
<p>{{ user.email }}</p>
<p>{{ user.phone_number }}</p>
</div>
</div>
<div class="col">
<div class="invoice-details">
<h3>To</h3>
<p>{{ customer.name }}</p>
<p>{{ customer.address }}</p>
<p>{{ customer.email }}</p>
<p>{{ customer.phone }}</p>
</div>
</div>
<div class="col">
<div class="invoice-details">
<h3>Invoice Details</h3>
<p><strong>Invoice Number:</strong> {{ invoice.invoice_number }}</p>
<p><strong>Issue Date:</strong> {{ invoice.issue_date.strftime('%d %b %Y') }}</p>
<p><strong>Due Date:</strong> {{ invoice.due_date.strftime('%d %b %Y') }}</p>
<p><strong>Status:</strong> {{ invoice.status.value.upper() }}</p>
</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Tax Rate</th>
<th>Tax Amount</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{% for item in invoice.items %}
<tr>
<td>{{ item.description }}</td>
<td>{{ item.quantity }}</td>
<td>${{ "%.2f"|format(item.unit_price) }}</td>
<td>{{ "%.2f"|format(item.tax_rate) }}%</td>
<td>${{ "%.2f"|format(item.tax_amount) }}</td>
<td>${{ "%.2f"|format(item.total) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="totals">
<table>
<tr>
<td>Subtotal</td>
<td class="text-right">${{ "%.2f"|format(invoice.subtotal) }}</td>
</tr>
<tr>
<td>Tax</td>
<td class="text-right">${{ "%.2f"|format(invoice.tax_amount) }}</td>
</tr>
<tr>
<td>Discount</td>
<td class="text-right">${{ "%.2f"|format(invoice.discount) }}</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td class="text-right"><strong>${{ "%.2f"|format(invoice.total) }}</strong></td>
</tr>
</table>
</div>
{% if invoice.notes %}
<div class="notes">
<h3>Notes</h3>
<p>{{ invoice.notes }}</p>
</div>
{% endif %}
{% if invoice.terms %}
<div class="terms">
<h3>Terms & Conditions</h3>
<p>{{ invoice.terms }}</p>
</div>
{% endif %}
<div class="footer">
<p>Invoice generated on {{ now.strftime('%d %b %Y %H:%M:%S') }}</p>
</div>
</body>
</html>
"""
with open(template_path, "w") as f:
f.write(template_content)
def generate_invoice_pdf(self, invoice: Invoice) -> Optional[str]:
"""Generate a PDF invoice."""
try:
self._ensure_template_exists()
# Get template
template = self.env.get_template("invoice.html")
# Prepare context
context = {
"invoice": invoice,
"user": invoice.user,
"customer": invoice.customer,
"now": datetime.now()
}
# Render HTML
html_content = template.render(**context)
# Generate PDF filename
filename = f"invoice_{invoice.invoice_number}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
output_path = self.output_dir / filename
# Generate PDF
HTML(string=html_content).write_pdf(str(output_path))
return str(output_path)
except Exception as e:
# Log error
print(f"Error generating invoice PDF: {e}")
return None