diff --git a/india_compliance/audit_trail/report/audit_trail/audit_trail.py b/india_compliance/audit_trail/report/audit_trail/audit_trail.py index 37000d094..5565f3104 100644 --- a/india_compliance/audit_trail/report/audit_trail/audit_trail.py +++ b/india_compliance/audit_trail/report/audit_trail/audit_trail.py @@ -69,7 +69,7 @@ def get_columns(self): def get_data(self): pass - def append_rows(self): + def append_rows(self, new_count, modified_count, doctype): pass def get_conditions(self): diff --git a/india_compliance/audit_trail/setup.py b/india_compliance/audit_trail/setup.py index fae387a22..4001102c1 100644 --- a/india_compliance/audit_trail/setup.py +++ b/india_compliance/audit_trail/setup.py @@ -17,17 +17,20 @@ def setup_fixtures(): def create_property_setters_for_versioning(): for doctype in get_audit_trail_doctypes(): + property_setter_data = { + "doctype_or_field": "DocType", + "doc_type": doctype, + "property": "track_changes", + "value": "1", + "property_type": "Check", + "is_system_generated": 1, + } + + if frappe.db.exists("Property Setter", property_setter_data): + continue + property_setter = frappe.new_doc("Property Setter") - property_setter.update( - { - "doctype_or_field": "DocType", - "doc_type": doctype, - "property": "track_changes", - "value": "1", - "property_type": "Check", - "is_system_generated": 1, - } - ) + property_setter.update(property_setter_data) property_setter.flags.ignore_permissions = True property_setter.insert() diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py index 1d6a8f87f..fb1b6c90c 100644 --- a/india_compliance/gst_india/api_classes/base.py +++ b/india_compliance/gst_india/api_classes/base.py @@ -16,7 +16,7 @@ class BaseAPI: API_NAME = "GST" BASE_PATH = "" - SENSITIVE_HEADERS = ("x-api-key",) + SENSITIVE_INFO = ("x-api-key",) def __init__(self, *args, **kwargs): self.settings = frappe.get_cached_doc("GST Settings") @@ -58,6 +58,9 @@ def fetch_credentials(self, gstin, service, require_password=True): self.username = row.username self.company = row.company + self._fetch_credentials(row, require_password=require_password) + + def _fetch_credentials(self, row, require_password=True): self.password = row.get_password(raise_exception=require_password) def get_url(self, *parts): @@ -94,7 +97,6 @@ def _make_request( params=params, headers={ # auto-generated hash, required by some endpoints - "requestid": self.generate_request_id(), **self.default_headers, **(headers or {}), }, @@ -102,11 +104,6 @@ def _make_request( log_headers = request_args.headers.copy() - # Mask sensitive headers - for header in self.SENSITIVE_HEADERS: - if header in log_headers: - log_headers[header] = "*****" - log = frappe._dict( **self.default_log_values, url=request_args.url, @@ -117,17 +114,20 @@ def _make_request( if method == "POST" and json: request_args.json = json + json_data = json.copy() if not request_args.params: - log.data = json + log.data = json_data else: log.data = { "params": request_args.params, - "body": json, + "body": json_data, } response_json = None try: + self.before_request(request_args) + response = requests.request(method, **request_args) if api_request_id := response.headers.get("x-amzn-RequestId"): log.request_id = api_request_id @@ -145,23 +145,15 @@ def _make_request( # Expect all successful responses to be JSON if not response_json: - frappe.throw(_("Error parsing response: {0}").format(response.content)) - else: - self.response = response_json - - # All error responses have a success key set to false - success_value = response_json.get("success", True) - if isinstance(success_value, str): - success_value = sbool(success_value) - - if not success_value and not self.handle_failed_response(response_json): - frappe.throw( - response_json.get("message") - # Fallback to response body if message is not present - or frappe.as_json(response_json, indent=4), - title=_("API Request Failed"), - ) + if "tar.gz" in request_args.url: + response_json = response.content + else: + frappe.throw( + _("Error parsing response: {0}").format(response.content) + ) + + response_json = self.process_response(response_json) return response_json.get("result", response_json) except Exception as e: @@ -169,7 +161,9 @@ def _make_request( raise e finally: - log.output = response_json + log.output = response_json.copy() + self.mask_sensitive_info(log) + enqueue_integration_request(**log) if self.sandbox_mode and not frappe.flags.ic_sandbox_message_shown: @@ -179,7 +173,29 @@ def _make_request( ) frappe.flags.ic_sandbox_message_shown = True - def handle_failed_response(self, response_json): + def before_request(self, request_args): + return + + def process_response(self, response): + self.handle_error_response(response) + self.response = response + return response + + def handle_error_response(self, response_json): + # All error responses have a success key set to false + success_value = response_json.get("success", True) + if isinstance(success_value, str): + success_value = sbool(success_value) + + if not success_value and not self.is_ignored_error(response_json): + frappe.throw( + response_json.get("message") + # Fallback to response body if message is not present + or frappe.as_json(response_json, indent=4), + title=_("API Request Failed"), + ) + + def is_ignored_error(self, response_json): # Override in subclass, return truthy value to stop frappe.throw pass @@ -219,3 +235,24 @@ def handle_http_code(self, status_code, response_json): def generate_request_id(self, length=12): return frappe.generate_hash(length=length) + + def mask_sensitive_info(self, log): + for key in self.SENSITIVE_INFO: + if key in log.request_headers: + log.request_headers[key] = "*****" + + if key in log.output: + log.output[key] = "*****" + + if not log.data: + return + + if key in log.get("data", {}): + log.data[key] = "*****" + + if key in log.get("data", {}).get("body", {}): + log.data["body"][key] = "*****" + + +def get_public_ip(): + return requests.get("https://api.ipify.org").text diff --git a/india_compliance/gst_india/api_classes/e_invoice.py b/india_compliance/gst_india/api_classes/e_invoice.py index 55f9179d6..6a90aa4c8 100644 --- a/india_compliance/gst_india/api_classes/e_invoice.py +++ b/india_compliance/gst_india/api_classes/e_invoice.py @@ -10,7 +10,7 @@ class EInvoiceAPI(BaseAPI): API_NAME = "e-Invoice" BASE_PATH = "ei/api" - SENSITIVE_HEADERS = BaseAPI.SENSITIVE_HEADERS + ("password",) + SENSITIVE_INFO = BaseAPI.SENSITIVE_INFO + ("password",) IGNORED_ERROR_CODES = { # Generate IRN errors "2150": "Duplicate IRN", @@ -21,6 +21,9 @@ class EInvoiceAPI(BaseAPI): # Cancel IRN errors "9999": "Invoice is not active", "4002": "EwayBill is already generated for this IRN", + # Invalid GSTIN error + "3028": "GSTIN is invalid", + "3029": "GSTIN is not active", } def setup(self, doc=None, *, company_gstin=None): @@ -50,10 +53,11 @@ def setup(self, doc=None, *, company_gstin=None): "gstin": company_gstin, "user_name": self.username, "password": self.password, + "requestid": self.generate_request_id(), } ) - def handle_failed_response(self, response_json): + def is_ignored_error(self, response_json): message = response_json.get("message", "").strip() for error_code in self.IGNORED_ERROR_CODES: @@ -103,3 +107,6 @@ def update_distance(self, result): def get_gstin_info(self, gstin): return self.get(endpoint="master/gstin", params={"gstin": gstin}) + + def sync_gstin_info(self, gstin): + return self.get(endpoint="master/syncgstin", params={"gstin": gstin}) diff --git a/india_compliance/gst_india/api_classes/e_waybill.py b/india_compliance/gst_india/api_classes/e_waybill.py index a7a0da669..4d096dba4 100644 --- a/india_compliance/gst_india/api_classes/e_waybill.py +++ b/india_compliance/gst_india/api_classes/e_waybill.py @@ -10,7 +10,7 @@ class EWaybillAPI(BaseAPI): API_NAME = "e-Waybill" BASE_PATH = "ewb/ewayapi" - SENSITIVE_HEADERS = BaseAPI.SENSITIVE_HEADERS + ("password",) + SENSITIVE_INFO = BaseAPI.SENSITIVE_INFO + ("password",) IGNORED_ERROR_CODES = { # Cancel e-waybill errors "312": "This eway bill is either not generated by you or cancelled", @@ -45,6 +45,7 @@ def setup(self, doc=None, *, company_gstin=None): "gstin": company_gstin, "username": self.username, "password": self.password, + "requestid": self.generate_request_id(), } ) @@ -82,7 +83,7 @@ def update_distance(self, result): ): result.distance = int(distance_match.group()) - def handle_failed_response(self, response_json): + def is_ignored_error(self, response_json): message = response_json.get("message", "") for error_code, error_message in self.IGNORED_ERROR_CODES.items(): diff --git a/india_compliance/gst_india/api_classes/public.py b/india_compliance/gst_india/api_classes/public.py index f9c8b2f03..f45a289a5 100644 --- a/india_compliance/gst_india/api_classes/public.py +++ b/india_compliance/gst_india/api_classes/public.py @@ -15,6 +15,7 @@ def setup(self): "Autofill Party Information based on GSTIN is not supported in sandbox mode" ) ) + self.default_headers.update({"requestid": self.generate_request_id()}) def get_gstin_info(self, gstin): response = self.get("search", params={"action": "TP", "gstin": gstin}) diff --git a/india_compliance/gst_india/api_classes/returns.py b/india_compliance/gst_india/api_classes/returns.py index d0baaccdf..6f6f63145 100644 --- a/india_compliance/gst_india/api_classes/returns.py +++ b/india_compliance/gst_india/api_classes/returns.py @@ -1,8 +1,196 @@ -from india_compliance.gst_india.api_classes.base import BaseAPI +import json +from base64 import b64decode, b64encode +from cryptography import x509 +from cryptography.hazmat.backends import default_backend -class ReturnsAPI(BaseAPI): +import frappe +from frappe import _ +from frappe.utils import add_to_date, cint, now_datetime + +from india_compliance.gst_india.api_classes.base import BaseAPI, get_public_ip +from india_compliance.gst_india.utils import merge_dicts, tar_gz_bytes_to_data +from india_compliance.gst_india.utils.cryptography import ( + aes_decrypt_data, + aes_encrypt_data, + encrypt_using_public_key, + hash_sha256, + hmac_sha256, +) + + +class PublicCertificate(BaseAPI): + BASE_PATH = "static" + + def get_gstn_public_certificate(self) -> str: + response = self.get(endpoint="gstn_g2b_prod_public") + self.settings.db_set("gstn_public_certificate", response.certificate) + + return response.certificate + + +class FilesAPI(BaseAPI): + BASE_PATH = "standard/gstn/files" + + def get_all(self, url_details): + response = frappe._dict() + self.encryption_key = b64decode(url_details.ek) + + for row in url_details.urls: + self.hash = row.get("hash") + self.ul = row.get("ul") + data = self.get(endpoint=self.ul) + + if not response: + response = data + else: + merge_dicts(response, data) + + return response + + def process_response(self, response): + computed_hash = hash_sha256(response) + if computed_hash != self.hash: + frappe.throw( + _( + "Hash of file doesn't match for {0}. File may be corrupted or tampered." + ).format(self.ul) + ) + + encrypted_data = tar_gz_bytes_to_data(response) + data = self.decrypt_data(encrypted_data) + data = json.loads(data, object_hook=frappe._dict) + + return data + + def decrypt_data(self, encrypted_json): + data = aes_decrypt_data(encrypted_json, self.encryption_key) + return b64decode(data).decode() + + +class ReturnsAuthenticate(BaseAPI): + def request_otp(self): + response = super().post( + json={ + "action": "OTPREQUEST", + "app_key": self.app_key, + "username": self.username, + }, + endpoint="authenticate", + ) + + if response.status_cd != 1: + return + + return response.update({"error_type": "otp_requested"}) + + def autheticate_with_otp(self, otp=None): + if not otp: + # reset auth token + frappe.db.set_value( + "GST Credential", + { + "gstin": self.company_gstin, + "username": self.username, + "service": "Returns", + }, + {"auth_token": None}, + ) + + self.auth_token = None + return self.request_otp() + + return super().post( + json={ + "action": "AUTHTOKEN", + "app_key": self.app_key, + "username": self.username, + "otp": otp, + }, + endpoint="authenticate", + ) + + def refresh_auth_token(self, auth_token): + return super().post( + json={ + "action": "REFRESHTOKEN", + "app_key": self.app_key, + "username": self.username, + "auth_token": auth_token, + }, + endpoint="authenticate", + ) + + def decrypt_response(self, response): + values = {} + + if response.get("auth_token"): + self.auth_token = response.auth_token + values["auth_token"] = response.auth_token + + if response.get("expiry"): + session_expiry = add_to_date( + None, minutes=cint(response.expiry), as_datetime=True + ) + self.session_expiry = session_expiry + values["session_expiry"] = session_expiry + + if response.get("sek"): + session_key = aes_decrypt_data(response.sek, self.app_key) + self.session_key = session_key + values["session_key"] = b64encode(session_key).decode() + + if values: + frappe.db.set_value( + "GST Credential", + { + "gstin": self.company_gstin, + "username": self.username, + "service": "Returns", + }, + values, + ) + + return response + + def encrypt_request(self, json): + if not json: + return + + if json.get("app_key"): + json["app_key"] = encrypt_using_public_key( + self.app_key, self.get_public_certificate() + ) + + if json.get("otp"): + json["otp"] = aes_encrypt_data(json.get("otp"), self.app_key) + + def get_public_certificate(self): + certificate = self.settings.gstn_public_certificate + + if not certificate: + certificate = PublicCertificate().get_gstn_public_certificate() + + cert = x509.load_pem_x509_certificate(certificate.encode(), default_backend()) + valid_up_to = cert.not_valid_after + + if valid_up_to < now_datetime(): + certificate = PublicCertificate().get_gstn_public_certificate() + + return certificate.encode() + + +class ReturnsAPI(ReturnsAuthenticate): API_NAME = "GST Returns" + BASE_PATH = "standard/gstn" + SENSITIVE_INFO = BaseAPI.SENSITIVE_INFO + ( + "auth-token", + "auth_token", + "app_key", + "sek", + "rek", + ) + IGNORED_ERROR_CODES = { "RETOTPREQUEST": "otp_requested", "EVCREQUEST": "otp_requested", @@ -14,9 +202,18 @@ class ReturnsAPI(BaseAPI): "RET2B1016": "no_docs_found", "RT-3BAS1009": "no_docs_found", "RET2B1018": "requested_before_cutoff_date", + "RTN_24": "queued", + "AUTH4033": "invalid_otp", # Invalid Session + # "AUTH4034": "invalid_otp", # Invalid OTP + "AUTH4038": "authorization_failed", # Session Expired + "RET11402": "authorization_failed", # API Authorization Failed for 2A + "RET2B1010": "authorization_failed", # API Authorization Failed for 2B } def setup(self, company_gstin): + if self.sandbox_mode: + frappe.throw(_("Sandbox mode not supported for Returns API")) + self.company_gstin = company_gstin self.fetch_credentials(self.company_gstin, "Returns", require_password=False) self.default_headers.update( @@ -24,40 +221,163 @@ def setup(self, company_gstin): "gstin": self.company_gstin, "state-cd": self.company_gstin[:2], "username": self.username, + "ip-usr": frappe.cache.hget("public_ip", "public_ip", get_public_ip), + "txn": self.generate_request_id(length=32), } ) - def handle_failed_response(self, response_json): - error_code = response_json.get("errorCode") + def _fetch_credentials(self, row, require_password=True): + self.app_key = row.app_key or self.generate_app_key() + self.auth_token = row.auth_token + self.session_key = b64decode(row.session_key or "") + self.session_expiry = row.session_expiry + + def _request( + self, + method, + action, + return_period=None, + params=None, + endpoint=None, + json=None, + otp=None, + ): + auth_token = self.get_auth_token() + + if not auth_token: + response = self.autheticate_with_otp(otp=otp) + if response.error_type in ["otp_requested", "invalid_otp"]: + return response + + headers = {"auth-token": auth_token} + if return_period: + headers["ret_period"] = return_period + + response = getattr(super(), method)( + params={"action": action, **(params or {})}, + headers=headers, + json=json, + endpoint=endpoint, + ) + + if response.error_type == "authorization_failed": + return self.autheticate_with_otp() + + return response + + def get(self, action, return_period, params=None, endpoint=None, otp=None): + params = {"gstin": self.company_gstin, **(params or {})} + return self._request("get", action, return_period, params, endpoint, None, otp) + + def post(self, action, params=None, endpoint=None, json=None, otp=None): + return self._request("post", action, None, params, endpoint, json, otp) + + def before_request(self, request_args): + self.encrypt_request(request_args.get("json")) + + def process_response(self, response): + self.handle_error_response(response) + response = self.decrypt_response(response) + return response + + def decrypt_response(self, response): + decrypted_rek = None + + if response.get("auth_token"): + return super().decrypt_response(response) + + if response.get("rek"): + decrypted_rek = aes_decrypt_data(response.rek, self.session_key) + + if response.get("data"): + decrypted_data = aes_decrypt_data(response.pop("data"), decrypted_rek) + + if response.get("hmac"): + hmac = hmac_sha256(decrypted_data, decrypted_rek) + if hmac != response.hmac: + frappe.throw(_("HMAC mismatch")) + + response.result = frappe.parse_json(b64decode(decrypted_data).decode()) + + return response + + def handle_error_response(self, response): + success_value = response.get("status_cd") != 0 + + if not success_value and not self.is_ignored_error(response): + frappe.throw( + response.get("error", {}).get("message") + # Fallback to response body if message is not present + or frappe.as_json(response, indent=4), + title=_("API Request Failed"), + ) + + def is_ignored_error(self, response): + error_code = response.get("error", {}).get("error_cd") if error_code in self.IGNORED_ERROR_CODES: - response_json.error_type = self.IGNORED_ERROR_CODES[error_code] + response.error_type = self.IGNORED_ERROR_CODES[error_code] return True - def get(self, action, return_period, otp=None, params=None): - return super().get( - params={"action": action, "gstin": self.company_gstin, **(params or {})}, - headers={ - "requestid": self.generate_request_id(), - "ret_period": return_period, - "otp": otp, + def generate_app_key(self): + app_key = self.generate_request_id(length=32) + frappe.db.set_value( + "GST Credential", + { + "gstin": self.company_gstin, + "username": self.username, + "service": "Returns", }, + {"app_key": app_key}, ) + return app_key + + def get_auth_token(self): + if not self.auth_token: + return None + + if self.session_expiry <= now_datetime(): + return None + + return self.auth_token + + def download_files(self, return_period, token, otp=None): + response = self.get( + "FILEDET", + return_period, + params={"ret_period": return_period, "token": token}, + endpoint="returns", + otp=otp, + ) + + if response.error_type == "queued": + return response + + return FilesAPI().get_all(response) + class GSTR2bAPI(ReturnsAPI): API_NAME = "GSTR-2B" - BASE_PATH = "returns/gstr2b" - def get_data(self, return_period, otp=None): - # TODO: Create further calls if more than one file to download - return self.get("GET2B", return_period, otp, {"rtnprd": return_period}) + def get_data(self, return_period, otp=None, file_num=None): + params = {"rtnprd": return_period} + if file_num: + params.update({"file_num": file_num}) + + return self.get( + "GET2B", return_period, params=params, endpoint="returns/gstr2b", otp=otp + ) class GSTR2aAPI(ReturnsAPI): API_NAME = "GSTR-2A" - BASE_PATH = "returns/gstr2a" def get_data(self, action, return_period, otp=None): - # TODO: Create further calls if more than one file to download - return self.get(action, return_period, otp, {"ret_period": return_period}) + return self.get( + action, + return_period, + params={"ret_period": return_period}, + endpoint="returns/gstr2a", + otp=otp, + ) diff --git a/india_compliance/gst_india/client_scripts/delivery_note.js b/india_compliance/gst_india/client_scripts/delivery_note.js index 8a13601e7..83529a9f4 100644 --- a/india_compliance/gst_india/client_scripts/delivery_note.js +++ b/india_compliance/gst_india/client_scripts/delivery_note.js @@ -2,6 +2,11 @@ const DOCTYPE = "Delivery Note"; setup_e_waybill_actions(DOCTYPE); frappe.ui.form.on(DOCTYPE, { + onload(frm) { + if (!gst_settings.enable_e_waybill) return; + show_sandbox_mode_indicator(); + }, + after_save(frm) { if ( frm.doc.docstatus || diff --git a/india_compliance/gst_india/client_scripts/e_invoice_actions.js b/india_compliance/gst_india/client_scripts/e_invoice_actions.js index a77ada732..0aa2644ae 100644 --- a/india_compliance/gst_india/client_scripts/e_invoice_actions.js +++ b/india_compliance/gst_india/client_scripts/e_invoice_actions.js @@ -3,6 +3,18 @@ frappe.ui.form.on("Sales Invoice", { if (frm.doc.__onload?.e_invoice_info?.is_generated_in_sandbox_mode) frm.get_field("irn").set_description("Generated in Sandbox Mode"); + if ( + frm.doc.irn && + frm.doc.docstatus == 2 && + frappe.perm.has_perm(frm.doctype, 0, "cancel", frm.doc.name) + ) { + frm.add_custom_button( + __("Mark as Cancelled"), + () => show_mark_e_invoice_as_cancelled_dialog(frm), + "e-Invoice" + ); + } + if (!is_e_invoice_applicable(frm)) return; if ( @@ -104,42 +116,7 @@ function show_cancel_e_invoice_dialog(frm, callback) { title: frm.doc.ewaybill ? __("Cancel e-Invoice and e-Waybill") : __("Cancel e-Invoice"), - fields: [ - { - label: "IRN Number", - fieldname: "irn", - fieldtype: "Data", - read_only: 1, - default: frm.doc.irn, - }, - { - label: "e-Waybill Number", - fieldname: "ewaybill", - fieldtype: "Data", - read_only: 1, - default: frm.doc.ewaybill || "", - }, - { - label: "Reason", - fieldname: "reason", - fieldtype: "Select", - reqd: 1, - default: "Data Entry Mistake", - options: [ - "Duplicate", - "Data Entry Mistake", - "Order Cancelled", - "Others", - ], - }, - { - label: "Remark", - fieldname: "remark", - fieldtype: "Data", - reqd: 1, - mandatory_depends_on: "eval: doc.reason == 'Others'", - }, - ], + fields: get_cancel_e_invoice_dialog_fields(frm), primary_action_label: frm.doc.ewaybill ? __("Cancel IRN & e-Waybill") : __("Cancel IRN"), @@ -162,6 +139,78 @@ function show_cancel_e_invoice_dialog(frm, callback) { d.show(); } +function show_mark_e_invoice_as_cancelled_dialog(frm) { + const d = new frappe.ui.Dialog({ + title: __("Update Cancelled e-Invoice Details"), + fields: get_cancel_e_invoice_dialog_fields(frm, true), + primary_action_label: __("Update"), + primary_action(values) { + frappe.call({ + method: "india_compliance.gst_india.utils.e_invoice.mark_e_invoice_as_cancelled", + args: { + doctype: frm.doctype, + docname: frm.doc.name, + values, + }, + callback: () => { + d.hide(); + frm.refresh(); + }, + }); + }, + }); + + d.show(); +} + +function get_cancel_e_invoice_dialog_fields(frm, manual_cancel = false) { + let fields = [ + { + label: "IRN Number", + fieldname: "irn", + fieldtype: "Data", + read_only: 1, + default: frm.doc.irn, + }, + { + label: "Reason", + fieldname: "reason", + fieldtype: "Select", + reqd: 1, + default: manual_cancel ? "Others" : "Data Entry Mistake", + options: ["Duplicate", "Data Entry Mistake", "Order Cancelled", "Others"], + }, + { + label: "Remark", + fieldname: "remark", + fieldtype: "Data", + reqd: 1, + mandatory_depends_on: "eval: doc.reason == 'Others'", + default: manual_cancel ? "Manually deleted from GSTR-1" : "", + }, + ]; + + if (manual_cancel) { + fields.push({ + label: "Cancelled On", + fieldname: "cancelled_on", + fieldtype: "Datetime", + reqd: 1, + default: frappe.datetime.now_datetime(), + }); + } else { + fields.splice(1, 0, { + label: "e-Waybill Number", + fieldname: "ewaybill", + fieldtype: "Data", + read_only: 1, + default: frm.doc.ewaybill || "", + }); + } + + return fields; +} + function is_e_invoice_applicable(frm) { return ( india_compliance.is_e_invoice_enabled() && diff --git a/india_compliance/gst_india/client_scripts/e_waybill_actions.js b/india_compliance/gst_india/client_scripts/e_waybill_actions.js index 9df883edd..980925914 100644 --- a/india_compliance/gst_india/client_scripts/e_waybill_actions.js +++ b/india_compliance/gst_india/client_scripts/e_waybill_actions.js @@ -1148,3 +1148,24 @@ function get_destination_address_name(frm) { return frm.doc.shipping_address_name || frm.doc.customer_address; } } + +function show_sandbox_mode_indicator() { + $(document).find(".form-sidebar .ic-sandbox-mode").remove(); + + if (!gst_settings.sandbox_mode) return; + + $(document) + .find(".form-sidebar .sidebar-image-section") + .after( + ` +
+ ` + ); +} diff --git a/india_compliance/gst_india/client_scripts/journal_entry.js b/india_compliance/gst_india/client_scripts/journal_entry.js index 89a963d4f..48b1d3c00 100644 --- a/india_compliance/gst_india/client_scripts/journal_entry.js +++ b/india_compliance/gst_india/client_scripts/journal_entry.js @@ -36,4 +36,4 @@ function _toggle_company_gstin(frm, reqd) { frm.set_df_property("company_gstin", "reqd", reqd); frm.refresh_field("company_gstin"); } -} \ No newline at end of file +} diff --git a/india_compliance/gst_india/client_scripts/payment_entry.js b/india_compliance/gst_india/client_scripts/payment_entry.js index 50b9d8dc9..058182df6 100644 --- a/india_compliance/gst_india/client_scripts/payment_entry.js +++ b/india_compliance/gst_india/client_scripts/payment_entry.js @@ -1,29 +1,51 @@ frappe.ui.form.on("Payment Entry", { - company: function(frm) { + company: function (frm) { frappe.call({ - 'method': 'frappe.contacts.doctype.address.address.get_default_address', - 'args': { - 'doctype': 'Company', - 'name': frm.doc.company + method: "frappe.contacts.doctype.address.address.get_default_address", + args: { + doctype: "Company", + name: frm.doc.company, + }, + callback: function (r) { + frm.set_value("company_address", r.message); }, - 'callback': function(r) { - frm.set_value('company_address', r.message); - } }); }, - party: function(frm) { - if (frm.doc.party_type == "Customer" && frm.doc.party) { - frappe.call({ - 'method': 'frappe.contacts.doctype.address.address.get_default_address', - 'args': { - 'doctype': 'Customer', - 'name': frm.doc.party - }, - 'callback': function(r) { - frm.set_value('customer_address', r.message); - } - }); - } - } + party: function (frm) { + update_gst_details( + frm, + "india_compliance.gst_india.overrides.payment_entry.update_party_details" + ); + }, + + customer_address(frm) { + update_gst_details(frm); + }, }); + +async function update_gst_details(frm, method) { + if ( + frm.doc.party_type != "Customer" || + !frm.doc.party || + frm.__updating_gst_details + ) + return; + + // wait for GSTINs to get fetched + await frappe.after_ajax(); + + args = { + doctype: frm.doc.doctype, + party_details: { + customer: frm.doc.party, + customer_address: frm.doc.customer_address, + billing_address_gstin: frm.doc.billing_address_gstin, + gst_category: frm.doc.gst_category, + company_gstin: frm.doc.company_gstin, + }, + company: frm.doc.company, + }; + + india_compliance.fetch_and_update_gst_details(frm, args, method); +} diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js index 4033caaf2..da60fda9d 100644 --- a/india_compliance/gst_india/client_scripts/purchase_invoice.js +++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js @@ -2,6 +2,11 @@ const DOCTYPE = "Purchase Invoice"; setup_e_waybill_actions(DOCTYPE); frappe.ui.form.on(DOCTYPE, { + onload(frm) { + if (!gst_settings.enable_e_waybill) return; + show_sandbox_mode_indicator(); + }, + after_save(frm) { if ( frm.doc.docstatus || @@ -18,7 +23,10 @@ frappe.ui.form.on(DOCTYPE, { 10 ); }, + refresh(frm) { + india_compliance.set_reconciliation_status(frm, "bill_no"); + if ( frm.doc.docstatus !== 1 || frm.doc.gst_category !== "Overseas" || @@ -37,4 +45,28 @@ frappe.ui.form.on(DOCTYPE, { __("Create") ); }, + + before_save: function (frm) { + // hack: values set in frm.doc are not available after save + if (frm._inward_supply) frm.doc._inward_supply = frm._inward_supply; + }, + + on_submit: function (frm) { + if (!frm._inward_supply) return; + + // go back to previous page and match the invoice with the inward supply + setTimeout(() => { + frappe.route_hooks.after_load = reco_frm => { + if (!reco_frm.purchase_reconciliation_tool) return; + purchase_reconciliation_tool.link_documents( + reco_frm, + frm.doc.name, + frm._inward_supply.name, + "Purchase Invoice", + false + ); + }; + frappe.set_route("Form", "Purchase Reconciliation Tool"); + }, 2000); + }, }); diff --git a/india_compliance/gst_india/client_scripts/sales_invoice.js b/india_compliance/gst_india/client_scripts/sales_invoice.js index 16ffd74ec..d6c6d94e6 100644 --- a/india_compliance/gst_india/client_scripts/sales_invoice.js +++ b/india_compliance/gst_india/client_scripts/sales_invoice.js @@ -24,6 +24,11 @@ frappe.ui.form.on(DOCTYPE, { }); }, + onload(frm) { + if (!(gst_settings.enable_e_waybill || gst_settings.enable_e_invoice)) return; + show_sandbox_mode_indicator(); + }, + before_submit(frm) { frm.doc._submitted_from_ui = 1; }, diff --git a/india_compliance/gst_india/constants/__init__.py b/india_compliance/gst_india/constants/__init__.py index 5b34f348b..903688f7c 100644 --- a/india_compliance/gst_india/constants/__init__.py +++ b/india_compliance/gst_india/constants/__init__.py @@ -1418,4 +1418,28 @@ SALES_DOCTYPES = set(sales_doctypes) BUG_REPORT_URL = "https://github.com/resilient-tech/india-compliance/issues/new" + +ORIGINAL_VS_AMENDED = ( + { + "original": "B2B", + "amended": "B2BA", + }, + { + "original": "CDNR", + "amended": "CDNRA", + }, + { + "original": "ISD", + "amended": "ISDA", + }, + { + "original": "IMPG", + "amended": "", + }, + { + "original": "IMPGSEZ", + "amended": "", + }, +) + E_INVOICE_MASTER_CODES_URL = "https://einvoice1.gst.gov.in/Others/MasterCodes" diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py index bd52d7a7f..16b2844c7 100644 --- a/india_compliance/gst_india/constants/custom_fields.py +++ b/india_compliance/gst_india/constants/custom_fields.py @@ -390,10 +390,20 @@ "default": "All Other ITC", "translatable": 0, }, + { + "fieldname": "reconciliation_status", + "label": "Reconciliation Status", + "fieldtype": "Select", + "insert_after": "eligibility_for_itc", + "print_hide": 1, + "options": ("\nNot Applicable\nReconciled\nUnreconciled\nIgnored"), + "no_copy": 1, + "read_only": 1, + }, { "fieldname": "gst_col_break", "fieldtype": "Column Break", - "insert_after": "eligibility_for_itc", + "insert_after": "reconciliation_status", }, { "fieldname": "itc_integrated_tax", @@ -526,11 +536,13 @@ { "fieldname": "place_of_supply", "label": "Place of Supply", - "fieldtype": "Data", + "fieldtype": "Autocomplete", + "options": get_place_of_supply_options(), "insert_after": "company_gstin", "print_hide": 1, - "read_only": 1, + "read_only": 0, "translatable": 0, + "depends_on": 'eval:doc.party_type === "Customer"', }, { "fieldname": "gst_column_break", @@ -547,7 +559,7 @@ "depends_on": 'eval:doc.party_type == "Customer"', }, { - "fieldname": "customer_gstin", + "fieldname": "billing_address_gstin", "label": "Customer GSTIN", "fieldtype": "Data", "insert_after": "customer_address", @@ -555,6 +567,19 @@ "print_hide": 1, "read_only": 1, "translatable": 0, + "depends_on": 'eval:doc.party_type === "Customer"', + }, + { + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Data", + "insert_after": "billing_address_gstin", + "read_only": 1, + "print_hide": 1, + "fetch_from": "customer_address.gst_category", + "translatable": 0, + "fetch_if_empty": 0, + "depends_on": 'eval:doc.party_type === "Customer"', }, ], "Journal Entry": [ @@ -675,7 +700,7 @@ "label": "e-Invoice Status", "fieldtype": "Select", "insert_after": "status", - "options": "\nPending\nGenerated\nAuto-Retry\nCancelled\nFailed\nNot Applicable", + "options": "\nPending\nGenerated\nAuto-Retry\nCancelled\nManually Cancelled\nFailed\nNot Applicable", "default": None, "hidden": 1, "no_copy": 1, @@ -693,6 +718,7 @@ "fieldtype": "Int", "insert_after": "vehicle_no", "print_hide": 1, + "no_copy": 1, "description": ( "Set as zero to update distance as per the e-Waybill portal (if available)" ), @@ -704,6 +730,7 @@ "insert_after": "transporter", "fetch_from": "transporter.gst_transporter_id", "print_hide": 1, + "no_copy": 1, "translatable": 0, }, { @@ -714,6 +741,7 @@ "default": "Road", "insert_after": "transporter_name", "print_hide": 1, + "no_copy": 1, "translatable": 0, }, { @@ -726,6 +754,7 @@ "default": "Regular", "insert_after": "lr_date", "print_hide": 1, + "no_copy": 1, "translatable": 0, }, ] @@ -747,6 +776,7 @@ "insert_after": "transporter_info", "options": "Supplier", "print_hide": 1, + "no_copy": 1, }, { "fieldname": "driver", @@ -755,6 +785,7 @@ "insert_after": "gst_transporter_id", "options": "Driver", "print_hide": 1, + "no_copy": 1, }, { "fieldname": "lr_no", @@ -762,6 +793,7 @@ "fieldtype": "Data", "insert_after": "driver", "print_hide": 1, + "no_copy": 1, "translatable": 0, "length": 30, }, @@ -771,6 +803,7 @@ "fieldtype": "Data", "insert_after": "lr_no", "print_hide": 1, + "no_copy": 1, "translatable": 0, "length": 15, }, @@ -787,6 +820,7 @@ "fetch_from": "transporter.supplier_name", "read_only": 1, "print_hide": 1, + "no_copy": 1, "translatable": 0, }, { @@ -796,6 +830,7 @@ "insert_after": "mode_of_transport", "fetch_from": "driver.full_name", "print_hide": 1, + "no_copy": 1, "translatable": 0, }, { @@ -805,6 +840,7 @@ "insert_after": "driver_name", "default": "Today", "print_hide": 1, + "no_copy": 1, }, *E_WAYBILL_DN_FIELDS, ] diff --git a/india_compliance/gst_india/data/test_e_invoice.json b/india_compliance/gst_india/data/test_e_invoice.json index 29db98e8a..61089bbec 100644 --- a/india_compliance/gst_india/data/test_e_invoice.json +++ b/india_compliance/gst_india/data/test_e_invoice.json @@ -1,596 +1,702 @@ { - "goods_item_with_ewaybill": { - "kwargs": { - "vehicle_no": "GJ07DL9009" - }, - "request_data": { - "BuyerDtls": { - "Addr1": "Test Address - 3", - "Gstin": "36AMBPG7773M002", - "LglNm": "_Test Registered Customer", - "Loc": "Test City", - "Pin": 500055, - "Pos": "02", - "Stcd": "36", - "TrdNm": "_Test Registered Customer" - }, - "DocDtls": { - "Dt": "16/09/2022", - "No": "test_invoice_no", - "Typ": "INV" - }, - "EwbDtls": { - "Distance": 0, - "TransMode": "1", - "VehNo": "GJ07DL9009", - "VehType": "R" - }, - "ItemList": [ - { - "AssAmt": 100000, - "CesAmt": 0, - "CesNonAdvlAmt": 0, - "CesRt": 0, - "CgstAmt": 0, - "Discount": 0, - "GstRt": 0, - "HsnCd": "61149090", - "IgstAmt": 0, - "IsServc": "N", - "PrdDesc": "Test Trading Goods 1", - "Qty": 1000, - "SgstAmt": 0, - "SlNo": "1", - "TotAmt": 100000, - "TotItemVal": 100000, - "Unit": "NOS", - "UnitPrice": 100 - } - ], - "PayDtls": { - "CrDay": 0, - "PaidAmt": 0, - "PaymtDue": 100000 - }, - "SellerDtls": { - "Addr1": "Test Address - 1", - "Gstin": "02AMBPG7773M002", - "LglNm": "_Test Indian Registered Company", - "Loc": "Test City", - "Pin": 171302, - "Stcd": "02", - "TrdNm": "_Test Indian Registered Company" - }, - "TranDtls": { - "RegRev": "N", - "SupTyp": "B2B", - "TaxSch": "GST" - }, - "ValDtls": { - "AssVal": 100000, - "CesVal": 0, - "CgstVal": 0, - "Discount": 0, - "IgstVal": 0, - "OthChrg": 0, - "RndOffAmt": 0, - "SgstVal": 0, - "TotInvVal": 100000 - }, - "Version": "1.1" - }, - "response_data": { - "info": [ - { - "InfCd": "EWBPPD", - "Desc": "Pin-Pin calc distance: 2467KM" - } - ], - "message": "IRN generated successfully", - "result": { - "AckDt": "2022-09-16 19:29:00", - "AckNo": 232210036743849, - "EwbDt": "2022-09-16 19:29:00", - "EwbNo": 391009149369, - "EwbValidTill": "2022-09-29 23:59:00", - "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7", - "Remarks": null, - "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NDM4NDksXCJBY2tEdFwiOlwiMjAyMi0wOS0xNiAxOToyOTowMFwiLFwiSXJuXCI6XCI3MDZkYWVjY2RhMGVmNmY4MThkYTc4ZjNhMmEwNWExMjg4NzMxMDU3MzczMDAyMjg5YjQ2YzMyMjkyODlhMmU3XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJTSU5WLUNGWS0wMDA2N1wiLFwiRHRcIjpcIjE2LzA5LzIwMjJcIn0sXCJTZWxsZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIlRyZE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJCdXllckR0bHNcIjp7XCJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJMZ2xObVwiOlwiX1Rlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiVHJkTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiUG9zXCI6XCIwMVwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDNcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjo1MDAwNTUsXCJTdGNkXCI6XCIzNlwifSxcIkRpc3BEdGxzXCI6e1wiTm1cIjpcIlRlc3QgSW5kaWFuIFJlZ2lzdGVyZWQgQ29tcGFueVwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDFcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjoxOTM1MDEsXCJTdGNkXCI6XCIwMVwifSxcIlNoaXBEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiVHJkTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDNcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjo1MDAwNTUsXCJTdGNkXCI6XCIzNlwifSxcIkl0ZW1MaXN0XCI6W3tcIkl0ZW1Ob1wiOjAsXCJTbE5vXCI6XCIxXCIsXCJJc1NlcnZjXCI6XCJOXCIsXCJQcmREZXNjXCI6XCJUZXN0IFRyYWRpbmcgR29vZHMgMVwiLFwiSHNuQ2RcIjpcIjYxMTQ5MDkwXCIsXCJRdHlcIjoxLjAsXCJVbml0XCI6XCJOT1NcIixcIlVuaXRQcmljZVwiOjEwMC4wLFwiVG90QW10XCI6MTAwLjAsXCJEaXNjb3VudFwiOjAsXCJBc3NBbXRcIjoxMDAuMCxcIkdzdFJ0XCI6MC4wLFwiSWdzdEFtdFwiOjAsXCJDZ3N0QW10XCI6MCxcIlNnc3RBbXRcIjowLFwiQ2VzUnRcIjowLFwiQ2VzQW10XCI6MCxcIkNlc05vbkFkdmxBbXRcIjowLFwiVG90SXRlbVZhbFwiOjEwMC4wfV0sXCJWYWxEdGxzXCI6e1wiQXNzVmFsXCI6MTAwLjAsXCJDZ3N0VmFsXCI6MCxcIlNnc3RWYWxcIjowLFwiSWdzdFZhbFwiOjAsXCJDZXNWYWxcIjowLFwiRGlzY291bnRcIjowLFwiT3RoQ2hyZ1wiOjAuMCxcIlJuZE9mZkFtdFwiOjAuMCxcIlRvdEludlZhbFwiOjEwMC4wfSxcIlBheUR0bHNcIjp7XCJDckRheVwiOjAsXCJQYWlkQW10XCI6MCxcIlBheW10RHVlXCI6MTAwLjB9LFwiRXdiRHRsc1wiOntcIlRyYW5zTW9kZVwiOlwiMVwiLFwiRGlzdGFuY2VcIjowLFwiVmVoTm9cIjpcIkdKMDdETDkwMDlcIixcIlZlaFR5cGVcIjpcIlJcIn19IiwiaXNzIjoiTklDIn0.ZOqrLJLsoXHf1QMRPBJoBesVluRB0a0ISsBGn6gqLuiJLfsAG1Oxmimqi9c7dboRnsW1eEj78Yps5D2A05WPMXwdkOy9Ahb_t4jXSGH-ijq_ed8z-xAtyiWH16YfIc9Zg020VkrlZiHdbfkx53hOwEA3aUhHIdPwQE5Kk-O9KWES3cttl9r5lrtzueTlTKB0GqXqiNlmuuQnCAJpWe34Coko1__kyPLLMdKgpOSB0EX2j7NjaZ5KPhu-GZHBtTvKczuSXvli6lwSQLaKpBm1IGvwMo2IzGW62pXp4XdMlcncLuc8wLTExSlKwHhsSspOxhMNBRx3NqcU0PQZOq050Q", - "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiU0lOVi1DRlktMDAwNjdcIixcIkRvY1R5cFwiOlwiSU5WXCIsXCJEb2NEdFwiOlwiMTYvMDkvMjAyMlwiLFwiVG90SW52VmFsXCI6MTAwLjAsXCJJdGVtQ250XCI6MSxcIk1haW5Ic25Db2RlXCI6XCI2MTE0OTA5MFwiLFwiSXJuXCI6XCI3MDZkYWVjY2RhMGVmNmY4MThkYTc4ZjNhMmEwNWExMjg4NzMxMDU3MzczMDAyMjg5YjQ2YzMyMjkyODlhMmU3XCIsXCJJcm5EdFwiOlwiMjAyMi0wOS0xNiAxOToyOTowMFwifSIsImlzcyI6Ik5JQyJ9.j7Fpl3ol0G6akp1-ukVzOK-8Dqoey3iKLf9SCaXGfb3crIcpniezqevH1qBTgCtUDYnOa0tRk5Nhyi-ER-W8Hu2a4Ug28AJFp3S8Xv2RwdMe9HvJN1b8KBKz6N4_WcO7wD2VcXyoEKDuTP2KFlXRjuZx7tBh5ttjQ4vRNtVwpR2qy-lRtMquEbZsJ-JOPBLUTXimdpVwt9EW8xKUxRKT_7-8kwK-DGHePADVBUjD6kv-GSpbxgfM4UAPg1TRlRz_BHMbbi9adZVZn5l9GA-WRuSP_7C-Qd_ucYg1cmP2zswh1XClMEjwjmxpuFkhqdDsRfl8unnEi--FxA0lvn4nmw", - "Status": "ACT", - "distance": 2467 - }, - "success": true - } - }, - "service_item": { - "kwargs": { - "item_code": "_Test Service Item", - "customer_address": "_Test Registered Customer-Billing", - "shipping_address_name": "_Test Registered Customer-Billing" - }, - "request_data": { - "BuyerDtls": { - "Addr1": "Test Address - 3", - "Gstin": "36AMBPG7773M002", - "LglNm": "_Test Registered Customer", - "Loc": "Test City", - "Pin": 500055, - "Pos": "02", - "Stcd": "36", - "TrdNm": "_Test Registered Customer" - }, - "DocDtls": { - "Dt": "17/09/2022", - "No": "test_invoice_no", - "Typ": "INV" - }, - "ItemList": [ - { - "AssAmt": 100.0, - "CesAmt": 0, - "CesNonAdvlAmt": 0, - "CesRt": 0, - "CgstAmt": 0, - "Discount": 0, - "GstRt": 0.0, - "HsnCd": "999900", - "IgstAmt": 0, - "IsServc": "Y", - "PrdDesc": "Test Service Item", - "Qty": 1.0, - "SgstAmt": 0, - "SlNo": "1", - "TotAmt": 100.0, - "TotItemVal": 100.0, - "Unit": "NOS", - "UnitPrice": 100.0 - } - ], - "PayDtls": { - "CrDay": 0, - "PaidAmt": 0, - "PaymtDue": 100.0 - }, - "SellerDtls": { - "Addr1": "Test Address - 1", - "Gstin": "02AMBPG7773M002", - "LglNm": "_Test Indian Registered Company", - "Loc": "Test City", - "Pin": 171302, - "Stcd": "02", - "TrdNm": "_Test Indian Registered Company" - }, - "TranDtls": { - "RegRev": "N", - "SupTyp": "B2B", - "TaxSch": "GST" - }, - "ValDtls": { - "AssVal": 100.0, - "CesVal": 0, - "CgstVal": 0, - "Discount": 0, - "IgstVal": 0, - "OthChrg": 0.0, - "RndOffAmt": 0.0, - "SgstVal": 0, - "TotInvVal": 100.0 - }, - "Version": "1.1" - }, - "response_data": { - "success": true, - "message": "IRN generated successfully", - "result": { - "AckNo": 232210036754863, - "AckDt": "2022-09-17 16:26:00", - "Irn": "68fb4fab44aee99fb23292478c4bd838e664837c9f1b04e3d9134ffed0b40b60", - "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTQ4NjMsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNjoyNjowMFwiLFwiSXJuXCI6XCI2OGZiNGZhYjQ0YWVlOTlmYjIzMjkyNDc4YzRiZDgzOGU2NjQ4MzdjOWYxYjA0ZTNkOTEzNGZmZWQwYjQwYjYwXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiWVwiLFwiUHJkRGVzY1wiOlwiVGVzdCBTZXJ2aWNlIEl0ZW1cIixcIkhzbkNkXCI6XCI5OTU0MTFcIixcIlF0eVwiOjEuMCxcIlVuaXRcIjpcIk5PU1wiLFwiVW5pdFByaWNlXCI6MTAwLjAsXCJUb3RBbXRcIjoxMDAuMCxcIkRpc2NvdW50XCI6MCxcIkFzc0FtdFwiOjEwMC4wLFwiR3N0UnRcIjowLjAsXCJJZ3N0QW10XCI6MCxcIkNnc3RBbXRcIjowLFwiU2dzdEFtdFwiOjAsXCJDZXNSdFwiOjAsXCJDZXNBbXRcIjowLFwiQ2VzTm9uQWR2bEFtdFwiOjAsXCJUb3RJdGVtVmFsXCI6MTAwLjB9XSxcIlZhbER0bHNcIjp7XCJBc3NWYWxcIjoxMDAuMCxcIkNnc3RWYWxcIjowLFwiU2dzdFZhbFwiOjAsXCJJZ3N0VmFsXCI6MCxcIkNlc1ZhbFwiOjAsXCJEaXNjb3VudFwiOjAsXCJPdGhDaHJnXCI6MC4wLFwiUm5kT2ZmQW10XCI6MC4wLFwiVG90SW52VmFsXCI6MTAwLjB9LFwiUGF5RHRsc1wiOntcIkNyRGF5XCI6MCxcIlBhaWRBbXRcIjowLFwiUGF5bXREdWVcIjoxMDAuMH0sXCJFd2JEdGxzXCI6e1wiRGlzdGFuY2VcIjowfX0iLCJpc3MiOiJOSUMifQ.rcfXkciqDJypX-xCqaUU3xAk2gccHK_qBD_FBIUsEr-SyWVs4LStgXwQEWUhTYnEfcGGm_sWX15ewC0jn9iWVFCNNFnjKc8vsFQqnbzvi-bnr6CWkjRXOxVqQTfis6PbtTrblojBq2hhBaT1B_ZlgLePi5qFNWnxxaHItjYtBEeBzW5JxzXWQTqESrBy02iLQgQMOexmQ6jKGdUR3tRRG5MVB7QfXbL9BpQr-DXbHhbDGllT2S_xyj9SBsN6ICeluCG-ZJdHE_kCoIxc8iY_nXneb2PsBciij14cHb96h4ddNZtapxTPTh4CumVDmAgLBSVsBTugp8vm0L-jd7n3dg", - "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIklOVlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiOTk1NDExXCIsXCJJcm5cIjpcIjY4ZmI0ZmFiNDRhZWU5OWZiMjMyOTI0NzhjNGJkODM4ZTY2NDgzN2M5ZjFiMDRlM2Q5MTM0ZmZlZDBiNDBiNjBcIixcIklybkR0XCI6XCIyMDIyLTA5LTE3IDE2OjI2OjAwXCJ9IiwiaXNzIjoiTklDIn0.A99BPXfKiGSNjnEcqmxc7RGutWakaeW0NMan9oC5yMw6zAoTNcVc34GtQKV7iajBZQhyiFrNwn5n6QtYOXafpitHcI_yrUWSojQBPPpPlslqj4hbnbCy7kmOZZ8mOKISHJrsIZJxpjRlSquIzfDN4aP1aT_qHDqwFqyA8RyJM-id5EpDaTrUFK12HwjKfAXHn4shEUBBgEWrHYOKK6VdpCNi6F_5I5bbJRivrvUJxMLjk3Ux9fyylqnEeyE2NThs9hFuV9EgoVzGE3FhfPZsooToAuG_npYEv3f6Q9KbOw3pNQ3NkqFwvmFjfNJLXdbxZIPe9fe9F1c-CRrIoNo_9w", - "Status": "ACT", - "EwbNo": null, - "EwbDt": null, - "EwbValidTill": null, - "Remarks": null - }, - "info": [ - { - "InfCd": "EWBERR", - "Desc": [ - { - "ErrorCode": "4019", - "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" - } - ] - } - ] - } - }, - "return_invoice": { - "kwargs": { - "qty": -1, - "is_return": 1, - "customer_address": "_Test Registered Customer-Billing", - "shipping_address_name": "_Test Registered Customer-Billing" - }, - "request_data": { - "BuyerDtls": { - "Addr1": "Test Address - 3", - "Gstin": "36AMBPG7773M002", - "LglNm": "_Test Registered Customer", - "Loc": "Test City", - "Pin": 500055, - "Pos": "02", - "Stcd": "36", - "TrdNm": "_Test Registered Customer" - }, - "DocDtls": { - "Dt": "17/09/2022", - "No": "test_invoice_no", - "Typ": "CRN" - }, - "ItemList": [ - { - "SlNo": "1", - "PrdDesc": "Test Trading Goods 1", - "IsServc": "N", - "HsnCd": "61149090", - "Unit": "NOS", - "Qty": 1.0, - "UnitPrice": 7.6, - "TotAmt": 7.6, - "Discount": 0, - "AssAmt": 7.6, - "GstRt": 12.0, - "IgstAmt": 0, - "CgstAmt": 0.46, - "SgstAmt": 0.46, - "CesRt": 0, - "CesAmt": 0, - "CesNonAdvlAmt": 0, - "TotItemVal": 8.52 - }, - { - "SlNo": "2", - "PrdDesc": "Test Trading Goods 1", - "IsServc": "N", - "HsnCd": "61149090", - "Unit": "NOS", - "Qty": 1.0, - "UnitPrice": 7.6, - "TotAmt": 7.6, - "Discount": 0, - "AssAmt": 7.6, - "GstRt": 12.0, - "IgstAmt": 0, - "CgstAmt": 0.45, - "SgstAmt": 0.45, - "CesRt": 0, - "CesAmt": 0, - "CesNonAdvlAmt": 0, - "TotItemVal": 8.5 - } - ], - "PayDtls": { - "CrDay": 0, - "PaidAmt": 0, - "PaymtDue": 0.0 - }, - "RefDtls": { - "PrecDocDtls": [ - { - "InvDt": "17/09/2022", - "InvNo": "SINV-CFY-00092" - } - ] - }, - "SellerDtls": { - "Addr1": "Test Address - 1", - "Gstin": "02AMBPG7773M002", - "LglNm": "_Test Indian Registered Company", - "Loc": "Test City", - "Pin": 171302, - "Stcd": "02", - "TrdNm": "_Test Indian Registered Company" - }, - "TranDtls": { - "RegRev": "N", - "SupTyp": "B2B", - "TaxSch": "GST" - }, - "ValDtls": { - "AssVal": 15.2, - "CesVal": 0, - "CgstVal": 0.91, - "Discount": 0, - "IgstVal": 0, - "OthChrg": 0.0, - "RndOffAmt": -0.02, - "SgstVal": 0.91, - "TotInvVal": 17.0 - }, - "Version": "1.1" - }, - "response_data": { - "success": true, - "message": "IRN generated successfully", - "result": { - "AckNo": 232210036755145, - "AckDt": "2022-09-17 17:05:00", - "Irn": "1c96258af085e45da556494ea5e5a7b401a598ab80af4136309c2dac7b54d795", - "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTUxNDUsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNzowNTowMFwiLFwiSXJuXCI6XCIxYzk2MjU4YWYwODVlNDVkYTU1NjQ5NGVhNWU1YTdiNDAxYTU5OGFiODBhZjQxMzYzMDljMmRhYzdiNTRkNzk1XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJDUk5cIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIlRyZE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiVGVzdCBUcmFkaW5nIEdvb2RzIDFcIixcIkhzbkNkXCI6XCI2MTE0OTA5MFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTk9TXCIsXCJVbml0UHJpY2VcIjoxMDAuMCxcIlRvdEFtdFwiOjEwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTAwLjAsXCJHc3RSdFwiOjAuMCxcIklnc3RBbXRcIjowLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxMDAuMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjEwMC4wLFwiQ2dzdFZhbFwiOjAsXCJTZ3N0VmFsXCI6MCxcIklnc3RWYWxcIjowLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxMDAuMH0sXCJQYXlEdGxzXCI6e1wiQ3JEYXlcIjowLFwiUGFpZEFtdFwiOjAsXCJQYXltdER1ZVwiOjAuMH0sXCJSZWZEdGxzXCI6e1wiUHJlY0RvY0R0bHNcIjpbe1wiSW52Tm9cIjpcIlNJTlYtQ0ZZLTAwMDkyXCIsXCJJbnZEdFwiOlwiMTcvMDkvMjAyMlwifV19LFwiRXdiRHRsc1wiOntcIkRpc3RhbmNlXCI6MH19IiwiaXNzIjoiTklDIn0.OZpYYN2pXIPJmLTi79NuVxVWWWGwdUzdnIEKXpgXyCEtXqgfwm9opf6EbyEt6wjwiYV4b2HWAfmkOXYJ-TO3dMc_98nOW8SeupGC6k-aV5kcDK9DNiRNWZBCiTMCRhlfmYC3oEZtd5f6yVoHvESl-QKeBlXcK2L8M1C0Guuq3QYqyhFp_gFqz_xY6ImKi65hEMDBghCaKINatRvPtFiwmtIspxUaHid5YcyqXdBxerEDjz-D2RjHOnhq1K_JX07X9xqcncLa106XltEeVFswJiEfqwtZTKDzkd4klymGJL5je7L1tgO8sNTKXGtFETKZfKmFSvxsMrBy1Dlp0LXXtg", - "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIkNSTlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiNjExNDkwOTBcIixcIklyblwiOlwiMWM5NjI1OGFmMDg1ZTQ1ZGE1NTY0OTRlYTVlNWE3YjQwMWE1OThhYjgwYWY0MTM2MzA5YzJkYWM3YjU0ZDc5NVwiLFwiSXJuRHRcIjpcIjIwMjItMDktMTcgMTc6MDU6MDBcIn0iLCJpc3MiOiJOSUMifQ.fgLb2ILeWK-MHxs61jZeo3kgy6w-sFXkVSh9GbzH0yWCeVx8MTsRwkKEXklPlmdwudG1lUPVjWsOioCgLL1Q8EE_wqnpGY00JEWr5X74GduBLe0lo5ZKtsSIVf10REy1E7_JJV8qitSZa3JeSqdjPlTUowFpxvPiw-nu3fyBP92IdVFbGO6oMvodI66kanqEyKj26Rn0nfnHP3KT3u61RBUSaVm_gch79cnPQanDPrJtd_0Ra2Vn7FoopdfcNIEdASB71IDRHsMFCNs8LyHTtFoJVI2LqU8wic_A6oWZkswOTBHdsBauMa_CMF9-2QbwTHFv60yvS7KuS2HvBw-oyQ", - "Status": "ACT", - "EwbNo": null, - "EwbDt": null, - "EwbValidTill": null, - "Remarks": null - }, - "info": [ - { - "InfCd": "EWBERR", - "Desc": [ - { - "ErrorCode": "4019", - "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" - } - ] - } - ] - } - }, - "debit_invoice": { - "kwargs": { - "is_debit_note": 1, - "qty": 0, - "customer_address": "_Test Registered Customer-Billing", - "shipping_address_name": "_Test Registered Customer-Billing" - }, - "request_data": { - "BuyerDtls": { - "Addr1": "Test Address - 3", - "Gstin": "36AMBPG7773M002", - "LglNm": "_Test Registered Customer", - "Loc": "Test City", - "Pin": 500055, - "Pos": "02", - "Stcd": "36", - "TrdNm": "_Test Registered Customer" - }, - "DocDtls": { - "Dt": "17/09/2022", - "No": "test_invoice_no", - "Typ": "DBN" - }, - "ItemList": [ - { - "AssAmt": 100.0, - "CesAmt": 0, - "CesNonAdvlAmt": 0, - "CesRt": 0, - "CgstAmt": 0, - "Discount": 0, - "GstRt": 0.0, - "HsnCd": "61149090", - "IgstAmt": 0, - "IsServc": "N", - "PrdDesc": "Test Trading Goods 1", - "Qty": 0.0, - "SgstAmt": 0, - "SlNo": "1", - "TotAmt": 100.0, - "TotItemVal": 100.0, - "Unit": "NOS", - "UnitPrice": 100.0 - } - ], - "PayDtls": { - "CrDay": 0, - "PaidAmt": 0, - "PaymtDue": 100.0 - }, - "SellerDtls": { - "Addr1": "Test Address - 1", - "Gstin": "02AMBPG7773M002", - "LglNm": "_Test Indian Registered Company", - "Loc": "Test City", - "Pin": 171302, - "Stcd": "02", - "TrdNm": "_Test Indian Registered Company" - }, - "TranDtls": { - "RegRev": "N", - "SupTyp": "B2B", - "TaxSch": "GST" - }, - "ValDtls": { - "AssVal": 100.0, - "CesVal": 0, - "CgstVal": 0, - "Discount": 0, - "IgstVal": 0, - "OthChrg": 0.0, - "RndOffAmt": 0.0, - "SgstVal": 0, - "TotInvVal": 100.0 - }, - "Version": "1.1" - }, - "response_data": { - "success": true, - "message": "IRN generated successfully", - "result": { - "AckNo": 232210036755701, - "AckDt": "2022-09-17 17:50:00", - "Irn": "24f37c80532583c6894d8153e2b12494daa80ddbb197f0fc2c1bac07db67f933", - "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTU3MDEsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNzo1MDowMFwiLFwiSXJuXCI6XCIyNGYzN2M4MDUzMjU4M2M2ODk0ZDgxNTNlMmIxMjQ5NGRhYTgwZGRiYjE5N2YwZmMyYzFiYWMwN2RiNjdmOTMzXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJEQk5cIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiVGVzdCBUcmFkaW5nIEdvb2RzIDFcIixcIkhzbkNkXCI6XCI2MTE0OTA5MFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTk9TXCIsXCJVbml0UHJpY2VcIjoxMDAuMCxcIlRvdEFtdFwiOjEwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTAwLjAsXCJHc3RSdFwiOjAuMCxcIklnc3RBbXRcIjowLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxMDAuMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjEwMC4wLFwiQ2dzdFZhbFwiOjAsXCJTZ3N0VmFsXCI6MCxcIklnc3RWYWxcIjowLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxMDAuMH0sXCJQYXlEdGxzXCI6e1wiQ3JEYXlcIjowLFwiUGFpZEFtdFwiOjAsXCJQYXltdER1ZVwiOjEwMC4wfSxcIkV3YkR0bHNcIjp7XCJEaXN0YW5jZVwiOjB9fSIsImlzcyI6Ik5JQyJ9.ad1NfDA-H8FgBHr_kaTeiVWUj-f8T6NXuLFWa1gprxGhACXoI9h6sU47U9PBxHcZ7qVXcYAKzA9CbNvAfxWCLKtjur5p85uIrksZYDD494lodGn3QeXbyjXMJeh7eM0mcKKN3Chp0TxaUfi9C7mA9W0R8HYKNnXIOT1CVlD-brrAw09_QiDsNgMhLBX5QfpHKIHPKCIEl_DgmWlnMzduy1iKYPpNreNPCV-J-ZaVQjxl93LjKBUb5AF1XWyWvPw_e8ePZEttviX_bU_Nnm1M4zCj-QWYzj8A0bauzl7kjp5UajEM7_7CLAI4sjZnqonGKYFfR5rf2Qj76exbs_pWGw", - "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIkRCTlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiNjExNDkwOTBcIixcIklyblwiOlwiMjRmMzdjODA1MzI1ODNjNjg5NGQ4MTUzZTJiMTI0OTRkYWE4MGRkYmIxOTdmMGZjMmMxYmFjMDdkYjY3ZjkzM1wiLFwiSXJuRHRcIjpcIjIwMjItMDktMTcgMTc6NTA6MDBcIn0iLCJpc3MiOiJOSUMifQ.TUE7iIvF9Orc1bEuOaxtSuj0D6vP1MwV-hcoh3IZqV7EKXwMmm0PkVrN87vRzSu97NMKfQIHJDRgPYv4prhuT0dshWJ9A_kC4jiRSm5Naj_R1egBKsv5ykTojOKKrkGy35DtdUcJK_FyiD0qFfmMmInFD8u6D8W83eEo93i99RONgVKUyCkd_uqs-cz1P-PTlsi2xWbeDVVIcRoAmf-lcsbwkl2Hn6ECHgorKJHPJC1FGo1jRQ2Ktq0ODiJdncplbxbdYN19vUz61JJB4DPzWtf8wOkX11N0fDhdUdEINfJURWEOIGQRYip5GIJuA2qdqxZieVk0CetnsckcGgfE8g", - "Status": "ACT", - "EwbNo": null, - "EwbDt": null, - "EwbValidTill": null, - "Remarks": null - }, - "info": [ - { - "InfCd": "EWBERR", - "Desc": [ - { - "ErrorCode": "4019", - "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" - } - ] - } - ] - } - }, - "cancel_e_invoice": { - "request_data": { - "Cnlrem": "Data Entry Mistake", - "Cnlrsn": "2", - "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" - }, - "response_data": { - "message": "E-Invoice is cancelled successfully", - "result": { - "CancelDate": "2022-09-17 18:09:00", - "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" - }, - "success": true - } - }, - "cancel_e_waybill": { - "request_data": { - "cancelRmrk": "Data Entry Mistake", - "cancelRsnCode": "3", - "ewbNo": "391009149369" - }, - "response_data": { - "message": "E-Way bill cancelled successfully", - "result": { - "cancelDate": "20/09/2022 12:10:00 PM", - "ewayBillNo": "391009149369" - }, - "success": true - } - }, - "dispatch_details": { - "DispDtls": { - "Nm": "Test Indian Registered Company", - "Addr1": "Test Address - 5", - "Loc": "Test City", - "Pin": 171302, - "Stcd": "02" - } - }, - "shipping_details": { - "ShipDtls": { - "Gstin": "36AMBPG7773M002", - "LglNm": "Test Registered Customer", - "TrdNm": "Test Registered Customer", - "Addr1": "Test Address - 4", - "Loc": "Test City", - "Pin": 500055, - "Stcd": "36" - } - }, - "foreign_transaction": { - "kwargs": { - "customer": "_Test Foreign Customer", - "port_address": "_Test Indian Unregistered Company-Billing", - "vehicle_no": "GJ07DL9009" - }, - "request_data": { - "BuyerDtls": { - "Addr1": "Test Address - 9", - "Gstin": "URP", - "LglNm": "_Test Foreign Customer", - "Loc": "Test City", - "Pin": 999999, - "Pos": "96", - "Stcd": "96", - "TrdNm": "_Test Foreign Customer" - }, - "DocDtls": { - "Dt": "16/09/2022", - "No": "test_invoice_no", - "Typ": "INV" - }, - "EwbDtls": { - "Distance": 0, - "TransMode": "1", - "VehNo": "GJ07DL9009", - "VehType": "R" - }, - "ItemList": [ - { - "AssAmt": 100000, - "CesAmt": 0, - "CesNonAdvlAmt": 0, - "CesRt": 0, - "CgstAmt": 0, - "Discount": 0, - "GstRt": 0, - "HsnCd": "61149090", - "IgstAmt": 0, - "IsServc": "N", - "PrdDesc": "Test Trading Goods 1", - "Qty": 1000, - "SgstAmt": 0, - "SlNo": "1", - "TotAmt": 100000, - "TotItemVal": 100000, - "Unit": "NOS", - "UnitPrice": 100 - } - ], - "PayDtls": { - "CrDay": 0, - "PaidAmt": 0, - "PaymtDue": 100000 - }, - "SellerDtls": { - "Addr1": "Test Address - 1", - "Gstin": "02AMBPG7773M002", - "LglNm": "_Test Indian Registered Company", - "Loc": "Test City", - "Pin": 171302, - "Stcd": "02", - "TrdNm": "_Test Indian Registered Company" - }, - "ShipDtls": { - "Gstin": "URP", - "LglNm": "Test Indian Unregistered Company", - "TrdNm": "Test Indian Unregistered Company", - "Addr1": "Test Address - 2", - "Loc": "Test City", - "Pin": 380015, - "Stcd": "24" - }, - "TranDtls": { - "RegRev": "N", - "SupTyp": "EXPWOP", - "TaxSch": "GST" - }, - "ValDtls": { - "AssVal": 100000, - "CesVal": 0, - "CgstVal": 0, - "Discount": 0, - "IgstVal": 0, - "OthChrg": 0, - "RndOffAmt": 0, - "SgstVal": 0, - "TotInvVal": 100000 - }, - "ExpDtls": { - "CntCode": "US", - "ShipBNo": "1234", - "ShipBDt": "2023-06-10", - "Port": "INABG1" - }, - "Version": "1.1" - } - } -} \ No newline at end of file + "goods_item_with_ewaybill": { + "kwargs": { + "vehicle_no": "GJ07DL9009" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "02", + "Stcd": "36", + "TrdNm": "_Test Registered Customer" + }, + "DocDtls": { + "Dt": "16/09/2022", + "No": "test_invoice_no", + "Typ": "INV" + }, + "EwbDtls": { + "Distance": 0, + "TransMode": "1", + "VehNo": "GJ07DL9009", + "VehType": "R" + }, + "ItemList": [ + { + "AssAmt": 100000, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 1000, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100000, + "TotItemVal": 100000, + "Unit": "NOS", + "UnitPrice": 100 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100000 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "02AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02", + "TrdNm": "_Test Indian Registered Company" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100000, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0, + "RndOffAmt": 0, + "SgstVal": 0, + "TotInvVal": 100000 + }, + "Version": "1.1" + }, + "response_data": { + "info": [ + { + "InfCd": "EWBPPD", + "Desc": "Pin-Pin calc distance: 2467KM" + } + ], + "message": "IRN generated successfully", + "result": { + "AckDt": "2022-09-16 19:29:00", + "AckNo": 232210036743849, + "EwbDt": "2022-09-16 19:29:00", + "EwbNo": 391009149369, + "EwbValidTill": "2022-09-29 23:59:00", + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7", + "Remarks": null, + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NDM4NDksXCJBY2tEdFwiOlwiMjAyMi0wOS0xNiAxOToyOTowMFwiLFwiSXJuXCI6XCI3MDZkYWVjY2RhMGVmNmY4MThkYTc4ZjNhMmEwNWExMjg4NzMxMDU3MzczMDAyMjg5YjQ2YzMyMjkyODlhMmU3XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJTSU5WLUNGWS0wMDA2N1wiLFwiRHRcIjpcIjE2LzA5LzIwMjJcIn0sXCJTZWxsZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIlRyZE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJCdXllckR0bHNcIjp7XCJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJMZ2xObVwiOlwiX1Rlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiVHJkTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiUG9zXCI6XCIwMVwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDNcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjo1MDAwNTUsXCJTdGNkXCI6XCIzNlwifSxcIkRpc3BEdGxzXCI6e1wiTm1cIjpcIlRlc3QgSW5kaWFuIFJlZ2lzdGVyZWQgQ29tcGFueVwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDFcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjoxOTM1MDEsXCJTdGNkXCI6XCIwMVwifSxcIlNoaXBEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiVHJkTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDNcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjo1MDAwNTUsXCJTdGNkXCI6XCIzNlwifSxcIkl0ZW1MaXN0XCI6W3tcIkl0ZW1Ob1wiOjAsXCJTbE5vXCI6XCIxXCIsXCJJc1NlcnZjXCI6XCJOXCIsXCJQcmREZXNjXCI6XCJUZXN0IFRyYWRpbmcgR29vZHMgMVwiLFwiSHNuQ2RcIjpcIjYxMTQ5MDkwXCIsXCJRdHlcIjoxLjAsXCJVbml0XCI6XCJOT1NcIixcIlVuaXRQcmljZVwiOjEwMC4wLFwiVG90QW10XCI6MTAwLjAsXCJEaXNjb3VudFwiOjAsXCJBc3NBbXRcIjoxMDAuMCxcIkdzdFJ0XCI6MC4wLFwiSWdzdEFtdFwiOjAsXCJDZ3N0QW10XCI6MCxcIlNnc3RBbXRcIjowLFwiQ2VzUnRcIjowLFwiQ2VzQW10XCI6MCxcIkNlc05vbkFkdmxBbXRcIjowLFwiVG90SXRlbVZhbFwiOjEwMC4wfV0sXCJWYWxEdGxzXCI6e1wiQXNzVmFsXCI6MTAwLjAsXCJDZ3N0VmFsXCI6MCxcIlNnc3RWYWxcIjowLFwiSWdzdFZhbFwiOjAsXCJDZXNWYWxcIjowLFwiRGlzY291bnRcIjowLFwiT3RoQ2hyZ1wiOjAuMCxcIlJuZE9mZkFtdFwiOjAuMCxcIlRvdEludlZhbFwiOjEwMC4wfSxcIlBheUR0bHNcIjp7XCJDckRheVwiOjAsXCJQYWlkQW10XCI6MCxcIlBheW10RHVlXCI6MTAwLjB9LFwiRXdiRHRsc1wiOntcIlRyYW5zTW9kZVwiOlwiMVwiLFwiRGlzdGFuY2VcIjowLFwiVmVoTm9cIjpcIkdKMDdETDkwMDlcIixcIlZlaFR5cGVcIjpcIlJcIn19IiwiaXNzIjoiTklDIn0.ZOqrLJLsoXHf1QMRPBJoBesVluRB0a0ISsBGn6gqLuiJLfsAG1Oxmimqi9c7dboRnsW1eEj78Yps5D2A05WPMXwdkOy9Ahb_t4jXSGH-ijq_ed8z-xAtyiWH16YfIc9Zg020VkrlZiHdbfkx53hOwEA3aUhHIdPwQE5Kk-O9KWES3cttl9r5lrtzueTlTKB0GqXqiNlmuuQnCAJpWe34Coko1__kyPLLMdKgpOSB0EX2j7NjaZ5KPhu-GZHBtTvKczuSXvli6lwSQLaKpBm1IGvwMo2IzGW62pXp4XdMlcncLuc8wLTExSlKwHhsSspOxhMNBRx3NqcU0PQZOq050Q", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiU0lOVi1DRlktMDAwNjdcIixcIkRvY1R5cFwiOlwiSU5WXCIsXCJEb2NEdFwiOlwiMTYvMDkvMjAyMlwiLFwiVG90SW52VmFsXCI6MTAwLjAsXCJJdGVtQ250XCI6MSxcIk1haW5Ic25Db2RlXCI6XCI2MTE0OTA5MFwiLFwiSXJuXCI6XCI3MDZkYWVjY2RhMGVmNmY4MThkYTc4ZjNhMmEwNWExMjg4NzMxMDU3MzczMDAyMjg5YjQ2YzMyMjkyODlhMmU3XCIsXCJJcm5EdFwiOlwiMjAyMi0wOS0xNiAxOToyOTowMFwifSIsImlzcyI6Ik5JQyJ9.j7Fpl3ol0G6akp1-ukVzOK-8Dqoey3iKLf9SCaXGfb3crIcpniezqevH1qBTgCtUDYnOa0tRk5Nhyi-ER-W8Hu2a4Ug28AJFp3S8Xv2RwdMe9HvJN1b8KBKz6N4_WcO7wD2VcXyoEKDuTP2KFlXRjuZx7tBh5ttjQ4vRNtVwpR2qy-lRtMquEbZsJ-JOPBLUTXimdpVwt9EW8xKUxRKT_7-8kwK-DGHePADVBUjD6kv-GSpbxgfM4UAPg1TRlRz_BHMbbi9adZVZn5l9GA-WRuSP_7C-Qd_ucYg1cmP2zswh1XClMEjwjmxpuFkhqdDsRfl8unnEi--FxA0lvn4nmw", + "Status": "ACT", + "distance": 2467 + }, + "success": true + } + }, + "service_item": { + "kwargs": { + "item_code": "_Test Service Item", + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "02", + "Stcd": "36", + "TrdNm": "_Test Registered Customer" + }, + "DocDtls": { + "Dt": "17/09/2022", + "No": "test_invoice_no", + "Typ": "INV" + }, + "ItemList": [ + { + "AssAmt": 100.0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0.0, + "HsnCd": "999900", + "IgstAmt": 0, + "IsServc": "Y", + "PrdDesc": "Test Service Item", + "Qty": 1.0, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100.0, + "TotItemVal": 100.0, + "Unit": "NOS", + "UnitPrice": 100.0 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100.0 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "02AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02", + "TrdNm": "_Test Indian Registered Company" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100.0, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": 0.0, + "SgstVal": 0, + "TotInvVal": 100.0 + }, + "Version": "1.1" + }, + "response_data": { + "success": true, + "message": "IRN generated successfully", + "result": { + "AckNo": 232210036754863, + "AckDt": "2022-09-17 16:26:00", + "Irn": "68fb4fab44aee99fb23292478c4bd838e664837c9f1b04e3d9134ffed0b40b60", + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTQ4NjMsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNjoyNjowMFwiLFwiSXJuXCI6XCI2OGZiNGZhYjQ0YWVlOTlmYjIzMjkyNDc4YzRiZDgzOGU2NjQ4MzdjOWYxYjA0ZTNkOTEzNGZmZWQwYjQwYjYwXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiWVwiLFwiUHJkRGVzY1wiOlwiVGVzdCBTZXJ2aWNlIEl0ZW1cIixcIkhzbkNkXCI6XCI5OTU0MTFcIixcIlF0eVwiOjEuMCxcIlVuaXRcIjpcIk5PU1wiLFwiVW5pdFByaWNlXCI6MTAwLjAsXCJUb3RBbXRcIjoxMDAuMCxcIkRpc2NvdW50XCI6MCxcIkFzc0FtdFwiOjEwMC4wLFwiR3N0UnRcIjowLjAsXCJJZ3N0QW10XCI6MCxcIkNnc3RBbXRcIjowLFwiU2dzdEFtdFwiOjAsXCJDZXNSdFwiOjAsXCJDZXNBbXRcIjowLFwiQ2VzTm9uQWR2bEFtdFwiOjAsXCJUb3RJdGVtVmFsXCI6MTAwLjB9XSxcIlZhbER0bHNcIjp7XCJBc3NWYWxcIjoxMDAuMCxcIkNnc3RWYWxcIjowLFwiU2dzdFZhbFwiOjAsXCJJZ3N0VmFsXCI6MCxcIkNlc1ZhbFwiOjAsXCJEaXNjb3VudFwiOjAsXCJPdGhDaHJnXCI6MC4wLFwiUm5kT2ZmQW10XCI6MC4wLFwiVG90SW52VmFsXCI6MTAwLjB9LFwiUGF5RHRsc1wiOntcIkNyRGF5XCI6MCxcIlBhaWRBbXRcIjowLFwiUGF5bXREdWVcIjoxMDAuMH0sXCJFd2JEdGxzXCI6e1wiRGlzdGFuY2VcIjowfX0iLCJpc3MiOiJOSUMifQ.rcfXkciqDJypX-xCqaUU3xAk2gccHK_qBD_FBIUsEr-SyWVs4LStgXwQEWUhTYnEfcGGm_sWX15ewC0jn9iWVFCNNFnjKc8vsFQqnbzvi-bnr6CWkjRXOxVqQTfis6PbtTrblojBq2hhBaT1B_ZlgLePi5qFNWnxxaHItjYtBEeBzW5JxzXWQTqESrBy02iLQgQMOexmQ6jKGdUR3tRRG5MVB7QfXbL9BpQr-DXbHhbDGllT2S_xyj9SBsN6ICeluCG-ZJdHE_kCoIxc8iY_nXneb2PsBciij14cHb96h4ddNZtapxTPTh4CumVDmAgLBSVsBTugp8vm0L-jd7n3dg", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIklOVlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiOTk1NDExXCIsXCJJcm5cIjpcIjY4ZmI0ZmFiNDRhZWU5OWZiMjMyOTI0NzhjNGJkODM4ZTY2NDgzN2M5ZjFiMDRlM2Q5MTM0ZmZlZDBiNDBiNjBcIixcIklybkR0XCI6XCIyMDIyLTA5LTE3IDE2OjI2OjAwXCJ9IiwiaXNzIjoiTklDIn0.A99BPXfKiGSNjnEcqmxc7RGutWakaeW0NMan9oC5yMw6zAoTNcVc34GtQKV7iajBZQhyiFrNwn5n6QtYOXafpitHcI_yrUWSojQBPPpPlslqj4hbnbCy7kmOZZ8mOKISHJrsIZJxpjRlSquIzfDN4aP1aT_qHDqwFqyA8RyJM-id5EpDaTrUFK12HwjKfAXHn4shEUBBgEWrHYOKK6VdpCNi6F_5I5bbJRivrvUJxMLjk3Ux9fyylqnEeyE2NThs9hFuV9EgoVzGE3FhfPZsooToAuG_npYEv3f6Q9KbOw3pNQ3NkqFwvmFjfNJLXdbxZIPe9fe9F1c-CRrIoNo_9w", + "Status": "ACT", + "EwbNo": null, + "EwbDt": null, + "EwbValidTill": null, + "Remarks": null + }, + "info": [ + { + "InfCd": "EWBERR", + "Desc": [ + { + "ErrorCode": "4019", + "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" + } + ] + } + ] + } + }, + "return_invoice": { + "kwargs": { + "qty": -1, + "is_return": 1, + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "02", + "Stcd": "36", + "TrdNm": "_Test Registered Customer" + }, + "DocDtls": { + "Dt": "17/09/2022", + "No": "test_invoice_no", + "Typ": "CRN" + }, + "ItemList": [ + { + "SlNo": "1", + "PrdDesc": "Test Trading Goods 1", + "IsServc": "N", + "HsnCd": "61149090", + "Unit": "NOS", + "Qty": 1.0, + "UnitPrice": 7.6, + "TotAmt": 7.6, + "Discount": 0, + "AssAmt": 7.6, + "GstRt": 12.0, + "IgstAmt": 0, + "CgstAmt": 0.46, + "SgstAmt": 0.46, + "CesRt": 0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "TotItemVal": 8.52 + }, + { + "SlNo": "2", + "PrdDesc": "Test Trading Goods 1", + "IsServc": "N", + "HsnCd": "61149090", + "Unit": "NOS", + "Qty": 1.0, + "UnitPrice": 7.6, + "TotAmt": 7.6, + "Discount": 0, + "AssAmt": 7.6, + "GstRt": 12.0, + "IgstAmt": 0, + "CgstAmt": 0.45, + "SgstAmt": 0.45, + "CesRt": 0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "TotItemVal": 8.5 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 0 + }, + "RefDtls": { + "PrecDocDtls": [ + { + "InvDt": "17/09/2022", + "InvNo": "SINV-CFY-00092" + } + ] + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "02AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02", + "TrdNm": "_Test Indian Registered Company" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 15.2, + "CesVal": 0, + "CgstVal": 0.91, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": -0.02, + "SgstVal": 0.91, + "TotInvVal": 17.0 + }, + "Version": "1.1" + }, + "response_data": { + "success": true, + "message": "IRN generated successfully", + "result": { + "AckNo": 232210036755145, + "AckDt": "2022-09-17 17:05:00", + "Irn": "1c96258af085e45da556494ea5e5a7b401a598ab80af4136309c2dac7b54d795", + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTUxNDUsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNzowNTowMFwiLFwiSXJuXCI6XCIxYzk2MjU4YWYwODVlNDVkYTU1NjQ5NGVhNWU1YTdiNDAxYTU5OGFiODBhZjQxMzYzMDljMmRhYzdiNTRkNzk1XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJDUk5cIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIlRyZE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiVGVzdCBUcmFkaW5nIEdvb2RzIDFcIixcIkhzbkNkXCI6XCI2MTE0OTA5MFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTk9TXCIsXCJVbml0UHJpY2VcIjoxMDAuMCxcIlRvdEFtdFwiOjEwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTAwLjAsXCJHc3RSdFwiOjAuMCxcIklnc3RBbXRcIjowLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxMDAuMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjEwMC4wLFwiQ2dzdFZhbFwiOjAsXCJTZ3N0VmFsXCI6MCxcIklnc3RWYWxcIjowLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxMDAuMH0sXCJQYXlEdGxzXCI6e1wiQ3JEYXlcIjowLFwiUGFpZEFtdFwiOjAsXCJQYXltdER1ZVwiOjAuMH0sXCJSZWZEdGxzXCI6e1wiUHJlY0RvY0R0bHNcIjpbe1wiSW52Tm9cIjpcIlNJTlYtQ0ZZLTAwMDkyXCIsXCJJbnZEdFwiOlwiMTcvMDkvMjAyMlwifV19LFwiRXdiRHRsc1wiOntcIkRpc3RhbmNlXCI6MH19IiwiaXNzIjoiTklDIn0.OZpYYN2pXIPJmLTi79NuVxVWWWGwdUzdnIEKXpgXyCEtXqgfwm9opf6EbyEt6wjwiYV4b2HWAfmkOXYJ-TO3dMc_98nOW8SeupGC6k-aV5kcDK9DNiRNWZBCiTMCRhlfmYC3oEZtd5f6yVoHvESl-QKeBlXcK2L8M1C0Guuq3QYqyhFp_gFqz_xY6ImKi65hEMDBghCaKINatRvPtFiwmtIspxUaHid5YcyqXdBxerEDjz-D2RjHOnhq1K_JX07X9xqcncLa106XltEeVFswJiEfqwtZTKDzkd4klymGJL5je7L1tgO8sNTKXGtFETKZfKmFSvxsMrBy1Dlp0LXXtg", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIkNSTlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiNjExNDkwOTBcIixcIklyblwiOlwiMWM5NjI1OGFmMDg1ZTQ1ZGE1NTY0OTRlYTVlNWE3YjQwMWE1OThhYjgwYWY0MTM2MzA5YzJkYWM3YjU0ZDc5NVwiLFwiSXJuRHRcIjpcIjIwMjItMDktMTcgMTc6MDU6MDBcIn0iLCJpc3MiOiJOSUMifQ.fgLb2ILeWK-MHxs61jZeo3kgy6w-sFXkVSh9GbzH0yWCeVx8MTsRwkKEXklPlmdwudG1lUPVjWsOioCgLL1Q8EE_wqnpGY00JEWr5X74GduBLe0lo5ZKtsSIVf10REy1E7_JJV8qitSZa3JeSqdjPlTUowFpxvPiw-nu3fyBP92IdVFbGO6oMvodI66kanqEyKj26Rn0nfnHP3KT3u61RBUSaVm_gch79cnPQanDPrJtd_0Ra2Vn7FoopdfcNIEdASB71IDRHsMFCNs8LyHTtFoJVI2LqU8wic_A6oWZkswOTBHdsBauMa_CMF9-2QbwTHFv60yvS7KuS2HvBw-oyQ", + "Status": "ACT", + "EwbNo": null, + "EwbDt": null, + "EwbValidTill": null, + "Remarks": null + }, + "info": [ + { + "InfCd": "EWBERR", + "Desc": [ + { + "ErrorCode": "4019", + "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" + } + ] + } + ] + } + }, + "debit_invoice": { + "kwargs": { + "is_debit_note": 1, + "qty": 0, + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "02", + "Stcd": "36", + "TrdNm": "_Test Registered Customer" + }, + "DocDtls": { + "Dt": "17/09/2022", + "No": "test_invoice_no", + "Typ": "DBN" + }, + "ItemList": [ + { + "AssAmt": 100.0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0.0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 0.0, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100.0, + "TotItemVal": 100.0, + "Unit": "NOS", + "UnitPrice": 100.0 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100.0 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "02AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02", + "TrdNm": "_Test Indian Registered Company" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100.0, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": 0.0, + "SgstVal": 0, + "TotInvVal": 100.0 + }, + "Version": "1.1" + }, + "response_data": { + "success": true, + "message": "IRN generated successfully", + "result": { + "AckNo": 232210036755701, + "AckDt": "2022-09-17 17:50:00", + "Irn": "24f37c80532583c6894d8153e2b12494daa80ddbb197f0fc2c1bac07db67f933", + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTU3MDEsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNzo1MDowMFwiLFwiSXJuXCI6XCIyNGYzN2M4MDUzMjU4M2M2ODk0ZDgxNTNlMmIxMjQ5NGRhYTgwZGRiYjE5N2YwZmMyYzFiYWMwN2RiNjdmOTMzXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJEQk5cIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiVGVzdCBUcmFkaW5nIEdvb2RzIDFcIixcIkhzbkNkXCI6XCI2MTE0OTA5MFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTk9TXCIsXCJVbml0UHJpY2VcIjoxMDAuMCxcIlRvdEFtdFwiOjEwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTAwLjAsXCJHc3RSdFwiOjAuMCxcIklnc3RBbXRcIjowLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxMDAuMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjEwMC4wLFwiQ2dzdFZhbFwiOjAsXCJTZ3N0VmFsXCI6MCxcIklnc3RWYWxcIjowLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxMDAuMH0sXCJQYXlEdGxzXCI6e1wiQ3JEYXlcIjowLFwiUGFpZEFtdFwiOjAsXCJQYXltdER1ZVwiOjEwMC4wfSxcIkV3YkR0bHNcIjp7XCJEaXN0YW5jZVwiOjB9fSIsImlzcyI6Ik5JQyJ9.ad1NfDA-H8FgBHr_kaTeiVWUj-f8T6NXuLFWa1gprxGhACXoI9h6sU47U9PBxHcZ7qVXcYAKzA9CbNvAfxWCLKtjur5p85uIrksZYDD494lodGn3QeXbyjXMJeh7eM0mcKKN3Chp0TxaUfi9C7mA9W0R8HYKNnXIOT1CVlD-brrAw09_QiDsNgMhLBX5QfpHKIHPKCIEl_DgmWlnMzduy1iKYPpNreNPCV-J-ZaVQjxl93LjKBUb5AF1XWyWvPw_e8ePZEttviX_bU_Nnm1M4zCj-QWYzj8A0bauzl7kjp5UajEM7_7CLAI4sjZnqonGKYFfR5rf2Qj76exbs_pWGw", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIkRCTlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiNjExNDkwOTBcIixcIklyblwiOlwiMjRmMzdjODA1MzI1ODNjNjg5NGQ4MTUzZTJiMTI0OTRkYWE4MGRkYmIxOTdmMGZjMmMxYmFjMDdkYjY3ZjkzM1wiLFwiSXJuRHRcIjpcIjIwMjItMDktMTcgMTc6NTA6MDBcIn0iLCJpc3MiOiJOSUMifQ.TUE7iIvF9Orc1bEuOaxtSuj0D6vP1MwV-hcoh3IZqV7EKXwMmm0PkVrN87vRzSu97NMKfQIHJDRgPYv4prhuT0dshWJ9A_kC4jiRSm5Naj_R1egBKsv5ykTojOKKrkGy35DtdUcJK_FyiD0qFfmMmInFD8u6D8W83eEo93i99RONgVKUyCkd_uqs-cz1P-PTlsi2xWbeDVVIcRoAmf-lcsbwkl2Hn6ECHgorKJHPJC1FGo1jRQ2Ktq0ODiJdncplbxbdYN19vUz61JJB4DPzWtf8wOkX11N0fDhdUdEINfJURWEOIGQRYip5GIJuA2qdqxZieVk0CetnsckcGgfE8g", + "Status": "ACT", + "EwbNo": null, + "EwbDt": null, + "EwbValidTill": null, + "Remarks": null + }, + "info": [ + { + "InfCd": "EWBERR", + "Desc": [ + { + "ErrorCode": "4019", + "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" + } + ] + } + ] + } + }, + "cancel_e_invoice": { + "request_data": { + "Cnlrem": "Data Entry Mistake", + "Cnlrsn": "2", + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" + }, + "response_data": { + "message": "E-Invoice is cancelled successfully", + "result": { + "CancelDate": "2022-09-17 18:09:00", + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" + }, + "success": true + } + }, + "cancel_e_waybill": { + "request_data": { + "cancelRmrk": "Data Entry Mistake", + "cancelRsnCode": "3", + "ewbNo": "391009149369" + }, + "response_data": { + "message": "E-Way bill cancelled successfully", + "result": { + "cancelDate": "20/09/2022 12:10:00 PM", + "ewayBillNo": "391009149369" + }, + "success": true + } + }, + "dispatch_details": { + "DispDtls": { + "Nm": "Test Indian Registered Company", + "Addr1": "Test Address - 5", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02" + } + }, + "shipping_details": { + "ShipDtls": { + "Gstin": "36AMBPG7773M002", + "LglNm": "Test Registered Customer", + "TrdNm": "Test Registered Customer", + "Addr1": "Test Address - 4", + "Loc": "Test City", + "Pin": 500055, + "Stcd": "36" + } + }, + "foreign_transaction": { + "kwargs": { + "customer": "_Test Foreign Customer", + "port_address": "_Test Indian Unregistered Company-Billing", + "vehicle_no": "GJ07DL9009" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 9", + "Gstin": "URP", + "LglNm": "_Test Foreign Customer", + "Loc": "Test City", + "Pin": 999999, + "Pos": "96", + "Stcd": "96", + "TrdNm": "_Test Foreign Customer" + }, + "DocDtls": { + "Dt": "16/09/2022", + "No": "test_invoice_no", + "Typ": "INV" + }, + "EwbDtls": { + "Distance": 0, + "TransMode": "1", + "VehNo": "GJ07DL9009", + "VehType": "R" + }, + "ItemList": [ + { + "AssAmt": 100000, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 1000, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100000, + "TotItemVal": 100000, + "Unit": "NOS", + "UnitPrice": 100 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100000 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "02AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02", + "TrdNm": "_Test Indian Registered Company" + }, + "ShipDtls": { + "Gstin": "URP", + "LglNm": "Test Indian Unregistered Company", + "TrdNm": "Test Indian Unregistered Company", + "Addr1": "Test Address - 2", + "Loc": "Test City", + "Pin": 380015, + "Stcd": "24" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "EXPWOP", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100000, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0, + "RndOffAmt": 0, + "SgstVal": 0, + "TotInvVal": 100000 + }, + "ExpDtls": { + "CntCode": "US", + "ShipBNo": "1234", + "ShipBDt": "2023-06-10", + "Port": "INABG1" + }, + "Version": "1.1" + } + }, + "duplicate_irn": { + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "02", + "Stcd": "36", + "TrdNm": "_Test Registered Customer" + }, + "DocDtls": { + "Dt": "03/10/2023", + "No": "test_invoice_no", + "Typ": "INV" + }, + "ItemList": [ + { + "AssAmt": 1400.0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 126.0, + "Discount": 0, + "GstRt": 18.0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 1.0, + "SgstAmt": 126.0, + "SlNo": "1", + "TotAmt": 1400.0, + "TotItemVal": 1652.0, + "Unit": "NOS", + "UnitPrice": 1400.0 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 1652.0 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "02AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 171302, + "Stcd": "02", + "TrdNm": "_Test Indian Registered Company" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 1400.0, + "CesVal": 0, + "CgstVal": 126.0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": 0.0, + "SgstVal": 126.0, + "TotInvVal": 1652.0 + }, + "Version": "1.1" + }, + "response_data": { + "error_code": "2150", + "message": "2150 : Duplicate IRN", + "result": [ + { + "Desc": { + "AckDt": "2023-10-02 16:26:00", + "AckNo": 232210036743849, + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" + }, + "InfCd": "DUPIRN" + } + ], + "success": false + } + }, + "get_e_invoice_by_irn": { + "request_data": "irn=706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7", + "response_data": { + "message": "E-Invoice fetched successfully", + "result": { + "AckDt": "2023-10-03 15:09:00", + "AckNo": 232210036743849, + "EwbDt": null, + "EwbNo": null, + "EwbValidTill": null, + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7", + "Remarks": null, + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE1MTNCODIxRUU0NkM3NDlBNjNCODZFMzE4QkY3MTEwOTkyODdEMUYiLCJ4NXQiOiJGUk80SWU1R3gwbW1PNGJqR0w5eEVKa29mUjgiLCJ0eXAiOiJKV1QifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoxMzIzMTAwNDYwNjM2MDEsXCJBY2tEdFwiOlwiMjAyMy0xMC0wMyAxNTowODozOVwiLFwiSXJuXCI6XCIyZWNjNDJhOGU4MWRjYmJiNTY5MjNlNGJkMDQ2Yzg2MWNlNzM3NDA2MGJkMzE4NzVjZTM1NDQ3ZTkyMmM5Mzk5XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCI5c1E1RTJcIixcIkR0XCI6XCIwMi8xMC8yMDIzXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJSZXNpbGllbnQgVGVzdFwiLFwiVHJkTm1cIjpcIlJlc2lsaWVudCBUZXN0XCIsXCJBZGRyMVwiOlwiU2h1YmhhbnB1cmFcIixcIkxvY1wiOlwiVmFkb2RhcmFcIixcIlBpblwiOjE3MTMwMixcIlN0Y2RcIjpcIjAyXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ29tcG9zaXRpb24gQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJfVGVzdCBSZWdpc3RlcmVkIENvbXBvc2l0aW9uIEN1c3RvbWVyXCIsXCJQb3NcIjpcIjM2XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gNlwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjUwMDA1NSxcIlN0Y2RcIjpcIjM2XCJ9LFwiSXRlbUxpc3RcIjpbe1wiSXRlbU5vXCI6MCxcIlNsTm9cIjpcIjFcIixcIklzU2VydmNcIjpcIk5cIixcIlByZERlc2NcIjpcIlRlc3QgVHJhZGluZyBHb29kcyAxXCIsXCJIc25DZFwiOlwiNjExNDkwOTBcIixcIlF0eVwiOjEuMCxcIlVuaXRcIjpcIk5PU1wiLFwiVW5pdFByaWNlXCI6MTIwMC4wLFwiVG90QW10XCI6MTIwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTIwMC4wLFwiR3N0UnRcIjoxOC4wLFwiSWdzdEFtdFwiOjIxNi4wLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxNDE2LjB9XSxcIlZhbER0bHNcIjp7XCJBc3NWYWxcIjoxMjAwLjAsXCJDZ3N0VmFsXCI6MCxcIlNnc3RWYWxcIjowLFwiSWdzdFZhbFwiOjIxNi4wLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxNDE2LjB9LFwiUGF5RHRsc1wiOntcIkNyRGF5XCI6MCxcIlBhaWRBbXRcIjowLFwiUGF5bXREdWVcIjoxNDE2LjB9fSIsImlzcyI6Ik5JQyBTYW5kYm94In0.B3VNUOB_DlHiiOgdl3MpMFiY3uQVrp2mTs0FUP9wxp_djYm3fRTraReju2Hq-eFn1FOE_lHuK7-oIqnjr1uTNOzB62pcQ_iF0M79OGhhVgGgelz-WQQH-DfdhktTn1OlA_ZjeIhU8qM3-ksfH5lncQ_chgLrK1lH3SsarXmqMxNR4vZY_arEniymEDluE9CMxlkvcCIXVcYiAQvP785aG3Lf2vzC7Z9dAToghxDAeU2b1wJ-k_i5SNYZOdIOfPfZWYhIDXsHW4hVkt4EQdkTV5HBTUYA29EIwbwzDfwdAszvGxJVJgtoXjJk5NWMhhUnq19XHJ0pAr8hUOdI6so9_w", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE1MTNCODIxRUU0NkM3NDlBNjNCODZFMzE4QkY3MTEwOTkyODdEMUYiLCJ4NXQiOiJGUk80SWU1R3gwbW1PNGJqR0w5eEVKa29mUjgiLCJ0eXAiOiJKV1QifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAyQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiOXNRNUUyXCIsXCJEb2NUeXBcIjpcIklOVlwiLFwiRG9jRHRcIjpcIjAyLzEwLzIwMjNcIixcIlRvdEludlZhbFwiOjE0MTYuMCxcIkl0ZW1DbnRcIjoxLFwiTWFpbkhzbkNvZGVcIjpcIjYxMTQ5MDkwXCIsXCJJcm5cIjpcIjJlY2M0MmE4ZTgxZGNiYmI1NjkyM2U0YmQwNDZjODYxY2U3Mzc0MDYwYmQzMTg3NWNlMzU0NDdlOTIyYzkzOTlcIixcIklybkR0XCI6XCIyMDIzLTEwLTAzIDE1OjA4OjM5XCJ9IiwiaXNzIjoiTklDIFNhbmRib3gifQ.fTW2bXRGYvCma3UpS0axy26c6dTW0nY9RtDI5lGX4x2n29iwdcJOHvBwr6rskVKvnLEWHX9VgxDu2Jknyeu6RBlPLD1pGTORATj0nWo_kNwXJi-l_--6J0WC5is8G11eK04Mnepk63SKrbPTr8uYRd5OigMhDmSCde_tqFfUIIMtnx8AY6z0snkx3KQaooPJW06eU_QaACgrmypE1q-hfpNJOhzlHy-a4VC4zW2HljUZkubbK_5Dyb0Zy2tGc15enZeHi9DhvDFEUqiyJavjwsijbugJumR81LyOC1M4JtFEWoMrCA8bsbwAAFUtRgVC6pDVNZlthHqHxObSf9yv6g", + "Status": "ACT" + }, + "success": true + } + } +} diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js index 4b603f5fe..85397837a 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js @@ -8,6 +8,8 @@ frappe.ui.form.on("Bill of Entry", { }, refresh(frm) { + india_compliance.set_reconciliation_status(frm, "bill_of_entry_no"); + if (frm.doc.docstatus === 0) return; // check if Journal Entry exists; diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json index 0fea1603f..ec1001839 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.json @@ -9,9 +9,9 @@ "engine": "InnoDB", "field_order": [ "naming_series", - "column_break_2wjs", - "amended_from", "column_break_hbfk", + "amended_from", + "column_break_2wjs", "section_break_fqdh", "purchase_invoice", "column_break_epap", @@ -31,6 +31,7 @@ "column_break_xjvk", "customs_expense_account", "column_break_zsed", + "reconciliation_status", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -269,13 +270,22 @@ "fieldname": "bill_of_lading_no", "fieldtype": "Data", "label": "Bill of Lading No." + }, + { + "fieldname": "reconciliation_status", + "fieldtype": "Select", + "label": "Reconciliation Status", + "no_copy": 1, + "options": "\nNot Applicable\nReconciled\nUnreconciled\nIgnored", + "print_hide": 1, + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-28 07:26:31.704232", + "modified": "2023-09-14 16:43:49.370165", "modified_by": "Administrator", "module": "GST India", "name": "Bill of Entry", diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py index c0ff0efef..7dc7d780c 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py @@ -41,6 +41,7 @@ def before_validate(self): def validate(self): self.validate_purchase_invoice() self.validate_taxes() + self.reconciliation_status = "Unreconciled" def on_submit(self): make_gl_entries(self.get_gl_entries()) diff --git a/india_compliance/gst_india/doctype/gst_credential/gst_credential.json b/india_compliance/gst_india/doctype/gst_credential/gst_credential.json index be47bc4d0..190f4689b 100644 --- a/india_compliance/gst_india/doctype/gst_credential/gst_credential.json +++ b/india_compliance/gst_india/doctype/gst_credential/gst_credential.json @@ -10,7 +10,11 @@ "service", "gstin", "username", - "password" + "password", + "app_key", + "session_key", + "auth_token", + "session_expiry" ], "fields": [ { @@ -49,11 +53,39 @@ "fieldtype": "Password", "in_list_view": 1, "label": "Password" + }, + { + "fieldname": "session_key", + "fieldtype": "Data", + "hidden": 1, + "label": "Session Key", + "read_only": 1 + }, + { + "fieldname": "auth_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Auth Token", + "read_only": 1 + }, + { + "fieldname": "app_key", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "App Key" + }, + { + "fieldname": "session_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Session Expiry Date", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2022-07-24 02:04:03.084096", + "modified": "2023-10-04 12:44:54.332474", "modified_by": "Administrator", "module": "GST India", "name": "GST Credential", diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/__init__.py b/india_compliance/gst_india/doctype/gst_inward_supply/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.js b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.js new file mode 100644 index 000000000..747be87f6 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Resilient Tech and contributors +// For license information, please see license.txt + +frappe.ui.form.on('GST Inward Supply', { + // refresh: function(frm) { + + // } +}); diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json new file mode 100644 index 000000000..f6091d6b6 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.json @@ -0,0 +1,402 @@ +{ + "actions": [], + "autoname": "format:GST-IS-{######}", + "creation": "2022-04-12 17:45:44.603856", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_1", + "company", + "column_break_3", + "company_gstin", + "section_break_4", + "supplier_name", + "column_break_8", + "supplier_gstin", + "section_break_8", + "bill_no", + "doc_type", + "place_of_supply", + "column_break_13", + "bill_date", + "supply_type", + "classification", + "is_reverse_charge", + "section_break_16", + "items", + "document_value", + "section_break_18", + "diffprcnt", + "column_break_15", + "port_code", + "section_break_27", + "irn_source", + "irn_number", + "column_break_30", + "irn_gen_date", + "section_break_25", + "itc_availability", + "return_period_2b", + "column_break_28", + "reason_itc_unavailability", + "gen_date_2b", + "section_break_32", + "other_return_period", + "original_bill_no", + "is_amended", + "column_break_36", + "amendment_type", + "original_bill_date", + "original_doc_type", + "section_break_22", + "action", + "link_doctype", + "column_break_27", + "match_status", + "link_name", + "section_break_43", + "sup_return_period", + "gstr_1_filled", + "gstr_3b_filled", + "column_break_47", + "gstr_1_filing_date", + "registration_cancel_date" + ], + "fields": [ + { + "fieldname": "section_break_1", + "fieldtype": "Section Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_gstin", + "fieldtype": "Data", + "label": "Company GSTIN" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Supplier Details" + }, + { + "fieldname": "supplier_name", + "fieldtype": "Data", + "label": "Supplier Name" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "supplier_gstin", + "fieldtype": "Data", + "label": "Supplier GSTIN" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Document Details" + }, + { + "fieldname": "doc_type", + "fieldtype": "Select", + "label": "Document Type", + "options": "\nInvoice\nCredit Note\nDebit Note\nISD Invoice\nISD Credit Note\nBill of Entry" + }, + { + "fieldname": "place_of_supply", + "fieldtype": "Data", + "label": "Place of Supply" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "supply_type", + "fieldtype": "Select", + "label": "Supply Type", + "options": "\nRegular\nSEZ Without Payment of Tax\nSEZ With Payment of Tax\nDeemend Exports" + }, + { + "fieldname": "classification", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Classification", + "options": "\nB2B\nB2BA\nCDNR\nCDNRA\nISD\nISDA\nIMPG\nIMPGSEZ" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "GST Inward Supply Item" + }, + { + "fieldname": "document_value", + "fieldtype": "Float", + "label": "Document Value" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break" + }, + { + "fieldname": "diffprcnt", + "fieldtype": "Data", + "label": "Applicable % of tax rate" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "fieldname": "port_code", + "fieldtype": "Data", + "label": "Port Code" + }, + { + "fieldname": "section_break_27", + "fieldtype": "Section Break" + }, + { + "fieldname": "irn_source", + "fieldtype": "Data", + "label": "IRN Source" + }, + { + "fieldname": "irn_number", + "fieldtype": "Data", + "label": "IRN Number" + }, + { + "fieldname": "column_break_30", + "fieldtype": "Column Break" + }, + { + "fieldname": "irn_gen_date", + "fieldtype": "Date", + "label": "IRN Generation Date" + }, + { + "fieldname": "section_break_25", + "fieldtype": "Section Break", + "label": "GSTR 2B Details" + }, + { + "fieldname": "itc_availability", + "fieldtype": "Select", + "label": "ITC Availability", + "options": "\nYes\nNo\nTemporary" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "reason_itc_unavailability", + "fieldtype": "Data", + "label": "Reason for ITC Unavailability" + }, + { + "fieldname": "section_break_32", + "fieldtype": "Section Break", + "label": "Amendment Details" + }, + { + "fieldname": "other_return_period", + "fieldtype": "Data", + "label": "Amendment/Original Period" + }, + { + "default": "0", + "fieldname": "is_amended", + "fieldtype": "Check", + "label": "Is Amended" + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fieldname": "amendment_type", + "fieldtype": "Data", + "label": "Amendment Type" + }, + { + "fieldname": "original_doc_type", + "fieldtype": "Select", + "label": "Original Document Type", + "options": "\nCredit Note\nDebit Note\nISD Invoice\nISD Credit Note" + }, + { + "fieldname": "section_break_22", + "fieldtype": "Section Break", + "label": "Links" + }, + { + "default": "No Action", + "fieldname": "action", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Action", + "options": "No Action\nAccept My Values\nAccept Supplier Values\nIgnore\nPending" + }, + { + "fieldname": "link_doctype", + "fieldtype": "Link", + "label": "Link Document Type", + "options": "DocType" + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "match_status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Match Status", + "options": "\nExact Match\nSuggested Match\nMismatch\nManual Match\nUnlinked\nAmended" + }, + { + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "label": "Link Name", + "options": "link_doctype", + "search_index": 1 + }, + { + "fieldname": "section_break_43", + "fieldtype": "Section Break", + "label": "Supplier Filing Status" + }, + { + "fieldname": "sup_return_period", + "fieldtype": "Data", + "label": "Return Period" + }, + { + "default": "0", + "fieldname": "gstr_1_filled", + "fieldtype": "Check", + "label": "GSTR 1 Filled" + }, + { + "default": "0", + "fieldname": "gstr_3b_filled", + "fieldtype": "Check", + "label": "GSTR 3B Filled" + }, + { + "fieldname": "column_break_47", + "fieldtype": "Column Break" + }, + { + "fieldname": "gstr_1_filing_date", + "fieldtype": "Date", + "label": "GSTR 1 Filing Date" + }, + { + "fieldname": "registration_cancel_date", + "fieldtype": "Date", + "label": "Registration Cancel Date" + }, + { + "fieldname": "return_period_2b", + "fieldtype": "Data", + "label": "2B Return Period" + }, + { + "fieldname": "gen_date_2b", + "fieldtype": "Date", + "label": "2B Generation Date" + }, + { + "default": "0", + "fieldname": "is_reverse_charge", + "fieldtype": "Check", + "label": "Reverse Charge" + }, + { + "fieldname": "bill_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Document Number" + }, + { + "fieldname": "bill_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Document Date" + }, + { + "fieldname": "original_bill_no", + "fieldtype": "Data", + "label": "Original Document Number" + }, + { + "fieldname": "original_bill_date", + "fieldtype": "Date", + "label": "Original Document Date" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-09-06 21:35:11.153554", + "modified_by": "Administrator", + "module": "GST India", + "name": "GST Inward Supply", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 + }, + { + "export": 1, + "read": 1, + "report": 1, + "role": "Accounts User" + } + ], + "search_fields": "bill_date, supplier_name, bill_no, document_value", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "supplier_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py new file mode 100644 index 000000000..4663c1ef6 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py @@ -0,0 +1,200 @@ +# Copyright (c) 2022, Resilient Tech and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form + +from india_compliance.gst_india.constants import ORIGINAL_VS_AMENDED + + +class GSTInwardSupply(Document): + def before_save(self): + if self.classification.endswith("A"): + self.is_amended = True + + if self.gstr_1_filing_date: + self.gstr_1_filled = True + + if self.match_status != "Amended" and ( + self.other_return_period or self.is_amended + ): + update_docs_for_amendment(self) + + +def create_inward_supply(transaction): + filters = { + "bill_no": transaction.bill_no, + "bill_date": transaction.bill_date, + "classification": transaction.classification, + "supplier_gstin": transaction.supplier_gstin, + } + + if name := frappe.get_value("GST Inward Supply", filters): + gst_inward_supply = frappe.get_doc("GST Inward Supply", name) + else: + gst_inward_supply = frappe.new_doc("GST Inward Supply") + + gst_inward_supply.update(transaction) + return gst_inward_supply.save(ignore_permissions=True) + + +def update_docs_for_amendment(doc): + fields = [ + "name", + "match_status", + "action", + "link_doctype", + "link_name", + "sup_return_period", + ] + if doc.is_amended: + original = frappe.db.get_value( + "GST Inward Supply", + filters={ + "bill_no": doc.original_bill_no, + "bill_date": doc.original_bill_date, + "supplier_gstin": doc.supplier_gstin, + "classification": get_other_classification(doc), + }, + fieldname=fields, + as_dict=True, + ) + if not original: + # handle amendment from amendments where original is not available + original = frappe.db.get_value( + "GST Inward Supply", + filters={ + "original_bill_no": doc.original_bill_no, + "original_bill_date": doc.original_bill_date, + "supplier_gstin": doc.supplier_gstin, + "classification": doc.classification, + "name": ["!=", doc.name], + }, + fieldname=fields, + as_dict=True, + ) + if not original: + return + + # handle future amendments + if ( + original.match_status == "Amended" + and original.link_name + and original.link_name != doc.name + and doc.is_new() + ): + frappe.db.set_value( + "GST Inward Supply", original.name, "link_name", doc.name + ) + + # new original + original = frappe.db.get_value( + "GST Inward Supply", original.link_name, fields + ) + + if original.match_status == "Amended": + return + + # update_original_from_amended + frappe.db.set_value( + "GST Inward Supply", + original.name, + { + "match_status": "Amended", + "action": "No Action", + "link_doctype": "GST Inward Supply", + "link_name": doc.name, + }, + ) + + # update_amended_from_original + doc.update( + { + "match_status": original.match_status, + "action": original.action, + "link_doctype": original.link_doctype, + "link_name": original.link_name, + } + ) + if not doc.other_return_period: + doc.other_return_period = original.sup_return_period + + ensure_valid_match(doc, original) + + else: + # Handle case where original is imported after amended + ensure_valid_match(doc, doc) + if doc.match_status == "Amended": + return + + doc.update( + { + "match_status": "Amended", + "action": "No Action", + } + ) + amended = frappe.db.get_value( + "GST Inward Supply", + filters={ + "original_bill_no": doc.bill_no, + "original_bill_date": doc.bill_date, + "supplier_gstin": doc.supplier_gstin, + "classification": get_other_classification(doc), + }, + fieldname=fields, + as_dict=True, + ) + + if not amended: + return + + # update_original_from_amended + doc.update( + { + "link_doctype": "GST Inward Supply", + "link_name": amended.name, + } + ) + + +def ensure_valid_match(doc, original): + """ + Where receiver GSTIN is amended, company cannot claim credit for the original document. + """ + + if doc.amendment_type != "Receiver GSTIN Amended": + return + + if original.link_doctype: + frappe.msgprint( + _( + "You have claimend credit for {0} {1} against GST Inward Supply {2} where receiver GSTIN is amended. The same has been reversed." + ).format( + original.link_doctype, + get_link_to_form(original.link_doctype, original.link_name), + get_link_to_form("GST Inward Supply", original.name), + ), + title=_("Invalid Match"), + ) + + doc.update( + { + "match_status": "Amended", + "action": "No Action", + "link_doctype": None, + "link_name": None, + } + ) + + +def get_other_classification(doc): + if doc.is_amended: + for row in ORIGINAL_VS_AMENDED: + if row["original"] in doc.classification: + return row["original"] + + for row in ORIGINAL_VS_AMENDED: + if row["original"] == doc.classification: + return row["amended"] or row["original"] diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/test_gst_inward_supply.py b/india_compliance/gst_india/doctype/gst_inward_supply/test_gst_inward_supply.py new file mode 100644 index 000000000..39b556615 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_inward_supply/test_gst_inward_supply.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Resilient Tech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestGSTInwardSupply(FrappeTestCase): + pass diff --git a/india_compliance/gst_india/doctype/gst_inward_supply_item/__init__.py b/india_compliance/gst_india/doctype/gst_inward_supply_item/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/india_compliance/gst_india/doctype/gst_inward_supply_item/gst_inward_supply_item.json b/india_compliance/gst_india/doctype/gst_inward_supply_item/gst_inward_supply_item.json new file mode 100644 index 000000000..36f99c2d6 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_inward_supply_item/gst_inward_supply_item.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "creation": "2022-04-12 17:40:41.914298", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_number", + "taxable_value", + "rate", + "igst", + "cgst", + "sgst", + "cess" + ], + "fields": [ + { + "fieldname": "item_number", + "fieldtype": "Int", + "label": "Item Number" + }, + { + "fieldname": "taxable_value", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Taxable Value" + }, + { + "fieldname": "rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Tax Rate" + }, + { + "fieldname": "igst", + "fieldtype": "Float", + "in_list_view": 1, + "label": "IGST Amount" + }, + { + "fieldname": "cgst", + "fieldtype": "Float", + "in_list_view": 1, + "label": "CGST Amount" + }, + { + "fieldname": "sgst", + "fieldtype": "Float", + "in_list_view": 1, + "label": "SGST Amount" + }, + { + "fieldname": "cess", + "fieldtype": "Float", + "label": "CESS Amount" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-04-12 17:40:41.914298", + "modified_by": "Administrator", + "module": "GST India", + "name": "GST Inward Supply Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_inward_supply_item/gst_inward_supply_item.py b/india_compliance/gst_india/doctype/gst_inward_supply_item/gst_inward_supply_item.py new file mode 100644 index 000000000..e7d7cc53f --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_inward_supply_item/gst_inward_supply_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Resilient Tech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class GSTInwardSupplyItem(Document): + pass diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.js b/india_compliance/gst_india/doctype/gst_settings/gst_settings.js index 2ac6bef70..668b6a2f6 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.js +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.js @@ -3,8 +3,6 @@ frappe.ui.form.on("GST Settings", { setup(frm) { - frm.get_field("credentials").grid.get_docfield("password").reqd = 1; - ["cgst_account", "sgst_account", "igst_account", "cess_account"].forEach( field => filter_accounts(frm, field) ); @@ -38,15 +36,6 @@ frappe.ui.form.on("GST Settings", { }, }); -frappe.ui.form.on("GST Credential", { - service(frm, cdt, cdn) { - const doc = frappe.get_doc(cdt, cdn); - const row = frm.get_field("credentials").grid.grid_rows_by_docname[doc.name]; - - row.toggle_reqd("password", doc.service !== "Returns"); - }, -}); - function filter_accounts(frm, account_field) { frm.set_query(account_field, "gst_accounts", (_, cdt, cdn) => { const row = frappe.get_doc(cdt, cdn); diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json index d76a3ed9a..bd555f921 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json @@ -51,7 +51,8 @@ "gst_accounts", "credentials_tab", "credentials", - "api_secret" + "api_secret", + "gstn_public_certificate" ], "fields": [ { @@ -364,12 +365,18 @@ "fieldname": "validate_gstin_status", "fieldtype": "Check", "label": "Refresh and Validate GSTIN Status in Transactions" + }, + { + "fieldname": "gstn_public_certificate", + "fieldtype": "Code", + "hidden": 1, + "label": "GSTN Public Certificate" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-24 19:19:53.537062", + "modified": "2023-10-05 20:27:28.909862", "modified_by": "Administrator", "module": "GST India", "name": "GST Settings", diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py index 11a575602..7699c48fc 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py @@ -144,7 +144,11 @@ def validate_credentials(self): return for credential in self.credentials: - if credential.service == "Returns" or credential.password: + if credential.service == "Returns": + self.validate_app_key(credential) + continue + + if credential.password: continue frappe.throw( @@ -170,6 +174,10 @@ def validate_credentials(self): alert=True, ) + def validate_app_key(self, credential): + if not credential.app_key or len(credential.app_key) != 32: + credential.app_key = frappe.generate_hash(length=32) + def validate_enable_api(self): if ( self.enable_api diff --git a/india_compliance/gst_india/doctype/gstin/gstin.py b/india_compliance/gst_india/doctype/gstin/gstin.py index b0ae3ca85..e56bb247b 100644 --- a/india_compliance/gst_india/doctype/gstin/gstin.py +++ b/india_compliance/gst_india/doctype/gstin/gstin.py @@ -42,14 +42,18 @@ def update_gstin_status(self): @frappe.whitelist() -def get_gstin_status(gstin, transaction_date=None, is_request_from_ui=0): +def get_gstin_status( + gstin, transaction_date=None, is_request_from_ui=0, force_update=0 +): """ Permission check not required as GSTIN details are public where GSTIN is known. """ if not gstin: return - if not is_status_refresh_required(gstin, transaction_date): + if not int(force_update) and not is_status_refresh_required( + gstin, transaction_date + ): if not frappe.db.exists("GSTIN", gstin): return @@ -176,8 +180,9 @@ def _throw(message): if gstin_doc.status not in ("Active", "Cancelled"): return _throw( - _("Status of Party GSTIN {1} is {0}").format(gstin_doc.status), - gstin_doc.gstin, + _("Status of Party GSTIN {1} is {0}").format( + gstin_doc.status, gstin_doc.gstin + ) ) diff --git a/india_compliance/gst_india/doctype/gstr_import_log/__init__.py b/india_compliance/gst_india/doctype/gstr_import_log/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.js b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.js new file mode 100644 index 000000000..68cd1a577 --- /dev/null +++ b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Resilient Tech and contributors +// For license information, please see license.txt + +frappe.ui.form.on('GSTR Import Log', { + // refresh: function(frm) { + + // } +}); diff --git a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json new file mode 100644 index 000000000..92ec72e0b --- /dev/null +++ b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json @@ -0,0 +1,106 @@ +{ + "actions": [], + "creation": "2022-04-12 18:57:59.122090", + "description": "This document will maintain time log of latest download of GST Returns", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gstin", + "return_type", + "classification", + "return_period", + "last_updated_on", + "data_not_found", + "request_id", + "request_time" + ], + "fields": [ + { + "fieldname": "gstin", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "GSTIN" + }, + { + "fieldname": "classification", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Classification", + "options": "\nB2B\nB2BA\nCDNR\nCDNRA\nISD\nISDA\nIMPG\nIMPGSEZ" + }, + { + "fieldname": "return_period", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Return Period" + }, + { + "fieldname": "last_updated_on", + "fieldtype": "Datetime", + "label": "Last updated on" + }, + { + "fieldname": "return_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Return Type" + }, + { + "default": "0", + "fieldname": "data_not_found", + "fieldtype": "Check", + "label": "Data not Found" + }, + { + "fieldname": "request_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Request ID" + }, + { + "fieldname": "request_time", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Request Time" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-09-06 09:18:47.885426", + "modified_by": "Administrator", + "module": "GST India", + "name": "GSTR Import Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "export": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager" + }, + { + "export": 1, + "read": 1, + "report": 1, + "role": "Accounts User" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "gstin" +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py new file mode 100644 index 000000000..98a33cb65 --- /dev/null +++ b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py @@ -0,0 +1,81 @@ +# Copyright (c) 2022, Resilient Tech and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import add_to_date + + +class GSTRImportLog(Document): + pass + + +def create_import_log( + gstin, + return_type, + return_period, + classification=None, + data_not_found=False, + request_id=None, + retry_after_mins=None, +): + frappe.enqueue( + _create_import_log, + queue="short", + now=frappe.flags.in_test, + gstin=gstin, + return_type=return_type, + return_period=return_period, + data_not_found=data_not_found, + classification=classification, + request_id=request_id, + retry_after_mins=retry_after_mins, + ) + + +def _create_import_log( + gstin, + return_type, + return_period, + classification=None, + data_not_found=False, + request_id=None, + retry_after_mins=None, +): + doctype = "GSTR Import Log" + fields = { + "gstin": gstin, + "return_type": return_type, + "return_period": return_period, + } + + # TODO: change classification to gstr_category + if classification: + fields["classification"] = classification + + if log := frappe.db.get_value(doctype, fields): + log = frappe.get_doc(doctype, log) + else: + log = frappe.get_doc({"doctype": doctype, **fields}) + + if retry_after_mins: + log.request_time = add_to_date(None, minutes=retry_after_mins) + + log.request_id = request_id + log.data_not_found = data_not_found + log.last_updated_on = frappe.utils.now() + log.save(ignore_permissions=True) + if request_id: + toggle_scheduled_jobs(False) + + +def toggle_scheduled_jobs(stopped): + scheduled_job = frappe.db.get_value( + "Scheduled Job Type", + { + "method": "india_compliance.gst_india.utils.gstr.download_queued_request", + }, + ) + + if scheduled_job: + frappe.db.set_value("Scheduled Job Type", scheduled_job, "stopped", stopped) diff --git a/india_compliance/gst_india/doctype/gstr_import_log/test_gstr_import_log.py b/india_compliance/gst_india/doctype/gstr_import_log/test_gstr_import_log.py new file mode 100644 index 000000000..de70f84ec --- /dev/null +++ b/india_compliance/gst_india/doctype/gstr_import_log/test_gstr_import_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Resilient Tech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestGSTRImportLog(FrappeTestCase): + pass diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py new file mode 100644 index 000000000..f1f06752a --- /dev/null +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py @@ -0,0 +1,1336 @@ +# Copyright (c) 2022, Resilient Tech and contributors +# For license information, please see license.txt + +from enum import Enum + +from dateutil.rrule import MONTHLY, rrule +from rapidfuzz import fuzz, process + +import frappe +from frappe.query_builder import Case +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Abs, IfNull, Sum +from frappe.utils import add_months, format_date, getdate, rounded + +from india_compliance.gst_india.constants import GST_TAX_TYPES +from india_compliance.gst_india.utils import ( + get_gst_accounts_by_type, + get_party_for_gstin, +) +from india_compliance.gst_india.utils.gstr import IMPORT_CATEGORY, ReturnType + + +class Fields(Enum): + FISCAL_YEAR = "fy" + SUPPLIER_GSTIN = "supplier_gstin" + BILL_NO = "bill_no" + PLACE_OF_SUPPLY = "place_of_supply" + REVERSE_CHARGE = "is_reverse_charge" + TAXABLE_VALUE = "taxable_value" + CGST = "cgst" + SGST = "sgst" + IGST = "igst" + CESS = "cess" + + +class Rule(Enum): + EXACT_MATCH = "Exact Match" + FUZZY_MATCH = "Fuzzy Match" + MISMATCH = "Mismatch" + ROUNDING_DIFFERENCE = "Rounding Difference" # <= 1 hardcoded + + +class MatchStatus(Enum): + EXACT_MATCH = "Exact Match" + SUGGESTED_MATCH = "Suggested Match" + MISMATCH = "Mismatch" + RESIDUAL_MATCH = "Residual Match" + MANUAL_MATCH = "Manual Match" + MISSING_IN_PI = "Missing in PI" + MISSING_IN_2A_2B = "Missing in 2A/2B" + + +# Summary of rules: +# GSTIN_RULES = [ +# {"Exact Match": ["E", "E", "E", "E", "E", 0, 0, 0, 0, 0]}, +# {"Suggested Match": ["E", "E", "F", "E", "E", 0, 0, 0, 0, 0]}, +# {"Suggested Match": ["E", "E", "E", "E", "E", 1, 1, 1, 1, 2]}, +# {"Suggested Match": ["E", "E", "F", "E", "E", 1, 1, 1, 1, 2]}, +# {"Mismatch": ["E", "E", "E", "N", "N", "N", "N", "N", "N", "N"]}, +# {"Mismatch": ["E", "E", "F", "N", "N", "N", "N", "N", "N", "N"]}, +# {"Residual Match": ["E", "E", "N", "E", "E", 1, 1, 1, 1, 2]}, +# ] + +# PAN_RULES = [ +# {"Mismatch": ["E", "N", "E", "E", "E", 1, 1, 1, 1, 2]}, +# {"Mismatch": ["E", "N", "F", "E", "E", 1, 1, 1, 1, 2]}, +# {"Mismatch": ["E", "N", "F", "N", "N", "N", "N", "N", "N", "N"]}, +# {"Residual Match": ["E", "N", "N", "E", "E", 1, 1, 1, 1, 2]}, +# ] + +GSTIN_RULES = ( + { + "match_status": MatchStatus.EXACT_MATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + Fields.BILL_NO: Rule.EXACT_MATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.EXACT_MATCH, + Fields.CGST: Rule.EXACT_MATCH, + Fields.SGST: Rule.EXACT_MATCH, + Fields.IGST: Rule.EXACT_MATCH, + Fields.CESS: Rule.EXACT_MATCH, + }, + }, + { + "match_status": MatchStatus.SUGGESTED_MATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + Fields.BILL_NO: Rule.FUZZY_MATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.EXACT_MATCH, + Fields.CGST: Rule.EXACT_MATCH, + Fields.SGST: Rule.EXACT_MATCH, + Fields.IGST: Rule.EXACT_MATCH, + Fields.CESS: Rule.EXACT_MATCH, + }, + }, + { + "match_status": MatchStatus.SUGGESTED_MATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + Fields.BILL_NO: Rule.EXACT_MATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.ROUNDING_DIFFERENCE, + Fields.CGST: Rule.ROUNDING_DIFFERENCE, + Fields.SGST: Rule.ROUNDING_DIFFERENCE, + Fields.IGST: Rule.ROUNDING_DIFFERENCE, + Fields.CESS: Rule.ROUNDING_DIFFERENCE, + }, + }, + { + "match_status": MatchStatus.SUGGESTED_MATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + Fields.BILL_NO: Rule.FUZZY_MATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.ROUNDING_DIFFERENCE, + Fields.CGST: Rule.ROUNDING_DIFFERENCE, + Fields.SGST: Rule.ROUNDING_DIFFERENCE, + Fields.IGST: Rule.ROUNDING_DIFFERENCE, + Fields.CESS: Rule.ROUNDING_DIFFERENCE, + }, + }, + { + "match_status": MatchStatus.MISMATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + Fields.BILL_NO: Rule.EXACT_MATCH, + # Fields.PLACE_OF_SUPPLY: Rule.MISMATCH, + # Fields.IS_REVERSE_CHARGE: Rule.MISMATCH, + # Fields.TAXABLE_VALUE: Rule.MISMATCH, + # Fields.CGST: Rule.MISMATCH, + # Fields.SGST: Rule.MISMATCH, + # Fields.IGST: Rule.MISMATCH, + # Fields.CESS: Rule.MISMATCH, + }, + }, + { + "match_status": MatchStatus.MISMATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + Fields.BILL_NO: Rule.FUZZY_MATCH, + # Fields.PLACE_OF_SUPPLY: Rule.MISMATCH, + # Fields.IS_REVERSE_CHARGE: Rule.MISMATCH, + # Fields.TAXABLE_VALUE: Rule.MISMATCH, + # Fields.CGST: Rule.MISMATCH, + # Fields.SGST: Rule.MISMATCH, + # Fields.IGST: Rule.MISMATCH, + # Fields.CESS: Rule.MISMATCH, + }, + }, + { + "match_status": MatchStatus.RESIDUAL_MATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH, + # Fields.BILL_NO: Rule.MISMATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.ROUNDING_DIFFERENCE, + Fields.CGST: Rule.ROUNDING_DIFFERENCE, + Fields.SGST: Rule.ROUNDING_DIFFERENCE, + Fields.IGST: Rule.ROUNDING_DIFFERENCE, + Fields.CESS: Rule.ROUNDING_DIFFERENCE, + }, + }, +) + + +PAN_RULES = ( + { + "match_status": MatchStatus.MISMATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + # Fields.SUPPLIER_GSTIN: Rule.MISMATCH, + Fields.BILL_NO: Rule.EXACT_MATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.ROUNDING_DIFFERENCE, + Fields.CGST: Rule.ROUNDING_DIFFERENCE, + Fields.SGST: Rule.ROUNDING_DIFFERENCE, + Fields.IGST: Rule.ROUNDING_DIFFERENCE, + Fields.CESS: Rule.ROUNDING_DIFFERENCE, + }, + }, + { + "match_status": MatchStatus.MISMATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + # Fields.SUPPLIER_GSTIN: Rule.MISMATCH, + Fields.BILL_NO: Rule.FUZZY_MATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.ROUNDING_DIFFERENCE, + Fields.CGST: Rule.ROUNDING_DIFFERENCE, + Fields.SGST: Rule.ROUNDING_DIFFERENCE, + Fields.IGST: Rule.ROUNDING_DIFFERENCE, + Fields.CESS: Rule.ROUNDING_DIFFERENCE, + }, + }, + { + "match_status": MatchStatus.MISMATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + # Fields.SUPPLIER_GSTIN: Rule.MISMATCH, + Fields.BILL_NO: Rule.FUZZY_MATCH, + # Fields.PLACE_OF_SUPPLY: Rule.MISMATCH, + # Fields.IS_REVERSE_CHARGE: Rule.MISMATCH, + # Fields.TAXABLE_VALUE: Rule.MISMATCH, + # Fields.CGST: Rule.MISMATCH, + # Fields.SGST: Rule.MISMATCH, + # Fields.IGST: Rule.MISMATCH, + # Fields.CESS: Rule.MISMATCH, + }, + }, + { + "match_status": MatchStatus.RESIDUAL_MATCH, + "rule": { + Fields.FISCAL_YEAR: Rule.EXACT_MATCH, + # Fields.SUPPLIER_GSTIN: Rule.MISMATCH, + # Fields.BILL_NO: Rule.MISMATCH, + Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH, + Fields.REVERSE_CHARGE: Rule.EXACT_MATCH, + Fields.TAXABLE_VALUE: Rule.ROUNDING_DIFFERENCE, + Fields.CGST: Rule.ROUNDING_DIFFERENCE, + Fields.SGST: Rule.ROUNDING_DIFFERENCE, + Fields.IGST: Rule.ROUNDING_DIFFERENCE, + Fields.CESS: Rule.ROUNDING_DIFFERENCE, + }, + }, +) + + +class InwardSupply: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + self.GSTR2 = frappe.qb.DocType("GST Inward Supply") + self.GSTR2_ITEM = frappe.qb.DocType("GST Inward Supply Item") + + def get_all(self, additional_fields=None, names=None, only_names=False): + query = self.with_period_filter(additional_fields) + if only_names and not names: + return + + elif names: + query = query.where(self.GSTR2.name.isin(names)) + + return query.run(as_dict=True) + + def get_unmatched(self, category, amended_category): + categories = [category, amended_category or None] + query = self.with_period_filter() + data = ( + query.where(IfNull(self.GSTR2.match_status, "") == "") + .where(self.GSTR2.classification.isin(categories)) + .run(as_dict=True) + ) + + for doc in data: + doc.fy = BaseUtil.get_fy(doc.bill_date) + + return BaseUtil.get_dict_for_key("supplier_gstin", data) + + def with_period_filter(self, additional_fields=None): + query = self.get_query(additional_fields) + periods = BaseUtil._get_periods(self.from_date, self.to_date) + + if self.gst_return == "GSTR 2B": + query = query.where((self.GSTR2.return_period_2b.isin(periods))) + else: + query = query.where( + (self.GSTR2.return_period_2b.isin(periods)) + | (self.GSTR2.sup_return_period.isin(periods)) + | (self.GSTR2.other_return_period.isin(periods)) + ) + + return query + + def get_query(self, additional_fields=None): + """ + Query without filtering for return period + """ + fields = self.get_fields(additional_fields) + query = ( + frappe.qb.from_(self.GSTR2) + .left_join(self.GSTR2_ITEM) + .on(self.GSTR2_ITEM.parent == self.GSTR2.name) + .where(self.company_gstin == self.GSTR2.company_gstin) + .where(IfNull(self.GSTR2.match_status, "") != "Amended") + .groupby(self.GSTR2_ITEM.parent) + .select(*fields, ConstantColumn("GST Inward Supply").as_("doctype")) + ) + if self.include_ignored == 0: + query = query.where(IfNull(self.GSTR2.action, "") != "Ignore") + + return query + + def get_fields(self, additional_fields=None, table=None): + if not table: + table = self.GSTR2 + + fields = [ + "bill_no", + "bill_date", + "name", + "supplier_gstin", + "is_reverse_charge", + "place_of_supply", + ] + + if additional_fields: + fields += additional_fields + + fields = [table[field] for field in fields] + fields += self.get_tax_fields(table) + + return fields + + def get_tax_fields(self, table): + fields = GST_TAX_TYPES[:-1] + ("taxable_value",) + + if table == frappe.qb.DocType("GST Inward Supply"): + return [Sum(self.GSTR2_ITEM[field]).as_(field) for field in fields] + + return [table[field] for field in fields] + + +class PurchaseInvoice: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + self.PI = frappe.qb.DocType("Purchase Invoice") + self.PI_TAX = frappe.qb.DocType("Purchase Taxes and Charges") + + def get_all(self, additional_fields=None, names=None, only_names=False): + query = self.get_query(additional_fields) + + if only_names and not names: + return + + elif only_names: + query = query.where(self.PI.name.isin(names)) + + elif names: + query = query.where( + (self.PI.posting_date[self.from_date : self.to_date]) + | (self.PI.name.isin(names)) + ) + + else: + query = query.where(self.PI.posting_date[self.from_date : self.to_date]) + + return query.run(as_dict=True) + + def get_unmatched(self, category): + gst_category = ( + ("Registered Regular", "Tax Deductor") + if category in ("B2B", "CDNR", "ISD") + else ("SEZ", "Overseas", "UIN Holders") + ) + is_return = 1 if category == "CDNR" else 0 + + query = ( + self.get_query(is_return=is_return) + .where(self.PI.posting_date[self.from_date : self.to_date]) + .where( + self.PI.name.notin( + PurchaseInvoice.query_matched_purchase_invoice( + self.from_date, self.to_date + ) + ) + ) + .where(self.PI.gst_category.isin(gst_category)) + .where(self.PI.is_return == is_return) + ) + + data = query.run(as_dict=True) + + for doc in data: + doc.fy = BaseUtil.get_fy(doc.bill_date or doc.posting_date) + + return BaseUtil.get_dict_for_key("supplier_gstin", data) + + def get_query(self, additional_fields=None, is_return=False): + PI_ITEM = frappe.qb.DocType("Purchase Invoice Item") + + fields = self.get_fields(additional_fields, is_return) + pi_item = ( + frappe.qb.from_(PI_ITEM) + .select( + Abs(Sum(PI_ITEM.taxable_value)).as_("taxable_value"), + PI_ITEM.parent, + ) + .groupby(PI_ITEM.parent) + ) + + query = ( + frappe.qb.from_(self.PI) + .left_join(self.PI_TAX) + .on(self.PI_TAX.parent == self.PI.name) + .left_join(pi_item) + .on(pi_item.parent == self.PI.name) + .where(self.company_gstin == self.PI.company_gstin) + .where(self.PI.docstatus == 1) + .where(IfNull(self.PI.reconciliation_status, "") != "Not Applicable") + .groupby(self.PI.name) + .select( + *fields, + pi_item.taxable_value, + ConstantColumn("Purchase Invoice").as_("doctype"), + ) + ) + + if self.include_ignored == 0: + query = query.where(IfNull(self.PI.reconciliation_status, "") != "Ignored") + + return query + + def get_fields(self, additional_fields=None, is_return=False): + gst_accounts = get_gst_accounts_by_type(self.company, "Input") + tax_fields = [ + self.query_tax_amount(account).as_(tax[:-8]) + for tax, account in gst_accounts.items() + ] + + fields = [ + "name", + "supplier_gstin", + "bill_no", + "place_of_supply", + "is_reverse_charge", + *tax_fields, + ] + + if is_return: + # return is initiated by the customer. So bill date may not be available or known. + fields += [self.PI.posting_date.as_("bill_date")] + else: + fields += ["bill_date"] + + if additional_fields: + fields += additional_fields + + return fields + + def query_tax_amount(self, account): + return Abs( + Sum( + Case() + .when( + self.PI_TAX.account_head == account, + self.PI_TAX.base_tax_amount_after_discount_amount, + ) + .else_(0) + ) + ) + + @staticmethod + def query_matched_purchase_invoice(from_date=None, to_date=None): + GSTR2 = frappe.qb.DocType("GST Inward Supply") + PI = frappe.qb.DocType("Purchase Invoice") + + query = ( + frappe.qb.from_(GSTR2) + .select("link_name") + .where(GSTR2.link_doctype == "Purchase Invoice") + .join(PI) + .on(PI.name == GSTR2.link_name) + ) + + if from_date and to_date: + query = query.where(PI.posting_date[from_date:to_date]) + + return query + + +class BillOfEntry: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + self.BOE = frappe.qb.DocType("Bill of Entry") + self.BOE_TAX = frappe.qb.DocType("Bill of Entry Taxes") + self.PI = frappe.qb.DocType("Purchase Invoice") + + def get_all(self, additional_fields=None, names=None, only_names=False): + query = self.get_query(additional_fields) + + if only_names and not names: + return + + elif only_names: + query = query.where(self.BOE.name.isin(names)) + + elif names: + query = query.where( + (self.BOE.posting_date[self.from_date : self.to_date]) + | (self.BOE.name.isin(names)) + ) + + else: + query = query.where(self.BOE.posting_date[self.from_date : self.to_date]) + + return query.run(as_dict=True) + + def get_unmatched(self, category): + gst_category = "SEZ" if category == "IMPGSEZ" else "Overseas" + + query = ( + self.get_query() + .where(self.PI.gst_category == gst_category) + .where(self.BOE.posting_date[self.from_date : self.to_date]) + .where( + self.BOE.name.notin( + BillOfEntry.query_matched_bill_of_entry( + self.from_date, self.to_date + ) + ) + ) + ) + + data = query.run(as_dict=True) + + for doc in data: + doc.fy = BaseUtil.get_fy(doc.bill_date or doc.posting_date) + + return BaseUtil.get_dict_for_key("supplier_gstin", data) + + def get_query(self, additional_fields=None): + fields = self.get_fields(additional_fields) + + query = ( + frappe.qb.from_(self.BOE) + .left_join(self.BOE_TAX) + .on(self.BOE_TAX.parent == self.BOE.name) + .join(self.PI) + .on(self.BOE.purchase_invoice == self.PI.name) + .where(self.BOE.docstatus == 1) + .groupby(self.BOE.name) + .select(*fields, ConstantColumn("Bill of Entry").as_("doctype")) + ) + + if self.include_ignored == 0: + query = query.where(IfNull(self.BOE.reconciliation_status, "") != "Ignored") + + return query + + def get_fields(self, additional_fields=None): + gst_accounts = get_gst_accounts_by_type(self.company, "Input") + tax_fields = [ + self.query_tax_amount(account).as_(tax[:-8]) + for tax, account in gst_accounts.items() + if account + ] + + fields = [ + self.BOE.name, + self.BOE.bill_of_entry_no.as_("bill_no"), + self.BOE.total_taxable_value.as_("taxable_value"), + self.BOE.bill_of_entry_date.as_("bill_date"), + self.BOE.posting_date, + self.PI.supplier_name, + *tax_fields, + ] + + # In IMPGSEZ supplier details are avaialble in 2A + purchase_fields = [ + "supplier_gstin", + "place_of_supply", + "is_reverse_charge", + "gst_category", + ] + + for field in purchase_fields: + fields.append( + Case() + .when(self.PI.gst_category == "SEZ", getattr(self.PI, field)) + .else_("") + .as_(field) + ) + + # Add only boe fields + if additional_fields: + boe_fields = frappe.db.get_table_columns("Bill of Entry") + for field in additional_fields: + if field in boe_fields: + fields.append(getattr(self.BOE, field)) + + return fields + + def query_tax_amount(self, account): + return Abs( + Sum( + Case() + .when( + self.BOE_TAX.account_head == account, + self.BOE_TAX.tax_amount, + ) + .else_(0) + ) + ) + + @staticmethod + def query_matched_bill_of_entry(from_date=None, to_date=None): + GSTR2 = frappe.qb.DocType("GST Inward Supply") + BOE = frappe.qb.DocType("Bill of Entry") + + query = ( + frappe.qb.from_(GSTR2) + .select("link_name") + .where(GSTR2.link_doctype == "Bill of Entry") + .join(BOE) + .on(BOE.name == GSTR2.link_name) + ) + + if from_date and to_date: + query = query.where(BOE.posting_date[from_date:to_date]) + + return query + + +class BaseReconciliation: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def get_all_inward_supply( + self, additional_fields=None, names=None, only_names=False + ): + return InwardSupply( + company_gstin=self.company_gstin, + from_date=self.inward_supply_from_date, + to_date=self.inward_supply_to_date, + gst_return=self.gst_return, + include_ignored=self.include_ignored, + ).get_all(additional_fields, names, only_names) + + def get_unmatched_inward_supply(self, category, amended_category): + return InwardSupply( + company_gstin=self.company_gstin, + from_date=self.inward_supply_from_date, + to_date=self.inward_supply_to_date, + gst_return=self.gst_return, + include_ignored=self.include_ignored, + ).get_unmatched(category, amended_category) + + def query_inward_supply(self, additional_fields=None): + query = InwardSupply( + company_gstin=self.company_gstin, + from_date=self.inward_supply_from_date, + to_date=self.inward_supply_to_date, + gst_return=self.gst_return, + include_ignored=self.include_ignored, + ) + + return query.with_period_filter(additional_fields) + + def get_all_purchase_invoice( + self, additional_fields=None, names=None, only_names=False + ): + return PurchaseInvoice( + company=self.company, + company_gstin=self.company_gstin, + from_date=self.purchase_from_date, + to_date=self.purchase_to_date, + include_ignored=self.include_ignored, + ).get_all(additional_fields, names, only_names) + + def get_unmatched_purchase(self, category): + return PurchaseInvoice( + company=self.company, + company_gstin=self.company_gstin, + from_date=self.purchase_from_date, + to_date=self.purchase_to_date, + include_ignored=self.include_ignored, + ).get_unmatched(category) + + def query_purchase_invoice(self, additional_fields=None): + return PurchaseInvoice( + company=self.company, + company_gstin=self.company_gstin, + include_ignored=self.include_ignored, + ).get_query(additional_fields) + + def get_all_bill_of_entry( + self, additional_fields=None, names=None, only_names=False + ): + return BillOfEntry( + company=self.company, + company_gstin=self.company_gstin, + from_date=self.purchase_from_date, + to_date=self.purchase_to_date, + include_ignored=self.include_ignored, + ).get_all(additional_fields, names, only_names) + + def get_unmatched_bill_of_entry(self, category): + return BillOfEntry( + company=self.company, + company_gstin=self.company_gstin, + from_date=self.purchase_from_date, + to_date=self.purchase_to_date, + include_ignored=self.include_ignored, + ).get_unmatched(category) + + def query_bill_of_entry(self, additional_fields=None): + return BillOfEntry( + company=self.company, + company_gstin=self.company_gstin, + include_ignored=self.include_ignored, + ).get_query(additional_fields) + + def get_unmatched_purchase_or_bill_of_entry(self, category): + """ + Returns dict of unmatched purchase and bill of entry data. + """ + if category in IMPORT_CATEGORY: + return self.get_unmatched_bill_of_entry(category) + + return self.get_unmatched_purchase(category) + + +class Reconciler(BaseReconciliation): + def reconcile(self, category, amended_category): + """ + Reconcile purchases and inward supplies for given category. + """ + # GSTIN Level matching + purchases = self.get_unmatched_purchase_or_bill_of_entry(category) + inward_supplies = self.get_unmatched_inward_supply(category, amended_category) + self.reconcile_for_rules(GSTIN_RULES, purchases, inward_supplies, category) + + # In case of IMPG GST in not available in 2A. So skip PAN level matching. + if category == "IMPG": + return + + # PAN Level matching + purchases = self.get_pan_level_data(purchases) + inward_supplies = self.get_pan_level_data(inward_supplies) + self.reconcile_for_rules(PAN_RULES, purchases, inward_supplies, category) + + def reconcile_for_rules(self, rules, purchases, inward_supplies, category): + if not (purchases and inward_supplies): + return + + for rule in rules: + self.reconcile_for_rule( + purchases, + inward_supplies, + rule.get("match_status").value, + rule.get("rule"), + category, + ) + + def reconcile_for_rule( + self, purchases, inward_supplies, match_status, rules, category + ): + """ + Sequentially reconcile invoices as per rules list. + - Reconciliation only done between invoices of same GSTIN. + - Where a match is found, update Inward Supply and Purchase Invoice. + """ + + for supplier_gstin in purchases: + if not inward_supplies.get(supplier_gstin) and category != "IMPG": + continue + + summary_diff = {} + if match_status == "Residual Match" and category != "CDNR": + summary_diff = self.get_summary_difference( + purchases[supplier_gstin], inward_supplies[supplier_gstin] + ) + + for purchase_invoice_name, purchase in ( + purchases[supplier_gstin].copy().items() + ): + if summary_diff and not ( + abs(summary_diff[purchase.bill_date.month]) < 2 + ): + continue + + for inward_supply_name, inward_supply in ( + inward_supplies[supplier_gstin].copy().items() + ): + if ( + summary_diff + and purchase.bill_date.month != inward_supply.bill_date.month + ): + continue + + if not self.is_doc_matching(purchase, inward_supply, rules): + continue + + self.update_matching_doc( + match_status, + purchase.name, + inward_supply.name, + purchase.doctype, + ) + + # Remove from current data to ensure matching is done only once. + purchases[supplier_gstin].pop(purchase_invoice_name) + inward_supplies[supplier_gstin].pop(inward_supply_name) + break + + def get_summary_difference(self, data1, data2): + """ + Returns dict with difference of monthly purchase for given supplier data. + Calculated only for Residual Match. + + Objective: Residual match is to match Invoices where bill no is completely different. + It should be matched for invoices of a given month only if difference in total invoice + value is negligible for purchase and inward supply. + """ + summary = {} + for doc in data1.values(): + summary.setdefault(doc.bill_date.month, 0) + summary[doc.bill_date.month] += BaseUtil.get_total_tax(doc) + + for doc in data2.values(): + summary.setdefault(doc.bill_date.month, 0) + summary[doc.bill_date.month] -= BaseUtil.get_total_tax(doc) + + return summary + + def is_doc_matching(self, purchase, inward_supply, rules): + """ + Returns true if all fields match from purchase and inward supply as per rules. + + param purchase: purchase doc + param inward_supply: inward supply doc + param rules: dict of rule against field to match + """ + + for field, rule in rules.items(): + if not self.is_field_matching(purchase, inward_supply, field.value, rule): + return False + + return True + + def is_field_matching(self, purchase, inward_supply, field, rule): + """ + Returns true if the field matches from purchase and inward supply as per the rule. + + param purchase: purchase doc + param inward_supply: inward supply doc + param field: field to match + param rule: rule applied to match + """ + + if rule == Rule.EXACT_MATCH: + return purchase[field] == inward_supply[field] + elif rule == Rule.FUZZY_MATCH: + return self.fuzzy_match(purchase, inward_supply) + elif rule == Rule.ROUNDING_DIFFERENCE: + return self.get_amount_difference(purchase, inward_supply, field) <= 1 + + def fuzzy_match(self, purchase, inward_supply): + """ + Returns true if the (cleaned) bill_no approximately match. + - For a fuzzy match, month of invoice and inward supply should be same. + - First check for partial ratio, with 100% confidence + - Next check for approximate match, with 90% confidence + """ + if abs((purchase.bill_date - inward_supply.bill_date).days) > 10: + return False + + if not purchase._bill_no: + purchase._bill_no = BaseUtil.get_cleaner_bill_no( + purchase.bill_no, purchase.fy + ) + + if not inward_supply._bill_no: + inward_supply._bill_no = BaseUtil.get_cleaner_bill_no( + inward_supply.bill_no, inward_supply.fy + ) + + partial_ratio = fuzz.partial_ratio(purchase._bill_no, inward_supply._bill_no) + if float(partial_ratio) == 100: + return True + + return ( + float(process.extractOne(purchase._bill_no, [inward_supply._bill_no])[1]) + >= 90.0 + ) + + def get_amount_difference(self, purchase, inward_supply, field): + if field == "cess": + BaseUtil.update_cess_amount(purchase) + + return abs(purchase.get(field, 0) - inward_supply.get(field, 0)) + + def update_matching_doc( + self, match_status, purchase_invoice_name, inward_supply_name, link_doctype + ): + """Update matching doc for records.""" + + if match_status == "Residual Match": + match_status = "Mismatch" + + inward_supply_fields = { + "match_status": match_status, + "link_doctype": link_doctype, + "link_name": purchase_invoice_name, + } + + frappe.db.set_value( + "GST Inward Supply", inward_supply_name, inward_supply_fields + ) + + def get_pan_level_data(self, data): + out = {} + for gstin, invoices in data.items(): + pan = gstin[2:-3] + out.setdefault(pan, {}) + out[pan].update(invoices) + + return out + + +class ReconciledData(BaseReconciliation): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.gstin_party_map = frappe._dict() + + def get_consolidated_data( + self, + purchase_names: list = None, + inward_supply_names: list = None, + prefix: str = None, + ): + data = self.get(purchase_names, inward_supply_names) + for doc in data: + purchase = doc.pop("_purchase_invoice", frappe._dict()) + inward_supply = doc.pop("_inward_supply", frappe._dict()) + + purchase.bill_date = format_date(purchase.bill_date) + inward_supply.bill_date = format_date(inward_supply.bill_date) + + doc.update(purchase) + doc.update( + {f"{prefix}_{key}": value for key, value in inward_supply.items()} + ) + doc.pan = doc.supplier_gstin[2:-3] + + return data + + def get_manually_matched_data(self, purchase_name: str, inward_supply_name: str): + """ + Get manually matched data for given purchase invoice and inward supply. + This can be used to show comparision of matched values. + """ + inward_supplies = self.get_all_inward_supply( + names=[inward_supply_name], only_names=True + ) + purchases = self.get_all_purchase_invoice_and_bill_of_entry( + "", [purchase_name], only_names=True + ) + + reconciliation_data = [ + frappe._dict( + { + "_inward_supply": inward_supplies[0] + if inward_supplies + else frappe._dict(), + "_purchase_invoice": purchases.get(purchase_name, frappe._dict()), + } + ) + ] + self.process_data(reconciliation_data, retain_doc=True) + return reconciliation_data[0] + + def get(self, purchase_names: list = None, inward_supply_names: list = None): + # TODO: update cess amount in purchase invoice + """ + Get Reconciliation data based on standard filters + Returns + - Inward Supply: for the return month as per 2A and 2B + - Purchase Invoice: All invoices matching with inward supply (irrespective of purchase period choosen) + Unmatched Purchase Invoice for the period choosen + + """ + + retain_doc = only_names = False + if inward_supply_names or purchase_names: + retain_doc = only_names = True + + inward_supplies = self.get_all_inward_supply(names=inward_supply_names) + purchases_and_bill_of_entry = self.get_all_purchase_invoice_and_bill_of_entry( + inward_supplies, purchase_names, only_names + ) + + reconciliation_data = [] + for doc in inward_supplies: + reconciliation_data.append( + frappe._dict( + { + "_inward_supply": doc, + "_purchase_invoice": purchases_and_bill_of_entry.pop( + doc.link_name, frappe._dict() + ), + } + ) + ) + + for doc in purchases_and_bill_of_entry.values(): + reconciliation_data.append(frappe._dict({"_purchase_invoice": doc})) + + self.process_data(reconciliation_data, retain_doc=retain_doc) + return reconciliation_data + + def get_all_inward_supply( + self, additional_fields=None, names=None, only_names=False + ): + inward_supply_fields = [ + "supplier_name", + "classification", + "match_status", + "action", + "link_doctype", + "link_name", + ] + + return super().get_all_inward_supply(inward_supply_fields, names, only_names) + + def get_all_purchase_invoice_and_bill_of_entry( + self, inward_supplies, purchase_names, only_names=False + ): + purchase_fields = [ + "supplier", + "supplier_name", + "is_return", + "gst_category", + "reconciliation_status", + ] + + boe_names = purchase_names + + if not only_names: + purchase_names = set() + boe_names = set() + for doc in inward_supplies: + if doc.link_doctype == "Purchase Invoice": + purchase_names.add(doc.link_name) + + elif doc.link_doctype == "Bill of Entry": + boe_names.add(doc.link_name) + + purchases = ( + super().get_all_purchase_invoice( + purchase_fields, purchase_names, only_names + ) + or [] + ) + + bill_of_entries = ( + super().get_all_bill_of_entry(purchase_fields, boe_names, only_names) or [] + ) + + if not purchases and not bill_of_entries: + return {} + + purchases.extend(bill_of_entries) + + return {doc.name: doc for doc in purchases} + + def process_data(self, reconciliation_data: list, retain_doc: bool = False): + """ + Process reconciliation data to add additional fields or update differences. + Cases: + - Missing in Purchase Invoice + - Missing in Inward Supply + - Update differences + + params: + - reconciliation_data: list of reconciliation data + Format: + [ + { + "_purchase_invoice": purchase invoice doc, + "_inward_supply": inward supply doc, + }, + ... + ] + + - retain_doc: retain vs pop doc from reconciliation data + + """ + default_dict = { + "supplier_name": "", + "supplier_gstin": "", + "bill_no": "", + "bill_date": "", + "match_status": "", + "purchase_invoice_name": "", + "inward_supply_name": "", + "taxable_value_difference": "", + "tax_difference": "", + "differences": "", + "action": "", + "classification": "", + } + + for data in reconciliation_data: + data.update(default_dict) + method = data.get if retain_doc else data.pop + + purchase = method("_purchase_invoice", frappe._dict()) + inward_supply = method("_inward_supply", frappe._dict()) + + self.update_fields(data, purchase, inward_supply) + self.update_amount_difference(data, purchase, inward_supply) + self.update_differences(data, purchase, inward_supply) + + if retain_doc and purchase: + BaseUtil.update_cess_amount(purchase) + + def update_fields(self, data, purchase, inward_supply): + for field in ("supplier_name", "supplier_gstin", "bill_no", "bill_date"): + data[field] = purchase.get(field) or inward_supply.get(field) + + data.update( + { + "supplier_name": data.supplier_name + or self.guess_supplier_name(data.supplier_gstin), + "purchase_doctype": purchase.get("doctype"), + "purchase_invoice_name": purchase.get("name"), + "inward_supply_name": inward_supply.get("name"), + "match_status": inward_supply.get("match_status"), + "action": inward_supply.get("action"), + "classification": inward_supply.get("classification") + or self.guess_classification(purchase), + } + ) + + # missing in purchase invoice + if not purchase: + data.match_status = MatchStatus.MISSING_IN_PI.value + + # missing in inward supply + elif not inward_supply: + data.match_status = MatchStatus.MISSING_IN_2A_2B.value + data.action = ( + "Ignore" + if purchase.get("reconciliation_status") == "Ignored" + else "No Action" + ) + + def update_amount_difference(self, data, purchase, inward_supply): + data.taxable_value_difference = rounded( + purchase.get("taxable_value", 0) - inward_supply.get("taxable_value", 0), + 2, + ) + + data.tax_difference = rounded( + BaseUtil.get_total_tax(purchase) - BaseUtil.get_total_tax(inward_supply), + 2, + ) + + def update_differences(self, data, purchase, inward_supply): + differences = [] + if self.is_exact_or_suggested_match(data): + if self.has_rounding_difference(data): + differences.append("Rounding Difference") + + elif not self.is_mismatch_or_manual_match(data): + return + + for field in Fields: + if field == Fields.BILL_NO: + continue + + if purchase.get(field.value) != inward_supply.get(field.value): + differences.append(field.name) + + data.differences = ", ".join(differences) + + def guess_supplier_name(self, gstin): + if party := self.gstin_party_map.get(gstin): + return party + + return self.gstin_party_map.setdefault( + gstin, get_party_for_gstin(gstin) or "Unknown" + ) + + @staticmethod + def guess_classification(doc): + GST_CATEGORIES = { + "Registered Regular": "B2B", + "SEZ": "IMPGSEZ", + "Overseas": "IMPG", + "UIN Holders": "B2B", + "Tax Deductor": "B2B", + } + + classification = GST_CATEGORIES.get(doc.gst_category) + if doc.is_return and classification == "B2B": + classification = "CDNR" + + if not classification and doc.get("doctype") == "Bill of Entry": + classification = "IMPG" + + return classification + + @staticmethod + def is_exact_or_suggested_match(data): + return data.match_status in ( + MatchStatus.EXACT_MATCH.value, + MatchStatus.SUGGESTED_MATCH.value, + ) + + @staticmethod + def is_mismatch_or_manual_match(data): + return data.match_status in ( + MatchStatus.MISMATCH.value, + MatchStatus.MANUAL_MATCH.value, + ) + + @staticmethod + def has_rounding_difference(data): + return ( + abs(data.taxable_value_difference) > 0.01 or abs(data.tax_difference) > 0.01 + ) + + +class BaseUtil: + @staticmethod + def get_fy(date): + if not date: + return + + # Standard for India as per GST + if date.month < 4: + return f"{date.year - 1}-{date.year}" + + return f"{date.year}-{date.year + 1}" + + @staticmethod + def get_cleaner_bill_no(bill_no, fy): + """ + - Attempts to return bill number without financial year. + - Removes trailing zeros from bill number. + """ + + fy = fy.split("-") + replace_list = [ + f"{fy[0]}-{fy[1]}", + f"{fy[0]}/{fy[1]}", + f"{fy[0]}-{fy[1][2:]}", + f"{fy[0]}/{fy[1][2:]}", + f"{fy[0][2:]}-{fy[1][2:]}", + f"{fy[0][2:]}/{fy[1][2:]}", + "/", # these are only special characters allowed in invoice + "-", + ] + + inv = bill_no + for replace in replace_list: + inv = inv.replace(replace, " ") + inv = " ".join(inv.split()).lstrip("0") + return inv + + @staticmethod + def get_dict_for_key(key, list): + new_dict = frappe._dict() + for data in list: + new_dict.setdefault(data[key], {})[data.name] = data + + return new_dict + + @staticmethod + def get_total_tax(doc): + total_tax = 0 + + for tax in GST_TAX_TYPES: + total_tax += doc.get(tax, 0) + + return total_tax + + @staticmethod + def update_cess_amount(doc): + doc.cess = doc.get("cess", 0) + doc.get("cess_non_advol", 0) + + @staticmethod + def get_periods(date_range, return_type: ReturnType, reversed=False): + """Returns a list of month (formatted as `MMYYYY`) in a fiscal year""" + if not date_range: + return [] + + date_range = (getdate(date_range[0]), getdate(date_range[1])) + end_date = min(date_range[1], BaseUtil._getdate(return_type)) + + # latest to oldest + return tuple( + BaseUtil._reversed(BaseUtil._get_periods(date_range[0], end_date), reversed) + ) + + @staticmethod + def _get_periods(start_date, end_date): + """Returns a list of month (formatted as `MMYYYY`) in given date range""" + + if isinstance(start_date, str): + start_date = getdate(start_date) + + if isinstance(end_date, str): + end_date = getdate(end_date) + + return [ + dt.strftime("%m%Y") + for dt in rrule(MONTHLY, dtstart=start_date, until=end_date) + ] + + @staticmethod + def _reversed(lst, reverse): + if reverse: + return reversed(lst) + return lst + + @staticmethod + def _getdate(return_type): + GSTR2B_GEN_DATE = 14 + if return_type == ReturnType.GSTR2B: + if getdate().day >= GSTR2B_GEN_DATE: + return add_months(getdate(), -1) + else: + return add_months(getdate(), -2) + + return getdate() diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/gstr_download_history.html b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/gstr_download_history.html new file mode 100644 index 000000000..9de95081c --- /dev/null +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/gstr_download_history.html @@ -0,0 +1,37 @@ +{{period}} | + {% previous_period = period; %} + {% } %} + {% for(let value of Object.values(data[period].shift())) { %} +{{ value }} | + {% } %} +
{{ value }} | + {% } %} +
+ | 2A / 2B | +Purchase | +||
---|---|---|---|---|
Document Links + {% if inward_supply.name %} + | {{ frappe.utils.get_form_link("GST Inward Supply", + inward_supply.name, true) }} | + {% else %} +- | + {% endif %} + + {% if purchase.name %} +{{ frappe.utils.get_form_link(purchase.doctype, + purchase.name, true)}} | + {% else %} +- | + {% endif %} +
Bill No + | {{ inward_supply.bill_no || '-' }} | +{{ purchase.bill_no || '-' }} | +||
Bill Date + | + {{ frappe.format(inward_supply.bill_date, {'fieldtype': 'Date'}) || '-' }} + | ++ {{ frappe.format(purchase.bill_date, {'fieldtype': 'Date'}) || '-' }} + | +||
Place of Supply | +{{ inward_supply.place_of_supply || '-' }} | +{{ purchase.place_of_supply || '-' }} | +||
Reverse Charge | +{{ inward_supply.is_reverse_charge || '-' }} | +{{ purchase.is_reverse_charge || '-' }} | +||
CGST + | {{ inward_supply.cgst || '-' }} | +{{ purchase.cgst || '-' }} | +||
SGST + | {{ inward_supply.sgst || '-' }} | +{{ purchase.sgst || '-' }} | +||
IGST + | {{ inward_supply.igst || '-' }} | +{{ purchase.igst || '-' }} | +||
CESS + | {{ inward_supply.cess || '-' }} | +{{ purchase.cess || '-' }} | +||
Taxable Amount + | {{ inward_supply.taxable_value || '-' }} | +{{ purchase.taxable_value || '-' }} | +
{{ __(\"No data available for selected filters\") }}
" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "gst_return", + "fieldtype": "Select", + "label": "GST Return", + "options": "GSTR 2B\nBoth GSTR 2A & 2B" + }, + { + "depends_on": "eval: !doc.reconciliation_data", + "fieldname": "not_reconciled", + "fieldtype": "HTML", + "options": "\n\t{{ __(\"Reconcile to view the data\") }}
" + }, + { + "fieldname": "section_break_cmfa", + "fieldtype": "Section Break", + "hidden": 1 + }, + { + "fieldname": "reconciliation_data", + "fieldtype": "JSON", + "label": "Reconciliation Data" + }, + { + "default": "0", + "fieldname": "is_modified", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Modified" + }, + { + "default": "0", + "fieldname": "include_ignored", + "fieldtype": "Check", + "label": "Include Ignored" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2023-09-14 12:57:00.756535", + "modified_by": "Administrator", + "module": "GST India", + "name": "Purchase Reconciliation Tool", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Accounts User", + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py new file mode 100644 index 000000000..d9562cd48 --- /dev/null +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py @@ -0,0 +1,1136 @@ +# Copyright (c) 2022, Resilient Tech and contributors +# For license information, please see license.txt + +import json +from typing import List + +import frappe +from frappe.model.document import Document +from frappe.query_builder.functions import IfNull +from frappe.utils.response import json_handler + +from india_compliance.gst_india.constants import ORIGINAL_VS_AMENDED +from india_compliance.gst_india.doctype.purchase_reconciliation_tool import ( + BaseUtil, + BillOfEntry, + PurchaseInvoice, + ReconciledData, + Reconciler, +) +from india_compliance.gst_india.utils import get_json_from_file, get_timespan_date_range +from india_compliance.gst_india.utils.exporter import ExcelExporter +from india_compliance.gst_india.utils.gstr import ( + IMPORT_CATEGORY, + GSTRCategory, + ReturnType, + download_gstr_2a, + download_gstr_2b, + save_gstr_2a, + save_gstr_2b, +) + + +class PurchaseReconciliationTool(Document): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ReconciledData = ReconciledData(**self.get_reco_doc()) + + def get_reco_doc(self): + fields = ( + "company", + "company_gstin", + "gst_return", + "purchase_from_date", + "purchase_to_date", + "inward_supply_from_date", + "inward_supply_to_date", + "include_ignored", + ) + return {field: self.get(field) for field in fields} + + def validate(self): + # reconcile purchases and inward supplies + if frappe.flags.in_install or frappe.flags.in_migrate: + return + + _Reconciler = Reconciler(**self.get_reco_doc()) + for row in ORIGINAL_VS_AMENDED: + _Reconciler.reconcile(row["original"], row["amended"]) + + self.ReconciledData = ReconciledData(**self.get_reco_doc()) + self.reconciliation_data = json.dumps( + self.ReconciledData.get(), default=json_handler + ) + + self.db_set("is_modified", 0) + + @frappe.whitelist() + def upload_gstr(self, return_type, period, file_path): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + return_type = ReturnType(return_type) + json_data = get_json_from_file(file_path) + if return_type == ReturnType.GSTR2A: + return save_gstr_2a(self.company_gstin, period, json_data) + + if return_type == ReturnType.GSTR2B: + return save_gstr_2b(self.company_gstin, period, json_data) + + @frappe.whitelist() + def download_gstr_2a(self, date_range, force=False, otp=None): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + return_type = ReturnType.GSTR2A + periods = BaseUtil.get_periods(date_range, return_type) + if not force: + periods = self.get_periods_to_download(return_type, periods) + + return download_gstr_2a(self.company_gstin, periods, otp) + + @frappe.whitelist() + def download_gstr_2b(self, date_range, otp=None): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + return_type = ReturnType.GSTR2B + periods = self.get_periods_to_download( + return_type, BaseUtil.get_periods(date_range, return_type) + ) + return download_gstr_2b(self.company_gstin, periods, otp) + + def get_periods_to_download(self, return_type, periods): + existing_periods = get_import_history( + self.company_gstin, + return_type, + periods, + pluck="return_period", + ) + + return [period for period in periods if period not in existing_periods] + + @frappe.whitelist() + def get_import_history(self, return_type, date_range, for_download=True): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + if not return_type: + return + + return_type = ReturnType(return_type) + periods = BaseUtil.get_periods(date_range, return_type, True) + history = get_import_history(self.company_gstin, return_type, periods) + + columns = [ + "Period", + "Classification", + "Status", + f"{'Downloaded' if for_download else 'Uploaded'} On", + ] + + settings = frappe.get_cached_doc("GST Settings") + + data = {} + for period in periods: + # TODO: skip if today is not greater than 14th return period's next months + data[period] = [] + status = "🟢 Downloaded" + for category in GSTRCategory: + if category.value == "ISDA" and return_type == ReturnType.GSTR2A: + continue + + if ( + not settings.enable_overseas_transactions + and category.value in IMPORT_CATEGORY + ): + continue + + download = next( + ( + log + for log in history + if log.return_period == period + and log.classification in (category.value, "") + ), + None, + ) + + status = "🟠Not Downloaded" + if download: + status = "🟢 Downloaded" + if download.data_not_found: + status = "🔵 Data Not Found" + if download.request_id: + status = "🔵 Queued" + + if not for_download: + status = status.replace("Downloaded", "Uploaded") + + _dict = { + "Classification": category.value + if return_type is ReturnType.GSTR2A + else "ALL", + "Status": status, + columns[-1]: "✅ " + + download.last_updated_on.strftime("%d-%m-%Y %H:%M:%S") + if download + else "", + } + if _dict not in data[period]: + data[period].append(_dict) + + return {"columns": columns, "data": data} + + @frappe.whitelist() + def get_return_period_from_file(self, return_type, file_path): + """ + Permissions check not necessary as response is not sensitive + """ + if not file_path: + return + + return_type = ReturnType(return_type) + try: + json_data = get_json_from_file(file_path) + if return_type == ReturnType.GSTR2A: + return json_data.get("fp") + + if return_type == ReturnType.GSTR2B: + return json_data.get("data").get("rtnprd") + + except Exception: + pass + + @frappe.whitelist() + def get_date_range(self, period): + """ + Permissions check not necessary as response is not sensitive + """ + if not period or period == "Custom": + return + + return get_timespan_date_range(period.lower(), self.company) + + @frappe.whitelist() + def get_invoice_details(self, purchase_name, inward_supply_name): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + return self.ReconciledData.get_manually_matched_data( + purchase_name, inward_supply_name + ) + + @frappe.whitelist() + def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + if not purchase_invoice_name or not inward_supply_name: + return + + purchases = [] + inward_supplies = [] + + # silently handle existing links + if isup_linked_with := frappe.db.get_value( + "GST Inward Supply", inward_supply_name, "link_name" + ): + self._unlink_documents((inward_supply_name,)) + purchases.append(isup_linked_with) + + link_doc = { + "link_doctype": link_doctype, + "link_name": purchase_invoice_name, + } + if pur_linked_with := frappe.db.get_all( + "GST Inward Supply", link_doc, pluck="name" + ): + self._unlink_documents((pur_linked_with)) + inward_supplies.append(pur_linked_with) + + link_doc["match_status"] = "Manual Match" + + # link documents + frappe.db.set_value( + "GST Inward Supply", + inward_supply_name, + link_doc, + ) + purchases.append(purchase_invoice_name) + inward_supplies.append(inward_supply_name) + + self.db_set("is_modified", 1) + + return self.ReconciledData.get(purchases, inward_supplies) + + @frappe.whitelist() + def unlink_documents(self, data): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + if isinstance(data, str): + data = frappe.parse_json(data) + + inward_supplies = set() + purchases = set() + boe = set() + + for doc in data: + inward_supplies.add(doc.get("inward_supply_name")) + + purchase_doctype = doc.get("purchase_doctype") + if purchase_doctype == "Purchase Invoice": + purchases.add(doc.get("purchase_invoice_name")) + + elif purchase_doctype == "Bill of Entry": + boe.add(doc.get("purchase_invoice_name")) + + self.set_reconciliation_status("Purchase Invoice", purchases, "Unreconciled") + self.set_reconciliation_status("Bill of Entry", boe, "Unreconciled") + self._unlink_documents(inward_supplies) + + self.db_set("is_modified", 1) + + return self.ReconciledData.get(purchases.union(boe), inward_supplies) + + def set_reconciliation_status(self, doctype, names, status): + if not names: + return + + frappe.db.set_value( + doctype, {"name": ("in", names)}, "reconciliation_status", status + ) + + def _unlink_documents(self, inward_supplies): + if not inward_supplies: + return + + GSTR2 = frappe.qb.DocType("GST Inward Supply") + ( + frappe.qb.update(GSTR2) + .set("link_doctype", "") + .set("link_name", "") + .set("match_status", "Unlinked") + .where(GSTR2.name.isin(inward_supplies)) + .run() + ) + + # Revert action performed + ( + frappe.qb.update(GSTR2) + .set("action", "No Action") + .where(GSTR2.name.isin(inward_supplies)) + .where(GSTR2.action.notin(("Ignore", "Pending"))) + .run() + ) + + @frappe.whitelist() + def apply_action(self, data, action): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + if isinstance(data, str): + data = frappe.parse_json(data) + + STATUS_MAP = { + "Accept My Values": "Reconciled", + "Accept Supplier Values": "Reconciled", + "Pending": "Unreconciled", + "Ignore": "Ignored", + } + + status = STATUS_MAP.get(action) + + inward_supplies = [] + purchases = [] + boe = [] + + for doc in data: + if action == "Ignore" and "Missing" not in doc.get("match_status"): + continue + + elif "Accept" in action and "Missing" in doc.get("match_status"): + continue + + if inward_supply_name := doc.get("inward_supply_name"): + inward_supplies.append(inward_supply_name) + + purchase_doctype = doc.get("purchase_doctype") + if purchase_doctype == "Purchase Invoice": + purchases.append(doc.get("purchase_invoice_name")) + + elif purchase_doctype == "Bill of Entry": + boe.append(doc.get("purchase_invoice_name")) + + if inward_supplies: + frappe.db.set_value( + "GST Inward Supply", {"name": ("in", inward_supplies)}, "action", action + ) + + self.set_reconciliation_status("Purchase Invoice", purchases, status) + self.set_reconciliation_status("Bill of Entry", boe, status) + + self.db_set("is_modified", 1) + + @frappe.whitelist() + def get_link_options(self, doctype, filters): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + + if isinstance(filters, dict): + filters = frappe._dict(filters) + + if doctype == "Purchase Invoice": + return self.get_purchase_invoice_options(filters) + + elif doctype == "GST Inward Supply": + return self.get_inward_supply_options(filters) + + elif doctype == "Bill of Entry": + return self.get_bill_of_entry_options(filters) + + def get_purchase_invoice_options(self, filters): + PI = frappe.qb.DocType("Purchase Invoice") + query = ( + self.ReconciledData.query_purchase_invoice(["gst_category", "is_return"]) + .where(PI.supplier_gstin.like(f"%{filters.supplier_gstin}%")) + .where(PI.bill_date[filters.bill_from_date : filters.bill_to_date]) + ) + + if not filters.show_matched: + query = query.where( + PI.name.notin(PurchaseInvoice.query_matched_purchase_invoice()) + ) + + return self._get_link_options(query.run(as_dict=True)) + + def get_inward_supply_options(self, filters): + GSTR2 = frappe.qb.DocType("GST Inward Supply") + query = ( + self.ReconciledData.query_inward_supply(["classification"]) + .where(GSTR2.supplier_gstin.like(f"%{filters.supplier_gstin}%")) + .where(GSTR2.bill_date[filters.bill_from_date : filters.bill_to_date]) + ) + + if filters.get("purchase_doctype") == "Purchase Invoice": + query = query.where(GSTR2.classification.notin(IMPORT_CATEGORY)) + elif filters.get("purchase_doctype") == "Bill of Entry": + query = query.where(GSTR2.classification.isin(IMPORT_CATEGORY)) + + if not filters.show_matched: + query = query.where(IfNull(GSTR2.link_name, "") == "") + + return self._get_link_options(query.run(as_dict=True)) + + def get_bill_of_entry_options(self, filters): + BOE = frappe.qb.DocType("Bill of Entry") + query = self.ReconciledData.query_bill_of_entry().where( + BOE.bill_of_entry_date[filters.bill_from_date : filters.bill_to_date] + ) + + if not filters.show_matched: + query = query.where( + BOE.name.notin(BillOfEntry.query_matched_bill_of_entry()) + ) + + return self._get_link_options(query.run(as_dict=True)) + + def _get_link_options(self, data): + for row in data: + row.value = row.label = row.name + if not row.get("classification"): + row.classification = self.ReconciledData.guess_classification(row) + + row.description = ( + f"{row.bill_no}, {row.bill_date}, Taxable Amount: {row.taxable_value}" + ) + row.description += ( + f", Tax Amount: {BaseUtil.get_total_tax(row)}, {row.classification}" + ) + + return data + + +def get_import_history( + company_gstin, return_type: ReturnType, periods: List[str], fields=None, pluck=None +): + if not (fields or pluck): + fields = ( + "return_period", + "classification", + "data_not_found", + "last_updated_on", + "request_id", + ) + + return frappe.db.get_all( + "GSTR Import Log", + filters={ + "gstin": company_gstin, + "return_type": return_type.value, + "return_period": ("in", periods), + }, + fields=fields, + pluck=pluck, + ) + + +@frappe.whitelist() +def generate_excel_attachment(data, doc): + frappe.has_permission("Purchase Reconciliation Tool", "email", throw=True) + + build_data = BuildExcel(doc, data, is_supplier_specific=True, email=True) + + xlsx_file, filename = build_data.export_data() + xlsx_data = xlsx_file.getvalue() + + # Upload attachment for email xlsx data using communication make() method + folder = frappe.form_dict.folder or "Home" + file_url = frappe.form_dict.file_url or "" + + file = frappe.get_doc( + { + "doctype": "File", + "attached_to_doctype": "Purchase Reconciliation Tool", + "attached_to_name": "Purchase Reconciliation Tool", + "folder": folder, + "file_name": f"{filename}.xlsx", + "file_url": file_url, + "is_private": 0, + "content": xlsx_data, + } + ) + file.save(ignore_permissions=True) + return [file] + + +@frappe.whitelist() +def download_excel_report(data, doc, is_supplier_specific=False): + frappe.has_permission("Purchase Reconciliation Tool", "export", throw=True) + + build_data = BuildExcel(doc, data, is_supplier_specific) + build_data.export_data() + + +def parse_params(fun): + def wrapper(*args, **kwargs): + args = [frappe.parse_json(arg) for arg in args] + kwargs = {k: frappe.parse_json(v) for k, v in kwargs.items()} + return fun(*args, **kwargs) + + return wrapper + + +class BuildExcel: + COLOR_PALLATE = frappe._dict( + { + "dark_gray": "d9d9d9", + "light_gray": "f2f2f2", + "dark_pink": "e6b9b8", + "light_pink": "f2dcdb", + "sky_blue": "c6d9f1", + "light_blue": "dce6f2", + "green": "d7e4bd", + "light_green": "ebf1de", + } + ) + + @parse_params + def __init__(self, doc, data, is_supplier_specific=False, email=False): + """ + :param doc: purchase reconciliation tool doc + :param data: data to be exported + :param is_supplier_specific: if true, data will be downloded for specific supplier + :param email: send the file as email + """ + self.doc = doc + self.data = data + self.is_supplier_specific = is_supplier_specific + self.email = email + self.set_headers() + self.set_filters() + + def export_data(self): + """Exports data to an excel file""" + excel = ExcelExporter() + excel.create_sheet( + sheet_name="Match Summary Data", + filters=self.filters, + headers=self.match_summary_header, + data=self.get_match_summary_data(), + ) + + if not self.is_supplier_specific: + excel.create_sheet( + sheet_name="Supplier Data", + filters=self.filters, + headers=self.supplier_header, + data=self.get_supplier_data(), + ) + + excel.create_sheet( + sheet_name="Invoice Data", + filters=self.filters, + merged_headers=self.get_merge_headers(), + headers=self.invoice_header, + data=self.get_invoice_data(), + ) + + excel.remove_sheet("Sheet") + + file_name = self.get_file_name() + if self.email: + xlsx_data = excel.save_workbook() + return [xlsx_data, file_name] + + excel.export(file_name) + + def set_headers(self): + """Sets headers for the excel file""" + + self.match_summary_header = self.get_match_summary_columns() + self.supplier_header = self.get_supplier_columns() + self.invoice_header = self.get_invoice_columns() + + def set_filters(self): + """Add filters to the sheet""" + + label = "2B" if self.doc.gst_return == "GSTR 2B" else "2A/2B" + self.period = ( + f"{self.doc.inward_supply_from_date} to {self.doc.inward_supply_to_date}" + ) + + self.filters = frappe._dict( + { + "Company Name": self.doc.company, + "GSTIN": self.doc.company_gstin, + f"Return Period ({label})": self.period, + } + ) + + def get_merge_headers(self): + """Returns merged_headers for the excel file""" + return frappe._dict( + { + "2A / 2B": ["inward_supply_bill_no", "inward_supply_cess"], + "Purchase Data": ["bill_no", "cess"], + } + ) + + def get_match_summary_data(self): + return self.process_data( + self.data.get("match_summary"), + self.match_summary_header, + ) + + def get_supplier_data(self): + return self.process_data( + self.data.get("supplier_summary"), self.supplier_header + ) + + def get_invoice_data(self): + data = ReconciledData(**self.doc).get_consolidated_data( + self.data.get("purchases"), + self.data.get("inward_supplies"), + prefix="inward_supply", + ) + + # TODO: Sanitize supplier name and gstin + self.supplier_name = data[0].get("supplier_name") + self.supplier_gstin = data[0].get("supplier_gstin") + + return self.process_data(data, self.invoice_header) + + def process_data(self, data, column_list): + """return required list of dict for the excel file""" + if not data: + return + + out = [] + fields = [d.get("fieldname") for d in column_list] + purchase_fields = [field.get("fieldname") for field in self.pr_columns] + for row in data: + new_row = {} + for field in fields: + if field not in row: + row[field] = None + + # pur data in row (for invoice_summary) is polluted for Missing in PI + if field in purchase_fields and not row.get("name"): + row[field] = None + + self.assign_value(field, row, new_row) + + out.append(new_row) + + return out + + def assign_value(self, field, source_data, target_data): + if source_data.get(field) is None: + target_data[field] = None + return + + if "is_reverse_charge" in field: + target_data[field] = "Yes" if source_data.get(field) else "No" + return + + target_data[field] = source_data.get(field) + + def get_file_name(self): + """Returns file name for the excel file""" + if not self.is_supplier_specific: + return f"{self.doc.company_gstin}_{self.period}_report" + + file_name = f"{self.supplier_name}_{self.supplier_gstin}" + return file_name.replace(" ", "_") + + def get_match_summary_columns(self): + """ + Defaults: + - bg_color: self.COLOR_PALLATE.dark_gray + - bg_color_data": self.COLOR_PALLATE.light_gray + - bold: 1 + - align_header: "center" + - align_data: "general" + - width: 20 + """ + return [ + { + "label": "Match Status", + "fieldname": "match_status", + "data_format": {"horizontal": "left"}, + "header_format": {"horizontal": "center"}, + }, + { + "label": "Count \n 2A/2B Docs", + "fieldname": "inward_supply_count", + "fieldtype": "Int", + "data_format": {"number_format": "#,##0"}, + }, + { + "label": "Count \n Purchase Docs", + "fieldname": "purchase_count", + "fieldtype": "Int", + "data_format": {"number_format": "#,##0"}, + }, + { + "label": "Taxable Amount Diff \n 2A/2B - Purchase", + "fieldname": "taxable_value_difference", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_pink, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.dark_pink, + }, + }, + { + "label": "Tax Difference \n 2A/2B - Purchase", + "fieldname": "tax_difference", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_pink, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.dark_pink, + }, + }, + { + "label": "%Action Taken", + "fieldname": "action_taken_count", + "data_format": {"number_format": "0.00%"}, + "width": 12, + }, + ] + + def get_supplier_columns(self): + return [ + { + "label": "Supplier Name", + "fieldname": "supplier_name", + "data_format": {"horizontal": "left"}, + }, + { + "label": "Supplier GSTIN", + "fieldname": "supplier_gstin", + "data_format": {"horizontal": "left"}, + }, + { + "label": "Count \n 2A/2B Docs", + "fieldname": "inward_supply_count", + "fieldtype": "Int", + "data_format": {"number_format": "#,##0"}, + }, + { + "label": "Count \n Purchase Docs", + "fieldname": "purchase_count", + "fieldtype": "Int", + "data_format": { + "number_format": "#,##0", + }, + }, + { + "label": "Taxable Amount Diff \n 2A/2B - Purchase", + "fieldname": "taxable_value_difference", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_pink, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.dark_pink, + }, + }, + { + "label": "Tax Difference \n 2A/2B - Purchase", + "fieldname": "tax_difference", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_pink, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.dark_pink, + }, + }, + { + "label": "%Action Taken", + "fieldname": "action_taken_count", + "data_format": {"number_format": "0.00%"}, + "header_format": { + "width": 12, + }, + }, + ] + + def get_invoice_columns(self): + self.pr_columns = [ + { + "label": "Bill No", + "fieldname": "bill_no", + "compare_with": "inward_supply_bill_no", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_green, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "Bill Date", + "fieldname": "bill_date", + "compare_with": "inward_supply_bill_date", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_green, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "GSTIN", + "fieldname": "supplier_gstin", + "compare_with": "inward_supply_supplier_gstin", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_green, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 15, + }, + }, + { + "label": "Place of Supply", + "fieldname": "place_of_supply", + "compare_with": "inward_supply_place_of_supply", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_green, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "Reverse Charge", + "fieldname": "is_reverse_charge", + "compare_with": "inward_supply_is_reverse_charge", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_green, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "Taxable Value", + "fieldname": "taxable_value", + "compare_with": "inward_supply_taxable_value", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_green, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "CGST", + "fieldname": "cgst", + "compare_with": "inward_supply_cgst", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_green, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "SGST", + "fieldname": "sgst", + "compare_with": "inward_supply_sgst", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_green, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "IGST", + "fieldname": "igst", + "compare_with": "inward_supply_igst", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_green, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + { + "label": "CESS", + "fieldname": "cess", + "compare_with": "inward_supply_cess", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_green, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.green, + "width": 12, + }, + }, + ] + self.inward_supply_columns = [ + { + "label": "Bill No", + "fieldname": "inward_supply_bill_no", + "compare_with": "bill_no", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "Bill Date", + "fieldname": "inward_supply_bill_date", + "compare_with": "bill_date", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "GSTIN", + "fieldname": "inward_supply_supplier_gstin", + "compare_with": "supplier_gstin", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 15, + }, + }, + { + "label": "Place of Supply", + "fieldname": "inward_supply_place_of_supply", + "compare_with": "place_of_supply", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "Reverse Charge", + "fieldname": "inward_supply_is_reverse_charge", + "compare_with": "is_reverse_charge", + "data_format": { + "horizontal": "left", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "Taxable Value", + "fieldname": "inward_supply_taxable_value", + "compare_with": "taxable_value", + "fieldtype": "Float", + "data_format": { + "number_format": "0.00", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "CGST", + "fieldname": "inward_supply_cgst", + "compare_with": "cgst", + "fieldtype": "Float", + "data_format": { + "number_format": "0.00", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "SGST", + "fieldname": "inward_supply_sgst", + "compare_with": "sgst", + "fieldtype": "Float", + "data_format": { + "number_format": "0.00", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "IGST", + "fieldname": "inward_supply_igst", + "compare_with": "igst", + "fieldtype": "Float", + "data_format": { + "number_format": "0.00", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + { + "label": "CESS", + "fieldname": "inward_supply_cess", + "compare_with": "cess", + "fieldtype": "Float", + "data_format": { + "number_format": "0.00", + "bg_color": self.COLOR_PALLATE.light_blue, + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.sky_blue, + "width": 12, + }, + }, + ] + inv_columns = [ + { + "label": "Action Status", + "fieldname": "action", + "data_format": {"horizontal": "left"}, + }, + { + "label": "Match Status", + "fieldname": "match_status", + "data_format": {"horizontal": "left"}, + }, + { + "label": "Supplier Name", + "fieldname": "supplier_name", + "data_format": {"horizontal": "left"}, + }, + { + "label": "PAN", + "fieldname": "pan", + "data_format": {"horizontal": "center"}, + "header_format": { + "width": 15, + }, + }, + { + "label": "Classification", + "fieldname": "classification", + "data_format": {"horizontal": "left"}, + "header_format": { + "width": 11, + }, + }, + { + "label": "Taxable Value Difference", + "fieldname": "taxable_value_difference", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_pink, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.dark_pink, + "width": 12, + }, + }, + { + "label": "Tax Difference", + "fieldname": "tax_difference", + "fieldtype": "Float", + "data_format": { + "bg_color": self.COLOR_PALLATE.light_pink, + "number_format": "0.00", + }, + "header_format": { + "bg_color": self.COLOR_PALLATE.dark_pink, + "width": 12, + }, + }, + ] + inv_columns.extend(self.inward_supply_columns) + inv_columns.extend(self.pr_columns) + return inv_columns diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py new file mode 100644 index 000000000..a9a7a7d98 --- /dev/null +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Resilient Tech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPurchaseReconciliationTool(FrappeTestCase): + pass diff --git a/india_compliance/gst_india/number_card/number_card.py b/india_compliance/gst_india/number_card/number_card.py index 40d9c4520..70e848b6f 100644 --- a/india_compliance/gst_india/number_card/number_card.py +++ b/india_compliance/gst_india/number_card/number_card.py @@ -23,7 +23,7 @@ def get_active_e_invoice_count_for_cancelled_invoices(filters=None): return 0 default_filters = get_default_filters(filters) - default_filters["exceptions"] = "Invoice Cancelled but e-Invoice Active" + default_filters["exceptions"] = "Invoice Cancelled but not e-Invoice" return get_e_invoice_summary_count(default_filters) diff --git a/india_compliance/gst_india/overrides/gl_entry.py b/india_compliance/gst_india/overrides/gl_entry.py index bfcf3650b..c6a0d5a03 100644 --- a/india_compliance/gst_india/overrides/gl_entry.py +++ b/india_compliance/gst_india/overrides/gl_entry.py @@ -16,7 +16,10 @@ def validate(doc, method=None): return frappe.throw( - _("Company GSTIN is a mandatory field for accounting of GST Accounts.") + _( + "Company GSTIN is a mandatory field for accounting of GST Accounts." + " Run `Update GSTIN` patch from GST Balance Report to update GSTIN in all transactions." + ) ) diff --git a/india_compliance/gst_india/overrides/payment_entry.py b/india_compliance/gst_india/overrides/payment_entry.py index 7f50cb8c1..220924167 100644 --- a/india_compliance/gst_india/overrides/payment_entry.py +++ b/india_compliance/gst_india/overrides/payment_entry.py @@ -1,8 +1,63 @@ import frappe -from frappe.utils import cstr +from frappe import _ +from frappe.contacts.doctype.address.address import get_default_address +from frappe.query_builder.functions import Sum +from frappe.utils import cstr, flt, getdate +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.utils import create_payment_ledger_entry +from erpnext.controllers.accounts_controller import get_advance_payment_entries +from india_compliance.gst_india.overrides.transaction import get_gst_details +from india_compliance.gst_india.overrides.transaction import ( + validate_transaction as validate_transaction_for_advance_payment, +) +from india_compliance.gst_india.utils import get_all_gst_accounts -def update_place_of_supply(doc, method): + +def validate(doc, method=None): + if not doc.taxes: + return + + if doc.party_type == "Customer": + # Presume is export with GST if GST accounts are present + doc.is_export_with_gst = 1 + validate_transaction_for_advance_payment(doc, method) + + else: + gst_accounts = get_all_gst_accounts(doc.company) + for row in doc.taxes: + if row.account_head in gst_accounts and row.tax_amount != 0: + frappe.throw( + _("GST Taxes are not allowed for Supplier Advance Payment Entry") + ) + + +def on_submit(doc, method=None): + make_gst_revesal_entry_from_advance_payment(doc) + + +def on_update_after_submit(doc, method=None): + make_gst_revesal_entry_from_advance_payment(doc) + + +@frappe.whitelist() +def update_party_details(party_details, doctype, company): + if isinstance(party_details, str): + party_details = frappe.parse_json(party_details) + + address = get_default_address("Customer", party_details.get("customer")) + party_details.update(customer_address=address) + + # Update address for update + response = { + "customer_address": address, # should be set first as gst_category and gstin is fetched from address + **get_gst_details(party_details, doctype, company, update_place_of_supply=True), + } + + return response + + +def update_place_of_supply(doc): country = frappe.get_cached_value("Company", doc.company, "country") if country != "India": return @@ -17,3 +72,246 @@ def update_place_of_supply(doc, method): doc.place_of_supply = ( cstr(address.gst_state_number) + "-" + cstr(address.gst_state) ) + + +def make_gst_revesal_entry_from_advance_payment(doc): + """ + This functionality aims to create a GST reversal entry where GST was paid in advance + + On Submit: Creates GLEs and PLEs for all references. + On Update after Submit: Creates GLEs for new references. Creates PLEs for all references. + """ + gl_dict = [] + + if not doc.taxes: + return + + for row in doc.get("references"): + gl_dict.extend(get_gl_for_advance_gst_reversal(doc, row)) + + if not gl_dict: + return + + # Creates GLEs and PLEs + make_gl_entries(gl_dict) + + +def get_gl_for_advance_gst_reversal(payment_entry, reference_row): + gl_dicts = [] + voucher_date = frappe.db.get_value( + reference_row.reference_doctype, reference_row.reference_name, "posting_date" + ) + posting_date = ( + payment_entry.posting_date + if getdate(payment_entry.posting_date) > getdate(voucher_date) + else voucher_date + ) + + taxes = get_proportionate_taxes_for_reversal(payment_entry, reference_row) + + if not taxes: + return gl_dicts + + total_amount = sum(taxes.values()) + + args = { + "posting_date": posting_date, + "voucher_detail_no": reference_row.name, + "remarks": f"Reversal for GST on Advance Payment Entry" + f" {payment_entry.name} against {reference_row.reference_doctype} {reference_row.reference_name}", + } + + # Reduce receivables + gl_entry = payment_entry.get_gl_dict( + { + "account": reference_row.account, + "credit": total_amount, + "credit_in_account_currency": total_amount, + "party_type": payment_entry.party_type, + "party": payment_entry.party, + "against_voucher_type": reference_row.reference_doctype, + "against_voucher": reference_row.reference_name, + **args, + }, + item=reference_row, + ) + + if frappe.db.exists("GL Entry", args): + # All existing PLE are delinked and new ones are created everytime on update + # refer: reconcile_against_document in utils.py + create_payment_ledger_entry( + [gl_entry], update_outstanding="No", cancel=0, adv_adj=1 + ) + + return gl_dicts + + gl_dicts.append(gl_entry) + + # Reverse taxes + for account, amount in taxes.items(): + gl_dicts.append( + payment_entry.get_gl_dict( + { + "account": account, + "debit": amount, + "debit_in_account_currency": amount, + "against_voucher_type": payment_entry.doctype, + "against_voucher": payment_entry.name, + **args, + }, + item=reference_row, + ) + ) + + return gl_dicts + + +def get_proportionate_taxes_for_reversal(payment_entry, reference_row): + """ + This function calculates proportionate taxes for reversal of GST paid in advance + """ + # Compile taxes + gst_accounts = get_all_gst_accounts(payment_entry.company) + taxes = {} + for row in payment_entry.taxes: + if row.account_head not in gst_accounts: + continue + + taxes.setdefault(row.account_head, 0) + taxes[row.account_head] += row.base_tax_amount + + if not taxes: + return + + # Ensure there is no rounding error + if ( + not payment_entry.unallocated_amount + and payment_entry.references[-1].idx == reference_row.idx + ): + return balance_taxes(payment_entry, reference_row, taxes) + + return get_proportionate_taxes_for_row(payment_entry, reference_row, taxes) + + +def get_proportionate_taxes_for_row(payment_entry, reference_row, taxes): + base_allocated_amount = payment_entry.calculate_base_allocated_amount_for_reference( + reference_row + ) + for account, amount in taxes.items(): + taxes[account] = flt( + amount * base_allocated_amount / payment_entry.base_paid_amount, 2 + ) + + return taxes + + +def balance_taxes(payment_entry, reference_row, taxes): + for account, amount in taxes.items(): + for allocation_row in payment_entry.references: + if allocation_row.reference_name == reference_row.reference_name: + continue + + taxes[account] = taxes[account] - flt( + amount + * payment_entry.calculate_base_allocated_amount_for_reference( + allocation_row + ) + / payment_entry.base_paid_amount, + 2, + ) + + return taxes + + +def get_advance_payment_entries_for_regional( + party_type, + party, + party_account, + order_doctype, + order_list=None, + include_unallocated=True, + against_all_orders=False, + limit=None, + condition=None, +): + """ + Get Advance Payment Entries with GST Taxes + """ + + payment_entries = get_advance_payment_entries( + party_type=party_type, + party=party, + party_account=party_account, + order_doctype=order_doctype, + order_list=order_list, + include_unallocated=include_unallocated, + against_all_orders=against_all_orders, + limit=limit, + condition=condition, + ) + + # if not Sales Invoice and is Payment Reconciliation + if not condition or not payment_entries: + return payment_entries + + company = frappe.db.get_value("Account", party_account, "company") + taxes = get_taxes_summary(company, payment_entries) + + for pe in payment_entries: + tax_row = taxes.get( + pe.reference_name, + frappe._dict(paid_amount=1, tax_amount=0, tax_amount_reversed=0), + ) + pe.amount += tax_row.tax_amount - tax_row.tax_amount_reversed + + return payment_entries + + +def adjust_allocations_for_taxes_in_payment_reconciliation(doc): + if not doc.allocation: + return + + taxes = get_taxes_summary(doc.company, doc.allocation) + taxes = { + tax.payment_entry: tax.paid_amount / (tax.paid_amount + tax.tax_amount) + for tax in taxes.values() + } + + for row in doc.allocation: + paid_proportion = taxes.get(row.reference_name, 1) + for field in ("amount", "allocated_amount", "unreconciled_amount"): + row.set(field, flt(row.get(field, 0) * paid_proportion, 2)) + + +def get_taxes_summary(company, payment_entries): + gst_accounts = get_all_gst_accounts(company) + references = [ + advance.reference_name + for advance in payment_entries + if advance.reference_type == "Payment Entry" + ] + + gl_entry = frappe.qb.DocType("GL Entry") + pe = frappe.qb.DocType("Payment Entry") + taxes = ( + frappe.qb.from_(gl_entry) + .join(pe) + .on(pe.name == gl_entry.voucher_no) + .select( + Sum(gl_entry.credit_in_account_currency).as_("tax_amount"), + Sum(gl_entry.debit_in_account_currency).as_("tax_amount_reversed"), + pe.name.as_("payment_entry"), + pe.paid_amount, + ) + .where(gl_entry.is_cancelled == 0) + .where(gl_entry.voucher_type == "Payment Entry") + .where(gl_entry.voucher_no.isin(references)) + .where(gl_entry.account.isin(gst_accounts)) + .where(gl_entry.company == company) + .groupby(gl_entry.voucher_no) + .run(as_dict=True) + ) + + taxes = {tax.payment_entry: tax for tax in taxes} + + return taxes diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 069679fc4..a2aa5f3c6 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -45,9 +45,34 @@ def validate(doc, method=None): update_itc_totals(doc) validate_supplier_invoice_number(doc) + validate_with_inward_supply(doc) + set_reconciliation_status(doc) + + +def set_reconciliation_status(doc): + reconciliation_status = "Not Applicable" + + if is_b2b_invoice(doc): + reconciliation_status = "Unreconciled" + + doc.reconciliation_status = reconciliation_status + + +def is_b2b_invoice(doc): + return not ( + doc.supplier_gstin in ["", None] + or doc.gst_category in ["Registered Composition", "Unregistered", "Overseas"] + or doc.supplier_gstin == doc.company_gstin + or doc.is_opening == "Yes" + or any(row for row in doc.items if row.is_non_gst == 1) + ) def update_itc_totals(doc, method=None): + # Set default value + if not doc.eligibility_for_itc: + doc.eligibility_for_itc = "All Other ITC" + # Initialize values doc.itc_integrated_tax = 0 doc.itc_state_tax = 0 @@ -99,7 +124,79 @@ def get_dashboard_data(data): reference_section["items"].append("Bill of Entry") update_dashboard_with_gst_logs( - "Purchase Invoice", data, "e-Waybill Log", "Integration Request" + "Purchase Invoice", + data, + "e-Waybill Log", + "Integration Request", + "GST Inward Supply", ) return data + + +def validate_with_inward_supply(doc): + if not doc.get("_inward_supply"): + return + + mismatch_fields = {} + for field in [ + "company", + "company_gstin", + "supplier_gstin", + "bill_no", + "bill_date", + "is_reverse_charge", + "place_of_supply", + ]: + if doc.get(field) != doc._inward_supply.get(field): + mismatch_fields[field] = doc._inward_supply.get(field) + + # mismatch for taxable_value + taxable_value = sum([item.taxable_value for item in doc.items]) + if taxable_value != doc._inward_supply.get("taxable_value"): + mismatch_fields["Taxable Value"] = doc._inward_supply.get("taxable_value") + + # mismatch for taxes + gst_accounts = get_gst_accounts_by_type(doc.company, "Input") + for tax in ["cgst", "sgst", "igst", "cess"]: + tax_amount = get_tax_amount(doc.taxes, gst_accounts[tax + "_account"]) + if tax == "cess": + tax_amount += get_tax_amount(doc.taxes, gst_accounts.cess_non_advol_account) + + if tax_amount == doc._inward_supply.get(tax): + continue + + mismatch_fields[tax.upper()] = doc._inward_supply.get(tax) + + if mismatch_fields: + message = ( + "Purchase Invoice does not match with releted GST Inward Supply.