GST compliance (India)
How the separate India Compliance app layers GST — entity shapes, validation rules, and the e-Invoice, e-Way Bill and GSTR workflows — on top of core ERPNext.
India’s GST (Goods and Services Tax) support does not ship inside ERPNext core. It was
deprecated in v13 and permanently removed in v14 — patches/v14_0/remove_india_localisation.py
force-deletes GST Settings, GST Account, GST HSN Code, GSTR 3B Report and related
doctypes from any site that doesn’t have the replacement app installed. Since then GST has lived
in a standalone Frappe app, India Compliance
(resilient-tech/india-compliance), installed
alongside ERPNext rather than merged into it. The Regional module page
already flags this; this page is the deep dive.
How it attaches to core
India Compliance never monkey-patches ERPNext. It hooks in through the same extension points any
regional app uses, all declared in the app’s hooks.py:
doc_events—validate,on_submitandbefore_cancelhandlers onSales Invoice,Purchase Invoice,Purchase Receipt,Delivery Note,Address,Customer,SupplierandCompanyrun the GST pipeline (tax split, e-Invoice/e-Way Bill generation, party GSTIN sync).regional_overrides["India"]— plugs into ERPNext’s own regional callbacks (get_regional_round_off_accounts,make_regional_gl_entries,update_regional_item_valuation_rate,get_regional_address_details) instead of overriding core controller code directly.scheduler_events— five cron jobs drive background automation (retrying failed e-Invoice/ e-Way Bill calls, nightly GSTR-2A/2B download and reconciliation, session-token refresh, scheduled e-Way Bill extension — see background automation).- Custom fields —
gstin,gst_category,place_of_supplyand the rest are injected onto existing core doctypes viacreate_custom_fields, not by editing the ERPNext doctype JSON.
Entity shapes
App-owned doctypes
| Entity | Kind | Purpose |
|---|---|---|
GST Settings |
Singleton config (outside the M/T/L/C scheme — see note below) | App-wide config: feature toggles, e-Way Bill/e-Invoice thresholds, HSN validation rule, RCM threshold, auto-reconciliation schedule. |
GST Account |
Child (of GST Settings) | Maps CGST/SGST/IGST/Cess to ledger Accounts per company and account_type (Input/Output/Purchase RCM/Sales RCM/Output Refund). |
GST Credential |
Child (of GST Settings) | NIC/GSTN portal session state (username, password, auth_token, session_expiry) per company + gstin + service (e-Waybill/e-Invoice vs Returns). One company can hold several — one per registered GSTIN. |
GSTIN |
Master (cache) | Portal-fetched registration status for a GSTIN string: status, is_blocked, gstr_1_filed_upto. |
PAN |
Master (cache) | Portal-fetched PAN status. |
GST HSN Code |
Master | HSN/SAC master with a default taxes table of item tax rates. |
e-Invoice Log |
Derived/log | One row per IRN — signed JWT payload, QR code, ack number, cancellation state. |
e-Waybill Log |
Derived/log | One row per e-Way Bill number — valid_upto, extension scheduling, cancellation state. |
GST Return Log |
Derived/log | Generic filing-state tracker for GSTR-1/3B/2A/2B, keyed {return_type}-{return_period}-{gstin}; holds filed/unfiled/books/reconciled snapshots as gzipped JSON attachments. |
GSTR Import Log |
Derived/log | Download bookkeeping — avoids re-downloading an already-fetched return period. |
GST Inward Supply |
Derived/log | One row per supplier-reported invoice from 2A/2B/IMS — the unit that Purchase Reconciliation matches against books. |
Bill of Entry |
Transaction (submittable) | Import customs duty document, enabling ITC claim on imports; carries items and a taxes table (India Compliance Taxes and Charges). |
Custom fields injected into core doctypes
| Core doctype | Key fields added | Purpose |
|---|---|---|
Company, Customer, Supplier |
gstin, gst_category |
Party-level GST registration and category (Registered Regular, SEZ, Overseas, Unregistered, …). |
Address |
gstin, gst_state, gst_state_number, gst_category |
Per-address GSTIN — see can a company have more than one GSTIN?. |
Item |
gst_hsn_code, is_ineligible_for_itc |
HSN/SAC classification and ITC eligibility. |
Purchase transactions (Purchase Order/Receipt/Invoice, Supplier Quotation) |
supplier_gstin, company_gstin, place_of_supply, gst_category |
company_gstin is fetch_from: company_address.gstin — resolved per transaction, not from a single company-level value. |
Sales transactions (Quotation/Sales Order/Delivery Note/Sales Invoice) |
billing_address_gstin, company_gstin, place_of_supply, gst_category, ecommerce_gstin |
Mirrors the purchase side; ecommerce_gstin supports TCS e-commerce operator sales. |
| All transaction Item rows | gst_hsn_code, gst_treatment, igst_rate/amount, cgst_rate/amount, sgst_rate/amount, cess_rate/amount, cess_non_advol_rate/amount |
Per-line GST split, computed by ItemGSTDetails — read-only, not user-entered. |
| Sales/Purchase/Advance Taxes and Charges rows | gst_tax_type |
Tags each tax row (cgst/sgst/igst/cess/…, _rcm and _refund variants) by reverse-mapping the row’s account_head through GST Account. |
Payment Entry |
company_gstin, place_of_supply, billing_address_gstin, gst_category |
GST applies to advances too, not just invoices. |
Tax Category |
is_inter_state, is_reverse_charge |
Drives auto-selection of the correct Sales/Purchase Taxes and Charges Template. |
Supplier |
gst_transporter_id, is_reverse_charge_applicable |
Transporter enrolment number; per-supplier RCM default. |
Validations
Regex constants live in gst_india/constants/__init__.py; the functions that apply them live in
gst_india/utils/__init__.py and gst_india/overrides/transaction.py.
GSTIN format and checksum
NORMAL = r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[Z1-9ABD-J]{1}[0-9A-Z]{1}$"
GOVT_DEPTID = r"^[0-9]{2}[A-Z]{4}[0-9]{5}[A-Z]{1}[0-9]{1}[Z]{1}[0-9]{1}$"
REGISTERED = re.compile(rf"{NORMAL}|{GOVT_DEPTID}")
NRI_ID = r"^[0-9]{4}[A-Z]{3}[0-9]{5}[N][R][0-9A-Z]{1}$"
OIDAR = r"^[9][9][0-9]{2}[A-Z]{3}[0-9]{5}[O][S][0-9A-Z]{1}$"
OVERSEAS = re.compile(rf"{NRI_ID}|{OIDAR}")
UNBODY = re.compile(r"^[0-9]{4}[A-Z]{3}[0-9]{5}[UO]{1}[N][A-Z0-9]{1}$") # UIN Holders
TDS = re.compile(r"^[0-9]{2}[A-Z]{4}[A-Z0-9]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[D][0-9A-Z]$")
TCS = re.compile(r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[C]{1}[0-9A-Z]{1}$")
PAN_NUMBER = re.compile(r"^[A-Z]{5}[0-9]{4}[A-Z]{1}$")
Chars 0-1 encode the state code, chars 2-11 embed a PAN (5 letters, 4 digits, 1 letter), char 12 is
the registration sequence for that PAN within the state, char 14 is a check digit.
validate_gstin_check_digit() verifies that digit with GSTN’s mod-36 Luhn-like algorithm over the
alphabet 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.
GST Category ↔ GSTIN consistency
validate_gst_category():
- No GSTIN ⇒
gst_categorymust beUnregisteredorOverseas. - GSTIN present ⇒
gst_categorycannot beUnregistered, and the GSTIN must match the regex registered against that specific category (aUIN Holdersparty’s GSTIN must matchUNBODY, aTax Deductor’s must matchTDS, and so on).
guess_gst_category() infers the category from the GSTIN pattern when the user hasn’t set one —
e.g. a GSTIN matching TDS/TCS defaults to Tax Deductor/Collector, one matching UNBODY to UIN
Holders, no GSTIN plus a non-India country to Overseas.
HSN/SAC
get_hsn_settings() reads GST Settings.min_hsn_digits (4/6/8, default 6) and derives the set of
acceptable lengths from VALID_HSN_LENGTHS = (4, 6, 8). _validate_hsn_codes() only warns before
submit but hard-throws at submit time on a missing or wrong-length HSN.
Place of supply and inter-state determination
get_place_of_supply() resolves per transaction type — sales resolves the customer’s billing or
shipping address per Accounts Settings.determine_address_tax_category_from; purchases use
company_gstin or supplier_gstin; overseas parties default to "96-Other Countries" unless an
India-based shipping address overrides it.
is_inter_state_supply() is the single check that decides CGST+SGST vs IGST: it compares
place_of_supply[:2] (destination state) against the transaction’s origin state code (from the
company or supplier GSTIN). SEZ parties are always treated as inter-state regardless of state
match.
Reverse charge
set_reverse_charge_as_per_gst_settings() auto-sets is_reverse_charge on a purchase when
GST Settings.enable_rcm_for_unregistered_supplier is on, the supplier’s gst_category is
Unregistered, and grand_total > rcm_threshold (default ₹5,000). A separate check enforces the
accounting identity base_gst_tax + base_reverse_charge_booked == 0 — the self-billed RCM
liability must exactly offset the applied tax.
e-Invoice applicability
validate_e_invoice_applicability() — B2C invoices (no billing_address_gstin) are always exempt;
same-GSTIN transfers are exempt; otherwise gated by GST Settings.enable_e_invoice and an
applicability date (global e_invoice_applicable_from, or a per-company override in
e_invoice_applicable_companies). There is no hard-coded turnover threshold in code — the
government’s turnover-based mandate is left for the business to configure via that date. A
separate check enforces e_invoice_reporting_time_limit_days (default 30) — invoices older than
that can no longer report an IRN.
e-Way Bill threshold
Flat e_waybill_threshold (default ₹50,000) for inter-state movement; a state-configurable
State-wise e-Waybill Threshold table for intra-state movement, falling back to the same default
if a state isn’t listed. e-Way Bills apply to goods only — validate_applicability() requires at
least one item whose HSN doesn’t start with the services prefix "99". Distance is only validated
locally for the same-pincode case (capped at 100 km); actual distance-based validity (valid_upto)
is computed server-side by the NIC/GSP API, not by the app.
Workflows
Tax calculation (CGST/SGST/IGST/Cess split)
A master validate_transaction() pipeline runs on every Sales/Purchase document: resolve and
validate place_of_supply → decide applicable GST accounts via is_inter_state_supply() →
ItemGSTDetails().update(doc) computes each item’s per-tax-type rate/amount from the doc’s tax
rows → any rounding remainder is pushed onto the last taxed item → the stored amounts are
cross-checked against the recomputed expected amounts within a small tolerance.
e-Invoice: generate → cancel
On Sales Invoice submit, if applicable and auto_generate_e_invoice is on, generate_e_invoice()
runs as a background job: builds the government JSON payload, calls the NIC/GSP API for an IRN,
handles duplicate-IRN and invalid-GSTIN error codes with a retry, and on success stores the IRN,
decodes the signed JWT for the QR code, and writes an e-Invoice Log. Failures route to a 5-minute
scheduler retry queue. Cancellation is restricted to a 24-hour window from acknowledgement, cascades
to cancel any bundled e-Way Bill first, then cancels the Sales Invoice itself.
e-Way Bill: generate → extend → cancel
Triggered on submit when e-Invoicing doesn’t apply (or bundled automatically with e-Invoice
generation via generate_e_waybill_with_e_invoice). Builds transaction/item/transporter data,
validates goods-only HSNs, calls the API, and stores the government-returned valid_upto.
Extensions can be scheduled ahead of time (outside the ±8h window normally allowed) and are
executed by a nightly scheduler job. Auto-cancellation on document cancellation is gated by
GST Settings.auto_cancel_e_waybill.
GSTR-1: classification and filing
Sales invoices are classified into GSTR-1 subcategories — B2B Regular/Reverse Charge, SEZ WP/WOP,
Deemed Export, Exports WP/WOP, B2C (Large), B2C (Others), Nil-Rated/Exempted, CDNR/CDNUR, Advances,
HSN Summary — through a chain of boolean-predicate classes (is_export, is_inter_state,
is_b2cl_inv, …). The B2C-Large threshold is date-tiered (get_b2c_limit()), since the government
changed it over time. Classified data is mapped into the government’s JSON schema and reconciled
against portal-filed data before filing; GSTIN.gstr_1_filed_upto blocks edits to already-filed
periods.
GSTR-2A/2B reconciliation (ITC matching)
Downloaded government data becomes GST Inward Supply rows. The Purchase Reconciliation Tool
matches them against Purchase Invoices/Bill of Entry in ordered passes — GSTIN-level match first,
then PAN-level — applying a rule cascade from Exact Match through Fuzzy Match (bill numbers
within 10 days, rapidfuzz similarity ≥90%) to a Rounding Difference tolerance (≤₹1) down to
Residual Match. Results write back as reconciliation_status and itc_claim_period on the
purchase document.
Background automation
All scheduled from hooks.py:
| Cron | Job | Purpose |
|---|---|---|
*/5 * * * * |
retry_e_invoice_e_waybill_generation |
Retry e-Invoice/e-Way Bill calls that failed with a GSP server error. |
*/5 * * * * |
download_queued_request |
Poll async large-GSTR-download requests. |
*/10 * * * * |
auto_refresh_authtoken |
Keep GST Credential “Returns” session tokens alive. |
0 2 * * * |
auto_download_gstr |
Nightly GSTR-2A/2B download. |
0 4 * * * |
auto_reconcile |
Nightly purchase-vs-inward-supply reconciliation. |
0 1 * * * |
extend_scheduled_e_waybills |
Execute user-scheduled e-Way Bill validity extensions. |
One company, many GSTINs
A Company can hold more than one GSTIN. Company.gstin is a single default field, but the value
that actually drives tax calculation on a transaction comes from company_gstin, which is
fetch_from: company_address.gstin — resolved from whichever linked Address the user selects on
that document. Since one Company can have many linked addresses (Frappe’s standard
Dynamic-Link address pattern), each carrying its own gstin/gst_category/gst_state, a company
naturally models one branch address — and one GSTIN — per state of operation. GST Credential is
hash-autonamed with no uniqueness constraint on company, so a company can also hold a separate
portal session per GSTIN, matching how the GST portal itself requires per-GSTIN logins.
GST Account (the CGST/SGST/IGST/Cess ledger mapping) is the one exception — it’s keyed by
company alone, so all of a company’s branches share the same GST ledger accounts unless set up
otherwise.
Related pages
Regional
The thin core-shipped regional module — Lower Deduction Certificate (India TDS) and Import Supplier Invoice (Italy) — and why most localisation, GST included, lives outside it.
Framework entities
Address, Contact and the other Frappe-framework entities that India Compliance’s custom
fields attach to.