Have you ever wondered how receipt generators work?
Like the ones used in Nadia, Stop2Shop or even Mat-Ice.
Even before I became a techie, I always wondered how text written on the interface magically turned into a formatted receipt with the company's branding and all.
At that point in my life, I didn't question things so much. Because I believed that some things don't have answers, or was it because I didn't have much tech-savvy peeps around me then? I guess we'll never know.
Well... all that has changed now.
I'll be taking you through a journey of how to implement one for yourself. Who knows... You might own a restaurant, a game center, or any form of business that involves generating receipts someday. It would be nice if you could build yours and customize it the way you want.
So I'll describe what we will be building.
We'll be building an application that allows a user to input details of what they've purchased in a web interface and download it with a template.
I've been really curious about software architecture for quite a while... and I've come to realize that the best way to build anything is to have a top view of what it will entail. That will also guide us as we build the application.
Here's a picture of what the architecture should look like:
Don't mind the name... I'm very lazy with names.
I used Gemini to generate the image. These AI models are getting really good.
So.. let's get back on track.
We'll be using flask for the backend of our application, html and tailwind for the frontend of our application, sqlite will be used for the database.
The following steps will go smoothly for Linux users. For the windows(noob) users here, I'll be dropping some articles you can refer to. If you have WSL(Windows Subsystem for Linux), good. You can follow along.
Make sure you have python installed. If you don't, visit python.org.
So I'll explain the architecture briefly before we get started.
You can see that there are two arrows in front of the user. The first pointing at the user indicates that the interface is being rendered to the user, while the arrow facing away refers to the users putting the details of what he has purchased.
As for the arrow pointing away from the form. That indicates that the information that has been put in by the user is being send to the backend(Flask) for processing, this is where the magic happens. The major information gotten from the from is first stored in our sqlite database so we can retrieve it later. Then a folder is generated on our machine to store the pdf generated. Then the backend generates and html format of the receipt, this is then injected into an already styled template which contains my branding colors, logo and the likes, CSS helps with the styling of the template. Then a library is used to convert the html file to a pdf format that can be downloaded.
new term:
library: A library is basically a collection of code that is used so often that it's packaged so a user won't need to rewrite the code everytime they need that functionality. The process involved in converting an html file to pdf is quite lengthy. But thanks to the library we don't have to think about it.
As for the arrow pointing away from the database. The admin page gets all the information stored on the database and displays them for the user to see.
Let's get started.
Make sure you have vscode installed. If you dont, use the link: Vscode Download.
Open your terminal and create a directory and enter the directory:
mkdir receipt_generator
cd receipt_generator
Now create your virtual environment:
python3 -m venv .venv
Then activate it:
source .venv/bin/activate
You should see a circle beside your terminal prompt now. Like this:
(my path name is different because I used a different folder. Just search for the circle. This might not show if you're on windows though. It might work for powershell. But it should show for WSl and any Linux distro.)

new term:
virtual environment: This is basically an enclosed space for your project development. Think of it as a controlled area for you to do your stuff without any external influence, except the one you create.
Once you press enter and type ls -a you should see the .venv/ folder in your code. It is going to hold all the packages we plan to install.
Now let's install our tools of trade.
pip install flask flask-sqlalchemy weasyprint gunicorn
you could also do this:
Create a file and call it requirements.txt and put this:
Flask
Flask-SQLAlchemy
WeasyPrint
gunicorn
Then run:
pip install -r requirements.txt
The above command will install everything in the requirements.txt file.
I'll explain what each of them is used for.
flask - This is our backend tool. It is built with python and allows for quick prototyping of applications. So our pdf generation, database access will be through this guy.
flask-sqlalchemy - Now, ideally, when it comes to database stuff for CRUD(Create, Read, Write, Update) operations. SQL(Sequel Query Language) is usually the de facto. But it can be stressful to write at times and could cause alot of issues if it's not written by a professional... our popular SQL injection attack could affect our application if we're not careful. Another reason why it's not advisable is because of abstraction... if we were to use a database like MySQL or PostgreSQL... the code needed for either of them would be slightly different, and that could affect the flexibility of our choice of database. So we need something that could allow us to plug and use what we want, that is where an ORM(Object Relational Mapper) comes in. Not only does it help with abstraction, but another very important use is the way it allows querying of a database using object-like syntax.
Below is an example for you to see the difference:
Sample code in raw SQL and SQLAlchemy that allows a user to query all receipts where total_amount > 3000.
SQL Alchemy
receipts = Receipt.query.filter(Receipt.total_amount > 3000).all()
for r in receipts:
print(r.customer_name, r.total_amount)
Raw SQL
cursor.execute("SELECT customer_name, total_amount FROM receipts WHERE total_amount > ?", (3000,))
rows = cursor.fetchall()
for row in rows:
print(row[0], row[1])
If you ask me... I'd say that using RAW SQL feels like leaving the Object Oriented Programming realm for a different realm and coming back a couple of times. I've had enough of realms in One Piece. I just wanna write code, man.
Weasyprint - This is the library that is helping us do the grunt work of converting html + css to pdf. Arigatou weasyprint!! (makes otaku noises..).
Gunicorn - This is a production-ready server that is optimized to handle requests very efficiently. It is where our Python code will run; this server will allow it to accept requests and act as needed. There are normal development servers that can be used while testing, but they should only be used for testing... such as django runserver(for django which is another python backend framework) or flask run(for flask).
Let's continue:
Create a file called app.py in that folder and put this code inside. I'll explain shortly:
from flask import Flask, render_template, request, send_file
from weasyprint import HTML
import os
from datetime import datetime
from db import db, Document
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///autodoc.db'
app.config['UPLOAD_FOLDER'] = 'uploads'
# Initialize DB with app
db.init_app(app)
# Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/form')
def form_page():
return render_template('form.html')
@app.route('/generate', methods=['POST'])
def generate():
# 1. Get Form Data
data = {
"name": request.form.get('name'),
"email": request.form.get('email'),
"doc_type": request.form.get('doc_type'),
"amount": request.form.get('amount'),
"date": datetime.now().strftime("%Y-%m-%d")
}
# 2. Render HTML to String
rendered_html = render_template('pdf_template.html', **data)
# 3. Convert to PDF
filename = f"{data['name']}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
HTML(string=rendered_html).write_pdf(filepath)
# 4. Save to DB
new_doc = Document(
name=data['name'],
email=data['email'],
doc_type=data['doc_type'],
amount=data['amount'],
filename=filename
)
db.session.add(new_doc)
db.session.commit()
return send_file(filepath, as_attachment=True)
@app.route('/admin')
def admin():
documents = Document.query.order_by(Document.created_at.desc()).all()
return render_template('admin.html', documents=documents)
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Create another file named db.py and put this inside:
# db.py
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
class Document(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
email = db.Column(db.String(100))
doc_type = db.Column(db.String(50))
amount = db.Column(db.Float)
filename = db.Column(db.String(200))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
I'll explain:
-
from flask import Flask, render_template, request, send_file- Flask is our main backend buddy.
-
Flask→ creates our web app. -
render_template→ shows HTML pages to users. -
request→ catches data users send through forms. -
send_file→ lets users download files like PDFs.
-
from weasyprint import HTML- WeasyPrint is a magical library that turns HTML + CSS into a PDF.
- Without it, converting a web page to PDF would be a nightmare.
-
import os- OS helps us talk to the computer’s file system.
- Example: creating folders, saving files, checking paths.
-
from datetime import datetime- Helps us track dates and times.
- Useful for timestamps like “2026-03-31 17:00” on receipts.
-
from db import db, Document-
db→ our database handler (SQLite). -
Document→ blueprint of what info we want to store (name, email, file, etc.).
-
Setting Up Flask
-
app = Flask(__name__)- Creates the app instance. Think of it as booting up your engine.
-
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///autodoc.db'- Tells Flask: “Hey, we’re using SQLite and this is our database file.”
-
app.config['UPLOAD_FOLDER'] = 'uploads'- Sets a folder to store PDFs.
-
db.init_app(app)- Connects the database to the Flask app.
-
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)- Makes the upload folder if it doesn’t exist yet.
-
exist_ok=True→ avoids error if the folder is already there.
Routes – The User Entry Points
-
@app.route('/')→ index page- The home page everyone sees first.
-
@app.route('/form')→ form page- Where users input their info (name, email, amount, document type).
-
@app.route('/generate', methods=['POST'])→ PDF generation- Triggered when the user submits the form.
- Here’s the magic step-by-step:
- Get Form Data
* Collect user inputs from the form using `request.form.get('name')`, `email`, etc.
* Add current date with `datetime.now().strftime("%Y-%m-%d")`.
- Render HTML to String
* `render_template('pdf_template.html', **data)` → injects form data into our styled HTML.
* Think: your info + pre-made receipt template = ready-to-go document.
- Convert HTML to PDF
* WeasyPrint takes the rendered HTML and writes a PDF file.
* Filename is dynamic: `name_20260331170000.pdf` (prevents overwriting old PDFs).
- Save to Database
* Create a new `Document` record.
* `db.session.add(new_doc)` → stage the data.
* `db.session.commit()` → save it permanently.
- Send PDF to User
* `send_file(filepath, as_attachment=True)` → lets user download it immediately.
-
@app.route('/admin')→ admin page- Fetches all documents from the database.
-
Document.query.order_by(Document.created_at.desc()).all()→ shows newest first. - Rendered in
admin.htmlso you can review all receipts.
Database Setup – db.py
-
db = SQLAlchemy()- The connection manager for all database stuff.
-
class Document(db.Model)- Blueprint of our table. Columns include:
-
id→ unique identifier for each document. -
name,email→ who the receipt is for. -
doc_type→ Invoice, Ticket, Certificate… -
amount→ money involved. -
filename→ PDF file name. -
created_at→ timestamp of creation.
-
Default values
-
default=datetime.utcnow→ if you forget the date, auto-fills with now.
-
Running the App
-
if __name__ == '__main__':- Standard Python check to see if this file is being run directly.
-
db.create_all()- Creates database tables if they don’t exist yet.
-
app.run(debug=True)- Launches Flask development server.
-
debug=True→ shows errors in real-time (helpful while coding).
Newbie Notes / Terms
-
Library
- Pre-written code you can reuse (WeasyPrint is one).
- Saves you hours of rewriting the same stuff.
-
Virtual Environment
- Isolated workspace for your project.
- Keeps dependencies separate from your system Python.
-
ORM (Object Relational Mapper)
- Lets you talk to databases using Python objects instead of raw SQL.
- Cleaner, safer, and database-agnostic.
-
CRUD
- Create, Read, Update, Delete → main actions you do with data.
Create a folder named templates and create 5 files, viz:
admin.html
base.html
form.html
index.html
pdf_template.html
admin.html
{% extends "base.html" %}
{% block content %}
<div class="py-10">
<div class="flex justify-between items-end mb-8">
<div>
<h1 class="font-bold text-slate-900 text-2xl">Document Archive</h1>
<p class="text-slate-500 text-sm">Review and manage all generated assets.</p>
</div>
</div>
<div class="bg-white shadow-sm border border-slate-200 rounded-2xl overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50 border-slate-200 border-b">
<th class="p-4 font-bold text-slate-500 text-xs uppercase tracking-widest">Client</th>
<th class="hidden sm:table-cell p-4 font-bold text-slate-500 text-xs uppercase tracking-widest">
Type</th>
<th class="p-4 font-bold text-slate-500 text-xs uppercase tracking-widest">Amount</th>
<th class="p-4 font-bold text-slate-500 text-xs text-right uppercase tracking-widest">Action
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for doc in documents %}
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="p-4">
<div class="font-semibold text-slate-800">{{ doc.name }}</div>
<div class="sm:hidden text-slate-400 text-xs">{{ doc.doc_type }}</div>
</td>
<td class="hidden sm:table-cell p-4 text-slate-600 text-sm">
<span class="bg-slate-100 px-2 py-1 rounded font-medium text-xs">{{ doc.doc_type }}</span>
</td>
<td class="p-4 font-bold text-indigo-600 text-sm">${{ "{:,.2f}".format(doc.amount) }}</td>
<td class="p-4 text-right">
<a href="/static/uploads/{{ doc.filename }}" download
class="inline-flex items-center bg-white hover:bg-slate-50 px-3 py-1.5 border border-slate-200 hover:border-slate-300 rounded-lg font-semibold text-slate-700 text-xs transition">
<svg class="mr-1 w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
PDF
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AutoDoc | Automation Pro</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
</style>
</head>
<body class="bg-slate-50 text-slate-900">
<nav class="top-0 z-50 sticky bg-white/80 backdrop-blur-md border-slate-200 border-b">
<div class="mx-auto px-4 sm:px-6 max-w-5xl">
<div class="flex justify-between items-center py-4">
<a href="/" class="flex items-center space-x-2">
<div class="flex justify-center items-center bg-indigo-600 rounded-lg w-8 h-8 font-bold text-white">
A</div>
<span class="font-bold text-slate-800 text-xl tracking-tight">AutoDoc</span>
</a>
<div class="hidden md:flex items-center space-x-8 font-medium text-sm">
<a href="/form" class="text-slate-600 hover:text-indigo-600 transition">Create New</a>
<a href="/admin" class="text-slate-600 hover:text-indigo-600 transition">Dashboard</a>
<a href="/form"
class="bg-indigo-600 hover:bg-indigo-700 shadow-sm px-4 py-2 rounded-full text-white transition">Get
Started</a>
</div>
<div class="md:hidden">
<a href="/admin" class="p-2 text-slate-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</a>
</div>
</div>
</div>
</nav>
<main class="mx-auto px-4 sm:px-6 max-w-5xl min-h-screen">
{% block content %}{% endblock %}
</main>
<footer class="bg-white mt-20 border-slate-200 border-t">
<div class="mx-auto px-4 py-12 max-w-5xl text-center">
<p class="text-slate-400 text-sm">Designed for Professional Automation Workflows</p>
</div>
</footer>
</body>
</html>
form.html
{% extends "base.html" %}
{% block content %}
<div class="flex justify-center py-12">
<div class="w-full max-w-xl">
<div class="mb-10 text-center">
<h1 class="font-bold text-slate-900 text-3xl">Document Generator</h1>
<p class="mt-2 text-slate-500">Fill in the details below to generate your secured PDF.</p>
</div>
<form action="/generate" method="POST"
class="bg-white shadow-slate-200/50 shadow-xl p-6 sm:p-10 border border-slate-100 rounded-2xl">
<div class="space-y-6">
<div class="group">
<label class="block mb-2 font-semibold text-slate-500 text-xs uppercase tracking-wider">Recipient
Name</label>
<input type="text" name="name" placeholder="John Doe" required
class="bg-slate-50 p-4 border-0 rounded-xl outline-none ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 w-full transition">
</div>
<div class="group">
<label class="block mb-2 font-semibold text-slate-500 text-xs uppercase tracking-wider">Email
Address</label>
<input type="email" name="email" placeholder="john@example.com" required
class="bg-slate-50 p-4 border-0 rounded-xl outline-none ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 w-full transition">
</div>
<div class="gap-6 grid grid-cols-1 sm:grid-cols-2">
<div>
<label
class="block mb-2 font-semibold text-slate-500 text-xs uppercase tracking-wider">Type</label>
<select name="doc_type"
class="bg-slate-50 p-4 border-0 rounded-xl outline-none ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 w-full appearance-none cursor-pointer">
<option value="Invoice">Invoice</option>
<option value="Event Ticket">Event Ticket</option>
<option value="Certificate">Certificate</option>
</select>
</div>
<div>
<label class="block mb-2 font-semibold text-slate-500 text-xs uppercase tracking-wider">Amount
($)</label>
<input type="number" name="amount" step="0.01" placeholder="0.00"
class="bg-slate-50 p-4 border-0 rounded-xl outline-none ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 w-full transition">
</div>
</div>
<button type="submit"
class="bg-slate-900 hover:bg-indigo-600 shadow-indigo-200 shadow-lg py-4 rounded-xl w-full font-bold text-white active:scale-[0.98] transition-all transform">
Generate & Download PDF
</button>
</div>
</form>
</div>
</div>
{% endblock %}
index.html
{% extends "base.html" %}
{% block content %}
<div class="py-20 text-center">
<h1 class="mb-6 font-extrabold text-gray-900 text-5xl">
Generate Professional Documents <span class="text-indigo-600">Instantly</span>.
</h1>
<p class="mx-auto mb-10 max-w-2xl text-gray-600 text-xl">
Automate your business workflow. Convert form data into beautiful,
print-ready PDFs for invoices, certificates, and tickets.
</p>
<div class="flex justify-center space-x-4">
<a href="/form"
class="bg-indigo-600 hover:bg-indigo-700 px-8 py-3 rounded-lg font-semibold text-white transition">
Start Generating
</a>
<a href="/admin"
class="bg-white hover:bg-gray-50 px-8 py-3 border border-gray-300 rounded-lg font-semibold transition">
View Admin Logs
</a>
</div>
</div>
{% endblock %}
pdf_template.html
<!DOCTYPE html>
<html>
<head>
<style>
@page {
margin: 0;
}
body {
font-family: 'Helvetica', sans-serif;
color: #1e293b;
margin: 0;
padding: 0;
}
.sidebar {
background: #4f46e5;
width: 10px;
height: 100%;
position: absolute;
}
.container {
padding: 60px;
}
.header {
display: flex;
justify-content: space-between;
border-bottom: 2px solid #f1f5f9;
padding-bottom: 20px;
}
.badge {
background: #eef2ff;
color: #4338ca;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.title {
font-size: 32px;
font-weight: bold;
color: #111827;
margin: 10px 0;
}
.meta-table {
width: 100%;
margin-top: 40px;
border-collapse: collapse;
}
.meta-table th {
text-align: left;
color: #64748b;
font-size: 10px;
text-transform: uppercase;
padding-bottom: 8px;
}
.meta-table td {
font-size: 16px;
padding-bottom: 20px;
}
.total-box {
margin-top: 60px;
background: #f8fafc;
padding: 30px;
border-radius: 12px;
text-align: right;
}
.total-label {
font-size: 14px;
color: #64748b;
}
.total-amount {
font-size: 36px;
font-weight: bold;
color: #4f46e5;
}
</style>
</head>
<body>
<div class="sidebar"></div>
<div class="container">
<div class="header">
<div>
<span class="badge">OFFICIAL {{ doc_type | upper }}</span>
<div class="title">AutoDoc System</div>
</div>
<div style="text-align: right; color: #94a3b8; font-size: 12px;">
Issued: {{ date }}<br>
Ref: #AD-{{ range(1000, 9999) | random }}
</div>
</div>
<table class="meta-table">
<tr>
<th>Issued To</th>
<th>Contact Email</th>
</tr>
<tr>
<td><strong>{{ name }}</strong></td>
<td>{{ email }}</td>
</tr>
</table>
<div class="total-box">
<div class="total-label">Total Amount Payable</div>
<div class="total-amount">${{ "{:,.2f}".format(amount|float) }}</div>
</div>
<div style="margin-top: 100px; font-size: 10px; color: #cbd5e1; text-align: center;">
This is a computer-generated document. No signature is required.
</div>
</div>
</body>
</html>
now run:
python3 app.py
This will run the application. You should see something like this:
Now open the link shown there in your brower:
This should come up:
That's all.
Watch out for the next article where I teach how to deploy it to a VPS with nginx and docker.
After this you should be able to share it with your friends so they can test it for you.



Top comments (0)