Merge pull request #31019 from frappe/mergify/bp/develop/pr-30978

fix: Multiple fixes in GST reporting (backport #30978)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 233b476..f28de3b 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -307,14 +307,15 @@
 				if self.is_internal_transfer():
 					if rate != d.rate:
 						d.rate = rate
-						d.discount_percentage = 0
-						d.discount_amount = 0
 						frappe.msgprint(
 							_(
 								"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
 							).format(d.idx),
 							alert=1,
 						)
+					d.discount_percentage = 0.0
+					d.discount_amount = 0.0
+					d.margin_rate_or_amount = 0.0
 
 	def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
 		supplied_items_cost = 0.0
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 19fedb3..70e2056 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -447,15 +447,16 @@
 						rate = flt(d.incoming_rate * d.conversion_factor, d.precision("rate"))
 						if d.rate != rate:
 							d.rate = rate
+							frappe.msgprint(
+								_(
+									"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
+								).format(d.idx),
+								alert=1,
+							)
 
-						d.discount_percentage = 0
-						d.discount_amount = 0
-						frappe.msgprint(
-							_(
-								"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
-							).format(d.idx),
-							alert=1,
-						)
+						d.discount_percentage = 0.0
+						d.discount_amount = 0.0
+						d.margin_rate_or_amount = 0.0
 
 			elif self.get("return_against"):
 				# Get incoming rate of return entry from reference document
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 2535180..8614fcb 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -264,6 +264,7 @@
 			regenerate_repayment_schedule(self.against_loan, cancel)
 
 	def allocate_amounts(self, repayment_details):
+		precision = cint(frappe.db.get_default("currency_precision")) or 2
 		self.set("repayment_details", [])
 		self.principal_amount_paid = 0
 		self.total_penalty_paid = 0
@@ -278,9 +279,9 @@
 
 		if interest_paid > 0:
 			if self.penalty_amount and interest_paid > self.penalty_amount:
-				self.total_penalty_paid = self.penalty_amount
+				self.total_penalty_paid = flt(self.penalty_amount, precision)
 			elif self.penalty_amount:
-				self.total_penalty_paid = interest_paid
+				self.total_penalty_paid = flt(interest_paid, precision)
 
 			interest_paid -= self.total_penalty_paid
 
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 06229bb..4d9a7e0 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -367,7 +367,8 @@
 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
 erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
 erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
+erpnext.patches.v13_0.requeue_recoverable_reposts
 erpnext.patches.v14_0.discount_accounting_separation
 erpnext.patches.v14_0.delete_employee_transfer_property_doctype
 erpnext.patches.v13_0.create_accounting_dimensions_in_orders
-erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
\ No newline at end of file
+erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
diff --git a/erpnext/patches/v13_0/requeue_recoverable_reposts.py b/erpnext/patches/v13_0/requeue_recoverable_reposts.py
new file mode 100644
index 0000000..f37c21c
--- /dev/null
+++ b/erpnext/patches/v13_0/requeue_recoverable_reposts.py
@@ -0,0 +1,21 @@
+import frappe
+
+
+def execute():
+	recoverable = ("QueryDeadlockError", "QueryTimeoutError", "JobTimeoutException")
+
+	failed_reposts = frappe.get_all(
+		"Repost Item Valuation",
+		fields=["name", "error_log"],
+		filters={
+			"status": "Failed",
+			"docstatus": 1,
+			"modified": (">", "2022-04-20"),
+			"error_log": ("is", "set"),
+		},
+	)
+	for riv in failed_reposts:
+		for exc in recoverable:
+			if exc in riv.error_log:
+				frappe.db.set_value("Repost Item Valuation", riv.name, "status", "Queued")
+				break
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 36ca2b2..2458756 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -367,7 +367,14 @@
 		customer.credit_limits[-1].db_insert()
 
 
-def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
+def create_internal_customer(
+	customer_name=None, represents_company=None, allowed_to_interact_with=None
+):
+	if not customer_name:
+		customer_name = represents_company
+	if not allowed_to_interact_with:
+		allowed_to_interact_with = represents_company
+
 	if not frappe.db.exists("Customer", customer_name):
 		customer = frappe.get_doc(
 			{
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0738bfb..6b8fb98 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -570,15 +570,12 @@
 			customer=customer_name,
 			cost_center="Main - TCP1",
 			expense_account="Cost of Goods Sold - TCP1",
-			do_not_submit=True,
 			qty=5,
 			rate=500,
 			warehouse="Stores - TCP1",
 			target_warehouse=target_warehouse,
 		)
 
-		dn.submit()
-
 		# qty after delivery
 		actual_qty_at_source = get_qty_after_transaction(warehouse="Stores - TCP1")
 		self.assertEqual(actual_qty_at_source, 475)
@@ -1000,6 +997,51 @@
 		self.assertEqual(dn2.items[0].returned_qty, 0)
 		self.assertEqual(dn2.per_billed, 100)
 
+	def test_internal_transfer_with_valuation_only(self):
+		from erpnext.selling.doctype.customer.test_customer import create_internal_customer
+
+		item = make_item().name
+		warehouse = "_Test Warehouse - _TC"
+		target = "Stores - _TC"
+		company = "_Test Company"
+		customer = create_internal_customer(represents_company=company)
+		rate = 42
+
+		frappe.get_doc(
+			{
+				"item_code": item,
+				"price_list": "Standard Selling",
+				"price_list_rate": 1000,
+				"doctype": "Item Price",
+			}
+		).insert()
+
+		make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
+		dn = create_delivery_note(
+			item_code=item,
+			company=company,
+			customer=customer,
+			qty=5,
+			rate=500,
+			warehouse=warehouse,
+			target_warehouse=target,
+			do_not_save=True,
+			do_not_submit=True,
+		)
+
+		self.assertEqual(dn.items[0].rate, 500)  # haven't saved yet
+		dn.save()
+
+		# rate should reset to incoming rate
+		self.assertEqual(dn.items[0].rate, rate)
+
+		# rate should reset again if discounts are fiddled with
+		dn.items[0].margin_type = "Amount"
+		dn.items[0].margin_rate_or_amount = 50
+		dn.save()
+
+		self.assertEqual(dn.items[0].rate, rate)
+
 
 def create_delivery_note(**args):
 	dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 236b944..328afc8 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -3,9 +3,11 @@
 
 import frappe
 from frappe import _
+from frappe.exceptions import QueryDeadlockError, QueryTimeoutError
 from frappe.model.document import Document
 from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
 from frappe.utils.user import get_users_with_role
+from rq.timeouts import JobTimeoutException
 
 import erpnext
 from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
@@ -15,6 +17,8 @@
 	repost_future_sle,
 )
 
+RecoverableErrors = (JobTimeoutException, QueryDeadlockError, QueryTimeoutError)
+
 
 class RepostItemValuation(Document):
 	def validate(self):
@@ -132,7 +136,7 @@
 
 		doc.set_status("Completed")
 
-	except Exception:
+	except Exception as e:
 		frappe.db.rollback()
 		traceback = frappe.get_traceback()
 		doc.log_error("Unable to repost item valuation")
@@ -142,9 +146,9 @@
 			message += "<br>" + "Traceback: <br>" + traceback
 		frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
 
-		notify_error_to_stock_managers(doc, message)
-		doc.set_status("Failed")
-		raise
+		if not isinstance(e, RecoverableErrors):
+			notify_error_to_stock_managers(doc, message)
+			doc.set_status("Failed")
 	finally:
 		if not frappe.flags.in_test:
 			frappe.db.commit()