Merge branch 'develop' into bom-update-tool
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 150f68b..c71ea36 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -204,7 +204,9 @@
 		if not self.account_currency:
 			self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
 
-		elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
+		gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
+
+		if gl_currency and self.account_currency != gl_currency:
 			if frappe.db.get_value("GL Entry", {"account": self.name}):
 				frappe.throw(_("Currency can not be changed after making entries using some other currency"))
 
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index efc063d..f9c9173 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -241,6 +241,28 @@
 		for doc in to_delete:
 			frappe.delete_doc("Account", doc)
 
+	def test_validate_account_currency(self):
+		from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+		if not frappe.db.get_value("Account", "Test Currency Account - _TC"):
+			acc = frappe.new_doc("Account")
+			acc.account_name = "Test Currency Account"
+			acc.parent_account = "Tax Assets - _TC"
+			acc.company = "_Test Company"
+			acc.insert()
+		else:
+			acc = frappe.get_doc("Account", "Test Currency Account - _TC")
+
+		self.assertEqual(acc.account_currency, "INR")
+
+		# Make a JV against this account
+		make_journal_entry(
+			"Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
+		)
+
+		acc.account_currency = "USD"
+		self.assertRaises(frappe.ValidationError, acc.save)
+
 
 def _make_test_records(verbose=None):
 	from frappe.test_runner import make_test_objects
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 57bc0a7..e6a46d0 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -249,8 +249,9 @@
 
 	def validate_warehouse(self, for_validate=True):
 		if self.update_stock and for_validate:
+			stock_items = self.get_stock_items()
 			for d in self.get("items"):
-				if not d.warehouse:
+				if not d.warehouse and d.item_code in stock_items:
 					frappe.throw(
 						_(
 							"Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}"
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index f681b34..e759ad0 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -68,7 +68,7 @@
 	def test_item_exists(self):
 		asset = create_asset(item_code="MacBook", do_not_save=1)
 
-		self.assertRaises(frappe.DoesNotExistError, asset.save)
+		self.assertRaises(frappe.ValidationError, asset.save)
 
 	def test_validate_item(self):
 		asset = create_asset(item_code="MacBook Pro", do_not_save=1)
diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py
index f97cd5e..e904af0 100644
--- a/erpnext/buying/utils.py
+++ b/erpnext/buying/utils.py
@@ -3,19 +3,19 @@
 
 
 import json
+from typing import Dict
 
 import frappe
 from frappe import _
-from frappe.utils import cint, cstr, flt
+from frappe.utils import cint, cstr, flt, getdate
 
 from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_end_of_life
 
 
-def update_last_purchase_rate(doc, is_submit):
+def update_last_purchase_rate(doc, is_submit) -> None:
 	"""updates last_purchase_rate in item table for each item"""
-	import frappe.utils
 
-	this_purchase_date = frappe.utils.getdate(doc.get("posting_date") or doc.get("transaction_date"))
+	this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
 
 	for d in doc.get("items"):
 		# get last purchase details
@@ -41,7 +41,7 @@
 		frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate))
 
 
-def validate_for_items(doc):
+def validate_for_items(doc) -> None:
 	items = []
 	for d in doc.get("items"):
 		if not d.qty:
@@ -49,40 +49,11 @@
 				continue
 			frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
 
-		# update with latest quantities
-		bin = frappe.db.sql(
-			"""select projected_qty from `tabBin` where
-			item_code = %s and warehouse = %s""",
-			(d.item_code, d.warehouse),
-			as_dict=1,
-		)
-
-		f_lst = {
-			"projected_qty": bin and flt(bin[0]["projected_qty"]) or 0,
-			"ordered_qty": 0,
-			"received_qty": 0,
-		}
-		if d.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"):
-			f_lst.pop("received_qty")
-		for x in f_lst:
-			if d.meta.get_field(x):
-				d.set(x, f_lst[x])
-
-		item = frappe.db.sql(
-			"""select is_stock_item,
-			is_sub_contracted_item, end_of_life, disabled from `tabItem` where name=%s""",
-			d.item_code,
-			as_dict=1,
-		)[0]
-
+		set_stock_levels(row=d)  # update with latest quantities
+		item = validate_item_and_get_basic_data(row=d)
+		validate_stock_item_warehouse(row=d, item=item)
 		validate_end_of_life(d.item_code, item.end_of_life, item.disabled)
 
-		# validate stock item
-		if item.is_stock_item == 1 and d.qty and not d.warehouse and not d.get("delivered_by_supplier"):
-			frappe.throw(
-				_("Warehouse is mandatory for stock Item {0} in row {1}").format(d.item_code, d.idx)
-			)
-
 		items.append(cstr(d.item_code))
 
 	if (
@@ -93,7 +64,57 @@
 		frappe.throw(_("Same item cannot be entered multiple times."))
 
 
-def check_on_hold_or_closed_status(doctype, docname):
+def set_stock_levels(row) -> None:
+	projected_qty = frappe.db.get_value(
+		"Bin",
+		{
+			"item_code": row.item_code,
+			"warehouse": row.warehouse,
+		},
+		"projected_qty",
+	)
+
+	qty_data = {
+		"projected_qty": flt(projected_qty),
+		"ordered_qty": 0,
+		"received_qty": 0,
+	}
+	if row.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"):
+		qty_data.pop("received_qty")
+
+	for field in qty_data:
+		if row.meta.get_field(field):
+			row.set(field, qty_data[field])
+
+
+def validate_item_and_get_basic_data(row) -> Dict:
+	item = frappe.db.get_values(
+		"Item",
+		filters={"name": row.item_code},
+		fieldname=["is_stock_item", "is_sub_contracted_item", "end_of_life", "disabled"],
+		as_dict=1,
+	)
+	if not item:
+		frappe.throw(_("Row #{0}: Item {1} does not exist").format(row.idx, frappe.bold(row.item_code)))
+
+	return item[0]
+
+
+def validate_stock_item_warehouse(row, item) -> None:
+	if (
+		item.is_stock_item == 1
+		and row.qty
+		and not row.warehouse
+		and not row.get("delivered_by_supplier")
+	):
+		frappe.throw(
+			_("Row #{1}: Warehouse is mandatory for stock Item {0}").format(
+				frappe.bold(row.item_code), row.idx
+			)
+		)
+
+
+def check_on_hold_or_closed_status(doctype, docname) -> None:
 	status = frappe.db.get_value(doctype, docname, "status")
 
 	if status in ("Closed", "On Hold"):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 72ac1b3..3a20d3f 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1267,17 +1267,9 @@
 		stock_items = []
 		item_codes = list(set(item.item_code for item in self.get("items")))
 		if item_codes:
-			stock_items = [
-				r[0]
-				for r in frappe.db.sql(
-					"""
-				select name from `tabItem`
-				where name in (%s) and is_stock_item=1
-			"""
-					% (", ".join(["%s"] * len(item_codes)),),
-					item_codes,
-				)
-			]
+			stock_items = frappe.db.get_values(
+				"Item", {"name": ["in", item_codes], "is_stock_item": 1}, pluck="name", cache=True
+			)
 
 		return stock_items