From a44df63a9119148adda806bae077f96330192bfb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Mar 2021 17:12:53 +0530 Subject: [PATCH 1/3] fix: Add warning for invalid GST invoice numbers GST Invoice numbers should be 16 characters alphanumeric with dash(/) or slash(-) only. Add check for doc.name before saving and warn about naming series. --- erpnext/hooks.py | 3 +++ erpnext/regional/india/utils.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 59639ffc437..f87769c182c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -278,6 +278,9 @@ doc_events = { ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, + ('Sales Invoice', 'Purchase Invoice'): { + 'validate': ['erpnext.regional.india.utils.validate_document_name'] + }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index cb30605291c..7980d0b9ffe 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe, re, json from frappe import _ 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.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount from erpnext.controllers.accounts_controller import get_taxes_and_charges @@ -148,6 +148,20 @@ def get_itemised_tax_breakup_data(doc, account_wise=False): def set_place_of_supply(doc, method=None): 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") + + 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.")) + + gst_doc_name_pattern = re.compile(r"^[a-zA-Z0-9\-/]+$") + if not gst_doc_name_pattern.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 def test_method(): '''test function''' From 9b3f5d5f678507027e8ffd1349259db9f46f6615 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 3 Mar 2021 12:38:52 +0530 Subject: [PATCH 2/3] test: add tests for gst invoice name checks --- erpnext/regional/india/test_utils.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 erpnext/regional/india/test_utils.py diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py new file mode 100644 index 00000000000..7ce27f6cf5a --- /dev/null +++ b/erpnext/regional/india/test_utils.py @@ -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") From 7c4c42ad67c5b358c055bc7e1664f3ea04dd3c9c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 3 Mar 2021 14:56:19 +0530 Subject: [PATCH 3/3] refactor: move regex patterns to global variables --- erpnext/regional/india/utils.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 7980d0b9ffe..1a618d6cf56 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -14,6 +14,13 @@ from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.utils import get_account_currency 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): if hasattr(doc, 'gst_state') and 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.")) 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 p.match(doc.gstin): + if not GSTIN_UIN_FORMAT.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")) 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 p.match(doc.gstin): + if not GSTIN_FORMAT.match(doc.gstin): frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of 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: return - p = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") - if not p.match(doc.pan): + if not PAN_NUMBER_FORMAT.match(doc.pan): frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN.")) def validate_tax_category(doc, method): @@ -152,14 +156,14 @@ 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.")) - gst_doc_name_pattern = re.compile(r"^[a-zA-Z0-9\-/]+$") - if not gst_doc_name_pattern.match(doc.name): + 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 @@ -814,4 +818,4 @@ def get_regional_round_off_accounts(company, account_list): account_list.extend(gst_account_list) - return account_list \ No newline at end of file + return account_list