Merge branch 'develop' into rfq-contact-email-set

This commit is contained in:
Marica 2021-02-11 14:45:09 +05:30 committed by GitHub
commit 819b8d0266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 436 additions and 132 deletions

View File

@ -3,6 +3,7 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@ -82,18 +83,37 @@ class PaymentRequest(Document):
self.make_communication_entry() self.make_communication_entry()
elif self.payment_channel == "Phone": elif self.payment_channel == "Phone":
controller = get_payment_gateway_controller(self.payment_gateway) self.request_phone_payment()
payment_record = dict(
reference_doctype="Payment Request", def request_phone_payment(self):
reference_docname=self.name, controller = get_payment_gateway_controller(self.payment_gateway)
payment_reference=self.reference_name, request_amount = self.get_request_amount()
grand_total=self.grand_total,
sender=self.email_to, payment_record = dict(
currency=self.currency, reference_doctype="Payment Request",
payment_gateway=self.payment_gateway reference_docname=self.name,
) payment_reference=self.reference_name,
controller.validate_transaction_currency(self.currency) request_amount=request_amount,
controller.request_for_payment(**payment_record) sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def get_request_amount(self):
data_of_completed_requests = frappe.get_all("Integration Request", filters={
'reference_doctype': self.doctype,
'reference_docname': self.name,
'status': 'Completed'
}, pluck="data")
if not data_of_completed_requests:
return self.grand_total
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
return request_amounts
def on_cancel(self): def on_cancel(self):
self.check_if_payment_entry_exists() self.check_if_payment_entry_exists()
@ -351,8 +371,8 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart" or args.mute_email: if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True pr.flags.mute_email = True
pr.insert(ignore_permissions=True)
if args.submit_doc: if args.submit_doc:
pr.insert(ignore_permissions=True)
pr.submit() pr.submit()
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
@ -412,8 +432,8 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
def get_gateway_details(args): def get_gateway_details(args):
"""return gateway and payment account of default payment gateway""" """return gateway and payment account of default payment gateway"""
if args.get("payment_gateway"): if args.get("payment_gateway_account"):
return get_payment_gateway_account(args.get("payment_gateway")) return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account

View File

@ -45,7 +45,8 @@ class TestPaymentRequest(unittest.TestCase):
def test_payment_request_linkings(self): def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com") pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - INR")
self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_doctype, "Sales Order")
self.assertEqual(pr.reference_name, so_inr.name) self.assertEqual(pr.reference_name, so_inr.name)
@ -54,7 +55,8 @@ class TestPaymentRequest(unittest.TestCase):
conversion_rate = get_exchange_rate("USD", "INR") conversion_rate = get_exchange_rate("USD", "INR")
si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com") pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - USD")
self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_doctype, "Sales Invoice")
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
@ -68,7 +70,7 @@ class TestPaymentRequest(unittest.TestCase):
so_inr = make_sales_order(currency="INR") so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
mute_email=1, submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1)
pe = pr.set_as_paid() pe = pr.set_as_paid()
so_inr = frappe.get_doc("Sales Order", so_inr.name) so_inr = frappe.get_doc("Sales Order", so_inr.name)
@ -79,7 +81,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.set_as_paid() pe = pr.set_as_paid()
@ -106,7 +108,7 @@ class TestPaymentRequest(unittest.TestCase):
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1) mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.create_payment_entry() pe = pr.create_payment_entry()
pr.load_from_db() pr.load_from_db()

View File

@ -21,7 +21,7 @@ frappe.ui.form.on('POS Closing Entry', {
return { filters: { 'status': 'Open', 'docstatus': 1 } }; return { filters: { 'status': 'Open', 'docstatus': 1 } };
}); });
if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime()); if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
if (frm.doc.docstatus === 1) set_html_data(frm); if (frm.doc.docstatus === 1) set_html_data(frm);
}, },

View File

@ -195,18 +195,43 @@ frappe.ui.form.on('POS Invoice', {
}, },
request_for_payment: function (frm) { request_for_payment: function (frm) {
if (!frm.doc.contact_mobile) {
frappe.throw(__('Please enter mobile number first.'));
}
frm.dirty();
frm.save().then(() => { frm.save().then(() => {
frappe.dom.freeze(); frappe.dom.freeze(__('Waiting for payment...'));
frappe.call({ frappe
method: 'create_payment_request', .call({
doc: frm.doc, method: 'create_payment_request',
}) doc: frm.doc
})
.fail(() => { .fail(() => {
frappe.dom.unfreeze(); frappe.dom.unfreeze();
frappe.msgprint('Payment request failed'); frappe.msgprint(__('Payment request failed'));
}) })
.then(() => { .then(({ message }) => {
frappe.msgprint('Payment request sent successfully'); const payment_request_name = message.name;
setTimeout(() => {
frappe.db.get_value('Payment Request', payment_request_name, ['status', 'grand_total']).then(({ message }) => {
if (message.status != 'Paid') {
frappe.dom.unfreeze();
frappe.msgprint({
message: __('Payment Request took too long to respond. Please try requesting for payment again.'),
title: __('Request Timeout')
});
} else if (frappe.dom.freeze_count != 0) {
frappe.dom.unfreeze();
cur_frm.reload_doc();
cur_pos.payment.events.submit_invoice();
frappe.show_alert({
message: __("Payment of {0} received successfully.", [format_currency(message.grand_total, frm.doc.currency, 0)]),
indicator: 'green'
});
}
});
}, 60000);
}); });
}); });
} }

View File

@ -384,22 +384,48 @@ class POSInvoice(SalesInvoice):
if not self.contact_mobile: if not self.contact_mobile:
frappe.throw(_("Please enter the phone number first")) frappe.throw(_("Please enter the phone number first"))
payment_gateway = frappe.db.get_value("Payment Gateway Account", { pay_req = self.get_existing_payment_request(pay)
"payment_account": pay.account, if not pay_req:
}) pay_req = self.get_new_payment_request(pay)
record = { pay_req.submit()
"payment_gateway": payment_gateway, else:
"dt": "POS Invoice", pay_req.request_phone_payment()
"dn": self.name,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"mode_of_payment": pay.mode_of_payment,
"recipient_id": self.contact_mobile,
"submit_doc": True
}
return make_payment_request(**record) return pay_req
def get_new_payment_request(self, mop):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": mop.account,
}, ["name"])
args = {
"dt": "POS Invoice",
"dn": self.name,
"recipient_id": self.contact_mobile,
"mode_of_payment": mop.mode_of_payment,
"payment_gateway_account": payment_gateway_account,
"payment_request_type": "Inward",
"party_type": "Customer",
"party": self.customer,
"return_doc": True
}
return make_payment_request(**args)
def get_existing_payment_request(self, pay):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": pay.account,
}, ["name"])
args = {
'doctype': 'Payment Request',
'reference_doctype': 'POS Invoice',
'reference_name': self.name,
'payment_gateway_account': payment_gateway_account,
'email_to': self.contact_mobile
}
pr = frappe.db.exists(args)
if pr:
return frappe.get_doc('Payment Request', pr[0][0])
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):

View File

@ -446,9 +446,13 @@ class SellingController(StockController):
check_list, chk_dupl_itm = [], [] check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")): if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return return
if self.doctype == "Sales Invoice" and self.is_consolidated:
return
if self.doctype == "POS Invoice":
return
for d in self.get('items'): for d in self.get('items'):
if self.doctype in ["POS Invoice","Sales Invoice"]: if self.doctype == "Sales Invoice":
stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or ''] stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note] non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note": elif self.doctype == "Delivery Note":

View File

@ -5,7 +5,7 @@ import datetime
class MpesaConnector(): class MpesaConnector():
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke", def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
live_url="https://safaricom.co.ke"): live_url="https://api.safaricom.co.ke"):
"""Setup configuration for Mpesa connector and generate new access token.""" """Setup configuration for Mpesa connector and generate new access token."""
self.env = env self.env = env
self.app_key = app_key self.app_key = app_key
@ -102,14 +102,14 @@ class MpesaConnector():
"BusinessShortCode": business_shortcode, "BusinessShortCode": business_shortcode,
"Password": encoded.decode("utf-8"), "Password": encoded.decode("utf-8"),
"Timestamp": time, "Timestamp": time,
"TransactionType": "CustomerPayBillOnline",
"Amount": amount, "Amount": amount,
"PartyA": int(phone_number), "PartyA": int(phone_number),
"PartyB": business_shortcode, "PartyB": reference_code,
"PhoneNumber": int(phone_number), "PhoneNumber": int(phone_number),
"CallBackURL": callback_url, "CallBackURL": callback_url,
"AccountReference": reference_code, "AccountReference": reference_code,
"TransactionDesc": description "TransactionDesc": description,
"TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline"
} }
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"} headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}

View File

@ -11,8 +11,10 @@
"consumer_secret", "consumer_secret",
"initiator_name", "initiator_name",
"till_number", "till_number",
"transaction_limit",
"sandbox", "sandbox",
"column_break_4", "column_break_4",
"business_shortcode",
"online_passkey", "online_passkey",
"security_credential", "security_credential",
"get_account_balance", "get_account_balance",
@ -84,10 +86,24 @@
"fieldname": "get_account_balance", "fieldname": "get_account_balance",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Get Account Balance" "label": "Get Account Balance"
},
{
"depends_on": "eval:(doc.sandbox==0)",
"fieldname": "business_shortcode",
"fieldtype": "Data",
"label": "Business Shortcode",
"mandatory_depends_on": "eval:(doc.sandbox==0)"
},
{
"default": "150000",
"fieldname": "transaction_limit",
"fieldtype": "Float",
"label": "Transaction Limit",
"non_negative": 1
} }
], ],
"links": [], "links": [],
"modified": "2020-09-25 20:21:38.215494", "modified": "2021-01-29 12:02:16.106942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Mpesa Settings", "name": "Mpesa Settings",

View File

@ -33,13 +33,34 @@ class MpesaSettings(Document):
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone") create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
def request_for_payment(self, **kwargs): def request_for_payment(self, **kwargs):
if frappe.flags.in_test: args = frappe._dict(kwargs)
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload request_amounts = self.split_request_amount_according_to_transaction_limit(args)
response = frappe._dict(get_payment_request_response_payload())
else:
response = frappe._dict(generate_stk_push(**kwargs))
self.handle_api_response("CheckoutRequestID", kwargs, response) for i, amount in enumerate(request_amounts):
args.request_amount = amount
if frappe.flags.in_test:
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
response = frappe._dict(get_payment_request_response_payload(amount))
else:
response = frappe._dict(generate_stk_push(**args))
self.handle_api_response("CheckoutRequestID", args, response)
def split_request_amount_according_to_transaction_limit(self, args):
request_amount = args.request_amount
if request_amount > self.transaction_limit:
# make multiple requests
request_amounts = []
requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4
for i in range(requests_to_be_made):
amount = self.transaction_limit
if i == requests_to_be_made - 1:
amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30
request_amounts.append(amount)
else:
request_amounts = [request_amount]
return request_amounts
def get_account_balance_info(self): def get_account_balance_info(self):
payload = dict( payload = dict(
@ -67,7 +88,8 @@ class MpesaSettings(Document):
req_name = getattr(response, global_id) req_name = getattr(response, global_id)
error = None error = None
create_request_log(request_dict, "Host", "Mpesa", req_name, error) if not frappe.db.exists('Integration Request', req_name):
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
if error: if error:
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
@ -80,6 +102,8 @@ def generate_stk_push(**kwargs):
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:]) mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
env = "production" if not mpesa_settings.sandbox else "sandbox" env = "production" if not mpesa_settings.sandbox else "sandbox"
# for sandbox, business shortcode is same as till number
business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
connector = MpesaConnector(env=env, connector = MpesaConnector(env=env,
app_key=mpesa_settings.consumer_key, app_key=mpesa_settings.consumer_key,
@ -87,10 +111,12 @@ def generate_stk_push(**kwargs):
mobile_number = sanitize_mobile_number(args.sender) mobile_number = sanitize_mobile_number(args.sender)
response = connector.stk_push(business_shortcode=mpesa_settings.till_number, response = connector.stk_push(
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total, business_shortcode=business_shortcode, amount=args.request_amount,
passcode=mpesa_settings.get_password("online_passkey"),
callback_url=callback_url, reference_code=mpesa_settings.till_number, callback_url=callback_url, reference_code=mpesa_settings.till_number,
phone_number=mobile_number, description="POS Payment") phone_number=mobile_number, description="POS Payment"
)
return response return response
@ -108,29 +134,72 @@ def verify_transaction(**kwargs):
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) 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) integration_request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(request.data)) transaction_data = frappe._dict(loads(integration_request.data))
total_paid = 0 # for multiple integration request made against a pos invoice
success = False # for reporting successfull callback to point of sale ui
if transaction_response['ResultCode'] == 0: if transaction_response['ResultCode'] == 0:
if request.reference_doctype and request.reference_docname: if integration_request.reference_doctype and integration_request.reference_docname:
try: try:
doc = frappe.get_doc(request.reference_doctype,
request.reference_docname)
doc.run_method("on_payment_authorized", 'Completed')
item_response = transaction_response["CallbackMetadata"]["Item"] item_response = transaction_response["CallbackMetadata"]["Item"]
amount = fetch_param_value(item_response, "Amount", "Name")
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt) pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname)
request.handle_success(transaction_response)
mpesa_receipts, completed_payments = get_completed_integration_requests_info(
integration_request.reference_doctype,
integration_request.reference_docname,
checkout_id
)
total_paid = amount + sum(completed_payments)
mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt])
if total_paid >= pr.grand_total:
pr.run_method("on_payment_authorized", 'Completed')
success = True
frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
integration_request.handle_success(transaction_response)
except Exception: except Exception:
request.handle_failure(transaction_response) integration_request.handle_failure(transaction_response)
frappe.log_error(frappe.get_traceback()) frappe.log_error(frappe.get_traceback())
else: else:
request.handle_failure(transaction_response) integration_request.handle_failure(transaction_response)
frappe.publish_realtime('process_phone_payment', doctype="POS Invoice", frappe.publish_realtime(
docname=transaction_data.payment_reference, user=request.owner, message=transaction_response) event='process_phone_payment',
doctype="POS Invoice",
docname=transaction_data.payment_reference,
user=integration_request.owner,
message={
'amount': total_paid,
'success': success,
'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else ''
},
)
def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
output_of_other_completed_requests = frappe.get_all("Integration Request", filters={
'name': ['!=', checkout_id],
'reference_doctype': reference_doctype,
'reference_docname': reference_docname,
'status': 'Completed'
}, pluck="output")
mpesa_receipts, completed_payments = [], []
for out in output_of_other_completed_requests:
out = frappe._dict(loads(out))
item_response = out["CallbackMetadata"]["Item"]
completed_amount = fetch_param_value(item_response, "Amount", "Name")
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
completed_payments.append(completed_amount)
mpesa_receipts.append(completed_mpesa_receipt)
return mpesa_receipts, completed_payments
def get_account_balance(request_payload): def get_account_balance(request_payload):
"""Call account balance API to send the request to the Mpesa Servers.""" """Call account balance API to send the request to the Mpesa Servers."""

View File

@ -9,6 +9,10 @@ from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import p
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
class TestMpesaSettings(unittest.TestCase): class TestMpesaSettings(unittest.TestCase):
def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self): def test_creation_of_payment_gateway(self):
create_mpesa_settings(payment_gateway_name="_Test") create_mpesa_settings(payment_gateway_name="_Test")
@ -40,6 +44,8 @@ class TestMpesaSettings(unittest.TestCase):
} }
})) }))
integration_request.delete()
def test_processing_of_callback_payload(self): def test_processing_of_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment") create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
@ -56,10 +62,16 @@ class TestMpesaSettings(unittest.TestCase):
# test payment request creation # test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment") self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
callback_response = get_payment_callback_payload() # submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0])
verify_transaction(**callback_response) verify_transaction(**callback_response)
# test creation of integration request # test creation of integration request
integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972") integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
# test integration request creation and successful update of the status on receiving callback response # test integration request creation and successful update of the status on receiving callback response
self.assertTrue(integration_request) self.assertTrue(integration_request)
@ -69,8 +81,120 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.reload() integration_request.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R") self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
self.assertEquals(integration_request.status, "Completed") self.assertEquals(integration_request.status, "Completed")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
integration_request.delete()
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
pos_invoice.contact_mobile = "093456543894"
pos_invoice.currency = "KES"
pos_invoice.save()
pr = pos_invoice.create_payment_request()
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
# create random receipt nos and send it as response to callback handler
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
integration_requests = []
for i in range(len(integration_req_ids)):
callback_response = get_payment_callback_payload(
Amount=500,
CheckoutRequestID=integration_req_ids[i],
MpesaReceiptNumber=mpesa_receipt_numbers[i]
)
# handle response manually
verify_transaction(**callback_response)
# test completion of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
self.assertEquals(integration_request.status, "Completed")
integration_requests.append(integration_request)
# check receipt number once all the integration requests are completed
pos_invoice.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers))
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
[d.delete() for d in integration_requests]
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
pos_invoice.contact_mobile = "093456543894"
pos_invoice.currency = "KES"
pos_invoice.save()
pr = pos_invoice.create_payment_request()
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
# submitting payment request creates integration requests with random id
integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
}, pluck="name")
# create random receipt nos and send it as response to callback handler
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
callback_response = get_payment_callback_payload(
Amount=500,
CheckoutRequestID=integration_req_ids[0],
MpesaReceiptNumber=mpesa_receipt_numbers[0]
)
# handle response manually
verify_transaction(**callback_response)
# test completion of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
self.assertEquals(integration_request.status, "Completed")
# now one request is completed
# second integration request fails
# now retrying payment request should make only one integration request again
pr = pos_invoice.create_payment_request()
new_integration_req_ids = frappe.get_all("Integration Request", filters={
'reference_doctype': pr.doctype,
'reference_docname': pr.name,
'name': ['not in', integration_req_ids]
}, pluck="name")
self.assertEquals(len(new_integration_req_ids), 1)
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
pr.reload()
pr.cancel()
pr.delete()
pos_invoice.delete()
def create_mpesa_settings(payment_gateway_name="Express"): def create_mpesa_settings(payment_gateway_name="Express"):
if frappe.db.exists("Mpesa Settings", payment_gateway_name): if frappe.db.exists("Mpesa Settings", payment_gateway_name):
@ -160,16 +284,19 @@ def get_test_account_balance_response():
} }
} }
def get_payment_request_response_payload(): def get_payment_request_response_payload(Amount=500):
"""Response received after successfully calling the stk push process request API.""" """Response received after successfully calling the stk push process request API."""
CheckoutRequestID = frappe.utils.random_string(10)
return { return {
"MerchantRequestID": "8071-27184008-1", "MerchantRequestID": "8071-27184008-1",
"CheckoutRequestID": "ws_CO_061020201133231972", "CheckoutRequestID": CheckoutRequestID,
"ResultCode": 0, "ResultCode": 0,
"ResultDesc": "The service request is processed successfully.", "ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": { "CallbackMetadata": {
"Item": [ "Item": [
{ "Name": "Amount", "Value": 500.0 }, { "Name": "Amount", "Value": Amount },
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" }, { "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
{ "Name": "TransactionDate", "Value": 20201006113336 }, { "Name": "TransactionDate", "Value": 20201006113336 },
{ "Name": "PhoneNumber", "Value": 254723575670 } { "Name": "PhoneNumber", "Value": 254723575670 }
@ -177,41 +304,26 @@ def get_payment_request_response_payload():
} }
} }
def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"):
def get_payment_callback_payload():
"""Response received from the server as callback after calling the stkpush process request API.""" """Response received from the server as callback after calling the stkpush process request API."""
return { return {
"Body":{ "Body":{
"stkCallback":{ "stkCallback":{
"MerchantRequestID":"19465-780693-1", "MerchantRequestID":"19465-780693-1",
"CheckoutRequestID":"ws_CO_061020201133231972", "CheckoutRequestID":CheckoutRequestID,
"ResultCode":0, "ResultCode":0,
"ResultDesc":"The service request is processed successfully.", "ResultDesc":"The service request is processed successfully.",
"CallbackMetadata":{ "CallbackMetadata":{
"Item":[ "Item":[
{ { "Name":"Amount", "Value":Amount },
"Name":"Amount", { "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber },
"Value":500 { "Name":"Balance" },
}, { "Name":"TransactionDate", "Value":20170727154800 },
{ { "Name":"PhoneNumber", "Value":254721566839 }
"Name":"MpesaReceiptNumber", ]
"Value":"LGR7OWQX0R"
},
{
"Name":"Balance"
},
{
"Name":"TransactionDate",
"Value":20170727154800
},
{
"Name":"PhoneNumber",
"Value":254721566839
} }
]
} }
} }
}
} }
def get_account_balance_callback_payload(): def get_account_balance_callback_payload():

View File

@ -126,7 +126,9 @@ class Customer(TransactionBase):
'''If Customer created from Lead, update lead status to "Converted" '''If Customer created from Lead, update lead status to "Converted"
update Customer link in Quotation, Opportunity''' update Customer link in Quotation, Opportunity'''
if self.lead_name: if self.lead_name:
frappe.db.set_value('Lead', self.lead_name, 'status', 'Converted', update_modified=False) lead = frappe.get_doc('Lead', self.lead_name)
lead.status = 'Converted'
lead.save()
def create_lead_address_contact(self): def create_lead_address_contact(self):
if self.lead_name: if self.lead_name:

View File

@ -477,7 +477,7 @@ erpnext.PointOfSale.ItemCart = class {
const taxes = frm.doc.taxes.map(t => { const taxes = frm.doc.taxes.map(t => {
return { return {
description: t.description, rate: t.rate description: t.description, rate: t.rate
} };
}); });
this.render_taxes(frm.doc.total_taxes_and_charges, taxes); this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
} }

View File

@ -153,32 +153,23 @@ erpnext.PointOfSale.Payment = class {
me.$payment_modes.find(`.${mode}-amount`).css('display', 'none'); me.$payment_modes.find(`.${mode}-amount`).css('display', 'none');
me.$payment_modes.find(`.${mode}-name`).css('display', 'inline'); me.$payment_modes.find(`.${mode}-name`).css('display', 'inline');
const doc = me.events.get_frm().doc;
me.selected_mode = me[`${mode}_control`]; me.selected_mode = me[`${mode}_control`];
me.selected_mode && me.selected_mode.$input.get(0).focus(); me.selected_mode && me.selected_mode.$input.get(0).focus();
const current_value = me.selected_mode ? me.selected_mode.get_value() : undefined; me.auto_set_remaining_amount();
!current_value && doc.grand_total > doc.paid_amount && me.selected_mode ?
me.selected_mode.set_value(doc.grand_total - doc.paid_amount) : '';
} }
}); });
frappe.realtime.on("process_phone_payment", function(data) { frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
frappe.dom.unfreeze(); const contact = frm.doc.contact_mobile;
cur_frm.reload_doc(); const request_button = $(this.request_for_payment_field.$input[0]);
let message = data["ResultDesc"]; if (contact) {
let title = __("Payment Failed"); request_button.removeClass('btn-default').addClass('btn-primary');
} else {
request_button.removeClass('btn-primary').addClass('btn-default');
}
});
if (data["ResultCode"] == 0) { this.setup_listener_for_payments();
title = __("Payment Received");
$('.btn.btn-xs.btn-default[data-fieldname=request_for_payment]').html(`Payment Received`);
me.events.submit_invoice();
}
frappe.msgprint({
"message": message,
"title": title
});
});
this.$payment_modes.on('click', '.shortcut', () => { this.$payment_modes.on('click', '.shortcut', () => {
const value = $(this).attr('data-value'); const value = $(this).attr('data-value');
@ -224,6 +215,41 @@ erpnext.PointOfSale.Payment = class {
}); });
} }
setup_listener_for_payments() {
frappe.realtime.on("process_phone_payment", (data) => {
const doc = this.events.get_frm().doc;
const { response, amount, success, failure_message } = data;
let message, title;
if (success) {
title = __("Payment Received");
if (amount >= doc.grand_total) {
frappe.dom.unfreeze();
message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
this.events.submit_invoice();
cur_frm.reload_doc();
} else {
message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]);
}
} else if (failure_message) {
message = failure_message;
title = __("Payment Failed");
}
frappe.msgprint({ "message": message, "title": title });
});
}
auto_set_remaining_amount() {
const doc = this.events.get_frm().doc;
const remaining_amount = doc.grand_total - doc.paid_amount;
const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
if (!current_value && remaining_amount > 0 && this.selected_mode) {
this.selected_mode.set_value(remaining_amount);
}
}
attach_shortcuts() { attach_shortcuts() {
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`); this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`);
@ -333,9 +359,11 @@ erpnext.PointOfSale.Payment = class {
fieldtype: 'Currency', fieldtype: 'Currency',
placeholder: __('Enter {0} amount.', [p.mode_of_payment]), placeholder: __('Enter {0} amount.', [p.mode_of_payment]),
onchange: function() { onchange: function() {
if (this.value || this.value == 0) { const current_value = frappe.model.get_value(p.doctype, p.name, 'amount');
frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value)) if (current_value != this.value) {
.then(() => me.update_totals_section()); frappe.model
.set_value(p.doctype, p.name, 'amount', flt(this.value))
.then(() => me.update_totals_section())
const formatted_currency = format_currency(this.value, currency); const formatted_currency = format_currency(this.value, currency);
me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency); me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);