feat: Allowed multiple payment requests against a reference document (#18988)

diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 73758be..eda59ab 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -20,7 +20,7 @@
 		if self.get("__islocal"):
 			self.status = 'Draft'
 		self.validate_reference_document()
-		self.validate_payment_request()
+		self.validate_payment_request_amount()
 		self.validate_currency()
 		self.validate_subscription_details()
 
@@ -28,10 +28,19 @@
 		if not self.reference_doctype or not self.reference_name:
 			frappe.throw(_("To create a Payment Request reference document is required"))
 
-	def validate_payment_request(self):
-		if frappe.db.get_value("Payment Request", {"reference_name": self.reference_name,
-			"name": ("!=", self.name), "status": ("not in", ["Initiated", "Paid"]), "docstatus": 1}, "name"):
-			frappe.throw(_("Payment Request already exists {0}".format(self.reference_name)))
+	def validate_payment_request_amount(self):
+		existing_payment_request_amount = \
+			get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
+
+		if existing_payment_request_amount:
+			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)
+
+				if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
+					frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount"
+						.format(self.reference_doctype)))
 
 	def validate_currency(self):
 		ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
@@ -271,7 +280,7 @@
 	args = frappe._dict(args)
 
 	ref_doc = frappe.get_doc(args.dt, args.dn)
-	grand_total = get_amount(ref_doc, args.dt)
+	grand_total = get_amount(ref_doc)
 	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))
@@ -281,17 +290,25 @@
 
 	gateway_account = get_gateway_details(args) or frappe._dict()
 
-	existing_payment_request = frappe.db.get_value("Payment Request",
-		{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ["!=", 2]})
-
 	bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
 		if args.get('party_type') else '')
 
+	existing_payment_request = None
+	if args.order_type == "Shopping Cart":
+		existing_payment_request = frappe.db.get_value("Payment Request",
+			{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)})
+
 	if existing_payment_request:
 		frappe.db.set_value("Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False)
 		pr = frappe.get_doc("Payment Request", existing_payment_request)
-
 	else:
+		if args.order_type != "Shopping Cart":
+			existing_payment_request_amount = \
+				get_existing_payment_request_amount(args.dt, args.dn)
+
+			if existing_payment_request_amount:
+				grand_total -= existing_payment_request_amount
+
 		pr = frappe.new_doc("Payment Request")
 		pr.update({
 			"payment_gateway_account": gateway_account.get("name"),
@@ -327,8 +344,9 @@
 
 	return pr.as_dict()
 
-def get_amount(ref_doc, dt):
+def get_amount(ref_doc):
 	"""get amount based on doctype"""
+	dt = ref_doc.doctype
 	if dt in ["Sales Order", "Purchase Order"]:
 		grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
 
@@ -347,6 +365,17 @@
 	else:
 		frappe.throw(_("Payment Entry is already created"))
 
+def get_existing_payment_request_amount(ref_dt, ref_dn):
+	existing_payment_request_amount = frappe.db.sql("""
+		select sum(grand_total)
+		from `tabPayment Request`
+		where
+			reference_doctype = %s
+			and reference_name = %s
+			and docstatus = 1
+	""", (ref_dt, ref_dn))
+	return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
+
 def get_gateway_details(args):
 	"""return gateway and payment account of default payment gateway"""
 	if args.get("payment_gateway"):
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index bfd6d54..188ab0a 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -37,12 +37,12 @@
 	def setUp(self):
 		if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
 			frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
-			
+
 		for method in payment_method:
-			if not frappe.db.get_value("Payment Gateway Account", {"payment_gateway": method["payment_gateway"], 
+			if not frappe.db.get_value("Payment Gateway Account", {"payment_gateway": method["payment_gateway"],
 				"currency": method["currency"]}, "name"):
 				frappe.get_doc(method).insert(ignore_permissions=True)
-			
+
 	def test_payment_request_linkings(self):
 		so_inr = make_sales_order(currency="INR")
 		pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com")
@@ -100,3 +100,23 @@
 			self.assertEqual(expected_gle[gle.account][1], gle.debit)
 			self.assertEqual(expected_gle[gle.account][2], gle.credit)
 			self.assertEqual(expected_gle[gle.account][3], gle.against_voucher)
+
+	def test_multiple_payment_entries_against_sales_order(self):
+		# Make Sales Order, grand_total = 1000
+		so = make_sales_order()
+
+		# Payment Request amount = 200
+		pr1 = make_payment_request(dt="Sales Order", dn=so.name,
+			recipient_id="nabin@erpnext.com", return_doc=1)
+		pr1.grand_total = 200
+		pr1.submit()
+
+		# Make a 2nd Payment Request
+		pr2 = make_payment_request(dt="Sales Order", dn=so.name,
+			recipient_id="nabin@erpnext.com", return_doc=1)
+
+		self.assertEqual(pr2.grand_total, 800)
+
+		# Try to make Payment Request more than SO amount, should give validation
+		pr2.grand_total = 900
+		self.assertRaises(frappe.ValidationError, pr2.save)
\ No newline at end of file