DEV Community

Archit Mittal
Archit Mittal

Posted on • Originally published at architmittal.com

Build a GST Invoice Generator in 80 Lines of Python

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:

  1. Auto-detects whether the transaction is intra-state (split into CGST + SGST) or inter-state (single IGST line).
  2. Computes line-level taxable amount, tax, and total.
  3. Generates a clean A4 PDF with your branding, HSN codes, and the mandatory GST breakdown.
  4. Names the file with the invoice number so it's audit-ready.

The full script

You'll need one library:

pip install reportlab
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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 qrcode and embed it via Image from reportlab.
  • Bulk run. Wrap generate_gst_invoice in 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 smtplib and 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)