Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

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_eventsvalidate, on_submit and before_cancel handlers on Sales Invoice, Purchase Invoice, Purchase Receipt, Delivery Note, Address, Customer, Supplier and Company run 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 fieldsgstin, gst_category, place_of_supply and the rest are injected onto existing core doctypes via create_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_category must be Unregistered or Overseas.
  • GSTIN present ⇒ gst_category cannot be Unregistered, and the GSTIN must match the regex registered against that specific category (a UIN Holders party’s GSTIN must match UNBODY, a Tax Deductor’s must match TDS, 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.

Was this page helpful?