mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-24 15:39:20 +00:00
Merge pull request #24776 from ankush/gst_invoice_validation
fix: Add warning for invalid GST invoice numbers
This commit is contained in:
@@ -278,6 +278,9 @@ doc_events = {
|
|||||||
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
|
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
|
||||||
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
|
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
|
||||||
},
|
},
|
||||||
|
('Sales Invoice', 'Purchase Invoice'): {
|
||||||
|
'validate': ['erpnext.regional.india.utils.validate_document_name']
|
||||||
|
},
|
||||||
"Contact": {
|
"Contact": {
|
||||||
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
|
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
|
||||||
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
|
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
|
||||||
|
|||||||
38
erpnext/regional/india/test_utils.py
Normal file
38
erpnext/regional/india/test_utils.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import frappe
|
||||||
|
from unittest.mock import patch
|
||||||
|
from erpnext.regional.india.utils import validate_document_name
|
||||||
|
|
||||||
|
|
||||||
|
class TestIndiaUtils(unittest.TestCase):
|
||||||
|
@patch("frappe.get_cached_value")
|
||||||
|
def test_validate_document_name(self, mock_get_cached):
|
||||||
|
mock_get_cached.return_value = "India" # mock country
|
||||||
|
posting_date = "2021-05-01"
|
||||||
|
|
||||||
|
invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",
|
||||||
|
"SI.2020.0001", "PI2021 - 001" ]
|
||||||
|
for name in invalid_names:
|
||||||
|
doc = frappe._dict(name=name, posting_date=posting_date)
|
||||||
|
self.assertRaises(frappe.ValidationError, validate_document_name, doc)
|
||||||
|
|
||||||
|
valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001",
|
||||||
|
"2020-PI-0001", "PI2020-0001" ]
|
||||||
|
for name in valid_names:
|
||||||
|
doc = frappe._dict(name=name, posting_date=posting_date)
|
||||||
|
try:
|
||||||
|
validate_document_name(doc)
|
||||||
|
except frappe.ValidationError:
|
||||||
|
self.fail("Valid name {} throwing error".format(name))
|
||||||
|
|
||||||
|
@patch("frappe.get_cached_value")
|
||||||
|
def test_validate_document_name_not_india(self, mock_get_cached):
|
||||||
|
mock_get_cached.return_value = "Not India"
|
||||||
|
doc = frappe._dict(name="SI$123", posting_date="2021-05-01")
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_document_name(doc)
|
||||||
|
except frappe.ValidationError:
|
||||||
|
self.fail("Regional validation related to India are being applied to other countries")
|
||||||
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, re, json
|
import frappe, re, json
|
||||||
from frappe import _
|
from frappe import _
|
||||||
import erpnext
|
import erpnext
|
||||||
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
|
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
|
||||||
from erpnext.regional.india import states, state_numbers
|
from erpnext.regional.india import states, state_numbers
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||||
@@ -14,6 +14,13 @@ from erpnext.accounts.general_ledger import make_gl_entries
|
|||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
|
|
||||||
|
|
||||||
|
GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - /
|
||||||
|
GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
|
||||||
|
GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
|
||||||
|
PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
|
||||||
|
|
||||||
|
|
||||||
def validate_gstin_for_india(doc, method):
|
def validate_gstin_for_india(doc, method):
|
||||||
if hasattr(doc, 'gst_state') and doc.gst_state:
|
if hasattr(doc, 'gst_state') and doc.gst_state:
|
||||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
doc.gst_state_number = state_numbers[doc.gst_state]
|
||||||
@@ -37,12 +44,10 @@ def validate_gstin_for_india(doc, method):
|
|||||||
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
|
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
|
||||||
|
|
||||||
if gst_category and gst_category == 'UIN Holders':
|
if gst_category and gst_category == 'UIN Holders':
|
||||||
p = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
|
if not GSTIN_UIN_FORMAT.match(doc.gstin):
|
||||||
if not p.match(doc.gstin):
|
|
||||||
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
|
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
|
||||||
else:
|
else:
|
||||||
p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
|
if not GSTIN_FORMAT.match(doc.gstin):
|
||||||
if not p.match(doc.gstin):
|
|
||||||
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
|
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
|
||||||
|
|
||||||
validate_gstin_check_digit(doc.gstin)
|
validate_gstin_check_digit(doc.gstin)
|
||||||
@@ -59,8 +64,7 @@ def validate_pan_for_india(doc, method):
|
|||||||
if doc.get('country') != 'India' or not doc.pan:
|
if doc.get('country') != 'India' or not doc.pan:
|
||||||
return
|
return
|
||||||
|
|
||||||
p = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
|
if not PAN_NUMBER_FORMAT.match(doc.pan):
|
||||||
if not p.match(doc.pan):
|
|
||||||
frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
|
frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
|
||||||
|
|
||||||
def validate_tax_category(doc, method):
|
def validate_tax_category(doc, method):
|
||||||
@@ -148,6 +152,20 @@ def get_itemised_tax_breakup_data(doc, account_wise=False):
|
|||||||
def set_place_of_supply(doc, method=None):
|
def set_place_of_supply(doc, method=None):
|
||||||
doc.place_of_supply = get_place_of_supply(doc, doc.doctype)
|
doc.place_of_supply = get_place_of_supply(doc, doc.doctype)
|
||||||
|
|
||||||
|
def validate_document_name(doc, method=None):
|
||||||
|
"""Validate GST invoice number requirements."""
|
||||||
|
country = frappe.get_cached_value("Company", doc.company, "country")
|
||||||
|
|
||||||
|
# Date was chosen as start of next FY to avoid irritating current users.
|
||||||
|
if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(doc.name) > 16:
|
||||||
|
frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series."))
|
||||||
|
|
||||||
|
if not GST_INVOICE_NUMBER_FORMAT.match(doc.name):
|
||||||
|
frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series."))
|
||||||
|
|
||||||
# don't remove this function it is used in tests
|
# don't remove this function it is used in tests
|
||||||
def test_method():
|
def test_method():
|
||||||
'''test function'''
|
'''test function'''
|
||||||
|
|||||||
Reference in New Issue
Block a user