diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py
index 9e2b00301a..83511299d7 100644
--- a/india_compliance/gst_india/api_classes/base.py
+++ b/india_compliance/gst_india/api_classes/base.py
@@ -85,6 +85,9 @@ def get(self, *args, **kwargs):
def post(self, *args, **kwargs):
return self._make_request("POST", *args, **kwargs)
+ def put(self, *args, **kwargs):
+ return self._make_request("PUT", *args, **kwargs)
+
def _make_request(
self,
method,
@@ -94,7 +97,7 @@ def _make_request(
json=None,
):
method = method.upper()
- if method not in ("GET", "POST"):
+ if method not in ("GET", "POST", "PUT"):
frappe.throw(_("Invalid method {0}").format(method))
request_args = frappe._dict(
@@ -108,7 +111,6 @@ def _make_request(
)
log_headers = request_args.headers.copy()
-
log = frappe._dict(
**self.default_log_values,
url=request_args.url,
@@ -116,7 +118,7 @@ def _make_request(
request_headers=log_headers,
)
- if method == "POST" and json:
+ if method in ["POST", "PUT"] and json:
request_args.json = json
json_data = json.copy()
@@ -135,6 +137,7 @@ def _make_request(
response = requests.request(method, **request_args)
if api_request_id := response.headers.get("x-amzn-RequestId"):
+ self.request_id = api_request_id
log.request_id = api_request_id
try:
@@ -258,7 +261,7 @@ def handle_http_code(self, status_code, response_json):
raise GatewayTimeoutError
def generate_request_id(self, length=12):
- return frappe.generate_hash(length=length)
+ return f"IC{frappe.generate_hash(length=length - 2)}".upper()
def mask_sensitive_info(self, log):
request_headers = log.request_headers
diff --git a/india_compliance/gst_india/api_classes/taxpayer_base.py b/india_compliance/gst_india/api_classes/taxpayer_base.py
index 2533063180..3900abf0b9 100644
--- a/india_compliance/gst_india/api_classes/taxpayer_base.py
+++ b/india_compliance/gst_india/api_classes/taxpayer_base.py
@@ -115,6 +115,9 @@ class TaxpayerAuthenticate(BaseAPI):
# "AUTH4034": "invalid_otp", # Invalid OTP
"AUTH4038": "authorization_failed", # Session Expired
"TEC4002": "invalid_public_key",
+ "RET13506": "OTP is either expired or incorrect",
+ "RET00003": "Return Form already ready to be filed", # Actions performed on portal directly
+ "RET09001": "Latest Summary is not available. Please generate summary and try again.", # Actions performed on portal directly
}
def request_otp(self):
@@ -189,6 +192,13 @@ def refresh_auth_token(self):
endpoint="authenticate",
)
+ def initiate_otp_for_evc(self, pan, form_type):
+ return self.get(
+ action="EVCOTP",
+ params={"pan": pan, "form_type": form_type},
+ endpoint="authenticate",
+ )
+
def decrypt_response(self, response):
values = {}
@@ -317,6 +327,7 @@ def _request(
self,
method,
action=None,
+ return_type=None,
return_period=None,
params=None,
endpoint=None,
@@ -331,6 +342,10 @@ def _request(
return response
headers = {"auth-token": auth_token}
+ if return_type:
+ headers["rtn_typ"] = return_type
+ headers["userrole"] = return_type
+
if return_period:
headers["ret_period"] = return_period
@@ -353,6 +368,9 @@ def get(self, *args, **kwargs):
def post(self, *args, **kwargs):
return self._request("post", *args, **kwargs)
+ def put(self, *args, **kwargs):
+ return self._request("put", *args, **kwargs)
+
def before_request(self, request_args):
self.encrypt_request(request_args.get("json"))
@@ -382,6 +400,23 @@ def decrypt_response(self, response):
return response
+ def encrypt_request(self, json):
+ if not json:
+ return
+
+ super().encrypt_request(json)
+
+ if json.get("data"):
+ b64_data = b64encode(frappe.as_json(json.get("data")).encode())
+ json["data"] = aes_encrypt_data(b64_data.decode(), self.session_key)
+
+ if json.get("st") == "EVC":
+ sid_key = json.get("sid").encode()
+ json["sign"] = hmac_sha256(b64_data, sid_key)
+
+ else:
+ json["hmac"] = hmac_sha256(b64_data, self.session_key)
+
def handle_error_response(self, response):
success_value = response.get("status_cd") != 0
diff --git a/india_compliance/gst_india/api_classes/taxpayer_returns.py b/india_compliance/gst_india/api_classes/taxpayer_returns.py
index 268fae30d3..dffda10347 100644
--- a/india_compliance/gst_india/api_classes/taxpayer_returns.py
+++ b/india_compliance/gst_india/api_classes/taxpayer_returns.py
@@ -27,6 +27,30 @@ def download_files(self, return_period, token, otp=None):
return_period, token, action="FILEDET", endpoint="returns", otp=otp
)
+ def get_return_status(self, return_period, reference_id, otp=None):
+ return self.get(
+ action="RETSTATUS",
+ return_period=return_period,
+ params={"ret_period": return_period, "ref_id": reference_id},
+ endpoint="returns",
+ otp=otp,
+ )
+
+ def proceed_to_file(self, return_type, return_period, otp=None):
+ return self.post(
+ return_type=return_type,
+ return_period=return_period,
+ json={
+ "action": "RETNEWPTF",
+ "data": {
+ "gstin": self.company_gstin,
+ "ret_period": return_period,
+ }, # "isnil": "N" / "Y"
+ },
+ endpoint="returns/gstrptf",
+ otp=otp,
+ )
+
class GSTR2bAPI(ReturnsAPI):
API_NAME = "GSTR-2B"
@@ -75,6 +99,7 @@ def setup(self, doc=None, *, company_gstin=None):
super().setup(company_gstin=company_gstin)
def get_gstr_1_data(self, action, return_period, otp=None):
+ # action: RETSUM for summary
return self.get(
action=action,
return_period=return_period,
@@ -91,3 +116,37 @@ def get_einvoice_data(self, section, return_period, otp=None):
endpoint="returns/einvoice",
otp=otp,
)
+
+ def save_gstr_1_data(self, return_period, data, otp=None):
+ return self.put(
+ return_period=return_period,
+ json={"action": "RETSAVE", "data": data},
+ endpoint="returns/gstr1",
+ otp=otp,
+ )
+
+ def reset_gstr_1_data(self, return_period, otp=None):
+ return self.post(
+ return_period=return_period,
+ json={
+ "action": "RESET",
+ "data": {
+ "gstin": self.company_gstin,
+ "ret_period": return_period,
+ },
+ },
+ endpoint="returns/gstr1",
+ otp=otp,
+ )
+
+ def file_gstr_1(self, return_period, summary_data, pan, evc_otp):
+ return self.post(
+ return_period=return_period,
+ json={
+ "action": "RETFILE",
+ "data": summary_data,
+ "st": "EVC",
+ "sid": f"{pan}|{evc_otp}",
+ },
+ endpoint="returns/gstr1",
+ )
diff --git a/india_compliance/gst_india/client_scripts/party.js b/india_compliance/gst_india/client_scripts/party.js
index f6e7931d56..9f70e26ff4 100644
--- a/india_compliance/gst_india/client_scripts/party.js
+++ b/india_compliance/gst_india/client_scripts/party.js
@@ -85,15 +85,7 @@ function validate_pan(doctype) {
let { pan } = frm.doc;
if (!pan || pan.length < 10) return;
- if (pan.length > 10) {
- frappe.throw(__("PAN should be 10 characters long"));
- }
-
- pan = pan.trim().toUpperCase();
-
- if (!PAN_REGEX.test(pan)) {
- frappe.throw(__("Invalid PAN format"));
- }
+ pan = india_compliance.validate_pan(pan);
frm.doc.pan = pan;
frm.refresh_field("pan");
diff --git a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py
index 3b23b2b4d3..e29b40661d 100644
--- a/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py
+++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py
@@ -3,15 +3,17 @@
import itertools
import frappe
-from frappe import unscrub
+from frappe import _, unscrub
from frappe.utils import flt
+from india_compliance.gst_india.api_classes.taxpayer_returns import GSTR1API
from india_compliance.gst_india.utils.__init__ import get_month_or_quarter_dict
-from india_compliance.gst_india.utils.gstr_1 import GSTR1_SubCategory
+from india_compliance.gst_india.utils.gstr_1 import GovJsonKey, GSTR1_SubCategory
from india_compliance.gst_india.utils.gstr_1.__init__ import (
CATEGORY_SUB_CATEGORY_MAPPING,
SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX,
SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE,
+ GSTR1_Category,
GSTR1_DataField,
)
from india_compliance.gst_india.utils.gstr_1.gstr_1_download import (
@@ -19,11 +21,19 @@
)
from india_compliance.gst_india.utils.gstr_1.gstr_1_json_map import (
GSTR1BooksData,
+ convert_to_internal_data_format,
summarize_retsum_data,
)
MONTH = list(get_month_or_quarter_dict().keys())[4:]
QUARTER = ["Jan-Mar", "Apr-Jun", "Jul-Sep", "Oct-Dec"]
+status_code_map = {
+ "P": "Processed",
+ "PE": "Processed with Errors",
+ "ER": "Error",
+ "IP": "In Progress",
+}
+MAXIMUM_UPLOAD_SIZE = 5200000
class SummarizeGSTR1:
@@ -178,9 +188,11 @@ def default_subcategory_summary(self, subcategory):
@staticmethod
def count_doc_issue_summary(summary_row, data_row):
- summary_row["no_of_records"] += data_row.get(
- GSTR1_DataField.TOTAL_COUNT.value, 0
- ) - data_row.get(GSTR1_DataField.CANCELLED_COUNT.value, 0)
+ summary_row["no_of_records"] += (
+ data_row.get(GSTR1_DataField.TOTAL_COUNT.value, 0)
+ - data_row.get(GSTR1_DataField.CANCELLED_COUNT.value, 0)
+ - data_row.get(GSTR1_DataField.DRAFT_COUNT.value, 0)
+ )
@staticmethod
def count_hsn_summary(summary_row):
@@ -229,15 +241,15 @@ def get_reconcile_gstr1_data(self, gov_data, books_data):
if not books_subdata and not gov_subdata:
continue
- is_list = False # Object Type for the subdata_value
+ # Object Type for the subdata_value
+ is_list = self.is_list(books_subdata, gov_subdata)
+
+ self.sanitize_books_data(books_subdata, is_list)
reconcile_subdata = {}
# Books vs Gov
for key, books_value in books_subdata.items():
- if not reconcile_subdata:
- is_list = isinstance(books_value, list)
-
gov_value = gov_subdata.get(key)
reconcile_row = self.get_reconciled_row(books_value, gov_value)
@@ -252,9 +264,6 @@ def get_reconcile_gstr1_data(self, gov_data, books_data):
# Update each row in Books Data
for row in books_values:
- if row.get("upload_status") == "Missing in Books":
- continue
-
if not gov_value:
row["upload_status"] = "Not Uploaded"
continue
@@ -269,9 +278,6 @@ def get_reconcile_gstr1_data(self, gov_data, books_data):
if key in books_subdata:
continue
- if not reconcile_subdata:
- is_list = isinstance(gov_value, list)
-
reconcile_subdata[key] = self.get_reconciled_row(None, gov_value)
if not update_books_match:
@@ -290,13 +296,21 @@ def get_reconcile_gstr1_data(self, gov_data, books_data):
if reconcile_subdata:
reconciled_data[subcategory] = reconcile_subdata
- if update_books_match:
- self.update_json_for("books", books_data)
-
+ self.update_json_for("books", books_data)
self.update_json_for("reconcile", reconciled_data)
return reconciled_data
+ def sanitize_books_data(self, books_subdata, is_list):
+ for key, value in books_subdata.copy().items():
+ values = value if is_list else [value]
+ if values[0].get("upload_status") == "Missing in Books":
+ del books_subdata[key]
+ continue
+
+ for row in values:
+ row.pop("upload_status", None)
+
@staticmethod
def get_reconciled_row(books_row, gov_row):
"""
@@ -405,6 +419,13 @@ def get_empty_row(row: dict, unrequired_keys=None):
return empty_row
+ @staticmethod
+ def is_list(books_subdata: dict, gov_subdata: dict):
+ book_row = next(iter(books_subdata.values()), None)
+ gov_row = next(iter(gov_subdata.values()), None)
+
+ return isinstance(book_row or gov_row, list)
+
class AggregateInvoices:
IGNORED_FIELDS = {GSTR1_DataField.TAX_RATE.value, GSTR1_DataField.DOC_VALUE.value}
@@ -543,7 +564,7 @@ def generate_gstr1_data(self, filters, callback=None):
data["books"] = self.normalize_data(books_data)
self.summarize_data(data)
- return callback and callback(data, filters)
+ return callback and callback(filters)
def generate_only_books_data(self, data, filters, callback=None):
status = "Not Filed"
@@ -554,7 +575,7 @@ def generate_only_books_data(self, data, filters, callback=None):
data["status"] = status
self.summarize_data(data)
- return callback and callback(data, filters)
+ return callback and callback(filters)
# GET DATA
def get_gov_gstr1_data(self):
@@ -673,6 +694,396 @@ def normalize_data(data):
return data
+class FileGSTR1:
+ def reset_gstr1(self):
+ # reset called after proceed to file
+ verify_request_in_progress(self)
+
+ self.db_set({"filing_status": "Not Filed"})
+
+ api = GSTR1API(self)
+ response = api.reset_gstr_1_data(self.return_period)
+
+ set_gstr1_actions(self, "reset", response.get("reference_id"), api.request_id)
+
+ def process_reset_gstr1(self):
+ if not self.actions:
+ return
+
+ api = GSTR1API(self)
+ response = None
+
+ doc = self.get_unprocessed_action("reset")
+
+ if not doc:
+ return
+
+ response = api.get_return_status(self.return_period, doc.token)
+
+ if response.get("status_cd") != "IP":
+ doc.db_set({"status": status_code_map.get(response.get("status_cd"))})
+ enqueue_notification(
+ self.return_period,
+ "reset",
+ response.get("status_cd"),
+ self.gstin,
+ )
+
+ if response.get("status_cd") == "P":
+ self.update_json_for("unfiled", {}, reset_reconcile=True)
+
+ return response
+
+ def upload_gstr1(self, json_data):
+ if not json_data:
+ return
+
+ verify_request_in_progress(self)
+
+ keys = {category.value for category in GovJsonKey}
+ if all(key not in json_data for key in keys):
+ frappe.msgprint(_("No data to upload"), indicator="red")
+ return
+
+ # upload data after proceed to file
+ self.db_set({"filing_status": "Not Filed"})
+
+ # remove error file if it exists
+ self.remove_json_for("upload_error")
+
+ # Make API Request
+ api = GSTR1API(self)
+ response = api.save_gstr_1_data(self.return_period, json_data)
+
+ set_gstr1_actions(self, "upload", response.get("reference_id"), api.request_id)
+
+ def process_upload_gstr1(self):
+ if not self.actions:
+ return
+
+ api = GSTR1API(self)
+ response = None
+
+ doc = self.get_unprocessed_action("upload")
+
+ if not doc:
+ return
+
+ response = api.get_return_status(self.return_period, doc.token)
+ status_cd = response.get("status_cd")
+
+ if status_cd != "IP":
+ doc.db_set({"status": status_code_map.get(status_cd)})
+ enqueue_notification(
+ self.return_period,
+ "upload",
+ status_cd,
+ self.gstin,
+ api.request_id if status_cd == "ER" else None,
+ )
+
+ if status_cd == "PE":
+ response["error_report"] = convert_to_internal_data_format(
+ response.get("error_report"), True
+ )
+ self.update_json_for("upload_error", response)
+
+ if status_cd == "P":
+ self.db_set({"filing_status": "Uploaded"})
+
+ self.update_json_for("unfiled", self.get_json_for("books"))
+ self.update_json_for("unfiled_summary", self.get_json_for("books_summary"))
+
+ self.update_json_for("reconcile", {})
+ self.update_json_for("reconcile_summary", {})
+
+ return response
+
+ def proceed_to_file_gstr1(self):
+ verify_request_in_progress(self)
+
+ api = GSTR1API(self)
+ response = api.proceed_to_file("GSTR1", self.return_period)
+
+ if response.error and response.error.error_cd == "RET00003":
+ return self.fetch_and_compare_summary(api)
+
+ set_gstr1_actions(
+ self, "proceed_to_file", response.get("reference_id"), api.request_id
+ )
+
+ def process_proceed_to_file_gstr1(self):
+ if not self.actions:
+ return
+
+ api = GSTR1API(self)
+ response = None
+
+ doc = self.get_unprocessed_action("proceed_to_file")
+
+ if not doc:
+ return
+
+ response = api.get_return_status(self.return_period, doc.token)
+
+ if response.get("status_cd") == "IP":
+ return response
+
+ doc.db_set({"status": status_code_map.get(response.get("status_cd"))})
+
+ return self.fetch_and_compare_summary(api, response)
+
+ def fetch_and_compare_summary(self, api, response=None):
+ if response is None:
+ response = {}
+
+ summary = api.get_gstr_1_data("RETSUM", self.return_period)
+ if summary.error:
+ return
+
+ self.update_json_for("authenticated_summary", summary)
+
+ mapped_summary = self.get_json_for("books_summary")
+ gov_summary = convert_to_internal_data_format(summary).get("summary")
+ gov_summary = summarize_retsum_data(gov_summary.values())
+
+ differing_categories = get_differing_categories(mapped_summary, gov_summary)
+
+ if not differing_categories:
+ self.db_set({"filing_status": "Ready to File"})
+ response["filing_status"] = "Ready to File"
+
+ else:
+ self.db_set({"filing_status": "Not Filed"})
+ response.update(
+ {
+ "filing_status": "Not Filed",
+ "differing_categories": differing_categories,
+ }
+ )
+ enqueue_notification(
+ self.return_period,
+ "proceed_to_file",
+ response.get("status_cd"),
+ self.gstin,
+ api.request_id,
+ )
+
+ return response
+
+ def file_gstr1(self, pan, otp):
+ verify_request_in_progress(self)
+
+ summary = self.get_json_for("authenticated_summary")
+ api = GSTR1API(self)
+ response = api.file_gstr_1(self.return_period, summary, pan, otp)
+
+ if response.error and response.error.error_cd == "RET09001":
+ self.db_set({"filing_status": "Not Filed"})
+ self.update_json_for("authenticated_summary", None)
+
+ if response.get("ack_num"):
+ frappe.db.set_value("GSTIN", self.gstin, "last_pan_used_for_gstr", pan)
+ self.db_set(
+ {
+ "filing_status": "Filed",
+ "filing_date": frappe.utils.nowdate(),
+ "acknowledgement_number": response.get("ack_num"),
+ }
+ )
+
+ set_gstr1_actions(self, "file", response.get("ack_num"), api.request_id)
+
+ self.remove_json_for("upload_error")
+
+ # TODO: 2nd phase Accounting Entry.
+
+ return response
+
+ def get_amendment_data(self):
+ authenticated_summary = convert_to_internal_data_format(
+ self.get_json_for("authenticated_summary")
+ ).get("summary")
+ authenticated_summary = summarize_retsum_data(authenticated_summary.values())
+
+ non_amended_entries = {
+ "total_igst_amount": 0,
+ "total_cgst_amount": 0,
+ "total_sgst_amount": 0,
+ "total_cess_amount": 0,
+ }
+ amended_liability = {}
+
+ for data in authenticated_summary:
+ if "Net Liability from Amendments" == data["description"]:
+ amended_liability = data
+ elif data.get("consider_in_total_taxable_value") or data.get(
+ "consider_in_total_tax"
+ ):
+ for key, value in data.items():
+ if key not in non_amended_entries:
+ continue
+
+ non_amended_entries[key] += value
+
+ return {
+ "non_amended_liability": non_amended_entries,
+ "amended_liability": amended_liability,
+ }
+
+
+def verify_request_in_progress(return_log):
+ for row in return_log.actions:
+ if not row.status:
+ frappe.throw(
+ _(
+ "There is a {0} request in progress. Please wait for the process to complete."
+ ).format(row.request_type)
+ )
+
+
+def get_differing_categories(mapped_summary, gov_summary):
+ KEYS_TO_COMPARE = {
+ "total_cess_amount",
+ "total_cgst_amount",
+ "total_igst_amount",
+ "total_sgst_amount",
+ "total_taxable_value",
+ }
+
+ # TODO: Check this for all categories
+ CATEGORY_KEYS = {
+ (GSTR1_Category.NIL_EXEMPT.value): {
+ "total_exempted_amount",
+ "total_nil_rated_amount",
+ "total_non_gst_amount",
+ },
+ (GSTR1_Category.DOC_ISSUE.value): {
+ "no_of_records",
+ },
+ }
+
+ IGNORED_CATEGORIES = {"Net Liability from Amendments"}
+
+ gov_summary = {row["description"]: row for row in gov_summary if row["indent"] == 0}
+ compared_categories = set()
+ differing_categories = set()
+
+ # This will intentionally skip the row in govt_summary with amended data
+ for row in mapped_summary:
+ if row["indent"] != 0:
+ continue
+
+ category = row["description"]
+ if category in IGNORED_CATEGORIES:
+ continue
+
+ compared_categories.add(category)
+ gov_entry = gov_summary.get(category, {})
+
+ keys_to_compare = CATEGORY_KEYS.get(category, KEYS_TO_COMPARE)
+
+ for key in keys_to_compare:
+ if gov_entry.get(key, 0) != row.get(key):
+ differing_categories.add(category)
+ break
+
+ for row in gov_summary.values():
+ # Amendments are with indent 1. Hence auto-skipped
+ category = row["description"]
+ if category in IGNORED_CATEGORIES:
+ continue
+
+ if category in compared_categories:
+ continue
+
+ keys_to_compare = CATEGORY_KEYS.get(row["description"], KEYS_TO_COMPARE)
+
+ for key in keys_to_compare:
+ if row.get(key, 0) != 0:
+ differing_categories.add(row["description"])
+ break
+
+ return differing_categories
+
+
+def set_gstr1_actions(doc, request_type, token, request_id):
+ if token:
+ doc.append(
+ "actions",
+ {
+ "request_type": request_type,
+ "token": token,
+ "creation_time": frappe.utils.now_datetime(),
+ },
+ )
+ doc.save()
+ enqueue_actions(token, request_id)
+
+
+def enqueue_actions(token, request_id):
+ frappe.enqueue(
+ "india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1.add_integration_request",
+ queue="long",
+ token=token,
+ request_id=request_id,
+ )
+
+
+def add_integration_request(token, request_id):
+ doc_name = frappe.db.get_value("Integration Request", {"request_id": request_id})
+ if doc_name:
+ frappe.db.set_value(
+ "GSTR Action", {"token": token}, {"integration_request": doc_name}
+ )
+
+
+def enqueue_notification(
+ return_period, request_type, status_cd, gstin, request_id=None
+):
+ frappe.enqueue(
+ "india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1.create_notification",
+ queue="long",
+ return_period=return_period,
+ request_type=request_type,
+ status_cd=status_cd,
+ gstin=gstin,
+ request_id=request_id,
+ )
+
+
+def create_notification(return_period, request_type, status_cd, gstin, request_id=None):
+ # request_id shows failure response
+ status_message_map = {
+ "P": f"Data {request_type} for GSTIN {gstin} and return period {return_period} has been successfully completed.",
+ "PE": f"Data {request_type} for GSTIN {gstin} and return period {return_period} is completed with errors",
+ "ER": f"Data {request_type} for GSTIN {gstin} and return period {return_period} has encountered errors",
+ }
+
+ if request_id and (
+ doc_name := frappe.db.get_value(
+ "Integration Request", {"request_id": request_id}
+ )
+ ):
+ document_type = "Integration Request"
+ document_name = doc_name
+ else:
+ document_type = document_name = "GSTR-1 Beta"
+
+ notification = frappe.get_doc(
+ {
+ "doctype": "Notification Log",
+ "for_user": frappe.session.user,
+ "type": "Alert",
+ "document_type": document_type,
+ "document_name": document_name,
+ "subject": f"Data {request_type} for GSTIN {gstin} and return period {return_period}",
+ "email_content": status_message_map.get(status_cd),
+ }
+ )
+ notification.insert()
+
+
def update_filters(filters, filing_preference):
is_quarterly = 1 if filing_preference == "Quarterly" else 0
diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js
index 6c5b7b5bf4..5ddf4c5142 100644
--- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js
+++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js
@@ -2,6 +2,24 @@
// For license information, please see license.txt
frappe.ui.form.on("GST Return Log", {
+ onload(frm) {
+ const attachFields = ['unfiled', 'unfiled_summary', 'filed', 'filed_summary', 'upload_error', 'authenticated_summary', 'books', 'books_summary', 'reconcile', 'reconcile_summary'];
+
+ attachFields.forEach(field => {
+ $(frm.fields_dict[field].wrapper).on('click', '.control-value a', function (e) {
+ e.preventDefault();
+
+ const args = {
+ cmd: "india_compliance.gst_india.doctype.gst_return_log.gst_return_log.download_file",
+ file_field: field,
+ name: frm.doc.name,
+ doctype: frm.doc.doctype,
+ file_name: `${field}.gz`
+ };
+ open_url_post(frappe.request.url, args);
+ });
+ });
+ },
refresh(frm) {
const [month_or_quarter, year] = india_compliance.get_month_year_from_period(
frm.doc.return_period, frm.doc.filing_preference === "Monthly" ? 0 : 1
diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json
index 95f1bb77df..79689d8653 100644
--- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json
+++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json
@@ -21,9 +21,11 @@
"section_break_oisv",
"unfiled",
"filed",
+ "upload_error",
"column_break_hxfu",
"unfiled_summary",
"filed_summary",
+ "authenticated_summary",
"computed_data_section",
"is_latest_data",
"section_break_emlz",
@@ -33,7 +35,9 @@
"reconciled_data_section",
"reconcile",
"column_break_ndup",
- "reconcile_summary"
+ "reconcile_summary",
+ "pending_request_info_section",
+ "actions"
],
"fields": [
{
@@ -192,7 +196,29 @@
{
"fieldname": "filing_preference",
"fieldtype": "Data",
- "label": "Filing Preference",
+ "label": "Filing Preference"
+ },
+ {
+ "fieldname": "upload_error",
+ "fieldtype": "Attach",
+ "label": "Upload Error Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "authenticated_summary",
+ "fieldtype": "Attach",
+ "label": "Authenticated Summary",
+ "read_only": 1
+ },
+ {
+ "fieldname": "pending_request_info_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "actions",
+ "fieldtype": "Table",
+ "label": "Actions",
+ "options": "GSTR Action",
"read_only": 1
}
],
diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py
index 81f83fd71a..52283eb89a 100644
--- a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py
+++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py
@@ -16,6 +16,7 @@
)
from india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1 import (
+ FileGSTR1,
GenerateGSTR1,
)
from india_compliance.gst_india.utils import is_production_api_enabled
@@ -23,7 +24,7 @@
DOCTYPE = "GST Return Log"
-class GSTReturnLog(GenerateGSTR1, Document):
+class GSTReturnLog(GenerateGSTR1, FileGSTR1, Document):
@property
def status(self):
return self.generation_status
@@ -195,6 +196,24 @@ def get_applicable_file_fields(self, settings=None):
return fields
+ def get_unprocessed_action(self, action):
+ for row in self.get("actions") or []:
+ if row.request_type == action and not row.status:
+ return row
+
+
+@frappe.whitelist()
+def download_file():
+ frappe.has_permission("GST Return Log", "read")
+
+ data = frappe._dict(frappe.local.form_dict)
+ frappe.response["filename"] = data["file_name"]
+
+ file = get_file_doc(data["doctype"], data["name"], data["file_field"])
+ frappe.response["filecontent"] = file.get_content(encodings=[])
+
+ frappe.response["type"] = "download"
+
def process_gstr_1_returns_info(company, gstin, response):
return_info = {}
@@ -302,6 +321,7 @@ def get_file_doc(doctype, docname, attached_to_field):
)
except frappe.DoesNotExistError:
+ frappe.clear_last_message()
return None
diff --git a/india_compliance/gst_india/doctype/gstin/gstin.json b/india_compliance/gst_india/doctype/gstin/gstin.json
index a5acf0c6fd..d0bcb0e52a 100644
--- a/india_compliance/gst_india/doctype/gstin/gstin.json
+++ b/india_compliance/gst_india/doctype/gstin/gstin.json
@@ -14,6 +14,7 @@
"last_updated_on",
"cancelled_date",
"is_blocked",
+ "last_pan_used_for_gstr",
"section_break_ttzc",
"gstr_1_filed_upto"
],
@@ -77,12 +78,18 @@
"fieldtype": "Date",
"hidden": 1,
"label": "GSTR-1 Filed Upto"
+ },
+ {
+ "fieldname": "last_pan_used_for_gstr",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Last PAN Used for GSTR"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-06-14 13:59:53.998262",
+ "modified": "2024-09-04 17:12:30.951878",
"modified_by": "Administrator",
"module": "GST India",
"name": "GSTIN",
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css
index 63ba3901e5..c7092e98c6 100644
--- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css
@@ -6,6 +6,10 @@ div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .section-body {
margin-top: 0;
}
+div[data-page-route="GSTR-1 Beta"] .section-body {
+ max-width: 100% !important;
+}
+
div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .form-tabs-list {
margin-top: var(--margin-sm);
display: flex;
@@ -100,13 +104,17 @@ div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item > .nav-link.active {
color: var(--primary);
}
-div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item:first-child:not(:has(.disabled)):hover {
+div[data-page-route="GSTR-1 Beta"]
+ .custom-tabs
+ > .nav-item:first-child:not(:has(.disabled)):hover {
background-color: var(--btn-default-hover-bg);
border-radius: 0.4rem 0 0 0.4rem;
color: var(--text-color);
}
-div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item:last-child:not(:has(.disabled)):hover {
+div[data-page-route="GSTR-1 Beta"]
+ .custom-tabs
+ > .nav-item:last-child:not(:has(.disabled)):hover {
background-color: var(--btn-default-hover-bg);
border-radius: 0 0.4rem 0.4rem 0;
color: var(--text-color);
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js
index 37c3c385b2..509b601834 100644
--- a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js
@@ -39,7 +39,7 @@ const GSTR1_SubCategory = {
DOC_ISSUE: "Document Issued",
SUPECOM_52: "Liable to collect tax u/s 52(TCS)",
- SUPECOM_9_5: "Liable to pay tax u/s 9(5)"
+ SUPECOM_9_5: "Liable to pay tax u/s 9(5)",
};
const INVOICE_TYPE = {
@@ -136,6 +136,9 @@ frappe.ui.form.on(DOCTYPE, {
),
"orange"
);
+ frm.page.set_primary_action(__("Generate"), () =>
+ frm.gstr1.gstr1_action.generate_gstr1_data(frm)
+ );
});
frappe.realtime.on("show_message", message => {
@@ -153,7 +156,7 @@ frappe.ui.form.on(DOCTYPE, {
});
frappe.realtime.on("gstr1_data_prepared", message => {
- const { data, filters } = message;
+ const { filters } = message;
if (
frm.doc.company_gstin !== filters.company_gstin ||
@@ -166,8 +169,8 @@ frappe.ui.form.on(DOCTYPE, {
frm.set_value("is_quarterly", filters.is_quarterly)
}
- frappe.after_ajax(() => {
- frm.doc.__gst_data = data ;
+ frm.taxpayer_api_call("generate_gstr1").then(r => {
+ frm.doc.__gst_data = r.message;
frm.trigger("load_gstr1_data");
});
});
@@ -203,15 +206,16 @@ frappe.ui.form.on(DOCTYPE, {
},
refresh(frm) {
- // Primary Action
frm.disable_save();
- frm.page.set_primary_action(__("Generate"), () =>
- frm.taxpayer_api_call("generate_gstr1")
- );
- // After indicator set in frappe refresh
- if (frm.doc.__gst_data) frm.gstr1.render_indicator();
- else frm.page.clear_indicator();
+ frm.gstr1?.render_form_actions();
+
+ if (!frm.doc.__gst_data) {
+ frm.page.clear_indicator();
+ return;
+ }
+
+ frm.gstr1.render_indicator();
},
load_gstr1_data(frm) {
@@ -250,6 +254,11 @@ class GSTR1 {
name: "filed",
_TabManager: FiledTab,
},
+ {
+ label: __("Error"),
+ name: "error",
+ _TabManager: ErrorTab,
+ },
];
constructor(frm) {
@@ -259,7 +268,7 @@ class GSTR1 {
init(frm) {
this.frm = frm;
- this.data = frm.doc._data;
+ this.data = null;
this.filters = [];
this.$wrapper = frm.fields_dict.tabs_html.$wrapper;
}
@@ -304,11 +313,20 @@ class GSTR1 {
return;
}
+ if (
+ this.data.status == "Ready to File" &&
+ ["books", "unfiled", "reconcile"].includes(tab_name)
+ ) {
+ tab.hide();
+ _tab.shown = false;
+ return;
+ }
+
tab.show();
_tab.shown = true;
tab.tabmanager.refresh_data(
- this.data[tab_name],
- this.data[`${tab_name}_summary`],
+ this.data[tab_name] || {},
+ this.data[`${tab_name}_summary`] || [],
this.status
);
});
@@ -432,9 +450,48 @@ class GSTR1 {
const color = this.status === "Filed" ? "green" : "orange";
this.$wrapper.find(`[data-fieldname="filed_tab"]`).html(tab_name);
+ this.$wrapper.find(`[data-fieldname="error_tab"]`).html("⛔ Error");
this.frm.page.set_indicator(this.status, color);
}
+ render_form_actions() {
+ this.gstr1_action = new GSTR1Action(this.frm);
+
+ // Custom Buttons
+ if (this.data) {
+ if (this.status === "Filed") return;
+ if (!is_gstr1_api_enabled()) return;
+
+ this.frm.add_custom_button(__("Reset"), () =>
+ this.gstr1_action.reset_gstr1_data()
+ );
+ }
+
+ // Primary Button
+ const actions = {
+ Generate: this.gstr1_action.generate_gstr1_data,
+ Upload: this.gstr1_action.upload_gstr1_data,
+ "Proceed to File": this.gstr1_action.proceed_to_file,
+ File: this.gstr1_action.file_gstr1_data,
+ };
+
+ let primary_button_label = {
+ "Not Filed": "Upload",
+ "Uploaded": "Proceed to File",
+ "Ready to File": "File",
+ }[this.status] || "Generate";
+
+ if (this.status === "Ready to File") {
+ this.frm.add_custom_button(__("Mark as Unfiled"), () => {
+ this.gstr1_action.mark_as_unfiled();
+ });
+ }
+
+ this.frm.page.set_primary_action(__(primary_button_label), () =>
+ actions[primary_button_label].call(this.gstr1_action)
+ );
+ }
+
// SETUP
setup_filter_button() {
@@ -668,6 +725,12 @@ class GSTR1 {
let element = $('[data-fieldname="data_section"]');
element.prepend(gst_liability_html);
}
+
+ show_suggested_jv_dialog() {
+ // Get JV table
+ // show in dialog as a confirm dialog
+ // on Create JV btn => create JV
+ }
}
class TabManager {
@@ -991,7 +1054,7 @@ class TabManager {
? `
${value}
Description | +Total IGST | +Total CGST | +Total SGST | +Total Cess | +
---|
OTP is either expired or incorrect.
`); + + this.toggle_actions(true); + return; + } + + this.filing_dialog.hide(); + + if (response.error?.error_cd === "RET09001") { + this.frm.page.set_primary_action("Upload", () => + this.upload_gstr1_data() + ); + this.frm.page.set_indicator("Not Filed", "orange"); + this.frm.gstr1.status = "Not Filed"; + frappe.msgprint( + __( + "Latest Summary is not available. Please generate summary and try again." + ) + ); + } + + if (response.ack_num) { + this.frm.taxpayer_api_call("generate_gstr1", { message: "Verifying filed GSTR-1" }).then(r => { + this.frm.doc.__gst_data = r.message; + this.frm.trigger("load_gstr1_data"); + this.frm.gstr1.show_suggested_jv_dialog(); + }); + } + } +} + +class GSTR1Action extends FileGSTR1Dialog { + RETRY_INTERVALS = [2000, 3000, 15000, 30000, 60000, 120000, 300000, 600000, 720000]; // 5 second, 15 second, 30 second, 1 min, 2 min, 5 min, 10 min, 12 min + + constructor(frm) { + super(frm); + this.frm = frm; + this.defaults = { + month_or_quarter: frm.doc.month_or_quarter, + year: frm.doc.year, + company_gstin: frm.doc.company_gstin, + }; + } + + generate_gstr1_data() { + this.frm.taxpayer_api_call("generate_gstr1").then(r => { + if (!r.message) return; + this.frm.doc.__gst_data = r.message; + this.frm.trigger("load_gstr1_data"); + + const request_types = ["upload", "reset", "proceed_to_file"]; + request_types.map(request_type => + this.fetch_status_with_retry(request_type, true) + ); + }); + } + + upload_gstr1_data() { + if (this.is_request_in_progress()) return; + + const action = "upload"; + + frappe.show_alert(__("Uploading data to GSTN")); + this.perform_gstr1_action(action, (response) => { + // No data to upload + if (response._server_messages && response._server_messages.length) { + this.toggle_actions(true); + return + } + + this.fetch_status_with_retry(action) + }); + } + + reset_gstr1_data() { + if (this.is_request_in_progress()) return; + + const action = "reset"; + + frappe.confirm( + __( + "All the details saved in different tables shall be deleted after reset.${__( + "Summary for the following categories has not matched. Please sync with GSTIN." + )}
+