import frappe
from frappe.utils import today

from erpnext.stock.get_item_details import get_item_details, get_price_list_currency_and_exchange_rate
from erpnext.tests.utils import ERPNextTestSuite


class TestGetItemDetail(ERPNextTestSuite):
	def test_get_item_detail_purchase_order(self):
		args = frappe._dict(
			{
				"item_code": "_Test Item",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"conversion_rate": 1.0,
				"price_list_currency": "USD",
				"plc_conversion_rate": 1.0,
				"doctype": "Purchase Order",
				"name": None,
				"supplier": "_Test Supplier",
				"transaction_date": None,
				"price_list": "_Test Buying Price List",
				"is_subcontracted": 0,
				"ignore_pricing_rule": 1,
				"qty": 1,
			}
		)
		details = get_item_details(args)
		self.assertEqual(details.get("price_list_rate"), 100)

	def test_bin_details_for_selling_doctypes(self):
		from erpnext.stock.doctype.item.test_item import make_item
		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

		item_code = make_item(properties={"is_stock_item": 1}).name

		make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse - _TC", qty=100, rate=100)
		make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse 1 - _TC", qty=50, rate=100)

		args = frappe._dict(
			{
				"item_code": item_code,
				"warehouse": "_Test Warehouse - _TC",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"currency": "INR",
				"conversion_rate": 1.0,
				"price_list": "_Test Price List",
				"price_list_currency": "INR",
				"plc_conversion_rate": 1.0,
				"transaction_date": None,
				"name": None,
				"ignore_pricing_rule": 1,
				"qty": 1,
			}
		)

		for doctype in ("Sales Order", "Quotation", "Sales Invoice", "Delivery Note", "Purchase Order"):
			with self.subTest(doctype=doctype):
				details = get_item_details(args.copy().update({"doctype": doctype}))

				self.assertEqual(details.get("actual_qty"), 100)
				self.assertEqual(details.get("company_total_stock"), 150)

	# making this test in get_item_details test file as feat/fix is present in that method
	def test_fetch_price_from_list_rate_on_doc_save(self):
		# create item
		item = frappe.get_doc(
			{
				"doctype": "Item",
				"item_code": "Test Item with Batch",
				"item_name": "Test Item with Batch",
				"item_group": "All Item Groups",
				"is_stock_item": 1,
				"has_batch_no": 1,
			}
		).insert()

		# create batch
		frappe.get_doc(
			{
				"doctype": "Batch",
				"batch_id": "BATCH01",
				"item": item.name,
			}
		).insert()

		# create item price
		frappe.get_doc(
			{
				"doctype": "Item Price",
				"price_list": "Standard Selling",
				"item_code": item.item_code,
				"price_list_rate": 50,
				"batch_no": "BATCH01",
			}
		).insert()

		# create purchase receipt to have some stock for delivery
		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

		make_purchase_receipt(
			item_code=item.item_code,
			warehouse="_Test Warehouse - _TC",
			qty=100,
			rate=100,
			batch_no="BATCH01",
		)

		# creating sales order just to create delivery note from it
		from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

		so = make_sales_order(item_code=item.item_code, qty=2, rate=75)

		from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note

		dn = make_delivery_note(so.name)

		# Test 1 : On creation of DN, item's batch won't be fetched and rate will remaing the same as in SO
		self.assertIsNone(dn.items[0].batch_no)
		self.assertEqual(dn.items[0].rate, 75)

		# Test 2 : On saving the DN, item's batch will be fetched and rate will be updated from Item Price
		dn.save()
		self.assertEqual(dn.items[0].batch_no, "BATCH01")
		self.assertEqual(dn.items[0].rate, 50)

	def test_maintain_same_rate_keeps_source_rate_on_refetch(self):
		"""#57436: with "maintain same rate" on, re-fetching a PR row mapped from a
		PO must keep the PO rate instead of pulling a newer, higher Item Price.

		The rate is validated on save, so it can never persist changed; assert the
		fetched rate directly to prove the newer Item Price is never picked up.
		"""
		from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
		from erpnext.stock.doctype.item.test_item import make_item

		def set_maintain_same_rate(value):
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate", value)
			frappe.clear_cache(doctype="Buying Settings")

		set_maintain_same_rate(1)

		item_code = make_item(properties={"is_stock_item": 1}).name
		po = create_purchase_order(item_code=item_code, qty=1, rate=100)

		# The PO may auto-insert an Item Price at 100; bump it to the newer, higher rate.
		item_price = frappe.db.get_value(
			"Item Price", {"item_code": item_code, "price_list": "Standard Buying"}
		)
		if item_price:
			frappe.db.set_value("Item Price", item_price, "price_list_rate", 120)
		else:
			frappe.get_doc(
				{
					"doctype": "Item Price",
					"price_list": "Standard Buying",
					"item_code": item_code,
					"price_list_rate": 120,
				}
			).insert()

		pr = make_purchase_receipt(po.name)
		pr.insert()

		def fetch_price_list_rate():
			ctx = frappe._dict(
				{
					"item_code": item_code,
					"doctype": "Purchase Receipt",
					"name": pr.name,
					"company": pr.company,
					"supplier": pr.supplier,
					"currency": pr.currency,
					"conversion_rate": 1.0,
					"price_list": "Standard Buying",
					"price_list_currency": pr.currency,
					"plc_conversion_rate": 1.0,
					"warehouse": pr.items[0].warehouse,
					"uom": pr.items[0].uom,
					"stock_uom": pr.items[0].stock_uom,
					"qty": pr.items[0].qty,
					"child_doctype": pr.items[0].doctype,
					"child_docname": pr.items[0].name,
					"is_return": 0,
					"is_internal_supplier": 0,
					"ignore_pricing_rule": 1,
				}
			)
			return get_item_details(ctx, pr).get("price_list_rate")

		# Rate stays at the PO rate; the newer Item Price (120) is not fetched.
		self.assertEqual(fetch_price_list_rate(), 100)

		# Control: without the setting the newer Item Price would be fetched.
		set_maintain_same_rate(0)
		self.assertEqual(fetch_price_list_rate(), 120)

	def test_maintain_same_rate_survives_refetch_with_discount(self):
		"""A mapped Purchase Receipt row that carries a source discount (rate != price
		list rate) must keep its rate when the row is re-fetched, so maintain-same-rate
		lets the document save. process_item_selection runs the same recompute the desk
		mirrors, so it covers the "discount discarded on refresh" concern end to end.
		"""
		from frappe.utils import flt

		from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order

		item, price_list = "_Test Item", "_Test Buying Price List"
		original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
		original_action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", "Stop")
		frappe.clear_cache(doctype="Buying Settings")

		try:
			for label, adjustment in (
				("percentage", {"discount_percentage": 10}),
				("amount", {"discount_amount": 10}),
			):
				with self.subTest(discount=label):
					# a controlled discounted PO: list rate 100, effective rate 90
					frappe.flags.dont_fetch_price_list_rate = True
					po = create_purchase_order(item_code=item, qty=1, do_not_save=True)
					po.buying_price_list = price_list
					po.items[0].price_list_rate = 100
					po.items[0].update(adjustment)
					po.items[0].rate = 90
					po.insert()
					po.submit()
					frappe.flags.dont_fetch_price_list_rate = False

					# a newer Item Price must not leak onto the mapped row on re-fetch
					item_price = frappe.db.get_value(
						"Item Price", {"item_code": item, "price_list": price_list}
					)
					if item_price:
						frappe.db.set_value("Item Price", item_price, "price_list_rate", 250)

					pr = make_purchase_receipt(po.name)
					pr.insert()
					pr.process_item_selection(item_idx=pr.items[0].idx)

					self.assertEqual(flt(pr.items[0].rate), 90)
					pr.save()  # must not raise the maintain-same-rate check
		finally:
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", original_action)
			frappe.clear_cache(doctype="Buying Settings")
			frappe.flags.dont_fetch_price_list_rate = False

	def test_apply_price_list_keeps_source_rate_when_maintain_same_rate(self):
		"""#57436: the bulk apply_price_list path (price list / party / conversion rate
		change) must also keep the source rate on mapped rows, not just re-fetch of a
		single row. Here a PR row carries its PO rate (175) while the current price list
		rate is 100; the bulk apply must keep 175.
		"""
		from frappe.utils import flt, nowdate

		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
		from erpnext.stock.get_item_details import apply_price_list

		item_code = "_Test Item"
		price_list = "_Test Buying Price List"

		original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
		frappe.clear_cache(doctype="Buying Settings")

		try:
			po = create_purchase_order(item_code=item_code, rate=175, qty=1)

			row_name = "pr-row-1"
			pr_doc = {
				"doctype": "Purchase Receipt",
				"items": [
					{
						"name": row_name,
						"item_code": item_code,
						"purchase_order_item": po.items[0].name,
						"price_list_rate": 175,
						"rate": 175,
					}
				],
			}
			ctx = frappe._dict(
				doctype="Purchase Receipt",
				supplier=po.supplier,
				company=po.company,
				currency=po.currency,
				conversion_rate=1.0,
				price_list=price_list,
				plc_conversion_rate=1.0,
				transaction_date=nowdate(),
				items=[
					frappe._dict(
						doctype="Purchase Receipt Item",
						parenttype="Purchase Receipt",
						item_code=item_code,
						child_docname=row_name,
						qty=1,
						uom=po.items[0].uom,
						stock_uom=po.items[0].stock_uom,
						conversion_factor=1.0,
					)
				],
			)

			result = apply_price_list(ctx, doc=pr_doc)
			self.assertEqual(flt(result["children"][0].get("price_list_rate")), 175)
		finally:
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
			frappe.clear_cache(doctype="Buying Settings")

	def test_maintain_same_rate_keeps_source_discount_on_refetch(self):
		"""A mapped source row with a discount has rate != price_list_rate. Re-fetch must
		return the source's rate and discount, not just the pre-discount price, or the
		recomputed rate diverges from the reference and fails maintain-same-rate on save.
		"""
		from frappe.utils import flt

		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order

		item_code = "_Test Item"
		price_list = "_Test Buying Price List"

		original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
		frappe.clear_cache(doctype="Buying Settings")

		try:
			# source PO carries the discount: list rate 100, 10% off, effective rate 90
			frappe.flags.dont_fetch_price_list_rate = True
			po = create_purchase_order(item_code=item_code, qty=1, do_not_save=True)
			po.buying_price_list = price_list
			po.items[0].price_list_rate = 100
			po.items[0].discount_percentage = 10
			po.items[0].rate = 90
			po.insert()
			po.submit()
			frappe.flags.dont_fetch_price_list_rate = False

			row_name = "pr-row-1"
			pr_doc = {
				"doctype": "Purchase Receipt",
				"items": [
					{"name": row_name, "item_code": item_code, "purchase_order_item": po.items[0].name}
				],
			}
			ctx = frappe._dict(
				item_code=item_code,
				doctype="Purchase Receipt",
				company=po.company,
				supplier=po.supplier,
				currency=po.currency,
				conversion_rate=1.0,
				price_list=price_list,
				price_list_currency=po.currency,
				plc_conversion_rate=1.0,
				warehouse="_Test Warehouse - _TC",
				uom=po.items[0].uom,
				stock_uom=po.items[0].stock_uom,
				qty=1,
				child_docname=row_name,
				is_return=0,
				is_internal_supplier=0,
				ignore_pricing_rule=1,
			)

			out = get_item_details(ctx, pr_doc)
			self.assertEqual(flt(out.get("price_list_rate")), 100)
			self.assertEqual(flt(out.get("rate")), 90)
			self.assertEqual(flt(out.get("discount_percentage")), 10)
		finally:
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
			frappe.clear_cache(doctype="Buying Settings")
			frappe.flags.dont_fetch_price_list_rate = False

	def test_refetch_restores_source_rate_after_target_edit(self):
		"""Editing a mapped row's rate then re-fetching must restore the persisted source
		rate (read from the linked row), not lock in the edit, so the document still saves.
		"""
		from frappe.utils import flt

		from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order

		item = "_Test Item"
		original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
		original_action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", "Stop")
		frappe.clear_cache(doctype="Buying Settings")

		try:
			po = create_purchase_order(item_code=item, qty=1, rate=90)
			pr = make_purchase_receipt(po.name)
			pr.insert()

			# user edits the mapped row to a non-source rate
			pr.items[0].price_list_rate = 200
			pr.items[0].rate = 200

			# a re-fetch must restore the persisted source (PO) rate, not keep the edit
			pr.process_item_selection(item_idx=pr.items[0].idx)
			self.assertEqual(flt(pr.items[0].rate), 90)
			pr.save()  # must not raise the maintain-same-rate check
		finally:
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", original_action)
			frappe.clear_cache(doctype="Buying Settings")

	def test_rate_lock_source_lookup_checks_permission(self):
		"""The lock reads source pricing via a direct DB read, so it must not disclose a
		source document's pricing to a caller who cannot read that document.
		"""
		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
		from erpnext.stock.get_item_details import get_rate_locked_source_row

		original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
		frappe.clear_cache(doctype="Buying Settings")

		role, email = "_Test Role Without PO Access", "_test_rate_lock_probe@example.com"
		try:
			po = create_purchase_order(item_code="_Test Item", qty=1, rate=90)
			pr_doc = {
				"doctype": "Purchase Receipt",
				"items": [{"name": "r1", "item_code": "_Test Item", "purchase_order_item": po.items[0].name}],
			}
			ctx = frappe._dict(doctype="Purchase Receipt", child_docname="r1")

			# an authorized caller receives the source row
			self.assertIsNotNone(get_rate_locked_source_row(ctx.copy(), dict(pr_doc)))

			if not frappe.db.exists("Role", role):
				frappe.get_doc({"doctype": "Role", "role_name": role, "desk_access": 1}).insert(
					ignore_permissions=True
				)
			if not frappe.db.exists("User", email):
				frappe.get_doc(
					{
						"doctype": "User",
						"email": email,
						"first_name": "Probe",
						"send_welcome_email": 0,
						"roles": [{"role": role}],
					}
				).insert(ignore_permissions=True)

			frappe.set_user(email)
			# a caller who cannot read the Purchase Order gets nothing
			self.assertIsNone(get_rate_locked_source_row(ctx.copy(), dict(pr_doc)))
		finally:
			frappe.set_user("Administrator")
			frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
			frappe.clear_cache(doctype="Buying Settings")

	@ERPNextTestSuite.change_settings("Buying Settings", {"maintain_same_rate": 1, "allow_multiple_items": 1})
	def test_rate_lock_checks_source_permission_once_per_document(self):
		from unittest.mock import patch

		from frappe.utils import add_days, nowdate

		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
		from erpnext.stock.get_item_details import get_rate_locked_source_row

		po = create_purchase_order(
			rm_items=[
				{
					"item_code": "_Test Item",
					"warehouse": "_Test Warehouse - _TC",
					"qty": 1,
					"rate": 90,
					"schedule_date": add_days(nowdate(), 1),
				}
				for _ in range(2)
			]
		)

		with patch("frappe.has_permission", wraps=frappe.has_permission) as has_permission:
			for row in po.items:
				ctx = frappe._dict(doctype="Purchase Receipt", purchase_order_item=row.name)
				self.assertIsNotNone(get_rate_locked_source_row(ctx, {"doctype": "Purchase Receipt"}))

		has_permission.assert_called_once()

	def test_rate_lock_keeps_each_rows_rate_for_batch_items(self):
		"""Batch rows mapped PR->PI must each keep their own rate, not collapse onto the first."""
		from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
		from erpnext.stock.doctype.item.test_item import make_item
		from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
		from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
			get_batch_from_bundle,
		)
		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

		original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
		frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
		frappe.clear_cache(doctype="Buying Settings")
		self.addCleanup(frappe.clear_cache, doctype="Buying Settings")
		self.addCleanup(frappe.db.set_single_value, "Buying Settings", "maintain_same_rate", original)

		def batched_item():
			code = make_item(
				properties={
					"is_stock_item": 1,
					"has_batch_no": 1,
					"create_new_batch": 1,
					"batch_number_series": "FBQ-.#####",
				}
			).name
			se = make_stock_entry(item_code=code, target="_Test Warehouse - _TC", qty=5, basic_rate=100)
			return code, get_batch_from_bundle(se.items[0].serial_and_batch_bundle)

		item_a, batch_a = batched_item()
		item_b, batch_b = batched_item()

		# one PO with both items at different rates
		po = create_purchase_order(item_code=item_a, qty=5, rate=28, do_not_save=True)
		po.append(
			"items",
			{
				"item_code": item_b,
				"qty": 5,
				"rate": 275,
				"warehouse": "_Test Warehouse - _TC",
				"schedule_date": frappe.utils.nowdate(),
			},
		)
		po.set_missing_values()
		po.insert()
		po.submit()

		# receive both against their own batches
		pr = make_purchase_receipt(po.name)
		for row in pr.items:
			row.use_serial_batch_fields = 1
		pr.items[0].batch_no = batch_a
		pr.items[1].batch_no = batch_b
		pr.insert()
		pr.submit()

		# the batch_no branch force-writes the fetched rate during mapping
		pi = make_purchase_invoice(pr.name)
		self.assertEqual(pi.items[0].rate, 28)
		self.assertEqual(pi.items[1].rate, 275)  # used to collapse onto the first row (28)

	def test_supplier_quotation_uses_buying_price_list_exchange_rate(self):
		for exchange_rate, for_buying in ((90, 1), (95, 0)):
			frappe.get_doc(
				{
					"doctype": "Currency Exchange",
					"date": today(),
					"from_currency": "EUR",
					"to_currency": "INR",
					"exchange_rate": exchange_rate,
					"for_buying": for_buying,
					"for_selling": not for_buying,
				}
			).insert()
		price_list = frappe.get_doc(
			{"doctype": "Price List", "price_list_name": "_Test EUR Buying", "currency": "EUR", "buying": 1}
		).insert()

		details = get_price_list_currency_and_exchange_rate(
			frappe._dict(
				doctype="Supplier Quotation",
				price_list=price_list.name,
				company="_Test Company",
				transaction_date=today(),
			)
		)

		self.assertEqual(details.plc_conversion_rate, 90)

	def make_batched_item_with_stock(self, quantities, **properties):
		from erpnext.stock.doctype.item.test_item import make_item
		from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
			get_batch_from_bundle,
		)
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		properties.update({"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "FBQ-.#####"})
		item_code = make_item(properties=properties).name
		batches = []
		for qty in quantities:
			se = make_stock_entry(
				item_code=item_code, target="_Test Warehouse - _TC", qty=qty, basic_rate=100
			)
			batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle))

		return item_code, batches

	def get_item_details_for_row(self, item_code, qty, batch_no=None):
		ctx = frappe._dict(
			{
				"doctype": "Delivery Note",
				"item_code": item_code,
				"company": "_Test Company",
				"warehouse": "_Test Warehouse - _TC",
				"qty": qty,
				"batch_no": batch_no,
				"use_serial_batch_fields": 1,
				"price_list": "_Test Price List",
				"currency": "INR",
				"conversion_rate": 1.0,
				"price_list_currency": "INR",
				"plc_conversion_rate": 1.0,
				"ignore_pricing_rule": 1,
			}
		)
		doc = {"doctype": "Delivery Note", "selling_price_list": "_Test Price List", "items": []}
		return get_item_details(ctx, doc=doc)

	def get_bundle_entries(self, bundles):
		return frappe.get_all(
			"Serial and Batch Entry", {"parent": ("in", bundles)}, ["serial_no", "batch_no"], order_by="idx"
		)

	def test_serial_nos_auto_picked_from_selected_batch(self):
		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note

		item_code, batches = self.make_batched_item_with_stock(
			[2, 3], has_serial_no=1, serial_no_series="SBP-SN-.#####"
		)

		with self.change_settings(
			"Stock Settings",
			{"pick_serial_and_batch_based_on": "FIFO", "auto_create_serial_and_batch_bundle_for_outward": 1},
		):
			details = self.get_item_details_for_row(item_code, 3, batch_no=batches[1])
			self.assertFalse(details.get("serial_no"))

			dn = create_delivery_note(
				item_code=item_code, qty=3, use_serial_batch_fields=1, batch_no=batches[1]
			)
			dn.reload()
			self.assertFalse(dn.items[0].serial_no)

			entries = self.get_bundle_entries([dn.items[0].serial_and_batch_bundle])
			self.assertEqual(len({d.serial_no for d in entries if d.serial_no}), 3)
			self.assertEqual({d.batch_no for d in entries}, {batches[1]})

	def test_reserved_serial_nos_picked_for_sales_order_delivery(self):
		from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
		from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

		item_code, batches = self.make_batched_item_with_stock(
			[2], has_serial_no=1, serial_no_series="SRV-SN-.#####"
		)

		with self.change_settings(
			"Stock Settings",
			{
				"pick_serial_and_batch_based_on": "FIFO",
				"auto_create_serial_and_batch_bundle_for_outward": 1,
				"enable_stock_reservation": 1,
				"allow_negative_stock": 0,
			},
		):
			so = make_sales_order(
				item_code=item_code, warehouse="_Test Warehouse - _TC", qty=2, rate=100, do_not_submit=True
			)
			so.reserve_stock = 1
			so.items[0].reserve_stock = 1
			so.submit()

			dn = make_delivery_note(so.name)
			dn.items[0].use_serial_batch_fields = 1
			dn.items[0].serial_no = None
			dn.insert()
			self.assertFalse(dn.items[0].serial_no)

			dn.submit()
			dn.reload()
			entries = self.get_bundle_entries([dn.items[0].serial_and_batch_bundle])
			self.assertEqual(len({d.serial_no for d in entries if d.serial_no}), 2)

	@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
	def test_rows_of_same_item_get_different_serial_nos(self):
		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note

		item_code, batches = self.make_batched_item_with_stock(
			[5], has_serial_no=1, serial_no_series="SRD-SN-.#####"
		)

		with self.change_settings(
			"Stock Settings",
			{"pick_serial_and_batch_based_on": "FIFO", "auto_create_serial_and_batch_bundle_for_outward": 1},
		):
			dn = create_delivery_note(
				item_code=item_code, qty=2, use_serial_batch_fields=1, do_not_submit=True
			)
			first_row = dn.items[0].as_dict()
			dn.append("items", {k: v for k, v in first_row.items() if k not in ("name", "idx")})
			dn.save()
			dn.submit()
			dn.reload()

			picked = [
				d.serial_no
				for d in self.get_bundle_entries([row.serial_and_batch_bundle for row in dn.items])
			]
			self.assertEqual(len(picked), 4)
			self.assertEqual(len(set(picked)), 4)

	def test_reserved_serial_nos_follow_selected_batch(self):
		from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
		from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

		item_code, batches = self.make_batched_item_with_stock(
			[2, 2], has_serial_no=1, serial_no_series="SRB-SN-.#####"
		)

		with self.change_settings(
			"Stock Settings",
			{
				"pick_serial_and_batch_based_on": "FIFO",
				"auto_create_serial_and_batch_bundle_for_outward": 1,
				"enable_stock_reservation": 1,
				"allow_negative_stock": 0,
			},
		):
			so = make_sales_order(
				item_code=item_code, warehouse="_Test Warehouse - _TC", qty=4, rate=100, do_not_submit=True
			)
			so.reserve_stock = 1
			so.items[0].reserve_stock = 1
			so.submit()

			dn = make_delivery_note(so.name)
			dn.items[0].update(
				{"qty": 2, "use_serial_batch_fields": 1, "batch_no": batches[1], "serial_no": None}
			)
			dn.insert()
			dn.submit()
			dn.reload()

			entries = self.get_bundle_entries([dn.items[0].serial_and_batch_bundle])
			self.assertEqual([d.batch_no for d in entries], [batches[1], batches[1]])

	def test_item_row_and_product_bundle_get_different_serial_nos(self):
		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
		from erpnext.stock.doctype.item.test_item import make_item

		item_code, batches = self.make_batched_item_with_stock(
			[3], has_serial_no=1, serial_no_series="SPB-SN-.#####"
		)
		bundle_item = make_item(properties={"is_stock_item": 0}).name
		make_product_bundle(bundle_item, [item_code])

		with self.change_settings(
			"Stock Settings",
			{
				"pick_serial_and_batch_based_on": "FIFO",
				"auto_create_serial_and_batch_bundle_for_outward": 1,
				"use_serial_batch_fields": 1,
			},
		):
			dn = create_delivery_note(
				item_code=item_code, qty=1, use_serial_batch_fields=1, do_not_submit=True
			)
			first_row = dn.items[0].as_dict()
			dn.append(
				"items",
				{k: v for k, v in first_row.items() if k not in ("name", "idx", "batch_no", "serial_no")}
				| {"item_code": bundle_item, "item_name": bundle_item},
			)
			dn.save()
			dn.submit()
			dn.reload()

			bundles = [
				row.serial_and_batch_bundle
				for row in dn.items + dn.packed_items
				if row.serial_and_batch_bundle
			]
			picked = [d.serial_no for d in self.get_bundle_entries(bundles)]
			self.assertEqual(len(picked), 2)
			self.assertEqual(len(set(picked)), 2)
