Merge branch 'develop' into quality-inspection-parameter-group

This commit is contained in:
Saqib Ansari 2021-02-12 11:19:25 +05:30
commit 138e98fef2
34 changed files with 644 additions and 179 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

@ -20,11 +20,16 @@ class POSClosingEntry(StatusUpdater):
self.validate_pos_invoices() self.validate_pos_invoices()
def validate_pos_closing(self): def validate_pos_closing(self):
user = frappe.get_all("POS Closing Entry", user = frappe.db.sql("""
filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile }, SELECT name FROM `tabPOS Closing Entry`
or_filters = { WHERE
"period_start_date": ("between", [self.period_start_date, self.period_end_date]), user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
"period_end_date": ("between", [self.period_start_date, self.period_end_date]) (period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
""", {
'user': self.user,
'profile': self.pos_profile,
'start': self.period_start_date,
'end': self.period_end_date
}) })
if user: if user:

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

@ -317,13 +317,14 @@ class POSInvoice(SalesInvoice):
) )
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list') selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
if customer_currency != profile.get('currency'):
self.set('currency', customer_currency)
else: else:
selling_price_list = profile.get('selling_price_list') selling_price_list = profile.get('selling_price_list')
if selling_price_list: if selling_price_list:
self.set('selling_price_list', selling_price_list) self.set('selling_price_list', selling_price_list)
if customer_currency != profile.get('currency'):
self.set('currency', customer_currency)
# set pos values in items # set pos values in items
for item in self.get("items"): for item in self.get("items"):
@ -383,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

@ -212,8 +212,8 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoice_by_customer = get_invoice_customer_map(invoices) invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 5 and closing_entry: if len(invoices) >= 5 and closing_entry:
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
else: else:
create_merge_logs(invoice_by_customer, closing_entry) create_merge_logs(invoice_by_customer, closing_entry)
@ -225,8 +225,8 @@ def unconsolidate_pos_invoices(closing_entry):
) )
if len(merge_logs) >= 5: if len(merge_logs) >= 5:
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
else: else:
cancel_merge_logs(merge_logs, closing_entry) cancel_merge_logs(merge_logs, closing_entry)

View File

@ -40,6 +40,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_22", "section_break_22",
"net_rate", "net_rate",
@ -783,6 +784,14 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
},
{ {
"fieldname": "sales_invoice_item", "fieldname": "sales_invoice_item",
"fieldtype": "Data", "fieldtype": "Data",
@ -795,7 +804,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:20:36.415791", "modified": "2021-01-30 21:43:21.488258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -45,6 +45,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
@ -811,12 +812,20 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:25:04.090630", "modified": "2021-01-30 21:42:37.796771",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -15,15 +15,51 @@ def execute(filters=None):
return columns, data return columns, data
def get_columns(): def get_columns():
return [ columns = [{
_("Payment Document") + "::130", "label": _("Payment Document Type"),
_("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":110", "fieldname": "payment_document_type",
_("Posting Date") + ":Date:100", "fieldtype": "Link",
_("Cheque/Reference No") + "::120", "options": "Doctype",
_("Clearance Date") + ":Date:100", "width": 130
_("Against Account") + ":Link/Account:170", },
_("Amount") + ":Currency:120" {
] "label": _("Payment Entry"),
"fieldname": "payment_entry",
"fieldtype": "Dynamic Link",
"options": "payment_document_type",
"width": 140
},
{
"label": _("Posting Date"),
"fieldname": "posting_date",
"fieldtype": "Date",
"width": 100
},
{
"label": _("Cheque/Reference No"),
"fieldname": "cheque_no",
"width": 120
},
{
"label": _("Clearance Date"),
"fieldname": "clearance_date",
"fieldtype": "Date",
"width": 100
},
{
"label": _("Against Account"),
"fieldname": "against",
"fieldtype": "Link",
"options": "Account",
"width": 170
},
{
"label": _("Amount"),
"fieldname": "amount",
"width": 120
}]
return columns
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""

View File

@ -40,6 +40,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
@ -726,13 +727,21 @@
"fieldname": "more_info_section_break", "fieldname": "more_info_section_break",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "More Information" "label": "More Information"
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-07 11:59:47.670951", "modified": "2021-01-30 21:44:41.816974",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

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

@ -24,6 +24,7 @@ class StockController(AccountsController):
self.validate_inspection() self.validate_inspection()
self.validate_serialized_batch() self.validate_serialized_batch()
self.validate_customer_provided_item() self.validate_customer_provided_item()
self.set_rate_of_stock_uom()
self.validate_internal_transfer() self.validate_internal_transfer()
self.validate_putaway_capacity() self.validate_putaway_capacity()
@ -313,7 +314,7 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def validate_warehouse(self): def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
warehouses = list(set([d.warehouse for d in warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)])) self.get("items") if getattr(d, "warehouse", None)]))
@ -329,6 +330,7 @@ class StockController(AccountsController):
warehouses.extend(from_warehouse) warehouses.extend(from_warehouse)
for w in warehouses: for w in warehouses:
validate_disabled_warehouse(w)
validate_warehouse_company(w, self.company) validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True): def update_billing_percentage(self, update_modified=True):
@ -395,6 +397,11 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 d.allow_zero_valuation_rate = 1
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self): def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
and self.is_internal_transfer(): and self.is_internal_transfer():

View File

@ -78,7 +78,9 @@ def get_scheduled_employees_for_popup(communication_medium):
def strip_number(number): def strip_number(number):
if not number: return if not number: return
# strip 0 from the start of the number for proper number comparisions # strip + and 0 from the start of the number for proper number comparisions
# eg. +7888383332 should match with 7888383332
# eg. 07888383332 should match with 7888383332 # eg. 07888383332 should match with 7888383332
number = number.lstrip('+')
number = number.lstrip('0') number = number.lstrip('0')
return number return number

View File

@ -19,15 +19,50 @@ def set_defaut_value_for_filters(filters):
if not filters.get('lead_age'): filters["lead_age"] = 60 if not filters.get('lead_age'): filters["lead_age"] = 60
def get_columns(): def get_columns():
return [ columns = [{
_("Lead") + ":Link/Lead:100", "label": _("Lead"),
_("Name") + "::100", "fieldname": "lead",
_("Organization") + "::100", "fieldtype": "Link",
_("Reference Document") + "::150", "options": "Lead",
_("Reference Name") + ":Dynamic Link/"+_("Reference Document")+":120", "width": 130
_("Last Communication") + ":Data:200", },
_("Last Communication Date") + ":Date:180" {
] "label": _("Name"),
"fieldname": "name",
"width": 120
},
{
"label": _("Organization"),
"fieldname": "organization",
"width": 120
},
{
"label": _("Reference Document Type"),
"fieldname": "reference_document_type",
"fieldtype": "Link",
"options": "Doctype",
"width": 100
},
{
"label": _("Reference Name"),
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"options": "reference_document_type",
"width": 140
},
{
"label": _("Last Communication"),
"fieldname": "last_communication",
"fieldtype": "Data",
"width": 200
},
{
"label": _("Last Communication Date"),
"fieldname": "last_communication_date",
"fieldtype": "Date",
"width": 100
}]
return columns
def get_data(filters): def get_data(filters):
lead_details = [] lead_details = []

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)
@ -71,6 +83,118 @@ class TestMpesaSettings(unittest.TestCase):
self.assertEquals(integration_request.status, "Completed") self.assertEquals(integration_request.status, "Completed")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "") 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", "")
[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

@ -1,5 +1,6 @@
frappe.listview_settings['Leave Application'] = { frappe.listview_settings['Leave Application'] = {
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"], add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
has_indicator_for_draft: 1,
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status === "Approved") { if (doc.status === "Approved") {
return [__("Approved"), "green", "status,=,Approved"]; return [__("Approved"), "green", "status,=,Approved"];

View File

@ -191,7 +191,6 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item)); item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item));
item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty); item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty);
} }
this._super(doc, cdt, cdn); this._super(doc, cdt, cdn);
}, },

View File

@ -4,7 +4,7 @@
erpnext.taxes_and_totals = erpnext.payments.extend({ erpnext.taxes_and_totals = erpnext.payments.extend({
setup: function() {}, setup: function() {},
apply_pricing_rule_on_item: function(item){ apply_pricing_rule_on_item: function(item) {
let effective_item_rate = item.price_list_rate; let effective_item_rate = item.price_list_rate;
let item_rate = item.rate; let item_rate = item.rate;
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) { if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
@ -26,6 +26,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
if (item.discount_amount) { if (item.discount_amount) {
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item)); item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
} }
frappe.model.set_value(item.doctype, item.name, "rate", item_rate); frappe.model.set_value(item.doctype, item.name, "rate", item_rate);

View File

@ -40,7 +40,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
cur_frm.cscript.set_gross_profit(item); cur_frm.cscript.set_gross_profit(item);
cur_frm.cscript.calculate_taxes_and_totals(); cur_frm.cscript.calculate_taxes_and_totals();
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
}); });
@ -1122,6 +1122,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}); });
} }
me.calculate_stock_uom_rate(doc, cdt, cdn);
}, },
conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) { conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) {
@ -1142,6 +1143,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.meta.has_field(doc.doctype, "price_list_currency")) { frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true); this.apply_price_list(item, true);
} }
this.calculate_stock_uom_rate(doc, cdt, cdn);
} }
}, },
@ -1162,9 +1164,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
qty: function(doc, cdt, cdn) { qty: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn); let item = frappe.get_doc(cdt, cdn);
this.conversion_factor(doc, cdt, cdn, true); this.conversion_factor(doc, cdt, cdn, true);
this.calculate_stock_uom_rate(doc, cdt, cdn);
this.apply_pricing_rule(item, true); this.apply_pricing_rule(item, true);
}, },
calculate_stock_uom_rate: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor);
refresh_field("stock_uom_rate", item.name, item.parentfield);
},
service_stop_date: function(frm, cdt, cdn) { service_stop_date: function(frm, cdt, cdn) {
var child = locals[cdt][cdn]; var child = locals[cdt][cdn];
@ -1275,7 +1283,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"], this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"],
company_currency, "items"); company_currency, "items");
this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount"], this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"],
this.frm.doc.currency, "items"); this.frm.doc.currency, "items");
if(this.frm.fields_dict["operations"]) { if(this.frm.fields_dict["operations"]) {

View File

@ -37,7 +37,7 @@ def validate_einvoice_fields(doc):
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
def raise_document_name_too_long_error(): def raise_document_name_too_long_error():

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

@ -47,6 +47,7 @@
"base_amount", "base_amount",
"base_net_amount", "base_net_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_43", "section_break_43",
"valuation_rate", "valuation_rate",
@ -634,12 +635,20 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"report_hide": 1 "report_hide": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-19 20:48:43.222229", "modified": "2021-01-30 21:39:40.174551",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation Item", "name": "Quotation Item",

View File

@ -46,6 +46,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_24", "section_break_24",
"net_rate", "net_rate",
@ -214,7 +215,6 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "UOM", "label": "UOM",
"options": "UOM", "options": "UOM",
"print_hide": 0,
"reqd": 1 "reqd": 1
}, },
{ {
@ -780,12 +780,20 @@
"fieldname": "manufacturing_section_section", "fieldname": "manufacturing_section_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Manufacturing Section" "label": "Manufacturing Section"
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-012-07 20:54:32.309460", "modified": "2021-01-30 21:35:07.617320",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

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);

View File

@ -47,6 +47,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_25", "section_break_25",
"net_rate", "net_rate",
@ -743,13 +744,21 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:31:27.029803", "modified": "2021-01-30 21:42:03.767968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -48,6 +48,7 @@
"base_rate", "base_rate",
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"stock_uom_rate",
"is_free_item", "is_free_item",
"section_break_29", "section_break_29",
"net_rate", "net_rate",
@ -874,6 +875,14 @@
"label": "Received Qty in Stock UOM", "label": "Received Qty in Stock UOM",
"print_hide": 1 "print_hide": 1
}, },
{
"depends_on": "eval: doc.uom != doc.stock_uom",
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"options": "currency",
"read_only": 1
},
{ {
"fieldname": "delivery_note_item", "fieldname": "delivery_note_item",
"fieldtype": "Data", "fieldtype": "Data",
@ -886,7 +895,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-26 16:50:56.479347", "modified": "2021-01-30 21:44:06.918515",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@ -27,10 +27,11 @@ class StockLedgerEntry(Document):
def validate(self): def validate(self):
self.flags.ignore_submit_comment = True self.flags.ignore_submit_comment = True
from erpnext.stock.utils import validate_warehouse_company from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
self.validate_mandatory() self.validate_mandatory()
self.validate_item() self.validate_item()
self.validate_batch() self.validate_batch()
validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company) validate_warehouse_company(self.warehouse, self.company)
self.scrub_posting_time() self.scrub_posting_time()
self.validate_and_set_fiscal_year() self.validate_and_set_fiscal_year()

View File

@ -202,8 +202,7 @@ class update_entries_after(object):
where where
item_code = %(item_code)s item_code = %(item_code)s
and warehouse = %(warehouse)s and warehouse = %(warehouse)s
and voucher_type = %(voucher_type)s and timestamp(posting_date, time_format(posting_time, '%H:%i:%s')) = timestamp(%(posting_date)s, time_format(%(posting_time)s, '%H:%i:%s'))
and voucher_no = %(voucher_no)s
order by order by
creation ASC creation ASC
for update for update

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe import _ from frappe import _
import json import json
from frappe.utils import flt, cstr, nowdate, nowtime from frappe.utils import flt, cstr, nowdate, nowtime, get_link_to_form
from six import string_types from six import string_types
@ -284,6 +284,10 @@ def is_group_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "is_group"): if frappe.db.get_value("Warehouse", warehouse, "is_group"):
frappe.throw(_("Group node warehouse is not allowed to select for transactions")) frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
def validate_disabled_warehouse(warehouse):
if frappe.db.get_value("Warehouse", warehouse, "disabled"):
frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
def update_included_uom_in_report(columns, result, include_uom, conversion_factors): def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
if not include_uom or not conversion_factors: if not include_uom or not conversion_factors:
return return