If you're a freelancer, consultant, or small business owner in India, you've probably paid ₹500–₹2000/month for invoicing software you barely use. After helping a CA friend automate his client billing last week, I wrote a clean GST invoice generator in 80 lines of Python. It handles HSN codes, CGST/SGST split for intra-state, IGST for inter-state, and outputs a professional PDF — all from a simple dictionary input.
This is the same script I now use to invoice my own automation consulting clients. Total cost: zero. Total time saved: about three hours per month.
What it does
Pass in your business details, the buyer's GSTIN, line items, and tax rates. The script:
- Auto-detects whether the transaction is intra-state (split into CGST + SGST) or inter-state (single IGST line).
- Computes line-level taxable amount, tax, and total.
- Generates a clean A4 PDF with your branding, HSN codes, and the mandatory GST breakdown.
- Names the file with the invoice number so it's audit-ready.
The full script
You'll need one library:
pip install reportlab
Here's the entire generator. Save it as gst_invoice.py:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from datetime import date
def generate_gst_invoice(seller, buyer, items, invoice_no, inv_date=None):
inv_date = inv_date or date.today().isoformat()
same_state = seller["state_code"] == buyer["state_code"]
rows = [["#", "Description", "HSN", "Qty", "Rate", "Taxable", "Tax%", "Tax", "Total"]]
sub_total = total_tax = grand_total = 0
for i, it in enumerate(items, 1):
taxable = round(it["qty"] * it["rate"], 2)
tax = round(taxable * it["gst"] / 100, 2)
line_total = round(taxable + tax, 2)
sub_total += taxable
total_tax += tax
grand_total += line_total
rows.append([i, it["desc"], it["hsn"], it["qty"],
f"{it['rate']:.2f}", f"{taxable:.2f}",
f"{it['gst']}%", f"{tax:.2f}", f"{line_total:.2f}"])
if same_state:
tax_rows = [["CGST", f"{total_tax/2:.2f}"], ["SGST", f"{total_tax/2:.2f}"]]
else:
tax_rows = [["IGST", f"{total_tax:.2f}"]]
tax_rows.append(["Grand Total (INR)", f"{grand_total:.2f}"])
filename = f"Invoice_{invoice_no}.pdf"
doc = SimpleDocTemplate(filename, pagesize=A4, title=f"Invoice {invoice_no}")
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"<b>{seller['name']}</b>", styles["Title"]))
story.append(Paragraph(f"{seller['address']}<br/>GSTIN: {seller['gstin']}", styles["Normal"]))
story.append(Spacer(1, 12))
story.append(Paragraph(f"<b>Tax Invoice #{invoice_no}</b> Date: {inv_date}", styles["Heading3"]))
story.append(Paragraph(f"<b>Bill To:</b> {buyer['name']}<br/>{buyer['address']}<br/>GSTIN: {buyer['gstin']}", styles["Normal"]))
story.append(Spacer(1, 12))
item_table = Table(rows, repeatRows=1, hAlign="LEFT")
item_table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1f2937")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("GRID", (0, 0), (-1, -1), 0.4, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 9),
("ALIGN", (3, 1), (-1, -1), "RIGHT"),
]))
story.append(item_table)
story.append(Spacer(1, 12))
summary = Table(tax_rows, hAlign="RIGHT", colWidths=[140, 90])
summary.setStyle(TableStyle([
("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
("LINEABOVE", (0, -1), (-1, -1), 0.8, colors.black),
("ALIGN", (1, 0), (1, -1), "RIGHT"),
]))
story.append(summary)
story.append(Spacer(1, 18))
story.append(Paragraph("<i>This is a computer-generated invoice and does not require a signature.</i>", styles["Italic"]))
doc.build(story)
return filename
if __name__ == "__main__":
seller = {"name": "Mittal Automation Studio", "address": "Sector 62, Noida, UP",
"gstin": "09ABCDE1234F1Z5", "state_code": "09"}
buyer = {"name": "Acme Retail Pvt Ltd", "address": "Bandra West, Mumbai, MH",
"gstin": "27FGHIJ5678K2Z9", "state_code": "27"}
items = [
{"desc": "Workflow automation setup", "hsn": "998313", "qty": 1, "rate": 45000, "gst": 18},
{"desc": "Monthly support retainer", "hsn": "998313", "qty": 3, "rate": 12000, "gst": 18},
]
out = generate_gst_invoice(seller, buyer, items, "INV-2026-014")
print(f"Generated: {out}")
How it works, in plain English
The state-code check is the entire trick. Indian GST law splits tax based on whether buyer and seller are in the same state — if yes, you charge CGST + SGST (each half the rate); if no, you charge IGST (full rate). Here we read the first two digits of the GSTIN (which encode the state) and branch accordingly.
The line-item loop computes taxable value, tax, and total per row, then accumulates the grand total. Everything else is layout — the seller block on top, the bill-to in the middle, line items in a dark-header table, and a right-aligned summary with the legally required tax breakdown.
Extending it
A few easy upgrades I've added in production:
-
Numbers in words. GST law requires the total amount to be spelled out. Add
num2words(grand_total, lang="en_IN")and append it to the summary. -
QR code. RBI now requires a UPI/payment QR on B2B invoices over ₹500 crore turnover. Generate one with
qrcodeand embed it viaImagefrom reportlab. -
Bulk run. Wrap
generate_gst_invoicein a loop over a CSV. I produce 40 client invoices on the 1st of every month with a single command. -
Email send. Pipe the PDF into
smtpliband you've replaced a ₹1500/month invoicing tool entirely.
Why this beats SaaS for solo operators
The real win isn't the ₹18,000/year you save. It's that the invoice template, the numbering scheme, and the data layout all live in your repo. When your CA asks for a year's worth of invoices in a specific format, it's a ten-second loop, not a support ticket.
If you're billing fewer than 200 invoices a month and your needs are reasonably stable, an 80-line script will outlast three SaaS subscription cycles.
Follow me on Twitter @automate_archit for daily AI automation tips. Code for this project: github.com/automate-archit/python-automation-starter
Top comments (0)