frappe/env/lib/python3.12/site-packages/stdnum/in_/gstin.py

163 lines
4.8 KiB
Python

# gstin.py - functions for handling Indian VAT numbers
#
# Copyright (C) 2021 Gaurav Chauhan
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
"""GSTIN (Goods and Services Tax identification number, Indian VAT number).
The Goods and Services Tax identification number (GSTIN) is a 15 digit unique
identifier assigned to all business entities in India registered under the
Goods and Services Tax (GST) Act, 2017.
Each GSTIN begins with a 2 digit state code, the next 10 characters are the
holder's PAN, the 13th character is an alphanumeric digit that represents the
number of GSTIN registrations made in a state or union territory for same the
PAN, the 14th character is 'Z' and the last character is an alphanumeric
check digit calculated using Luhn mod 36 algorithm.
More information:
* https://bajajfinserv.in/insights/what-is-goods-and-service-tax-identification-number
* https://ddvat.gov.in/docs/List%20of%20State%20Code.pdf
* https://en.wikipedia.org/wiki/Goods_and_Services_Tax_(India)
>>> validate('27AAPFU0939F1ZV')
'27AAPFU0939F1ZV'
>>> validate('27AAPFU0939F1Z')
Traceback (most recent call last):
...
InvalidLength: ...
>>> validate('369296450896540')
Traceback (most recent call last):
...
InvalidFormat: ...
>>> validate('27AAPFU0939F1AA')
Traceback (most recent call last):
...
InvalidComponent: ...
>>> validate('27AAPFU0939F1ZO')
Traceback (most recent call last):
...
InvalidChecksum: ...
>>> to_pan('27AAPFU0939F1ZV')
'AAPFU0939F'
>>> info('27AAPFU0939F1ZV')['state']
'Maharashtra'
"""
from __future__ import annotations
import re
from stdnum import luhn
from stdnum.exceptions import *
from stdnum.in_ import pan
from stdnum.util import clean
_GSTIN_RE = re.compile(r'^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]{3}$')
_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
_STATE_CODES = {
'01': 'Jammu and Kashmir',
'02': 'Himachal Pradesh',
'03': 'Punjab',
'04': 'Chandigarh',
'05': 'Uttarakhand',
'06': 'Haryana',
'07': 'Delhi',
'08': 'Rajasthan',
'09': 'Uttar Pradesh',
'10': 'Bihar',
'11': 'Sikkim',
'12': 'Arunachal Pradesh',
'13': 'Nagaland',
'14': 'Manipur',
'15': 'Mizoram',
'16': 'Tripura',
'17': 'Meghalaya',
'18': 'Assam',
'19': 'West Bengal',
'20': 'Jharkhand',
'21': 'Orissa',
'22': 'Chattisgarh',
'23': 'Madhya Pradesh',
'24': 'Gujarat',
'25': 'Daman and Diu',
'26': 'Dadar and Nagar Haveli',
'27': 'Maharashtra',
'28': 'Andhra Pradesh',
'29': 'Karnataka',
'30': 'Goa',
'31': 'Lakshadweep',
'32': 'Kerala',
'33': 'Tamil Nadu',
'34': 'Puducherry',
'35': 'Anadaman and Nicobar Islands',
'36': 'Telangana',
'37': 'Andhra Pradesh (New)',
}
def compact(number: str) -> str:
"""Convert the number to the minimal representation. This strips the
number of any valid separators and removes surrounding whitespace."""
return clean(number, ' -').upper().strip()
def validate(number: str) -> str:
"""Check if the number provided is a valid GSTIN. This checks the length,
formatting and check digit."""
number = compact(number)
if len(number) != 15:
raise InvalidLength()
if not _GSTIN_RE.match(number):
raise InvalidFormat()
if number[:2] not in _STATE_CODES or number[12] == '0' or number[13] != 'Z':
raise InvalidComponent()
pan.validate(number[2:12])
luhn.validate(number, _ALPHABET)
return number
def is_valid(number: str) -> bool:
"""Check if the number provided is a valid GSTIN. This checks the length,
formatting and check digit."""
try:
return bool(validate(number))
except ValidationError:
return False
def to_pan(number: str) -> str:
"""Convert the number to a PAN."""
number = compact(number)
return number[2:12]
def info(number: str) -> dict[str, str | int | None]:
"""Provide information that can be decoded locally from GSTIN (without
API)."""
number = validate(number)
return {
'state': _STATE_CODES.get(number[:2]),
'pan': number[2:12],
'holder_type': pan.info(number[2:12])['holder_type'],
'initial': number[6],
'registration_count': _ALPHABET.index(number[12]),
}