GST Invoice Maker for a Small Shop
Turn a day's raw transactions into a tax invoice grouped by GST rate.
1Introduction: the problem it solves
Walk into any small shop and ask how the GST return gets filed. In most cases the answer is a notebook, a calculator and an evening lost at the end of every month. The shopkeeper writes each sale down, and then has to sort those sales by tax rate before anything can be claimed.
The sorting is the part a computer should be doing. The shop already has the data — it is just in the wrong shape.
A kirana shop, a stationery shop, or any small trader who files GST returns.
Why it is worth computerising
The work being replaced is not difficult; it is repetitive, and that is what makes it error-prone. Every sale has to be written down, every written line has to be found again at the end of the month, and each one has to be put under the right tax rate before a single figure can be claimed. A shop doing forty sales a day is sorting twelve hundred lines by hand every month.
A mistake in that sorting is not caught by anybody. The total still looks like a total. It is only questioned if the return is scrutinised, by which time the mistake is months old. A program does the same sorting the same way every time, and takes a second over it, which is why this job is worth computerising even in a shop that is otherwise entirely on paper.
Objectives
- To replace the shopkeeper's handwritten day book with a file the computer can read
- To group a day's sales by GST rate automatically, which is the form the return demands
- To calculate the tax under each rate and the grand total without arithmetic by hand
- To produce a printable invoice that can be handed to a customer or filed for the return
- To keep the tax rates in one place, so a change in the law is a one-line edit
2How the job is done today
Before writing anything it is worth asking how the work is handled at present, and why each of those answers falls short. These were examined:
What most small shops actually use. It costs nothing and works, but the month-end sorting by tax rate is slow and a single mis-added column changes what is claimed.
Full-featured, and priced and shaped for shops much larger than a corner kirana. Most of what it does is never used, and the shopkeeper must learn its way of working.
A real step up, and common. It breaks down when the categories change or a formula is dragged one row short, and it cannot produce a formatted invoice by itself.
3Functionalities
- Reads the day's sales from a plain CSV file the shopkeeper can type in any spreadsheet
- Looks up the GST rate for each item's category (5%, 12% or 18%)
- Groups the taxable value under each rate, which is exactly how the return wants it
- Works out CGST/SGST totals and the grand total
- Writes a printable invoice to a text file, not just to the screen
The functions that provide them
| Function | Arguments | What it does |
|---|---|---|
read_sales() | filename | Read the day's raw transactions from a CSV file. |
group_by_rate() | rows | Add up the taxable value under each GST rate. |
write_invoice() | rows, totals, filename | Write the finished invoice out as a text file, ready to print. |
4Technical details
| Language | Python 3 |
| Storage | Plain text and CSV files |
| Modules used |
|
5How the data is stored
Two files. `sales.csv` is the input the shop types, one row per sale. `invoice.txt` is what the program produces. Keeping the input as CSV matters: the shopkeeper can edit it in any spreadsheet without touching Python.
sales.csv — the input, one row per sale
| Field | Type | Description |
|---|---|---|
item | text | Name of the item sold, as it should appear on the invoice |
category | text | grocery, stationery or electronics — decides the GST rate |
qty | integer | How many units were sold |
price | decimal | Price of one unit, before tax |
invoice.txt — the output
| Field | Type | Description |
|---|---|---|
Item lines | text | One line per sale: item, quantity, rate and amount in fixed columns |
Rate blocks | text | Taxable value and GST under each of the 5%, 12% and 18% slabs |
Totals | text | Taxable value, total GST and the grand total |
Sample contents of sales.csv
| item | category | qty | price |
|---|---|---|---|
Rice 5kg | grocery | 2 | 310.00 |
Notebook | stationery | 10 | 45.00 |
Pen box | stationery | 3 | 120.00 |
Table fan | electronics | 1 | 1450.00 |
6How it works, step by step
read_sales() opens sales.csv and turns each row into a dictionary, converting qty to int and price to float.
group_by_rate() looks up each item's category in the RATES table and adds its value to that rate's running total.
For each rate, tax = taxable value x rate / 100. The totals are added up for the grand total.
write_invoice() formats everything with str.format() column widths and writes it to invoice.txt.
7Source code
# ---------------------------------------------------------------------------
# gst_invoice.py
#
# Reads a day's sales from a CSV file, groups them by GST rate, works out the
# tax under each rate, and writes a printable tax invoice to a text file.
#
# The shop types its sales into sales.csv using any spreadsheet. The program
# never asks the shopkeeper to learn anything new.
# ---------------------------------------------------------------------------
import csv # for reading sales.csv without splitting strings by hand
from datetime import date # for putting today's date on the invoice
# The GST rate that applies to each category of goods. Keeping it here, in one
# place, means a change in the law is a one-line edit rather than a hunt
# through the program.
RATES = {"grocery": 5, "stationery": 12, "electronics": 18}
def read_sales(filename):
"""Read the day's raw transactions from a CSV file."""
rows = [] # every sale will be collected here
# newline="" is what the csv module asks for; it stops blank lines appearing
# on Windows.
with open(filename, "r", newline="") as f:
# DictReader uses the first line of the file as the column names, so a
# row arrives as {"item": "Rice 5kg", "category": "grocery", ...}
for row in csv.DictReader(f):
# Everything read from a file is text, so the two numeric columns
# have to be converted before any arithmetic is possible.
row["qty"] = int(row["qty"])
row["price"] = float(row["price"])
rows.append(row)
return rows
def group_by_rate(rows):
"""Add up the taxable value under each GST rate."""
totals = {} # rate -> taxable value at that rate
for row in rows:
rate = RATES[row["category"]] # look up the rate for this item
value = row["qty"] * row["price"] # what this line is worth before tax
# .get(rate, 0) gives 0 the first time a rate is met, so there is no need
# to create the entry separately before adding to it.
totals[rate] = totals.get(rate, 0) + value
return totals
def write_invoice(rows, totals, filename):
"""Write the finished invoice out as a text file, ready to print."""
with open(filename, "w") as f:
f.write("LAMBDALAB TRADERS — TAX INVOICE\n")
f.write("Date: " + str(date.today()) + "\n")
f.write("-" * 46 + "\n")
# The numbers in {:<16} and {:>10} are column widths. < means align to
# the left, > to the right, which is what makes the columns line up.
f.write("{:<16}{:>4}{:>10}{:>12}\n".format("ITEM", "QTY", "RATE", "AMOUNT"))
# One line per sale.
for row in rows:
amount = row["qty"] * row["price"]
f.write("{:<16}{:>4}{:>10.2f}{:>12.2f}\n".format(
row["item"], row["qty"], row["price"], amount))
f.write("-" * 46 + "\n")
taxable = sum(totals.values()) # the value of everything sold
tax_total = 0 # running total of the tax itself
# One block per rate, in increasing order of rate, which is the order the
# GST return expects them in.
for rate in sorted(totals):
tax = totals[rate] * rate / 100
tax_total = tax_total + tax
f.write("Taxable @ {:>2}%{:>19.2f} GST {:>8.2f}\n".format(
rate, totals[rate], tax))
f.write("-" * 46 + "\n")
f.write("{:<30}{:>16.2f}\n".format("Taxable value", taxable))
f.write("{:<30}{:>16.2f}\n".format("Total GST", tax_total))
f.write("{:<30}{:>16.2f}\n".format("GRAND TOTAL", taxable + tax_total))
f.write("-" * 46 + "\n")
f.write("Prepared with LambdaLab Billing\n")
# --- the program itself ----------------------------------------------------
# Read, group, write. Each step is a function above, so any one of them can be
# tested on its own.
sales = read_sales("sales.csv")
rate_totals = group_by_rate(sales)
write_invoice(sales, rate_totals, "invoice.txt")
print("Invoice written to invoice.txt")
print(open("invoice.txt").read()) # show it on screen as well, to check ititem,category,qty,price
Rice 5kg,grocery,2,310.00
Notebook,stationery,10,45.00
Pen box,stationery,3,120.00
Table fan,electronics,1,1450.00The full report as a PDF — cover page, certificate, index, code, output, future scope and references, ready to print and fill in. Or the code on its own as a zip.
8Sample output
This is a real run, not a mock-up — the transcript below is what the program actually printed.
C:\LambdaLab\Projects\GST-Invoice> python gst_invoice.py
Invoice written to invoice.txt
LAMBDALAB TRADERS — TAX INVOICE
Date: 2026-08-26
----------------------------------------------
ITEM QTY RATE AMOUNT
Rice 5kg 2 310.00 620.00
Notebook 10 45.00 450.00
Pen box 3 120.00 360.00
Table fan 1 1450.00 1450.00
----------------------------------------------
Taxable @ 5% 620.00 GST 31.00
Taxable @ 12% 810.00 GST 97.20
Taxable @ 18% 1450.00 GST 261.00
----------------------------------------------
Taxable value 2880.00
Total GST 389.20
GRAND TOTAL 3269.20
----------------------------------------------
Prepared with LambdaLab BillingRunning it also wrote invoice.txt. This is what that file held afterwards:
LAMBDALAB TRADERS — TAX INVOICE
Date: 2026-08-26
----------------------------------------------
ITEM QTY RATE AMOUNT
Rice 5kg 2 310.00 620.00
Notebook 10 45.00 450.00
Pen box 3 120.00 360.00
Table fan 1 1450.00 1450.00
----------------------------------------------
Taxable @ 5% 620.00 GST 31.00
Taxable @ 12% 810.00 GST 97.20
Taxable @ 18% 1450.00 GST 261.00
----------------------------------------------
Taxable value 2880.00
Total GST 389.20
GRAND TOTAL 3269.20
----------------------------------------------
Prepared with LambdaLab Billing9Testing
Every case below was actually run and the result recorded as it appeared — including the ones expected to fail.
| Test case | Expected | Actual |
|---|---|---|
| Normal run with four sales | Grand total of the four items | GRAND TOTAL 3269.20 |
| Sales file with a header but no rows | An invoice totalling zero | GRAND TOTAL 0.00 |
| A single sale | 10.00 taxable at 12%, total 11.20 | GRAND TOTAL 11.20 |
| Category not in the rate table | KeyError naming the unknown category | KeyError: 'toiletries' |
| sales.csv missing altogether | FileNotFoundError | FileNotFoundError: [Errno 2] No such file or directory: 'sales.csv' |
Two kinds of case are in there on purpose. The boundary cases test the edge of a rule, where a program is most often wrong by one. The failure cases check that it refuses cleanly and says why, instead of quietly producing a wrong answer.
10Advantages
Set against the ways the job is done today:
- The month-end sorting by tax rate stops being manual work
- Every invoice is arithmetically correct, every time
- The rate table is in one place, so a change in the law is one edit
- The shop keeps its data in a spreadsheet it already understands
- A printed invoice can be handed over or filed without being rewritten
11Future scope
What this version does not do
A report should be honest about its own limits, and each of these is the reason for one of the additions below:
- One shop and one day at a time; there is no month view yet
- The category of an item must be typed correctly, as there is no master item list
- Returns and credit notes, which carry negative values, are not handled
- The invoice is plain text, so it prints without a logo or a rupee symbol
Proposed enhancements
This is also where you make the project yours. Pick one or two of these, or something nobody here thought of:
- Read the shop's existing spreadsheet directly instead of a CSV export
- Print each invoice to a PDF so it can be emailed to the customer
- Keep a running month total, so the return is ready on the 1st
- Add a customer table and print the buyer's GSTIN on the invoice
- Handle returns and credit notes, which carry negative values
12What you may have to teach yourself
CBSE expects some self-learning in a project, and says so. For this one, that means:
- How your local shop's GST categories actually map to rates — ask them
- str.format() width and alignment, which is what makes the columns line up
- Optional: reportlab or fpdf, if you want real PDF invoices
13Conclusion
The software does what it set out to do. A day's transactions, typed into a file the shopkeeper already knows how to edit, come back as a tax invoice grouped exactly the way the return requires, with the arithmetic done identically every time.
Writing it made clear how much of the original work was not really about tax at all, but about sorting. Once the sorting was given to the computer, what remained was three multiplications and a total.
14References
Every report needs a bibliography. This one used:
- Computer Science with Python, Class XII — the NCERT / CBSE prescribed textbook, for the chapters on file handling and working with text and CSV files
- Computer Science with Python, Class XI — for functions, lists, dictionaries and string handling
- Python 3 documentation — https://docs.python.org/3/
- CBSE Senior School Curriculum, Computer Science (Subject Code 083) — the project guidelines this report follows
- LambdaLab — https://www.lambdalab.in