232 lines
8.6 KiB
Python
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 |