Data AnalyticsPractical workplace data tasks

Check IGST vs CGST+SGST on every invoice line in a sales register

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 6 min read
In this article (6 sections)

Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).

The short answer

To check tax type in a sales register, take the supplier's state code (the first two digits of your own GSTIN) and the place of supply recorded on each invoice. If the two match, the line should carry CGST and SGST in equal halves; if they differ, it should carry IGST. Compare that expectation with the columns actually filled, recompute tax from rate times taxable value, and put every mismatch on a review list rather than correcting it in place.

The one rule the check rests on

The CBIC's GST FAQ states it plainly: if the place of supply and the location of the supplier are in the same State, the supply is intra-State. In that case CGST and SGST (or UTGST) apply; otherwise the supply is inter-State and IGST applies. Note what the rule compares. It is the supplier's location against the place of supply, not the supplier against the buyer's GSTIN. The two usually agree, which is exactly why billing staff learn the shortcut "same state as the customer means CGST+SGST", and why that shortcut fails on the lines that matter.

State codes are the two-digit numbers at the start of every GSTIN and in the place-of-supply field of an e-invoice. The GST e-invoice portal publishes the full state code master; keep a copy in the workbook as a lookup sheet instead of typing names.

What goes wrong in practice

Four patterns account for most of what this check finds:

SymptomTypical cause
IGST charged, place of supply in your own stateBilling clerk used the buyer's GSTIN state instead of the delivery state
CGST+SGST charged, place of supply outside your stateCustomer master copied from a sister branch; tax template not updated
Tax amount differs from rate times taxable valueRate changed on the item but not on the invoice, or a discount applied after tax
Place of supply blankOlder invoice template, or a manual invoice keyed in later

None of them is visible from a monthly total. The total tax can look reasonable while individual lines carry the wrong kind of tax, which then surfaces as a mismatch in the buyer's input credit or a query at return time.

The check in pandas

The register below is synthetic, and so are the GSTINs. The supplier is registered in Uttar Pradesh (state code 09).

python
import pandas as pd

SUPPLIER_GSTIN = "09AAACX1234A1ZV"                  # synthetic; 09 = Uttar Pradesh
STATES = {"06": "Haryana", "07": "Delhi", "09": "Uttar Pradesh", "27": "Maharashtra",
          "29": "Karnataka"}                          # subset of the GST state code list

# Synthetic sales register. place_of_supply is what the invoice records, as a state code.
inv = pd.DataFrame({
    "inv_no": ["NOI/26-27/0101", "NOI/26-27/0102", "NOI/26-27/0103", "NOI/26-27/0104",
               "NOI/26-27/0105", "NOI/26-27/0106", "NOI/26-27/0107"],
    "buyer_gstin": ["09AAGCW3456D1ZE", "07AAFCY5678B1Z3", "27AABCZ9012C1ZI", "06AAGCW3456D1ZE",
                    None, "29AAGPR4821K1ZA", "09AAGCW3456D1ZE"],
    "place_of_supply": ["09", "07", "09", "06", "09", "29", None],
    "rate": [18, 18, 18, 12, 18, 18, 5],
    "taxable": [100000.0, 45000.0, 60000.0, 25000.0, 8000.0, 150000.0, 20000.0],
    "igst": [0.0, 8100.0, 10800.0, 0.0, 0.0, 18000.0, 0.0],
    "cgst": [9000.0, 0.0, 0.0, 1500.0, 720.0, 0.0, 500.0],
    "sgst": [9000.0, 0.0, 0.0, 1500.0, 720.0, 0.0, 500.0],
})

supplier_state = SUPPLIER_GSTIN[:2]
inv["buyer_state"] = inv["buyer_gstin"].str[:2]
inv["expected"] = inv["place_of_supply"].map(
    lambda p: None if pd.isna(p) else ("CGST+SGST" if p == supplier_state else "IGST"))
inv["charged"] = inv.apply(lambda r: "IGST" if r.igst > 0 and r.cgst == r.sgst == 0
                           else "CGST+SGST" if r.igst == 0 and r.cgst > 0 else "mixed/none", axis=1)
tax_due = (inv["taxable"] * inv["rate"] / 100).round(2)
charged_total = inv["igst"] + inv["cgst"] + inv["sgst"]

def findings(r, due, total):
    out = []
    if pd.isna(r.expected):
        out.append("place of supply missing: cannot decide tax type")
    elif r.expected != r.charged:
        out.append(f"charged {r.charged}, place of supply {r.place_of_supply} implies {r.expected}")
    if abs(total - due) > 1:
        out.append(f"tax {total:,.0f} vs {due:,.0f} at {r.rate}%")
    if r.cgst != r.sgst:
        out.append("CGST and SGST differ")
    if isinstance(r.buyer_state, str) and isinstance(r.place_of_supply, str) and r.buyer_state != r.place_of_supply:
        out.append(f"buyer registered in {STATES.get(r.buyer_state, r.buyer_state)}, "
                   f"supply placed in {STATES.get(r.place_of_supply, r.place_of_supply)}: confirm")
    return "; ".join(out)

inv["check"] = [findings(r, d, t) for r, d, t in zip(inv.itertuples(), tax_due, charged_total)]
print(inv.loc[inv["check"] != "", ["inv_no", "place_of_supply", "charged", "check"]]
      .to_string(index=False))
assert (inv["check"] != "").sum() == 4

Seven invoices go in and four come out on the list:

  • 0103 charged IGST because the buyer is registered in Maharashtra, but the invoice itself records Uttar Pradesh as the place of supply. Either the tax or the place of supply field is wrong, and only the person who knows where the goods went can say which.
  • 0104 is the reverse: a Haryana place of supply billed with CGST and SGST.
  • 0106 has the right tax type but ₹18,000 of IGST on ₹1,50,000 at 18%, which should be ₹27,000. The amount looks like a 12% calculation.
  • 0107 has no place of supply at all, so the script refuses to guess.

Invoice 0105 has no buyer GSTIN (a sale to an unregistered customer) and passes, because its tax type is still decided by the recorded place of supply.

Three design choices

Expected type comes from the invoice's own place of supply. The script does not try to work out where the supply should be placed. That decision depends on the kind of supply and the contract, and a script that silently overrides it would produce confident wrong answers. It only checks internal consistency: is the tax charged the tax that this place of supply implies?

Buyer-state mismatches are informational. The GST portal's own GSTR-2B FAQ discusses invoices where the supplier and the place of supply are in one State while the recipient is in another, so such invoices exist and are not automatically wrong. The note "confirm" sends the line to a person without calling it an error.

Tolerance of ₹1 on recomputed tax. Invoices are usually rounded per line or per invoice, so an exact equality test would flood the list with paise differences. Keep the tolerance small, state it in the output, and widen it only if you know your billing software rounds at invoice level.

The same test in Excel

If the register has place of supply in column C, taxable value in E, rate in F, IGST in G, CGST in H and SGST in I, and your own state code sits in a named cell Own_State, three helper columns in J, K and L do the job:

text
Expected   =IF(C2="","MISSING",IF(TEXT(C2,"00")=Own_State,"CGST+SGST","IGST"))
Charged    =IF(AND(G2>0,H2=0,I2=0),"IGST",IF(AND(G2=0,H2>0,I2>0),"CGST+SGST","MIXED"))
Flag       =IF(OR(J2<>K2,H2<>I2,ABS(G2+H2+I2-E2*F2/100)>1),"REVIEW","")

TEXT(C2,"00") keeps state code 09 from turning into 9 when the column was stored as a number, which is the most common reason this check reports every line as inter-State. Filter the Flag column to REVIEW and sort by taxable value so the largest lines are read first.

Before and after the check

Validate the GSTIN format first, so that a truncated or mistyped number does not produce a wrong state code; the steps are in validate GSTIN and PAN formats before they reach a report. After correcting the register, the purchase side of the same problem appears when you reconcile GSTR-2B with the purchase register.

This article covers a data-consistency check, not tax advice. Rates, exemptions and place-of-supply rules for particular transactions should be confirmed with your tax adviser or the notifications on the CBIC site.

Turning a rule like this into a repeatable check with a clear exception list is a core habit in our Data Analyst course, where learners build such controls on realistic registers before they build dashboards on top.

Frequently asked questions

Is the buyer's GSTIN state the same as the place of supply?

Often, but not always. Goods bought by a Maharashtra company and delivered in Uttar Pradesh can have a place of supply different from the buyer's registration state. Treat a mismatch as a question for whoever raised the invoice, not as an automatic error.

Can a script decide the place of supply for me?

No. The place of supply depends on the nature of the supply and the facts of the transaction. The script checks that the tax charged is consistent with the place of supply recorded on the invoice; deciding that place is a job for the accounts team or a tax adviser.

Why compare CGST with SGST separately?

On an intra-State supply the two are levied at equal rates, so on a single line they should be equal. A difference usually means a manual edit or a rate typed into only one column.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.

This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.

Explore Data Analytics with Generative AI
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.