feat(india): Improve E-way Bill Cancellation. (#31088)

diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index ef24ce7..580e646 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -150,26 +150,29 @@
 
 			if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
 				const action = () => {
-					let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
-					message += '<br><br>';
-					message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
-
-					const dialog = frappe.msgprint({
-						title: __('Update E-Way Bill Cancelled Status?'),
-						message: message,
-						indicator: 'orange',
-						primary_action: {
-							action: function() {
-								frappe.call({
-									method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
-									args: { doctype, docname: name },
-									freeze: true,
-									callback: () => frm.reload_doc() && dialog.hide()
-								});
-							}
+					// This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side.
+					frappe.confirm(
+						__("Have you cancelled e-way bill on the portal?"),
+						() => {
+							frappe.call({
+								method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill",
+								args: { doctype, docname: name },
+								freeze: true,
+								callback: () => frm.reload_doc(),
+							});
 						},
-						primary_action_label: __('Yes')
-					});
+						() => {
+							frappe.show_alert(
+								{
+									message: __(
+										"Please cancel e-way bill on the portal first."
+									),
+									indicator: "orange",
+								},
+								5
+							);
+						}
+					);
 				};
 				add_custom_button(__("Cancel E-Way Bill"), action);
 			}
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index e5a1a59..9add09b 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -803,6 +803,8 @@
 		self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
 		# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
 		self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
+		# ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details.
+		self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn"
 		self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
 		self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
 
@@ -1205,23 +1207,22 @@
 			log_error(data)
 			self.raise_error(True)
 
-	def cancel_eway_bill(self, eway_bill, reason, remark):
+	def get_ewb_details(self):
+		"""
+		Get e-Waybill Details by IRN API documentaion for validation is not added yet.
+		https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations
+		NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT".
+		"""
 		headers = self.get_headers()
-		data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
-		headers["username"] = headers["user_name"]
-		del headers["user_name"]
-		try:
-			res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
-			if res.get("success"):
-				self.invoice.ewaybill = ""
-				self.invoice.eway_bill_cancelled = 1
-				self.invoice.flags.updater_reference = {
-					"doctype": self.invoice.doctype,
-					"docname": self.invoice.name,
-					"label": _("E-Way Bill Cancelled - {}").format(remark),
-				}
-				self.update_invoice()
+		irn = self.invoice.irn
+		if not irn:
+			frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first."))
 
+		try:
+			params = "?irn={irn}".format(irn=irn)
+			res = self.make_request("get", self.ewaybill_details_url + params, headers)
+			if res.get("success"):
+				return res.get("result")
 			else:
 				raise RequestFailed
 
@@ -1230,9 +1231,65 @@
 			self.raise_error(errors=errors)
 
 		except Exception:
-			log_error(data)
+			log_error()
 			self.raise_error(True)
 
+	def update_ewb_details(self, ewb_details=None):
+		# for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice.
+		if not self.invoice.irn:
+			frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first."))
+		if not ewb_details:
+			ewb_details = self.get_ewb_details()
+		if ewb_details:
+			self.invoice.ewaybill = ewb_details.get("EwbNo")
+			self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill")
+			self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1
+			self.update_invoice()
+
+	def cancel_eway_bill(self):
+		ewb_details = self.get_ewb_details()
+		if ewb_details:
+			ewb_no = str(ewb_details.get("EwbNo"))
+			ewb_status = ewb_details.get("Status")
+			if ewb_status == "CNL":
+				self.invoice.ewaybill = ""
+				self.invoice.eway_bill_cancelled = 1
+				self.invoice.flags.updater_reference = {
+					"doctype": self.invoice.doctype,
+					"docname": self.invoice.name,
+					"label": _("E-Way Bill Cancelled"),
+				}
+				self.update_invoice()
+				frappe.msgprint(
+					_("E-Way Bill Cancelled successfully"),
+					indicator="green",
+					alert=True,
+				)
+			elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no:
+				msg = _("E-Way Bill {} is still active.").format(bold(ewb_no))
+				msg += "<br><br>"
+				msg += _(
+					"You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system."
+				)
+				frappe.msgprint(msg)
+			elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no:
+				# if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice.
+				msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format(
+					bold(ewb_no), bold(self.invoice.ewaybill)
+				)
+				msg += "<hr/>"
+				msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no))
+				frappe.msgprint(msg)
+				self.update_ewb_details(ewb_details=ewb_details)
+			else:
+				# this block should not be ever called but added incase there is any change in API.
+				msg = _("Unknown E-Way Status Code {}.").format(ewb_status)
+				msg += "<br><br>"
+				msg += _("Please contact your system administrator.")
+				frappe.throw(msg)
+		else:
+			frappe.msgprint(_("E-Way Bill Details not found for this IRN."))
+
 	def sanitize_error_message(self, message):
 		"""
 		On validation errors, response message looks something like this:
@@ -1383,12 +1440,22 @@
 
 @frappe.whitelist()
 def cancel_eway_bill(doctype, docname):
-	# NOTE: cancel_eway_bill api is disabled by Adequare.
-	# gsp_connector = GSPConnector(doctype, docname)
-	# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+	# NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly.
+	# https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html#
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.cancel_eway_bill()
 
-	frappe.db.set_value(doctype, docname, "ewaybill", "")
-	frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
+
+@frappe.whitelist()
+def get_ewb_details(doctype, docname):
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.get_ewb_details()
+
+
+@frappe.whitelist()
+def update_ewb_details(doctype, docname):
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.update_ewb_details()
 
 
 @frappe.whitelist()