Merge pull request #41020 from ruthra-kumar/fix_incorrect_exc_gain_loss_for_pe_against_journals

fix: incorrect exc gain/loss for PE against JE for payable accounts
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index c4a99c0..41ed66c 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -92,7 +92,7 @@
 			self.status = "Draft"
 		self.validate_reference_document()
 		self.validate_payment_request_amount()
-		self.validate_currency()
+		# self.validate_currency()
 		self.validate_subscription_details()
 
 	def validate_reference_document(self):
@@ -335,21 +335,17 @@
 			}
 		)
 
+		if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
+			amount = payment_entry.base_paid_amount
+		else:
+			amount = self.grand_total
+
+		payment_entry.received_amount = amount
+		payment_entry.get("references")[0].allocated_amount = amount
+
 		for dimension in get_accounting_dimensions():
 			payment_entry.update({dimension: self.get(dimension)})
 
-		if payment_entry.difference_amount:
-			company_details = get_company_defaults(ref_doc.company)
-
-			payment_entry.append(
-				"deductions",
-				{
-					"account": company_details.exchange_gain_loss_account,
-					"cost_center": company_details.cost_center,
-					"amount": payment_entry.difference_amount,
-				},
-			)
-
 		if submit:
 			payment_entry.insert(ignore_permissions=True)
 			payment_entry.submit()
@@ -479,6 +475,12 @@
 		pr = frappe.get_doc("Payment Request", draft_payment_request)
 	else:
 		pr = frappe.new_doc("Payment Request")
+
+		if not args.get("payment_request_type"):
+			args["payment_request_type"] = (
+				"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
+			)
+
 		pr.update(
 			{
 				"payment_gateway_account": gateway_account.get("name"),
@@ -538,9 +540,9 @@
 	elif dt in ["Sales Invoice", "Purchase Invoice"]:
 		if not ref_doc.get("is_pos"):
 			if ref_doc.party_account_currency == ref_doc.currency:
-				grand_total = flt(ref_doc.outstanding_amount)
+				grand_total = flt(ref_doc.grand_total)
 			else:
-				grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
+				grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
 		elif dt == "Sales Invoice":
 			for pay in ref_doc.payments:
 				if pay.type == "Phone" and pay.account == payment_account:
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 70de886..9320608 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -86,6 +86,8 @@
 		pr = make_payment_request(
 			dt="Purchase Invoice",
 			dn=si_usd.name,
+			party_type="Supplier",
+			party="_Test Supplier USD",
 			recipient_id="user@example.com",
 			mute_email=1,
 			payment_gateway_account="_Test Gateway - USD",
@@ -98,6 +100,51 @@
 
 		self.assertEqual(pr.status, "Paid")
 
+	def test_multiple_payment_entry_against_purchase_invoice(self):
+		purchase_invoice = make_purchase_invoice(
+			customer="_Test Supplier USD",
+			debit_to="_Test Payable USD - _TC",
+			currency="USD",
+			conversion_rate=50,
+		)
+
+		pr = make_payment_request(
+			dt="Purchase Invoice",
+			party_type="Supplier",
+			party="_Test Supplier USD",
+			dn=purchase_invoice.name,
+			recipient_id="user@example.com",
+			mute_email=1,
+			payment_gateway_account="_Test Gateway - USD",
+			return_doc=1,
+		)
+
+		pr.grand_total = pr.grand_total / 2
+
+		pr.submit()
+		pr.create_payment_entry()
+
+		purchase_invoice.load_from_db()
+		self.assertEqual(purchase_invoice.status, "Partly Paid")
+
+		pr = make_payment_request(
+			dt="Purchase Invoice",
+			party_type="Supplier",
+			party="_Test Supplier USD",
+			dn=purchase_invoice.name,
+			recipient_id="user@example.com",
+			mute_email=1,
+			payment_gateway_account="_Test Gateway - USD",
+			return_doc=1,
+		)
+
+		pr.save()
+		pr.submit()
+		pr.create_payment_entry()
+
+		purchase_invoice.load_from_db()
+		self.assertEqual(purchase_invoice.status, "Paid")
+
 	def test_payment_entry(self):
 		frappe.db.set_value(
 			"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a31f011..c2a028b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -534,6 +534,7 @@
 	"Supplier Quotation Item",
 	"Payment Reconciliation",
 	"Payment Reconciliation Allocation",
+	"Payment Request",
 ]
 
 get_matching_queries = (
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7c2c439..263501f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -357,6 +357,7 @@
 erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
 erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
 erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
+erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
 # below migration patch should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
 erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
diff --git a/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py b/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py
new file mode 100644
index 0000000..fc50b60
--- /dev/null
+++ b/erpnext/patches/v15_0/create_accounting_dimensions_in_payment_request.py
@@ -0,0 +1,7 @@
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+	create_accounting_dimensions_for_doctype,
+)
+
+
+def execute():
+	create_accounting_dimensions_for_doctype(doctype="Payment Request")
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 17fc2b7..4b23af1 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -384,7 +384,6 @@
 				)
 
 		target.flags.ignore_permissions = ignore_permissions
-		target.delivery_date = nowdate()
 		target.run_method("set_missing_values")
 		target.run_method("calculate_taxes_and_totals")
 
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 57c14db..01dfe80 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -134,6 +134,7 @@
 
 		sales_order.naming_series = "_T-Quotation-"
 		sales_order.transaction_date = nowdate()
+		sales_order.delivery_date = nowdate()
 		sales_order.insert()
 
 	def test_make_sales_order_with_terms(self):
@@ -164,6 +165,7 @@
 
 		sales_order.naming_series = "_T-Quotation-"
 		sales_order.transaction_date = nowdate()
+		sales_order.delivery_date = nowdate()
 		sales_order.insert()
 
 		# Remove any unknown taxes if applied
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 238de34..c0ec359 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -9,7 +9,17 @@
 from frappe import _
 from frappe.model.mapper import get_mapped_doc
 from frappe.query_builder.functions import Sum
-from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
+from frappe.utils import (
+	cint,
+	comma_or,
+	cstr,
+	flt,
+	format_time,
+	formatdate,
+	get_link_to_form,
+	getdate,
+	nowdate,
+)
 
 import erpnext
 from erpnext.accounts.general_ledger import process_gl_map
@@ -645,8 +655,8 @@
 						)
 					)
 
-				work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order)
-				job_card_link = frappe.utils.get_link_to_form("Job Card", job_card)
+				work_order_link = get_link_to_form("Work Order", self.work_order)
+				job_card_link = get_link_to_form("Job Card", job_card)
 				frappe.throw(
 					_(
 						"Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}."
@@ -1355,9 +1365,24 @@
 
 		return finished_item_row
 
+	def validate_serial_batch_bundle_type(self, serial_and_batch_bundle):
+		if (
+			frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "type_of_transaction")
+			!= "Outward"
+		):
+			frappe.throw(
+				_(
+					"The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}"
+				).format(get_link_to_form("Serial and Batch Bundle", serial_and_batch_bundle)),
+				title=_("Invalid Serial and Batch Bundle"),
+			)
+
 	def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
 		for d in self.get("items"):
 			if cstr(d.s_warehouse):
+				if d.serial_and_batch_bundle and self.docstatus == 1:
+					self.validate_serial_batch_bundle_type(d.serial_and_batch_bundle)
+
 				sle = self.get_sl_entries(
 					d,
 					{
@@ -1374,6 +1399,21 @@
 				):
 					sle.dependant_sle_voucher_detail_no = finished_item_row.name
 
+				if sle.serial_and_batch_bundle and self.docstatus == 2:
+					bundle_id = frappe.get_cached_value(
+						"Serial and Batch Bundle",
+						{
+							"voucher_detail_no": d.name,
+							"voucher_no": self.name,
+							"is_cancelled": 0,
+							"type_of_transaction": "Outward",
+						},
+						"name",
+					)
+
+					if bundle_id:
+						sle.serial_and_batch_bundle = bundle_id
+
 				sl_entries.append(sle)
 
 	def make_serial_and_batch_bundle_for_transfer(self):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 34b4ac2..1a7b01c 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1754,6 +1754,41 @@
 			self.assertTrue(frappe.db.exists("Serial No", serial_no))
 			self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
 
+	def test_serial_batch_bundle_type_of_transaction(self):
+		item = make_item(
+			"Test Use Serial and Batch Item SN Item",
+			{
+				"has_batch_no": 1,
+				"is_stock_item": 1,
+				"create_new_batch": 1,
+				"batch_naming_series": "Test-SBBTYT-NNS.#####",
+			},
+		).name
+
+		se = make_stock_entry(
+			item_code=item,
+			qty=2,
+			target="_Test Warehouse - _TC",
+			use_serial_batch_fields=1,
+		)
+
+		batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
+
+		se = make_stock_entry(
+			item_code=item,
+			qty=2,
+			source="_Test Warehouse - _TC",
+			target="Stores - _TC",
+			use_serial_batch_fields=0,
+			batch_no=batch_no,
+			do_not_submit=True,
+		)
+
+		se.reload()
+		sbb = se.items[0].serial_and_batch_bundle
+		frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward")
+		self.assertRaises(frappe.ValidationError, se.submit)
+
 
 def make_serialized_item(**args):
 	args = frappe._dict(args)