fix: Multiple issues in purchase invoice submission (#34600)

* fix: Multiple issues in purchase invoice submission

* fix: Base grand total calculation

* chore: Calculate base grand total separately only in multi currency docs

* fix: Add gl entry for round off
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index b79af71..a617447 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -117,7 +117,7 @@
 		self.validate_expense_account()
 		self.set_against_expense_account()
 		self.validate_write_off_account()
-		self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
+		self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
 		self.create_remarks()
 		self.set_status()
 		self.validate_purchase_receipt_if_update_stock()
@@ -232,7 +232,7 @@
 		)
 
 		if (
-			cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
+			cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
 			and not self.is_return
 			and not self.is_internal_supplier
 		):
@@ -581,6 +581,7 @@
 
 		self.make_supplier_gl_entry(gl_entries)
 		self.make_item_gl_entries(gl_entries)
+		self.make_precision_loss_gl_entry(gl_entries)
 
 		if self.check_asset_cwip_enabled():
 			self.get_asset_gl_entry(gl_entries)
@@ -975,6 +976,28 @@
 							item.item_tax_amount, item.precision("item_tax_amount")
 						)
 
+	def make_precision_loss_gl_entry(self, gl_entries):
+		round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+			self.company, "Purchase Invoice", self.name
+		)
+
+		precision_loss = self.get("base_net_total") - flt(
+			self.get("net_total") * self.conversion_rate, self.precision("net_total")
+		)
+
+		if precision_loss:
+			gl_entries.append(
+				self.get_gl_dict(
+					{
+						"account": round_off_account,
+						"against": self.supplier,
+						"credit": precision_loss,
+						"cost_center": self.cost_center or round_off_cost_center,
+						"remarks": _("Net total calculation precision loss"),
+					}
+				)
+			)
+
 	def get_asset_gl_entry(self, gl_entries):
 		arbnb_account = self.get_company_default("asset_received_but_not_billed")
 		eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 5cda276..db61995 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -145,7 +145,7 @@
 
 		self.set_against_income_account()
 		self.validate_time_sheets_are_submitted()
-		self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
+		self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
 		if not self.is_return:
 			self.validate_serial_numbers()
 		else:
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 3705fcf..390af0d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -515,6 +515,8 @@
 				parent_dict.update({"customer": parent_dict.get("party_name")})
 
 			self.pricing_rules = []
+			basic_item_details_map = {}
+
 			for item in self.get("items"):
 				if item.get("item_code"):
 					args = parent_dict.copy()
@@ -533,7 +535,17 @@
 					if self.get("is_subcontracted"):
 						args["is_subcontracted"] = self.is_subcontracted
 
-					ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
+					basic_details = basic_item_details_map.get(item.item_code)
+					ret, basic_item_details = get_item_details(
+						args,
+						self,
+						for_validate=True,
+						overwrite_warehouse=False,
+						return_basic_details=True,
+						basic_details=basic_details,
+					)
+
+					basic_item_details_map.setdefault(item.item_code, basic_item_details)
 
 					for fieldname, value in ret.items():
 						if item.meta.get_field(fieldname) and value is not None:
@@ -1232,7 +1244,7 @@
 				)
 			)
 
-	def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
+	def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
 		from erpnext.controllers.status_updater import get_allowance_for
 
 		item_allowance = {}
@@ -1245,17 +1257,20 @@
 
 		total_overbilled_amt = 0.0
 
+		reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
+		reference_details = self.get_billing_reference_details(
+			reference_names, ref_dt + " Item", based_on
+		)
+
 		for item in self.get("items"):
 			if not item.get(item_ref_dn):
 				continue
 
-			ref_amt = flt(
-				frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on),
-				self.precision(based_on, item),
-			)
+			ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
+
 			if not ref_amt:
 				frappe.msgprint(
-					_("System will not check overbilling since amount for Item {0} in {1} is zero").format(
+					_("System will not check over billing since amount for Item {0} in {1} is zero").format(
 						item.item_code, ref_dt
 					),
 					title=_("Warning"),
@@ -1302,6 +1317,16 @@
 				alert=True,
 			)
 
+	def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
+		return frappe._dict(
+			frappe.get_all(
+				reference_doctype,
+				filters={"name": ("in", reference_names)},
+				fields=["name", based_on],
+				as_list=1,
+			)
+		)
+
 	def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
 		"""
 		Returns Sum of Amount of
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 489ec6e..2df39c8 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -35,7 +35,14 @@
 
 
 @frappe.whitelist()
-def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
+def get_item_details(
+	args,
+	doc=None,
+	for_validate=False,
+	overwrite_warehouse=True,
+	return_basic_details=False,
+	basic_details=None,
+):
 	"""
 	args = {
 	        "item_code": "",
@@ -73,7 +80,13 @@
 		if doc.get("doctype") == "Purchase Invoice":
 			args["bill_date"] = doc.get("bill_date")
 
-	out = get_basic_details(args, item, overwrite_warehouse)
+	if not basic_details:
+		out = get_basic_details(args, item, overwrite_warehouse)
+	else:
+		out = basic_details
+
+	basic_details = out.copy()
+
 	get_item_tax_template(args, item, out)
 	out["item_tax_rate"] = get_item_tax_map(
 		args.company,
@@ -141,7 +154,11 @@
 		out.amount = flt(args.qty) * flt(out.rate)
 
 	out = remove_standard_fields(out)
-	return out
+
+	if return_basic_details:
+		return out, basic_details
+	else:
+		return out
 
 
 def remove_standard_fields(details):
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 21a0a55..fc20545 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -58,11 +58,11 @@
 
 	def compare_values(self, ref_doc, fields, doc=None):
 		for reference_doctype, ref_dn_list in ref_doc.items():
+			prev_doc_detail_map = self.get_prev_doc_reference_details(
+				ref_dn_list, reference_doctype, fields
+			)
 			for reference_name in ref_dn_list:
-				prevdoc_values = frappe.db.get_value(
-					reference_doctype, reference_name, [d[0] for d in fields], as_dict=1
-				)
-
+				prevdoc_values = prev_doc_detail_map.get(reference_name)
 				if not prevdoc_values:
 					frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
 
@@ -70,6 +70,19 @@
 					if prevdoc_values[field] is not None and field not in self.exclude_fields:
 						self.validate_value(field, condition, prevdoc_values[field], doc)
 
+	def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):
+		prev_doc_detail_map = {}
+		details = frappe.get_all(
+			reference_doctype,
+			filters={"name": ("in", reference_names)},
+			fields=["name"] + [d[0] for d in fields],
+		)
+
+		for d in details:
+			prev_doc_detail_map.setdefault(d.name, d)
+
+		return prev_doc_detail_map
+
 	def validate_rate_with_reference_doc(self, ref_details):
 		if self.get("is_internal_supplier"):
 			return
@@ -77,23 +90,23 @@
 		buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
 
 		if self.doctype in buying_doctypes:
-			action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
-			settings_doc = "Buying Settings"
+			action, role_allowed_to_override = frappe.get_cached_value(
+				"Buying Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
+			)
 		else:
-			action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action")
-			settings_doc = "Selling Settings"
+			action, role_allowed_to_override = frappe.get_cached_value(
+				"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
+			)
 
 		for ref_dt, ref_dn_field, ref_link_field in ref_details:
+			reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
+			reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
 			for d in self.get("items"):
 				if d.get(ref_link_field):
-					ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
+					ref_rate = reference_details.get(d.get(ref_link_field))
 
 					if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
 						if action == "Stop":
-							role_allowed_to_override = frappe.db.get_single_value(
-								settings_doc, "role_to_override_stop_action"
-							)
-
 							if role_allowed_to_override not in frappe.get_roles():
 								frappe.throw(
 									_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
@@ -109,6 +122,16 @@
 								indicator="orange",
 							)
 
+	def get_reference_details(self, reference_names, reference_doctype):
+		return frappe._dict(
+			frappe.get_all(
+				reference_doctype,
+				filters={"name": ("in", reference_names)},
+				fields=["name", "rate"],
+				as_list=1,
+			)
+		)
+
 	def get_link_filters(self, for_doctype):
 		if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):
 			fieldname = self.prev_link_mapper[for_doctype]["fieldname"]