refactor: move scan api to stock utils; add item_info
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index abea5fc..0868a6b 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -21,9 +21,7 @@
 		//     batch_no: "LOT12", // present if batch was scanned
 		//     serial_no: "987XYZ", // present if serial no was scanned
 		// }
-		this.scan_api =
-			opts.scan_api ||
-			"erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number";
+		this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode";
 	}
 
 	process_scan() {
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 400be4c..99afe81 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -10,6 +10,7 @@
 
 from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
 from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups
+from erpnext.stock.utils import scan_barcode
 
 
 def search_by_term(search_term, warehouse, price_list):
@@ -152,38 +153,7 @@
 
 @frappe.whitelist()
 def search_for_serial_or_batch_or_barcode_number(search_value: str) -> Dict[str, Optional[str]]:
-
-	# search barcode no
-	barcode_data = frappe.db.get_value(
-		"Item Barcode",
-		{"barcode": search_value},
-		["barcode", "parent as item_code"],
-		as_dict=True,
-	)
-	if barcode_data:
-		return barcode_data
-
-	# search serial no
-	serial_no_data = frappe.db.get_value(
-		"Serial No",
-		search_value,
-		["name as serial_no", "item_code", "batch_no"],
-		as_dict=True,
-	)
-	if serial_no_data:
-		return serial_no_data
-
-	# search batch no
-	batch_no_data = frappe.db.get_value(
-		"Batch",
-		search_value,
-		["name as batch_no", "item as item_code"],
-		as_dict=True,
-	)
-	if batch_no_data:
-		return batch_no_data
-
-	return {}
+	return scan_barcode(search_value)
 
 
 def get_conditions(search_term):
diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py
new file mode 100644
index 0000000..9ee0c9f
--- /dev/null
+++ b/erpnext/stock/tests/test_utils.py
@@ -0,0 +1,31 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.utils import scan_barcode
+
+
+class TestStockUtilities(FrappeTestCase):
+	def test_barcode_scanning(self):
+		simple_item = make_item(properties={"barcodes": [{"barcode": "12399"}]})
+		self.assertEqual(scan_barcode("12399")["item_code"], simple_item.name)
+
+		batch_item = make_item(properties={"has_batch_no": 1, "create_new_batch": 1})
+		batch = frappe.get_doc(doctype="Batch", item=batch_item.name).insert()
+
+		batch_scan = scan_barcode(batch.name)
+		self.assertEqual(batch_scan["item_code"], batch_item.name)
+		self.assertEqual(batch_scan["batch_no"], batch.name)
+		self.assertEqual(batch_scan["has_batch_no"], 1)
+		self.assertEqual(batch_scan["has_serial_no"], 0)
+
+		serial_item = make_item(properties={"has_serial_no": 1})
+		serial = frappe.get_doc(
+			doctype="Serial No", item_code=serial_item.name, serial_no=frappe.generate_hash()
+		).insert()
+
+		serial_scan = scan_barcode(serial.name)
+		self.assertEqual(serial_scan["item_code"], serial_item.name)
+		self.assertEqual(serial_scan["serial_no"], serial.name)
+		self.assertEqual(serial_scan["has_batch_no"], 0)
+		self.assertEqual(serial_scan["has_serial_no"], 1)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 4f1891f..d40218e 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -3,6 +3,7 @@
 
 
 import json
+from typing import Dict, Optional
 
 import frappe
 from frappe import _
@@ -548,3 +549,51 @@
 		)
 
 	return bool(reposting_pending)
+
+
+@frappe.whitelist()
+def scan_barcode(search_value: str) -> Dict[str, Optional[str]]:
+
+	# search barcode no
+	barcode_data = frappe.db.get_value(
+		"Item Barcode",
+		{"barcode": search_value},
+		["barcode", "parent as item_code"],
+		as_dict=True,
+	)
+	if barcode_data:
+		return _update_item_info(barcode_data)
+
+	# search serial no
+	serial_no_data = frappe.db.get_value(
+		"Serial No",
+		search_value,
+		["name as serial_no", "item_code", "batch_no"],
+		as_dict=True,
+	)
+	if serial_no_data:
+		return _update_item_info(serial_no_data)
+
+	# search batch no
+	batch_no_data = frappe.db.get_value(
+		"Batch",
+		search_value,
+		["name as batch_no", "item as item_code"],
+		as_dict=True,
+	)
+	if batch_no_data:
+		return _update_item_info(batch_no_data)
+
+	return {}
+
+
+def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]:
+	if item_code := scan_result.get("item_code"):
+		if item_info := frappe.get_cached_value(
+			"Item",
+			item_code,
+			["has_batch_no", "has_serial_no"],
+			as_dict=True,
+		):
+			scan_result.update(item_info)
+	return scan_result