Generate a PDF Invoice from a Web Form with Flask + WeasyPrint
Generate a PDF Invoice from a Web Form with Flask + WeasyPrint
If you already collect invoice details in a simple web form, the next step is usually: generate a clean PDF and download it. This Flask + WeasyPrint setup does exactly that. User submits a form, Flask renders an HTML invoice template, and WeasyPrint converts that HTML into a PDF you can download right away.
Typical use cases
- Quick internal invoices (freelance, side projects, small business)
- Generate PDFs from structured form data (quotes, receipts, reports)
- Create consistent PDFs using HTML/CSS instead of “drawing” PDFs manually
Install
pip install flask weasyprint
Note: WeasyPrint may require OS-level libraries depending on your system (common on Linux). If install fails, check the WeasyPrint docs for your OS.
Improved script (safer, cleaner, returns PDF download)
Changes from the original:
- Returns the PDF as a download instead of saving a fixed
invoice.pdfon disk - Basic input cleanup + allowlist fields (avoid passing everything blindly into the template)
- Uses in-memory PDF (
BytesIO) - Optional stylesheet support
- Better error handling and production-friendly defaults
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from typing import Dict
from flask import Flask, render_template, request, send_file, abort
from weasyprint import HTML, CSS
app = Flask(__name__)
# Optional: basic request size limit (helps avoid giant form posts)
app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 # 1 MB
BASE_DIR = Path(__file__).resolve().parent
INVOICE_CSS = BASE_DIR / "static" / "invoice.css"
# Only accept the fields you expect (avoid dumping all request.form into templates)
ALLOWED_FIELDS = {
"invoice_number",
"invoice_date",
"bill_to_name",
"bill_to_email",
"bill_to_address",
"notes",
"subtotal",
"tax",
"total",
}
def clean_form_data(form) -> Dict[str, str]:
"""
Keep only allowed keys and do light cleanup.
You can replace this with WTForms/Pydantic if you want stronger validation.
"""
data: Dict[str, str] = {}
for k in ALLOWED_FIELDS:
if k in form:
# Strip whitespace and keep it simple
data[k] = form.get(k, "").strip()
# Minimal required fields (adjust to your needs)
if not data.get("invoice_number") or not data.get("bill_to_name"):
abort(400, description="Missing required fields: invoice_number and bill_to_name")
return data
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "GET":
return render_template("form.html")
# POST
data = clean_form_data(request.form)
# Render invoice HTML from a template
# IMPORTANT: do not mark user input as |safe in Jinja templates
html_str = render_template("invoice.html", data=data)
# Build WeasyPrint HTML and generate PDF bytes in memory
# base_url helps resolve relative paths (images, etc.) if needed.
html = HTML(string=html_str, base_url=str(BASE_DIR))
stylesheets = []
if INVOICE_CSS.exists():
stylesheets.append(CSS(filename=str(INVOICE_CSS)))
pdf_bytes = html.write_pdf(stylesheets=stylesheets)
# Nice filename for the download
filename = f"invoice-{data['invoice_number']}.pdf"
return send_file(
BytesIO(pdf_bytes),
mimetype="application/pdf",
as_attachment=True,
download_name=filename,
)
if __name__ == "__main__":
# Debug off by default for safety; enable only while developing locally
app.run(host="127.0.0.1", port=5000, debug=False)
Suggested folder structure
your_project/
app.py
templates/
form.html
invoice.html
static/
invoice.css (optional)
Minimal templates (examples)
templates/form.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice Form</title>
</head>
<body>
<h1>Create Invoice</h1>
<form method="post">
<label>Invoice #</label><br>
<input name="invoice_number" required><br><br>
<label>Invoice date</label><br>
<input name="invoice_date" placeholder="2026-02-05"><br><br>
<label>Bill to name</label><br>
<input name="bill_to_name" required><br><br>
<label>Bill to email</label><br>
<input name="bill_to_email"><br><br>
<label>Address</label><br>
<textarea name="bill_to_address"></textarea><br><br>
<label>Subtotal</label><br>
<input name="subtotal"><br><br>
<label>Tax</label><br>
<input name="tax"><br><br>
<label>Total</label><br>
<input name="total"><br><br>
<label>Notes</label><br>
<textarea name="notes"></textarea><br><br>
<button type="submit">Generate PDF</button>
</form>
</body>
</html>
templates/invoice.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice {{ data.invoice_number }}</title>
</head>
<body>
<h1>Invoice</h1>
<p><strong>Invoice #:</strong> {{ data.invoice_number }}</p>
<p><strong>Date:</strong> {{ data.invoice_date }}</p>
<h3>Bill To</h3>
<p>{{ data.bill_to_name }}</p>
<p>{{ data.bill_to_email }}</p>
<p style="white-space: pre-line;">{{ data.bill_to_address }}</p>
<hr>
<p><strong>Subtotal:</strong> {{ data.subtotal }}</p>
<p><strong>Tax:</strong> {{ data.tax }}</p>
<p><strong>Total:</strong> {{ data.total }}</p>
<hr>
<p style="white-space: pre-line;">{{ data.notes }}</p>
</body>
</html>
Run it
python app.py
Open: http://127.0.0.1:5000, fill the form, and you’ll get a PDF download.
Small production notes
- Don’t run Flask’s built-in server in production. Use a WSGI server (gunicorn/uwsgi) behind a reverse proxy.
- Validate numeric fields (subtotal/tax/total) if you rely on them for calculations.
- If you store PDFs, generate unique filenames and keep them outside your code folder.
Comments
Post a Comment