Merge branch 'develop' of https://github.com/frappe/erpnext into mpesa-integration
diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json
index 50fc3bb..51fc3f7 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:mode_of_payment",
@@ -28,7 +29,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Type",
- "options": "Cash\nBank\nGeneral"
+ "options": "Cash\nBank\nGeneral\nPhone"
},
{
"fieldname": "accounts",
@@ -45,7 +46,9 @@
],
"icon": "fa fa-credit-card",
"idx": 1,
- "modified": "2020-09-18 17:26:09.703215",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-09-18 17:57:23.835236",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Mode of Payment",
diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json
index 8dc2628..12e6f5e 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json
+++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json
@@ -1,313 +1,98 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2015-12-23 21:31:52.699821",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2015-12-23 21:31:52.699821",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "field_order": [
+ "payment_gateway",
+ "payment_channel",
+ "is_default",
+ "column_break_4",
+ "payment_account",
+ "currency",
+ "payment_request_message",
+ "message",
+ "message_examples"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_gateway",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
+ "fieldname": "payment_gateway",
+ "fieldtype": "Link",
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Gateway",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Gateway",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "label": "Payment Gateway",
+ "options": "Payment Gateway",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "is_default",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Default",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "is_default",
+ "fieldtype": "Check",
+ "label": "Is Default"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
+ "fieldname": "payment_account",
+ "fieldtype": "Link",
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "label": "Payment Account",
+ "options": "Account",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "payment_account.account_currency",
"fieldname": "currency",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldtype": "Read Only",
+ "label": "Currency"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_request_message",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval: doc.payment_channel !== \"Phone\"",
+ "fieldname": "payment_request_message",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Please click on the link below to make your payment",
- "fieldname": "message",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Default Payment Request Message",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "Please click on the link below to make your payment",
+ "fieldname": "message",
+ "fieldtype": "Small Text",
+ "label": "Default Payment Request Message"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "message_examples",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Message Examples",
- "length": 0,
- "no_copy": 0,
- "options": "<pre><h5>Message Example</h5>\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n</pre>\n",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "message_examples",
+ "fieldtype": "HTML",
+ "label": "Message Examples",
+ "options": "<pre><h5>Message Example</h5>\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n</pre>\n"
+ },
+ {
+ "default": "Email",
+ "fieldname": "payment_channel",
+ "fieldtype": "Select",
+ "label": "Payment Channel",
+ "options": "\nEmail\nPhone"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-05-16 22:43:34.970491",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Gateway Account",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-09-20 13:30:27.722852",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Gateway Account",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json
index 8eadfd0..2ee356a 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.json
+++ b/erpnext/accounts/doctype/payment_request/payment_request.json
@@ -48,6 +48,7 @@
"section_break_7",
"payment_gateway",
"payment_account",
+ "payment_channel",
"payment_order",
"amended_from"
],
@@ -230,6 +231,7 @@
"label": "Recipient Message And Payment Details"
},
{
+ "depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "print_format",
"fieldtype": "Select",
"label": "Print Format"
@@ -241,6 +243,7 @@
"label": "To"
},
{
+ "depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "subject",
"fieldtype": "Data",
"in_global_search": 1,
@@ -277,16 +280,18 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.payment_request_type == 'Inward'",
+ "depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel != \"Phone\"",
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
+ "depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "message",
"fieldtype": "Text",
"label": "Message"
},
{
+ "depends_on": "eval: doc.payment_channel != \"Phone\"",
"fieldname": "message_examples",
"fieldtype": "HTML",
"label": "Message Examples",
@@ -347,12 +352,21 @@
"options": "Payment Request",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fetch_from": "payment_gateway_account.payment_channel",
+ "fieldname": "payment_channel",
+ "fieldtype": "Select",
+ "label": "Payment Channel",
+ "options": "\nEmail\nPhone",
+ "read_only": 1
}
],
"in_create": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-07-17 14:06:42.185763",
+ "modified": "2020-09-18 12:24:14.178853",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index e93ec95..8eba647 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -36,7 +36,7 @@
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if (hasattr(ref_doc, "order_type") \
and getattr(ref_doc, "order_type") != "Shopping Cart"):
- ref_amount = get_amount(ref_doc)
+ ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
@@ -76,11 +76,24 @@
or self.flags.mute_email:
send_mail = False
- if send_mail:
+ if send_mail and self.payment_channel != "Phone":
self.set_payment_request_url()
self.send_email()
self.make_communication_entry()
+ elif self.payment_channel == "Phone":
+ controller = get_payment_gateway_controller(self.payment_gateway)
+ payment_record = dict(
+ reference_doctype="Payment Request",
+ reference_docname=self.name,
+ grand_total=self.grand_total,
+ 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 on_cancel(self):
self.check_if_payment_entry_exists()
self.set_as_cancelled()
@@ -105,13 +118,14 @@
return False
def set_payment_request_url(self):
- if self.payment_account:
+ if self.payment_account and self.payment_channel != "Phone":
self.payment_url = self.get_payment_url()
if self.payment_url:
self.db_set('payment_url', self.payment_url)
- if self.payment_url or not self.payment_gateway_account:
+ if self.payment_url or not self.payment_gateway_account \
+ or (self.payment_gateway_account and self.payment_channel == "Phone"):
self.db_set('status', 'Initiated')
def get_payment_url(self):
@@ -280,7 +294,9 @@
args = frappe._dict(args)
ref_doc = frappe.get_doc(args.dt, args.dn)
- grand_total = get_amount(ref_doc)
+ gateway_account = get_gateway_details(args) or frappe._dict()
+
+ grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
@@ -288,8 +304,6 @@
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
grand_total = grand_total - loyalty_amount
- gateway_account = get_gateway_details(args) or frappe._dict()
-
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
if args.get('party_type') else '')
@@ -314,6 +328,7 @@
"payment_gateway_account": gateway_account.get("name"),
"payment_gateway": gateway_account.get("payment_gateway"),
"payment_account": gateway_account.get("payment_account"),
+ "payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency,
"grand_total": grand_total,
@@ -344,7 +359,7 @@
return pr.as_dict()
-def get_amount(ref_doc):
+def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
@@ -356,6 +371,12 @@
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
+ elif dt == "POS Invoice":
+ for pay in ref_doc.payments:
+ if pay.type == "Phone" and pay.account == payment_account:
+ grand_total = pay.amount
+ break
+
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 3be4304..bedf5e5 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -142,6 +142,23 @@
frm: cur_frm
})
},
+
+ request_for_payment: function (frm) {
+ frm.save().then(() => {
+ frappe.dom.freeze();
+ frappe.call({
+ method: 'create_payment_request',
+ doc: frm.doc,
+ })
+ .fail(() => {
+ frappe.dom.unfreeze();
+ frappe.msgprint('Payment request failed');
+ })
+ .then(() => {
+ frappe.msgprint('Payment request sent successfully');
+ });
+ });
+ }
})
$.extend(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }))
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index ba68df7..155b95e 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -15,6 +15,7 @@
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
+from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from six import iteritems
@@ -313,6 +314,26 @@
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
+ def create_payment_request(self):
+ for pay in self.payments:
+
+ if pay.type == "Phone":
+ payment_gateway = frappe.db.get_value("Payment Gateway Account", {
+ "payment_account": pay.account,
+ })
+ record = {
+ "payment_gateway": payment_gateway,
+ "dt": "POS Invoice",
+ "dn": self.name,
+ "payment_request_type": "Inward",
+ "party_type": "Customer",
+ "party": self.customer,
+ "recipient_id": self.contact_mobile,
+ "submit_doc": True
+ }
+
+ return make_payment_request(**record)
+
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 51ac7cf..f6acd72 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -794,7 +794,7 @@
return acc
-def create_payment_gateway_account(gateway):
+def create_payment_gateway_account(gateway, payment_channel="Email"):
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
company = frappe.db.get_value("Global Defaults", None, "default_company")
@@ -829,7 +829,8 @@
"is_default": 1,
"payment_gateway": gateway,
"payment_account": bank_account.name,
- "currency": bank_account.account_currency
+ "currency": bank_account.account_currency,
+ "payment_channel": payment_channel
}).insert(ignore_permissions=True)
except frappe.DuplicateEntryError:
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/__init__.py
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 0000000..2c4d4bb
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/account_balance.html
@@ -0,0 +1,28 @@
+
+{% if not jQuery.isEmptyObject(data) %}
+<h5 style="margin-top: 20px;"> {{ __("Balance Details") }} </h5>
+<table class="table table-bordered small">
+ <thead>
+ <tr>
+ <th style="width: 20%">{{ __("Account Type") }}</th>
+ <th style="width: 20%" class="text-right">{{ __("Current Balance") }}</th>
+ <th style="width: 20%" class="text-right">{{ __("Available Balance") }}</th>
+ <th style="width: 20%" class="text-right">{{ __("Reserved Balance") }}</th>
+ <th style="width: 20%" class="text-right">{{ __("Uncleared Balance") }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for(const [key, value] of Object.entries(data)) { %}
+ <tr>
+ <td> {%= key %} </td>
+ <td class="text-right"> {%= value["current_balance"] %} </td>
+ <td class="text-right"> {%= value["available_balance"] %} </td>
+ <td class="text-right"> {%= value["reserved_balance"] %} </td>
+ <td class="text-right"> {%= value["uncleared_balance"] %} </td>
+ </tr>
+ {% } %}
+ </tbody>
+</table>
+{% else %}
+<p style="margin-top: 30px;"> Account Balance Information Not Available. </p>
+{% endif %}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
new file mode 100644
index 0000000..d79cdaa
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
@@ -0,0 +1,118 @@
+import base64
+import requests
+from requests.auth import HTTPBasicAuth
+import datetime
+
+class MpesaConnector():
+ def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
+ live_url="https://safaricom.co.ke"):
+ self.env = env
+ self.app_key = app_key
+ self.app_secret = app_secret
+ if env == "sandbox":
+ self.base_url = sandbox_url
+ else:
+ self.base_url = live_url
+ self.authenticate()
+
+ def authenticate(self):
+ """
+ To make Mpesa API calls, you will need to authenticate your app. This method is used to fetch the access token
+ required by Mpesa. Mpesa supports client_credentials grant type. To authorize your API calls to Mpesa,
+ you will need a Basic Auth over HTTPS authorization token. The Basic Auth string is a base64 encoded string
+ of your app's client key and client secret.
+
+ Returns:
+ access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa.
+ """
+ authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials"
+ authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri)
+ r = requests.get(
+ authenticate_url,
+ auth=HTTPBasicAuth(self.app_key, self.app_secret)
+ )
+ self.authentication_token = r.json()['access_token']
+ return r.json()['access_token']
+
+ def get_balance(self, initiator=None, security_credential=None, party_a=None, identifier_type=None,
+ remarks=None, queue_timeout_url=None,result_url=None):
+ """
+ This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
+ Args:
+ initiator (str): Username used to authenticate the transaction.
+ security_credential (str): Generate from developer portal.
+ command_id (str): AccountBalance.
+ party_a (int): Till number being queried.
+ identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code)
+ remarks (str): Comments that are sent along with the transaction(maximum 100 characters).
+ queue_timeout_url (str): The url that handles information of timed out transactions.
+ result_url (str): The url that receives results from M-Pesa api call.
+
+ Returns:
+ OriginatorConverstionID (str): The unique request ID for tracking a transaction.
+ ConversationID (str): The unique request ID returned by mpesa for each request made
+ ResponseDescription (str): Response Description message
+ """
+
+ payload = {
+ "Initiator": initiator,
+ "SecurityCredential": security_credential,
+ "CommandID": "AccountBalance",
+ "PartyA": party_a,
+ "IdentifierType": identifier_type,
+ "Remarks": remarks,
+ "QueueTimeOutURL": queue_timeout_url,
+ "ResultURL": result_url
+ }
+ headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
+ saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query")
+ r = requests.post(saf_url, headers=headers, json=payload)
+ return r.json()
+
+ def stk_push(self, business_shortcode=None, passcode=None, amount=None, callback_url=None, reference_code=None,
+ phone_number=None, description=None):
+ """
+ This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
+ Args:
+ business_shortcode (int): The short code of the organization.
+ passcode (str): Get from developer portal
+ amount (int): The amount being transacted
+ callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API.
+ reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
+ phone_number(int): The Mobile Number to receive the STK Pin Prompt.
+ description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters
+
+ Success Response:
+ CustomerMessage(str): Messages that customers can understand.
+ CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request.
+ ResponseDescription(str): Describes Success or failure
+ MerchantRequestID(str): This is a global unique Identifier for any submitted payment request.
+ ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03
+
+ Error Reponse:
+ requestId(str): This is a unique requestID for the payment request
+ errorCode(str): This is a predefined code that indicates the reason for request failure.
+ errorMessage(str): This is a predefined code that indicates the reason for request failure.
+ """
+
+ time = str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
+ password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time)
+ encoded = base64.b64encode(bytes(password, encoding='utf8'))
+ payload = {
+ "BusinessShortCode": business_shortcode,
+ "Password": encoded.decode("utf-8"),
+ "Timestamp": time,
+ "TransactionType": "CustomerPayBillOnline",
+ "Amount": amount,
+ "PartyA": int(phone_number),
+ "PartyB": business_shortcode,
+ "PhoneNumber": int(phone_number),
+ "CallBackURL": callback_url,
+ "AccountReference": reference_code,
+ "TransactionDesc": description
+ }
+ headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
+
+ saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest")
+ r = requests.post(saf_url, headers=headers, json=payload)
+ return r.json()
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py
new file mode 100644
index 0000000..0d3912e
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_custom_fields.py
@@ -0,0 +1,48 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+def create_custom_pos_fields():
+ """
+ Create custom fields corresponding to POS Settings and POS Invoice
+ """
+ pos_field = {
+ "POS Invoice": [
+ {
+ "fieldname": "request_for_payment",
+ "label": "Request for Payment",
+ "fieldtype": "Button",
+ "hidden": 1,
+ "insert_after": "contact_email"
+ },
+ ]
+ }
+ if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
+ create_custom_fields(pos_field)
+
+ record_dict = [{
+ "doctype": "POS Field",
+ "fieldname": "contact_mobile",
+ "label": "Mobile No",
+ "fieldtype": "Data",
+ "options": "Phone",
+ "parenttype": "POS Settings",
+ "parent": "POS Settings",
+ "parentfield": "invoice_fields"
+ },
+ {
+ "doctype": "POS Field",
+ "fieldname": "request_for_payment",
+ "label": "Request for Payment",
+ "fieldtype": "Button",
+ "parenttype": "POS Settings",
+ "parent": "POS Settings",
+ "parentfield": "invoice_fields"
+ }
+ ]
+ create_pos_settings(record_dict)
+
+def create_pos_settings(record_dict):
+ for record in record_dict:
+ if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}):
+ continue
+ frappe.get_doc(record).insert()
\ 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
new file mode 100644
index 0000000..239a0bc
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Mpesa Settings', {
+ 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
new file mode 100644
index 0000000..fc7b310
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
@@ -0,0 +1,135 @@
+{
+ "actions": [],
+ "autoname": "field:payment_gateway_name",
+ "creation": "2020-09-10 13:21:27.398088",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_gateway_name",
+ "consumer_key",
+ "consumer_secret",
+ "initiator_name",
+ "till_number",
+ "sandbox",
+ "column_break_4",
+ "online_passkey",
+ "security_credential",
+ "get_account_balance",
+ "account_balance"
+ ],
+ "fields": [
+ {
+ "fieldname": "payment_gateway_name",
+ "fieldtype": "Data",
+ "label": "Payment Gateway Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "consumer_key",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Consumer Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "consumer_secret",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Consumer Secret",
+ "reqd": 1
+ },
+ {
+ "fieldname": "till_number",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Till Number",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "sandbox",
+ "fieldtype": "Check",
+ "label": "Sandbox"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "online_passkey",
+ "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"
+ }
+ ],
+ "links": [],
+ "modified": "2020-09-25 20:21:38.215494",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "Mpesa Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
new file mode 100644
index 0000000..3af0baa
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+
+from __future__ import unicode_literals
+from json import loads, dumps
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+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 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
+from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import create_custom_pos_fields
+
+class MpesaSettings(Document):
+ supported_currencies = ["KES"]
+
+ def validate_transaction_currency(self, currency):
+ if currency not in self.supported_currencies:
+ frappe.throw(_("Please select another payment method. Mpesa does not support transactions in currency '{0}'").format(currency))
+
+ def on_update(self):
+ create_custom_pos_fields()
+ create_payment_gateway('Mpesa-' + self.payment_gateway_name, settings='Mpesa Settings', controller=self.payment_gateway_name)
+ create_mode_of_payment('Mpesa-' + self.payment_gateway_name)
+ call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone")
+
+ 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 getattr(response, "requestId"):
+ req_name = getattr(response, "requestId")
+ error = response
+ else:
+ # global checkout id used as request name
+ req_name = getattr(response, global_id)
+ error = None
+
+ create_request_log(request_dict, "Host", "Mpesa", req_name, error)
+
+ if error:
+ frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
+
+def generate_stk_push(**kwargs):
+ args = frappe._dict(kwargs)
+ try:
+ callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
+
+ mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
+ 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"))
+
+ response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
+ passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
+ callback_url=callback_url, reference_code=args.payment_request_name,
+ phone_number=args.sender, description="POS Payment")
+
+ return response
+
+ except Exception:
+ frappe.log_error(title=_("Mpesa Express Transaction Error"))
+ frappe.throw(_("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error"))
+
+@frappe.whitelist(allow_guest=True)
+def verify_transaction(**kwargs):
+ """ Verify the transaction result received via callback """
+ transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
+
+ checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
+ request = frappe.get_doc("Integration Request", checkout_id)
+ 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.process_response('error', transaction_response)
+ except Exception:
+ request.process_response('error', transaction_response)
+ frappe.log_error(frappe.get_traceback())
+
+ else:
+ request.process_response('error', transaction_response)
+
+ 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
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
new file mode 100644
index 0000000..4aa970e
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestMpesaSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py
index 84f7f5a..78a5fce 100644
--- a/erpnext/erpnext_integrations/utils.py
+++ b/erpnext/erpnext_integrations/utils.py
@@ -3,6 +3,7 @@
from frappe import _
import base64, hashlib, hmac
from six.moves.urllib.parse import urlparse
+from erpnext import get_default_company
def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
def innerfn(fn):
@@ -41,3 +42,22 @@
server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
return server_url
+
+def create_mode_of_payment(gateway):
+ payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
+ "payment_gateway": gateway
+ }, ['payment_account'])
+
+ if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
+ mode_of_payment = frappe.get_doc({
+ "doctype": "Mode of Payment",
+ "mode_of_payment": gateway,
+ "enabled": 1,
+ "type": "General",
+ "account": {
+ "doctype": "Mode of Payment Account",
+ "company": get_default_company(),
+ "default_account": payment_gateway_account
+ }
+ })
+ mode_of_payment.insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 7f0cabe..35cd408 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -174,6 +174,24 @@
}
})
+ frappe.realtime.on("process_phone_payments", function(data) {
+ frappe.msgprint({message: 'help', title:'now'})
+ // frappe.dom.unfreeze();
+ // let message = data["ResultDesc"];
+ // let title = __("Payment Failed");
+ // const frm = me.events.get_frm();
+
+ // if (data["ResultCode"] == 0) {
+ // title = __("Payment Received");
+ // $('[data-fieldname=request_for_payment]').text("Paid")
+ // }
+
+ // frappe.msgprint({
+ // "message": message,
+ // "title": title
+ // });
+ });
+
this.$payment_modes.on('click', '.shortcut', function(e) {
const value = $(this).attr('data-value');
me.selected_mode.set_value(value);
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
index 21fa4c3..20c6342 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
@@ -7,6 +7,10 @@
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
frm.refresh_field("quotation_series");
}
+
+ frm.set_query('payment_gateway_account', function() {
+ return { 'filters': { 'payment_channel': "Email" } };
+ });
},
enabled: function(frm) {
if (frm.doc.enabled === 1) {