From a26c6b4c2d884202cd16901cfaf43a9b062c6d1f Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 25 Sep 2020 23:56:39 +0530 Subject: [PATCH] fix: fetch account balance info --- .../mpesa_settings/account_balance.html | 28 +++++ .../doctype/mpesa_settings/mpesa_settings.js | 25 ++++- .../mpesa_settings/mpesa_settings.json | 33 +++++- .../doctype/mpesa_settings/mpesa_settings.py | 104 +++++++++++++++--- 4 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html new file mode 100644 index 0000000000..2c4d4bbdec --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html @@ -0,0 +1,28 @@ + +{% if not jQuery.isEmptyObject(data) %} +
{{ __("Balance Details") }}
+ + + + + + + + + + + + {% for(const [key, value] of Object.entries(data)) { %} + + + + + + + + {% } %} + +
{{ __("Account Type") }}{{ __("Current Balance") }}{{ __("Available Balance") }}{{ __("Reserved Balance") }}{{ __("Uncleared Balance") }}
{%= key %} {%= value["current_balance"] %} {%= value["available_balance"] %} {%= value["reserved_balance"] %} {%= value["uncleared_balance"] %}
+{% else %} +

Account Balance Information Not Available.

+{% endif %} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js index 48e0c0bd35..239a0bc9b2 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js @@ -2,6 +2,27 @@ // For license information, please see license.txt frappe.ui.form.on('Mpesa Settings', { - // refresh: function(frm) { - // } + onload_post_render: function(frm) { + frm.events.setup_account_balance_html(frm); + }, + + get_account_balance: function(frm) { + if (!frm.initiator_name && !frm.security_credentials) return; + frappe.call({ + method: "get_account_balance_info", + doc: frm.doc + }); + }, + + setup_account_balance_html: function(frm) { + console.log(frm.doc.account_balance) + $("div").remove(".form-dashboard-section.custom"); + frm.dashboard.add_section( + frappe.render_template('account_balance', { + data: JSON.parse(frm.doc.account_balance) + }) + ); + frm.dashboard.show(); + } + }); diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json index 9c0bef1584..fc7b310c08 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json @@ -9,10 +9,14 @@ "payment_gateway_name", "consumer_key", "consumer_secret", - "column_break_4", + "initiator_name", "till_number", + "sandbox", + "column_break_4", "online_passkey", - "sandbox" + "security_credential", + "get_account_balance", + "account_balance" ], "fields": [ { @@ -58,11 +62,32 @@ "fieldtype": "Password", "label": " Online PassKey", "reqd": 1 + }, + { + "fieldname": "initiator_name", + "fieldtype": "Data", + "label": "Initiator Name" + }, + { + "fieldname": "security_credential", + "fieldtype": "Small Text", + "label": "Security Credential" + }, + { + "fieldname": "account_balance", + "fieldtype": "Long Text", + "hidden": 1, + "label": "Account Balance", + "read_only": 1 + }, + { + "fieldname": "get_account_balance", + "fieldtype": "Button", + "label": "Get Account Balance" } ], - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-10 09:07:28.557461", + "modified": "2020-09-25 20:21:38.215494", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Mpesa Settings", diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py index c92c1b23bc..3af0baaa50 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py @@ -4,17 +4,14 @@ from __future__ import unicode_literals -import json -import requests -from six.moves.urllib.parse import urlencode +from json import loads, dumps import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import get_url, call_hook_method, cint, flt, cstr +from frappe.utils import call_hook_method from frappe.integrations.utils import create_request_log, create_payment_gateway from frappe.utils import get_request_site_address -from frappe.utils.password import get_decrypted_password from frappe.utils import get_request_site_address from erpnext.erpnext_integrations.utils import create_mode_of_payment from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector @@ -35,16 +32,29 @@ class MpesaSettings(Document): def request_for_payment(self, **kwargs): response = frappe._dict(generate_stk_push(**kwargs)) + self.handle_api_response("CheckoutRequestID", kwargs, response) + + def get_account_balance_info(self): + payload = dict( + reference_doctype="Mpesa Settings", + reference_docname=self.name, + doc_details=vars(self) + ) + response = frappe._dict(get_account_balance(payload)) + self.handle_api_response("ConversationID", payload, response) + + def handle_api_response(self, global_id, request_dict, response): # check error response - if hasattr(response, "requestId"): + if getattr(response, "requestId"): req_name = getattr(response, "requestId") error = response else: # global checkout id used as request name - req_name = getattr(response, "CheckoutRequestID") + req_name = getattr(response, global_id) error = None - create_request_log(kwargs, "Host", "Mpesa", req_name, error) + create_request_log(request_dict, "Host", "Mpesa", req_name, error) + if error: frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) @@ -76,24 +86,84 @@ def verify_transaction(**kwargs): """ Verify the transaction result received via callback """ transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) - checkout_id = getattr(transaction_response, "CheckoutRequestID") + checkout_id = getattr(transaction_response, "CheckoutRequestID", "") request = frappe.get_doc("Integration Request", checkout_id) - transaction_data = frappe._dict(json.loads(request.data)) + transaction_data = frappe._dict(loads(request.data)) if transaction_response['ResultCode'] == 0: if transaction_data.reference_doctype and transaction_data.reference_docname: try: frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed') - request.db_set('output', transaction_response) - request.db_set('status', 'Completed') + request.process_response('error', transaction_response) except Exception: - request.db_set('error', transaction_response) - request.db_set('status', 'Failed') + request.process_response('error', transaction_response) frappe.log_error(frappe.get_traceback()) else: - request.db_set('error', transaction_response) - request.db_set('status', 'Failed') + request.process_response('error', transaction_response) - frappe.publish_realtime('process_phone_payment', after_commit=True, user=request.owner, message=transaction_response) \ No newline at end of file + frappe.publish_realtime('process_phone_payment', after_commit=True, doctype=transaction_data.reference_doctype, + docname=transaction_data.reference_docname, user=request.owner, message=transaction_response) + +def get_account_balance(request_payload): + """ Call account balance API to send the request to the Mpesa Servers """ + try: + mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname")) + env = "production" if not mpesa_settings.sandbox else "sandbox" + connector = MpesaConnector(env=env, + app_key=mpesa_settings.consumer_key, + app_secret=mpesa_settings.get_password("consumer_secret")) + + # callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info" + callback_url = "https://b014ca8e7957.ngrok.io/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info" + + response = connector.get_balance(mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, 4, mpesa_settings.name, callback_url, callback_url) + return response + except Exception: + frappe.log_error(title=_("Account Balance Processing Error")) + frappe.throw(title=_("Error"), message=_("Please check your configuration and try again")) + +@frappe.whitelist(allow_guest=True) +def process_balance_info(**kwargs): + + account_balance_response = frappe._dict(kwargs["Result"]) + + conversation_id = getattr(account_balance_response, "ConversationID", "") + request = frappe.get_doc("Integration Request", conversation_id) + + if request.status == "Completed": + return + + transaction_data = frappe._dict(loads(request.data)) + frappe.logger().debug(account_balance_response) + + if account_balance_response["ResultCode"] == 0: + try: + result_params = account_balance_response["ResultParameters"]["ResultParameter"] + for param in result_params: + if param["Key"] == "AccountBalance": + balance_info = param["Value"] + balance_info = convert_to_json(balance_info) + + ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname) + ref_doc.db_set("account_balance", balance_info) + + request.process_response('output', account_balance_response) + except: + request.process_response('error', account_balance_response) + frappe.log_error(title=_("Mpesa Account Balance Processing Error"), message=account_balance_response) + else: + request.process_response('error', account_balance_response) + +def convert_to_json(balance_info): + balance_dict = frappe._dict() + for account_info in balance_info.split("&"): + account_info = account_info.split('|') + balance_dict[account_info[0]] = dict( + current_balance=account_info[2], + available_balance=account_info[3], + reserved_balance=account_info[4], + uncleared_balance=account_info[5] + ) + return dumps(balance_dict) \ No newline at end of file