# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt

import frappe
from frappe import _dict

from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
from erpnext.stock.doctype.pick_list.pick_list import (
	create_delivery,
	create_delivery_note,
	create_dn_for_pick_lists,
	create_stock_entry,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
	get_batch_from_bundle,
	get_serial_nos_from_bundle,
	make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
	EmptyStockReconciliationItemsError,
)
from erpnext.tests.utils import ERPNextTestSuite


class TestPickList(ERPNextTestSuite):
	def test_pick_list_picks_warehouse_for_each_item(self):
		item_code = make_item().name
		try:
			frappe.get_doc(
				{
					"doctype": "Stock Reconciliation",
					"company": "_Test Company",
					"purpose": "Opening Stock",
					"expense_account": "Temporary Opening - _TC",
					"items": [
						{
							"item_code": item_code,
							"warehouse": "_Test Warehouse - _TC",
							"valuation_rate": 100,
							"qty": 5,
						}
					],
				}
			).submit()
		except EmptyStockReconciliationItemsError:
			pass

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"locations": [
					{
						"item_code": item_code,
						"qty": 5,
						"stock_qty": 5,
						"conversion_factor": 1,
						"sales_order": "_T-Sales Order-1",
						"sales_order_item": "_T-Sales Order-1_item",
					}
				],
			}
		)
		pick_list.set_item_locations()

		self.assertEqual(pick_list.locations[0].item_code, item_code)
		self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
		self.assertEqual(pick_list.locations[0].qty, 5)

	def test_pick_list_splits_row_according_to_warehouse_availability(self):
		try:
			frappe.get_doc(
				{
					"doctype": "Stock Reconciliation",
					"company": "_Test Company",
					"purpose": "Opening Stock",
					"expense_account": "Temporary Opening - _TC",
					"items": [
						{
							"item_code": "_Test Item Warehouse Group Wise Reorder",
							"warehouse": "_Test Warehouse Group-C1 - _TC",
							"valuation_rate": 100,
							"qty": 5,
						}
					],
				}
			).submit()
		except EmptyStockReconciliationItemsError:
			pass

		try:
			frappe.get_doc(
				{
					"doctype": "Stock Reconciliation",
					"company": "_Test Company",
					"purpose": "Opening Stock",
					"expense_account": "Temporary Opening - _TC",
					"items": [
						{
							"item_code": "_Test Item Warehouse Group Wise Reorder",
							"warehouse": "_Test Warehouse 2 - _TC",
							"valuation_rate": 400,
							"qty": 10,
						}
					],
				}
			).submit()
		except EmptyStockReconciliationItemsError:
			pass

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"locations": [
					{
						"item_code": "_Test Item Warehouse Group Wise Reorder",
						"qty": 1000,
						"stock_qty": 1000,
						"conversion_factor": 1,
						"sales_order": "_T-Sales Order-1",
						"sales_order_item": "_T-Sales Order-1_item",
					}
				],
			}
		)

		pick_list.set_item_locations()

		self.assertEqual(pick_list.locations[0].item_code, "_Test Item Warehouse Group Wise Reorder")
		self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse Group-C1 - _TC")
		self.assertEqual(pick_list.locations[0].qty, 5)

		self.assertEqual(pick_list.locations[1].item_code, "_Test Item Warehouse Group Wise Reorder")
		self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse 2 - _TC")
		self.assertEqual(pick_list.locations[1].qty, 10)

	def test_pick_list_shows_serial_no_for_serialized_item(self):
		serial_nos = ["SADD-0001", "SADD-0002", "SADD-0003", "SADD-0004", "SADD-0005"]

		for serial_no in serial_nos:
			if not frappe.db.exists("Serial No", serial_no):
				frappe.get_doc(
					{
						"doctype": "Serial No",
						"company": "_Test Company",
						"item_code": "_Test Serialized Item",
						"serial_no": serial_no,
					}
				).insert()

		stock_reconciliation = frappe.get_doc(
			{
				"doctype": "Stock Reconciliation",
				"purpose": "Stock Reconciliation",
				"company": "_Test Company",
				"items": [
					{
						"item_code": "_Test Serialized Item",
						"warehouse": "_Test Warehouse - _TC",
						"valuation_rate": 100,
						"reconcile_all_serial_batch": 1,
						"qty": 5,
						"serial_and_batch_bundle": make_serial_batch_bundle(
							frappe._dict(
								{
									"item_code": "_Test Serialized Item",
									"warehouse": "_Test Warehouse - _TC",
									"qty": 5,
									"rate": 100,
									"type_of_transaction": "Inward",
									"do_not_submit": True,
									"voucher_type": "Stock Reconciliation",
									"serial_nos": serial_nos,
								}
							)
						).name,
					}
				],
			}
		)

		try:
			stock_reconciliation.submit()
		except EmptyStockReconciliationItemsError:
			pass

		so = make_sales_order(
			item_code="_Test Serialized Item", warehouse="_Test Warehouse - _TC", qty=5, rate=1000
		)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"locations": [
					{
						"item_code": "_Test Serialized Item",
						"qty": 1000,
						"stock_qty": 1000,
						"conversion_factor": 1,
						"warehouse": "_Test Warehouse - _TC",
						"sales_order": so.name,
						"sales_order_item": so.items[0].name,
					}
				],
			}
		)

		pick_list.save()
		pick_list.submit()

		self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
		self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
		self.assertEqual(pick_list.locations[0].qty, 5)
		self.assertEqual(
			get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), serial_nos
		)

	def test_pick_list_shows_batch_no_for_batched_item(self):
		# check if oldest batch no is picked
		item = frappe.db.exists("Item", {"item_name": "Batched Item"})
		if not item:
			item = create_item("Batched Item")
			item.has_batch_no = 1
			item.create_new_batch = 1
			item.batch_number_series = "B-BATCH-.##"
			item.save()
		else:
			item = frappe.get_doc("Item", {"item_name": "Batched Item"})

		pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)

		pr1.load_from_db()
		oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)

		pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"purpose": "Material Transfer",
				"locations": [
					{
						"item_code": "Batched Item",
						"qty": 1,
						"stock_qty": 1,
						"conversion_factor": 1,
					}
				],
			}
		)
		pick_list.set_item_locations()
		self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)

		pr1.cancel()
		pr2.cancel()

	def test_pick_list_warehouse_for_batched_item(self):
		"""
		Test that pick list respects company based warehouse assignment for batched items.

		This test verifies that when creating a pick list for a batched item,
		the system correctly identifies and assigns the appropriate warehouse
		based on the company.
		"""
		from erpnext.stock.doctype.batch.test_batch import make_new_batch

		batch_company = frappe.get_doc(
			{
				"doctype": "Company",
				"company_name": "Batch Company",
				"default_currency": "INR",
				"country": "India",
			}
		)
		batch_company.insert()

		batch_warehouse = frappe.get_doc(
			{
				"doctype": "Warehouse",
				"warehouse_name": "Batch Warehouse",
				"company": batch_company.name,
			}
		)
		batch_warehouse.insert()

		batch_item = frappe.db.exists("Item", "Batch Warehouse Item")
		if not batch_item:
			batch_item = create_item("Batch Warehouse Item")
			batch_item.has_batch_no = 1
			batch_item.create_new_batch = 1
			batch_item.save()
		else:
			batch_item = frappe.get_doc("Item", "Batch Warehouse Item")

		batch_no = make_new_batch(item_code=batch_item.name, batch_id="B-WH-ITEM-001")

		make_stock_entry(
			item_code=batch_item.name,
			qty=5,
			company=batch_company.name,
			to_warehouse=batch_warehouse.name,
			batch_no=batch_no.name,
			rate=100.0,
		)
		make_stock_entry(
			item_code=batch_item.name,
			qty=5,
			to_warehouse="_Test Warehouse - _TC",
			batch_no=batch_no.name,
			rate=100.0,
		)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": batch_company.name,
				"purpose": "Material Transfer",
				"locations": [
					{
						"item_code": batch_item.name,
						"qty": 10,
						"stock_qty": 10,
						"conversion_factor": 1,
					}
				],
			}
		)

		pick_list.set_item_locations()
		self.assertEqual(len(pick_list.locations), 1)
		self.assertEqual(pick_list.locations[0].qty, 5)
		self.assertEqual(pick_list.locations[0].batch_no, batch_no.name)
		self.assertEqual(pick_list.locations[0].warehouse, batch_warehouse.name)

	def test_pick_list_warehouse_validation(self):
		"""check if the warehouse validations are triggered"""
		from erpnext.stock.doctype.pick_list.pick_list import (
			IncorrectWarehouseValidationError,
			MissingWarehouseValidationError,
		)

		warehouse_item = create_item("Warehouse Item")
		temp_company = frappe.get_doc(
			{
				"doctype": "Company",
				"company_name": "Temp Company",
				"default_currency": "INR",
				"country": "India",
			}
		).insert()
		temp_warehouse = frappe.get_doc(
			{"doctype": "Warehouse", "warehouse_name": "Temp Warehouse", "company": temp_company.name}
		).insert()

		make_stock_entry(item_code=warehouse_item.name, qty=10, rate=100.0, to_warehouse=temp_warehouse.name)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": temp_company.name,
				"purpose": "Material Transfer",
				"pick_manually": 1,
				"locations": [
					{
						"item_code": warehouse_item.name,
						"qty": 5,
						"stock_qty": 5,
						"conversion_factor": 1,
					}
				],
			}
		)

		self.assertRaises(MissingWarehouseValidationError, pick_list.insert)
		pick_list.locations[0].warehouse = "_Test Warehouse - _TC"
		self.assertRaises(IncorrectWarehouseValidationError, pick_list.insert)
		pick_list.locations[0].warehouse = temp_warehouse.name
		pick_list.insert()

	def test_pick_list_for_batched_and_serialised_item(self):
		# check if oldest batch no and serial nos are picked
		item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
		if not item:
			item = create_item("Batched and Serialised Item")
			item.has_batch_no = 1
			item.create_new_batch = 1
			item.has_serial_no = 1
			item.batch_number_series = "B-BATCH-.##"
			item.serial_no_series = "S-.####"
			item.save()
		else:
			item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"})

		pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)

		pr1.load_from_db()
		oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
		oldest_serial_nos = get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle)

		pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"purpose": "Material Transfer",
				"locations": [
					{
						"item_code": "Batched and Serialised Item",
						"qty": 2,
						"stock_qty": 2,
						"conversion_factor": 1,
					}
				],
			}
		)
		pick_list.set_item_locations()
		pick_list.submit()
		pick_list.reload()

		self.assertEqual(
			get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no
		)
		self.assertEqual(
			get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos
		)

		pick_list.cancel()
		pr1.cancel()
		pr2.cancel()

	def test_pick_list_for_items_from_multiple_sales_orders(self):
		item_code = make_item().name
		try:
			frappe.get_doc(
				{
					"doctype": "Stock Reconciliation",
					"company": "_Test Company",
					"purpose": "Opening Stock",
					"expense_account": "Temporary Opening - _TC",
					"items": [
						{
							"item_code": item_code,
							"warehouse": "_Test Warehouse - _TC",
							"valuation_rate": 100,
							"qty": 10,
						}
					],
				}
			).submit()
		except EmptyStockReconciliationItemsError:
			pass

		sales_order = frappe.get_doc(
			{
				"doctype": "Sales Order",
				"customer": "_Test Customer",
				"company": "_Test Company",
				"items": [
					{
						"item_code": item_code,
						"qty": 10,
						"delivery_date": frappe.utils.today(),
						"warehouse": "_Test Warehouse - _TC",
					}
				],
			}
		)
		sales_order.submit()

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"locations": [
					{
						"item_code": item_code,
						"qty": 5,
						"stock_qty": 5,
						"conversion_factor": 1,
						"sales_order": "_T-Sales Order-1",
						"sales_order_item": "_T-Sales Order-1_item",
					},
					{
						"item_code": item_code,
						"qty": 5,
						"stock_qty": 5,
						"conversion_factor": 1,
						"sales_order": sales_order.name,
						"sales_order_item": sales_order.items[0].name,
					},
				],
			}
		)
		pick_list.set_item_locations()

		self.assertEqual(pick_list.locations[0].item_code, item_code)
		self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
		self.assertEqual(pick_list.locations[0].qty, 5)
		self.assertEqual(pick_list.locations[0].sales_order_item, "_T-Sales Order-1_item")

		self.assertEqual(pick_list.locations[1].item_code, item_code)
		self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse - _TC")
		self.assertEqual(pick_list.locations[1].qty, 5)
		self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)

	@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
	def test_pick_list_for_items_with_multiple_UOM(self):
		item_code = make_item(
			uoms=[
				{"uom": "Nos", "conversion_factor": 1},
				{"uom": "Hand", "conversion_factor": 5},
				{"uom": "Unit", "conversion_factor": 0.5},
			]
		).name
		purchase_receipt = make_purchase_receipt(item_code=item_code, qty=10)
		purchase_receipt.submit()

		sales_order = frappe.get_doc(
			{
				"doctype": "Sales Order",
				"customer": "_Test Customer",
				"company": "_Test Company",
				"items": [
					{
						"item_code": item_code,
						"qty": 1,
						"uom": "Hand",
						"delivery_date": frappe.utils.today(),
						"warehouse": "_Test Warehouse - _TC",
					},
					{
						"item_code": item_code,
						"qty": 1,
						"conversion_factor": 1,
						"delivery_date": frappe.utils.today(),
						"warehouse": "_Test Warehouse - _TC",
					},
				],
			}
		).insert()

		sales_order.submit()

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"locations": [
					{
						"item_code": item_code,
						"qty": 2,
						"stock_qty": 1,
						"uom": "Unit",
						"conversion_factor": 0.5,
						"sales_order": sales_order.name,
						"sales_order_item": sales_order.items[0].name,
					},
					{
						"item_code": item_code,
						"qty": 1,
						"stock_qty": 1,
						"conversion_factor": 1,
						"sales_order": sales_order.name,
						"sales_order_item": sales_order.items[1].name,
					},
				],
			}
		)
		pick_list.set_item_locations()
		pick_list.submit()

		delivery_note = create_delivery_note(pick_list.name)
		pick_list.load_from_db()

		#  pick list stk_qty / dn conversion_factor = dn qty (1/5 = 0.2)
		self.assertEqual(
			pick_list.locations[0].picked_qty,
			delivery_note.items[0].qty * delivery_note.items[0].conversion_factor,
		)
		self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
		self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)

		pick_list.cancel()
		sales_order.cancel()
		purchase_receipt.cancel()

	def test_pick_list_grouping_before_print(self):
		def _compare_dicts(a, b):
			"compare dicts but ignore missing keys in `a`"
			for key, value in a.items():
				self.assertEqual(b.get(key), value, msg=f"{key} doesn't match")

		# nothing should be grouped
		pl = frappe.get_doc(
			doctype="Pick List",
			group_same_items=True,
			locations=[
				_dict(item_code="A", warehouse="X", qty=1, picked_qty=2),
				_dict(item_code="B", warehouse="X", qty=1, picked_qty=2),
				_dict(item_code="A", warehouse="Y", qty=1, picked_qty=2),
				_dict(item_code="B", warehouse="Y", qty=1, picked_qty=2),
			],
		)
		pl.before_print()
		self.assertEqual(len(pl.locations), 4)

		# grouping should not happen if group_same_items is False
		pl = frappe.get_doc(
			doctype="Pick List",
			group_same_items=False,
			locations=[
				_dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
				_dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
				_dict(item_code="A", warehouse="X", qty=3, picked_qty=2),
				_dict(item_code="B", warehouse="Y", qty=2, picked_qty=2),
			],
		)
		pl.before_print()
		self.assertEqual(len(pl.locations), 4)

		# grouping should halve the number of items
		pl.group_same_items = True
		pl.before_print()
		self.assertEqual(len(pl.locations), 2)

		expected_items = [
			_dict(item_code="A", warehouse="X", qty=8, picked_qty=3),
			_dict(item_code="B", warehouse="Y", qty=6, picked_qty=4),
		]
		for expected_item, created_item in zip(expected_items, pl.locations, strict=False):
			_compare_dicts(expected_item, created_item)

	def test_multiple_dn_creation(self):
		sales_order_1 = frappe.get_doc(
			{
				"doctype": "Sales Order",
				"customer": "_Test Customer",
				"company": "_Test Company",
				"items": [
					{
						"item_code": "_Test Item",
						"qty": 1,
						"conversion_factor": 1,
						"delivery_date": frappe.utils.today(),
					}
				],
			}
		).insert()
		sales_order_1.submit()
		sales_order_2 = frappe.get_doc(
			{
				"doctype": "Sales Order",
				"customer": "_Test Customer 1",
				"company": "_Test Company",
				"items": [
					{
						"item_code": "_Test Item 2",
						"qty": 1,
						"conversion_factor": 1,
						"delivery_date": frappe.utils.today(),
					},
				],
			}
		).insert()
		sales_order_2.submit()
		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"customer": "_Test Customer",
				"locations": [
					{
						"item_code": "_Test Item",
						"qty": 1,
						"stock_qty": 1,
						"conversion_factor": 1,
						"sales_order": sales_order_1.name,
						"sales_order_item": sales_order_1.items[0].name,
					},
					{
						"item_code": "_Test Item 2",
						"qty": 1,
						"stock_qty": 1,
						"conversion_factor": 1,
						"sales_order": sales_order_2.name,
						"sales_order_item": sales_order_2.items[0].name,
					},
				],
			}
		)
		pick_list.set_item_locations()
		pick_list.submit()
		create_delivery_note(pick_list.name)
		for dn in frappe.get_all(
			"Delivery Note",
			filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"},
			fields=["name"],
		):
			for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
				self.assertEqual(dn_item.item_code, "_Test Item")
				self.assertEqual(dn_item.against_sales_order, sales_order_1.name)
				self.assertEqual(dn_item.against_pick_list, pick_list.name)
				self.assertEqual(dn_item.pick_list_item, pick_list.locations[0].name)

		for dn in frappe.get_all(
			"Delivery Note",
			filters={"against_pick_list": pick_list.name, "customer": "_Test Customer 1"},
			fields=["name"],
		):
			for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
				self.assertEqual(dn_item.item_code, "_Test Item 2")
				self.assertEqual(dn_item.against_sales_order, sales_order_2.name)
				self.assertEqual(dn_item.against_pick_list, pick_list.name)
				self.assertEqual(dn_item.pick_list_item, pick_list.locations[1].name)
		# test DN creation without so
		pick_list_1 = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"purpose": "Delivery",
				"locations": [
					{
						"item_code": "_Test Item",
						"qty": 1,
						"stock_qty": 1,
						"conversion_factor": 1,
					},
					{
						"item_code": "_Test Item 2",
						"qty": 2,
						"stock_qty": 2,
						"conversion_factor": 1,
					},
				],
			}
		)
		pick_list_1.set_item_locations()
		pick_list_1.submit()
		create_delivery_note(pick_list_1.name)
		for dn in frappe.get_all(
			"Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields=["name"]
		):
			for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
				if dn_item.item_code == "_Test Item":
					self.assertEqual(dn_item.qty, 1)
				if dn_item.item_code == "_Test Item 2":
					self.assertEqual(dn_item.qty, 2)

	@ERPNextTestSuite.change_settings("Stock Settings", {"use_serial_batch_fields": 1})
	def test_sales_invoice_from_pick_list_copies_old_batch_serial_fields(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			f"_Test PLSI Old Fields {frappe.generate_hash(length=8)}",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"create_new_batch": 1,
				"has_serial_no": 1,
				"batch_number_series": f"PLSI-OLD-B-{frappe.generate_hash(length=6)}-.#####",
				"serial_no_series": f"PLSI-OLD-S-{frappe.generate_hash(length=6)}-.#####",
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=2, basic_rate=100)
		sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=2, rate=100)

		pick_list = create_pick_list(sales_order.name)
		pick_list.submit()
		pick_list_item = pick_list.locations[0]

		self.assertTrue(pick_list_item.use_serial_batch_fields)
		self.assertTrue(pick_list_item.batch_no)
		self.assertTrue(pick_list_item.serial_no)

		sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
		sales_invoice_item = sales_invoice.items[0]

		self.assertEqual(sales_invoice.update_stock, 1)
		self.assertEqual(sales_invoice_item.against_pick_list, pick_list.name)
		self.assertEqual(sales_invoice_item.pick_list_item, pick_list_item.name)
		self.assertEqual(sales_invoice_item.use_serial_batch_fields, 1)
		self.assertEqual(sales_invoice_item.batch_no, pick_list_item.batch_no)
		self.assertEqual(
			set(sales_invoice_item.serial_no.split("\n")), set(pick_list_item.serial_no.split("\n"))
		)

	@ERPNextTestSuite.change_settings("Stock Settings", {"use_serial_batch_fields": 0})
	def test_sales_invoice_from_pick_list_copies_serial_and_batch_bundle(self):
		frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)

		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			f"_Test PLSI Bundle {frappe.generate_hash(length=8)}",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"create_new_batch": 1,
				"has_serial_no": 1,
				"batch_number_series": f"PLSI-BND-B-{frappe.generate_hash(length=6)}-.#####",
				"serial_no_series": f"PLSI-BND-S-{frappe.generate_hash(length=6)}-.#####",
			},
		).name

		stock_entry = make_stock_entry(item=item, to_warehouse=warehouse, qty=2, basic_rate=100)
		batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
		serial_nos = get_serial_nos_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
		sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=2, rate=100)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"customer": "_Test Customer",
				"items_based_on": "Sales Order",
				"purpose": "Delivery",
				"pick_manually": 1,
				"locations": [
					{
						"item_code": item,
						"warehouse": warehouse,
						"qty": 2,
						"stock_qty": 2,
						"picked_qty": 2,
						"conversion_factor": 1,
						"sales_order": sales_order.name,
						"sales_order_item": sales_order.items[0].name,
						"use_serial_batch_fields": 0,
					}
				],
			}
		).insert()

		from erpnext.stock.serial_batch_bundle import SerialBatchCreation

		pick_list.locations[0].serial_and_batch_bundle = (
			SerialBatchCreation(
				{
					"item_code": item,
					"warehouse": warehouse,
					"voucher_type": "Pick List",
					"voucher_no": pick_list.name,
					"voucher_detail_no": pick_list.locations[0].name,
					"qty": -2,
					"batches": frappe._dict({batch_no: 2}),
					"serial_nos": serial_nos,
					"type_of_transaction": "Outward",
					"company": "_Test Company",
					"do_not_submit": True,
				}
			)
			.make_serial_and_batch_bundle()
			.name
		)
		pick_list.locations[0].db_set(
			{
				"use_serial_batch_fields": 0,
				"batch_no": None,
				"serial_no": None,
				"serial_and_batch_bundle": pick_list.locations[0].serial_and_batch_bundle,
			}
		)
		pick_list.reload()
		pick_list_item = pick_list.locations[0]

		self.assertFalse(pick_list_item.use_serial_batch_fields)
		self.assertTrue(pick_list_item.serial_and_batch_bundle)

		sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
		sales_invoice_item = sales_invoice.items[0]

		self.assertEqual(sales_invoice_item.against_pick_list, pick_list.name)
		self.assertEqual(sales_invoice_item.pick_list_item, pick_list_item.name)
		self.assertFalse(sales_invoice_item.use_serial_batch_fields)
		self.assertTrue(sales_invoice_item.serial_and_batch_bundle)
		self.assertNotEqual(
			sales_invoice_item.serial_and_batch_bundle, pick_list_item.serial_and_batch_bundle
		)
		self.assertEqual(
			get_batch_from_bundle(sales_invoice_item.serial_and_batch_bundle),
			get_batch_from_bundle(pick_list_item.serial_and_batch_bundle),
		)
		self.assertEqual(
			set(get_serial_nos_from_bundle(sales_invoice_item.serial_and_batch_bundle)),
			set(get_serial_nos_from_bundle(pick_list_item.serial_and_batch_bundle)),
		)

	def test_sales_invoice_from_sales_order_pick_list_updates_sales_order(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item().name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=5, basic_rate=100)
		sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=5, rate=100)

		pick_list = create_pick_list(sales_order.name)
		pick_list.submit()

		sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
		sales_invoice_item = sales_invoice.items[0]

		self.assertEqual(sales_invoice_item.sales_order, sales_order.name)
		self.assertEqual(sales_invoice_item.so_detail, sales_order.items[0].name)
		self.assertEqual(sales_invoice_item.against_pick_list, pick_list.name)
		self.assertEqual(sales_invoice_item.pick_list_item, pick_list.locations[0].name)

		sales_invoice.submit()
		pick_list.reload()
		sales_order.reload()

		self.assertEqual(pick_list.locations[0].delivered_qty, pick_list.locations[0].picked_qty)
		self.assertEqual(pick_list.per_delivered, 100)
		self.assertEqual(pick_list.delivery_status, "Fully Delivered")
		self.assertEqual(pick_list.status, "Completed")

		self.assertEqual(sales_order.items[0].picked_qty, 5)
		self.assertEqual(sales_order.items[0].delivered_qty, 5)
		self.assertEqual(sales_order.per_delivered, 100)
		self.assertEqual(sales_order.delivery_status, "Fully Delivered")
		self.assertEqual(sales_order.per_billed, 100)
		self.assertEqual(sales_order.billing_status, "Fully Billed")
		self.assertEqual(sales_order.status, "Completed")

	def test_sales_invoice_against_pick_list_requires_update_stock(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item().name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=5, basic_rate=100)
		sales_order = make_sales_order(item_code=item, warehouse=warehouse, qty=5, rate=100)

		pick_list = create_pick_list(sales_order.name)
		pick_list.submit()

		sales_invoice = create_delivery(pick_list.name, target="Sales Invoice")
		sales_invoice.update_stock = 0

		self.assertRaisesRegex(
			frappe.ValidationError,
			"Update Stock.*Pick List",
			sales_invoice.save,
		)

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

		warehouse = "_Test Warehouse - _TC"
		test_stock_item = "_Test Stock Item"

		# Ensure stock item exists
		if not frappe.db.exists("Item", test_stock_item):
			create_item(
				item_code=test_stock_item,
				is_stock_item=1,
			)

		# Add initial stock qty
		make_stock_entry(item_code=test_stock_item, to_warehouse=warehouse, qty=15)

		# Create SO for 10 qty
		sales_order_1 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)

		# Create and Submit picklist for SO
		picklist_1 = create_pick_list(sales_order_1.name)
		picklist_1.submit()

		# Create DN for 5 qty
		dn = create_delivery_note(picklist_1.name)
		dn.items[0].qty = 5
		dn.save()
		dn.submit()

		# Verify partly delivered state
		picklist_1.reload()
		self.assertEqual(picklist_1.status, "Partly Delivered")

		# Create another SO (10 qty)
		sales_order_2 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)

		# Expected pick qty = 5
		picklist_2 = create_pick_list(sales_order_2.name)
		self.assertEqual(picklist_2.locations[0].qty, 5)

	def test_picklist_with_multi_uom(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
		make_stock_entry(item=item, to_warehouse=warehouse, qty=1000)

		so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box")
		pl = create_pick_list(so.name)
		# pick half the qty
		for loc in pl.locations:
			loc.picked_qty = loc.stock_qty / 2
		pl.save()
		pl.submit()

		so.reload()
		self.assertEqual(so.per_picked, 50)

	def test_picklist_for_batch_item(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			properties={"is_stock_item": 1, "has_batch_no": 1, "batch_number_series": "PICKLT-.######"}
		).name

		# create batch
		for batch_id in ["PICKLT-000001", "PICKLT-000002"]:
			if not frappe.db.exists("Batch", batch_id):
				frappe.get_doc(
					{
						"doctype": "Batch",
						"batch_id": batch_id,
						"item": item,
					}
				).insert()

		make_stock_entry(
			item=item,
			to_warehouse=warehouse,
			qty=50,
			basic_rate=100,
			batches=frappe._dict({"PICKLT-000001": 30, "PICKLT-000002": 20}),
		)

		so = make_sales_order(item_code=item, qty=25.0, rate=100)
		pl = create_pick_list(so.name)
		pl.submit()
		# pick half the qty
		for loc in pl.locations:
			self.assertEqual(loc.qty, 25.0)
			self.assertTrue(loc.serial_and_batch_bundle)

		pl.save()
		pl.submit()

		so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
		pl1 = create_pick_list(so1.name)
		pl1.submit()

		# pick half the qty
		for loc in pl1.locations:
			self.assertEqual(loc.qty, 5.0)
			self.assertTrue(loc.serial_and_batch_bundle)

			data = frappe.get_all(
				"Serial and Batch Entry",
				fields=["qty", "batch_no"],
				filters={"parent": loc.serial_and_batch_bundle},
			)

			for d in data:
				self.assertTrue(d.batch_no in ["PICKLT-000001", "PICKLT-000002"])
				if d.batch_no == "PICKLT-000001":
					self.assertEqual(d.qty, 5.0 * -1)
				elif d.batch_no == "PICKLT-000002":
					self.assertEqual(d.qty, 5.0 * -1)

		pl1.cancel()
		pl.cancel()

	def test_picklist_for_serial_item(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PICKLT-.######"}
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=50, basic_rate=100)

		so = make_sales_order(item_code=item, qty=25.0, rate=100)
		pl = create_pick_list(so.name)
		pl.submit()
		picked_serial_nos = []
		# pick half the qty
		for loc in pl.locations:
			self.assertEqual(loc.qty, 25.0)
			self.assertTrue(loc.serial_and_batch_bundle)

			data = frappe.get_all(
				"Serial and Batch Entry",
				fields=["serial_no"],
				filters={"parent": loc.serial_and_batch_bundle},
			)

			picked_serial_nos = [d.serial_no for d in data]
			self.assertEqual(len(picked_serial_nos), 25)

		so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
		pl1 = create_pick_list(so1.name)
		pl1.submit()
		# pick half the qty
		for loc in pl1.locations:
			self.assertEqual(loc.qty, 10.0)
			self.assertTrue(loc.serial_and_batch_bundle)

			data = frappe.get_all(
				"Serial and Batch Entry",
				fields=["qty", "batch_no"],
				filters={"parent": loc.serial_and_batch_bundle},
			)

			self.assertEqual(len(data), 10)
			for d in data:
				self.assertTrue(d.serial_no not in picked_serial_nos)

		pl1.cancel()
		pl.cancel()

	def test_picklist_with_bundles(self):
		warehouse = "_Test Warehouse - _TC"

		quantities = [5, 2]
		bundle, components = create_product_bundle(quantities, warehouse=warehouse)
		bundle_items = dict(zip(components, quantities, strict=False))
		so = make_sales_order(item_code=bundle, qty=3, rate=42)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertEqual(len(pl.locations), 2)
		for item in pl.locations:
			self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3)

		# check picking status on sales order
		pl.submit()
		so.reload()
		self.assertEqual(so.per_picked, 100)

		# deliver
		dn = create_delivery_note(pl.name).submit()
		self.assertEqual(dn.items[0].rate, 42)
		self.assertEqual(dn.packed_items[0].warehouse, warehouse)
		so.reload()
		self.assertEqual(so.per_delivered, 100)

	def test_picklist_with_partial_bundles(self):
		# from self.globalTestRecords
		warehouse = "_Test Warehouse - _TC"

		quantities = [5, 2]
		bundle, components = create_product_bundle(quantities, warehouse=warehouse)

		so = make_sales_order(item_code=bundle, qty=4, rate=42)

		pl = create_pick_list(so.name)
		for loc in pl.locations:
			loc.picked_qty = loc.qty / 2

		pl.save().submit()
		so.reload()
		self.assertEqual(so.per_picked, 50)

		# deliver half qty
		dn = create_delivery_note(pl.name).submit()
		self.assertEqual(dn.items[0].rate, 42)
		so.reload()
		self.assertEqual(so.per_delivered, 50)

		pl = create_pick_list(so.name)
		pl.save().submit()
		so.reload()
		self.assertEqual(so.per_picked, 100)

		# deliver remaining
		dn = create_delivery_note(pl.name).submit()
		self.assertEqual(dn.items[0].rate, 42)
		so.reload()
		self.assertEqual(so.per_delivered, 100)

	def test_pick_list_status(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(properties={"is_stock_item": 1}).name
		make_stock_entry(item=item, to_warehouse=warehouse, qty=10)

		so = make_sales_order(item_code=item, qty=10, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		pl.reload()
		self.assertEqual(pl.status, "Draft")

		pl.submit()
		pl.reload()
		self.assertEqual(pl.status, "Open")

		dn = create_delivery_note(pl.name)
		dn.save()
		pl.reload()
		self.assertEqual(pl.status, "Open")

		dn.submit()
		pl.reload()
		self.assertEqual(pl.status, "Completed")

		dn.cancel()
		pl.reload()
		self.assertEqual(pl.status, "Open")

		pl.cancel()
		pl.reload()
		self.assertEqual(pl.status, "Cancelled")

	def test_pick_list_partial_transfer_status(self):
		"""Partial Stock Entries from a Pick List should track transferred_qty and drive the
		Partially Transferred / Completed status, and allow further transfers for the remainder."""
		from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

		item = make_item(properties={"is_stock_item": 1}).name
		source_warehouse = "_Test Warehouse - _TC"
		target_warehouse = create_warehouse("_Test Transfer Target Warehouse")
		make_stock_entry(item=item, to_warehouse=source_warehouse, qty=10)

		pick_list = frappe.get_doc(
			{
				"doctype": "Pick List",
				"company": "_Test Company",
				"purpose": "Material Transfer",
				"pick_manually": 1,
				"locations": [
					{
						"item_code": item,
						"qty": 10,
						"stock_qty": 10,
						"conversion_factor": 1,
						"warehouse": source_warehouse,
						"picked_qty": 10,
					}
				],
			}
		)
		pick_list.submit()
		self.assertEqual(pick_list.status, "Open")

		# Transfer 4 of the 10 picked units.
		se1 = frappe.get_doc(create_stock_entry(pick_list.as_dict()))
		self.assertEqual(se1.items[0].qty, 10)
		se1.items[0].qty = 4
		se1.items[0].t_warehouse = target_warehouse
		se1.submit()

		pick_list.reload()
		self.assertEqual(pick_list.locations[0].transferred_qty, 4)
		self.assertEqual(pick_list.status, "Partially Transferred")

		# The next Stock Entry should only offer the remaining 6 units.
		se2 = frappe.get_doc(create_stock_entry(pick_list.as_dict()))
		self.assertEqual(se2.items[0].qty, 6)
		se2.items[0].t_warehouse = target_warehouse
		se2.submit()

		pick_list.reload()
		self.assertEqual(pick_list.locations[0].transferred_qty, 10)
		self.assertEqual(pick_list.status, "Completed")

		# Cancelling the last entry rolls transferred_qty and status back.
		se2.cancel()
		pick_list.reload()
		self.assertEqual(pick_list.locations[0].transferred_qty, 4)
		self.assertEqual(pick_list.status, "Partially Transferred")

	def test_create_second_delivery_note_with_fully_delivered_location(self):
		# When one pick list item is fully delivered by the first Delivery Note
		# and another item is still pending, creating a second Delivery Note from
		# the Pick List must not create a zero-qty row for the delivered item.
		warehouse = "_Test Warehouse - _TC"
		item_a = make_item(properties={"is_stock_item": 1}).name
		item_b = make_item(properties={"is_stock_item": 1}).name
		make_stock_entry(item=item_a, to_warehouse=warehouse, qty=20)
		make_stock_entry(item=item_b, to_warehouse=warehouse, qty=20)

		so = make_sales_order(
			item_list=[
				{"item_code": item_a, "warehouse": warehouse, "qty": 10, "rate": 100},
				{"item_code": item_b, "warehouse": warehouse, "qty": 5, "rate": 100},
			]
		)

		pl = create_pick_list(so.name)
		pl.save().submit()

		# First Delivery Note: fully deliver item_a, drop item_b.
		dn1 = create_delivery_note(pl.name)
		for row in list(dn1.items):
			if row.item_code == item_b:
				dn1.remove(row)
		dn1.save().submit()

		pl.reload()
		delivered = {loc.item_code: loc.delivered_qty for loc in pl.locations}
		self.assertEqual(delivered[item_a], 10)
		self.assertEqual(delivered[item_b], 0)

		# Second Delivery Note for the remaining item must succeed and must not
		# include a zero-qty row for the already delivered item_a.
		dn2 = create_delivery_note(pl.name)
		self.assertEqual(len(dn2.items), 1)
		self.assertEqual(dn2.items[0].item_code, item_b)
		self.assertEqual(dn2.items[0].qty, 5)

	def test_pick_list_validation(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item("Test Non Serialized Pick List Item", properties={"is_stock_item": 1}).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=10)

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		pl.submit()
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=4, rate=100)
		pl = create_pick_list(so.name)
		self.assertFalse(pl.locations)

	def test_pick_list_warehouse_for_work_order(self):
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.manufacturing.doctype.work_order.work_order import create_pick_list, make_work_order
		from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

		# Create Warehouses for Work Order
		source_warehouse = create_warehouse("_Test WO Warehouse")
		wip_warehouse = create_warehouse("_Test WIP Warehouse", company="_Test Company")
		fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")

		# Create Finished Good Item
		fg_item = make_item("Test Work Order Finished Good Item", properties={"is_stock_item": 1}).name

		# Create Raw Material Item
		rm_item = make_item("Test Work Order Raw Material Item", properties={"is_stock_item": 1}).name

		# Create BOM
		bom = make_bom(item=fg_item, rate=100, raw_materials=[rm_item])

		# Create Inward entry for Raw Material
		make_stock_entry(item=rm_item, to_warehouse=wip_warehouse, qty=10)
		make_stock_entry(item=rm_item, to_warehouse=source_warehouse, qty=10)

		# Create Work Order
		wo = make_work_order(item=fg_item, qty=5, bom_no=bom.name, company="_Test Company")
		wo.required_items[0].source_warehouse = source_warehouse
		wo.fg_warehouse = fg_warehouse
		wo.skip_transfer = True
		wo.submit()

		# Create Pick List
		pl = create_pick_list(wo.name, for_qty=wo.qty)

		# System prioritises the Source Warehouse
		self.assertEqual(pl.locations[0].warehouse, source_warehouse)
		self.assertEqual(pl.locations[0].item_code, rm_item)
		self.assertEqual(pl.locations[0].qty, 5)

		# Create Outward Entry from Source Warehouse
		make_stock_entry(item=rm_item, from_warehouse=source_warehouse, qty=10)
		pl.set_item_locations()

		# System should pick other available warehouses
		self.assertEqual(pl.locations[0].warehouse, wip_warehouse)
		self.assertEqual(pl.locations[0].item_code, rm_item)
		self.assertEqual(pl.locations[0].qty, 5)

	def test_pick_list_validation_for_serial_no(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Serialized Pick List Item",
			properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-SPLI-.####"},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=10)

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.locations[0].qty = 5
		pl.save()
		pl.submit()
		self.assertTrue(pl.locations[0].serial_no)
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertTrue(pl.locations[0].serial_no)
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=4, rate=100)
		pl = create_pick_list(so.name)
		self.assertFalse(pl.locations)

	def test_pick_list_validation_for_batch_no(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Batch Pick List Item",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"batch_number_series": "BATCH-SPLI-.####",
				"create_new_batch": 1,
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=10)

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.locations[0].qty = 5
		pl.save()
		pl.submit()
		self.assertTrue(pl.locations[0].batch_no)
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertTrue(pl.locations[0].batch_no)
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=4, rate=100)
		pl = create_pick_list(so.name)
		self.assertFalse(pl.locations)

	def test_pick_list_validation_for_batch_no_and_serial_item(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Serialized Batch Pick List Item",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"batch_number_series": "SN-BT-BATCH-SPLI-.####",
				"create_new_batch": 1,
				"has_serial_no": 1,
				"serial_no_series": "SN-BT-SPLI-.####",
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=10)

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.locations[0].qty = 5
		pl.save()
		pl.submit()
		self.assertTrue(pl.locations[0].batch_no)
		self.assertTrue(pl.locations[0].serial_no)
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=5, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertTrue(pl.locations[0].batch_no)
		self.assertTrue(pl.locations[0].serial_no)
		self.assertEqual(pl.locations[0].qty, 5.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=4, rate=100)
		pl = create_pick_list(so.name)
		self.assertFalse(pl.locations)

	def test_pick_list_validation_for_multiple_batches_and_sales_order(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Batch Pick List Item For Multiple Batches",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####",
				"create_new_batch": 1,
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
		make_stock_entry(item=item, to_warehouse=warehouse, qty=5)

		so = make_sales_order(item_code=item, qty=6, rate=100)

		pl1 = create_pick_list(so.name)
		pl1.save()
		self.assertEqual(pl1.locations[0].qty, 5.0)
		self.assertEqual(pl1.locations[1].qty, 1.0)

		so = make_sales_order(item_code=item, qty=4, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertEqual(pl.locations[0].qty, 4.0)
		self.assertTrue(hasattr(pl, "locations"))

		pl1.submit()

		pl.reload()
		pl.submit()
		self.assertEqual(pl.locations[0].qty, 4.0)
		self.assertTrue(hasattr(pl, "locations"))

	def test_pick_list_for_multiple_sales_order_with_multiple_batches(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Batch Pick List Item For Multiple Batches and Sales Order",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####",
				"create_new_batch": 1,
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)

		so = make_sales_order(item_code=item, qty=10, rate=100)

		pl1 = create_pick_list(so.name)
		pl1.save()
		self.assertEqual(pl1.locations[0].qty, 10)

		so = make_sales_order(item_code=item, qty=110, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertEqual(pl.locations[0].qty, 90.0)
		self.assertEqual(pl.locations[1].qty, 20.0)
		self.assertTrue(hasattr(pl, "locations"))

		pl1.submit()

		pl.reload()
		pl.submit()
		self.assertEqual(pl.locations[0].qty, 90.0)
		self.assertEqual(pl.locations[1].qty, 20.0)
		self.assertTrue(hasattr(pl, "locations"))

	def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Serial No Pick List Item For Multiple Batches and Sales Order",
			properties={
				"is_stock_item": 1,
				"has_serial_no": 1,
				"serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####",
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)

		so = make_sales_order(item_code=item, qty=10, rate=100)

		pl1 = create_pick_list(so.name)
		pl1.save()
		self.assertEqual(pl1.locations[0].qty, 10)

		serial_nos = pl1.locations[0].serial_no.split("\n")
		self.assertEqual(len(serial_nos), 10)

		so = make_sales_order(item_code=item, qty=110, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertEqual(pl.locations[0].qty, 110.0)
		self.assertTrue(hasattr(pl, "locations"))

		new_serial_nos = pl.locations[0].serial_no.split("\n")
		self.assertEqual(len(new_serial_nos), 110)

		for sn in serial_nos:
			self.assertFalse(sn in new_serial_nos)

		pl1.submit()

		pl.reload()
		pl.submit()
		self.assertEqual(pl.locations[0].qty, 110.0)
		self.assertTrue(hasattr(pl, "locations"))

	def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Non Serialized Pick List Item For Multiple Batches and Sales Order",
			properties={
				"is_stock_item": 1,
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)

		so = make_sales_order(item_code=item, qty=10, rate=100)

		pl1 = create_pick_list(so.name)
		pl1.save()
		self.assertEqual(pl1.locations[0].qty, 10)

		so = make_sales_order(item_code=item, qty=110, rate=100)

		pl = create_pick_list(so.name)
		pl.save()
		self.assertEqual(pl.locations[0].qty, 110.0)
		self.assertTrue(hasattr(pl, "locations"))

		pl1.submit()

		pl.reload()
		pl.submit()
		self.assertEqual(pl.locations[0].qty, 110.0)
		self.assertTrue(hasattr(pl, "locations"))

		so = make_sales_order(item_code=item, qty=110, rate=100)
		pl = create_pick_list(so.name)
		pl.save()

		self.assertEqual(pl.locations[0].qty, 80.0)

	def test_validate_picked_qty_with_manual_option(self):
		warehouse = "_Test Warehouse - _TC"
		non_serialized_item = make_item(
			"Test Non Serialized Pick List Item For Manual Option", properties={"is_stock_item": 1}
		).name

		serialized_item = make_item(
			"Test Serialized Pick List Item For Manual Option",
			properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-HSNMSPLI-.####"},
		).name

		batched_item = make_item(
			"Test Batched Pick List Item For Manual Option",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"batch_number_series": "SN-HBNMSPLI-.####",
				"create_new_batch": 1,
			},
		).name

		make_stock_entry(item=non_serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100)
		make_stock_entry(item=serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100)
		make_stock_entry(item=batched_item, to_warehouse=warehouse, qty=10, basic_rate=100)

		so = make_sales_order(
			item_code=non_serialized_item, qty=10, rate=100, do_not_save=True, warehouse=warehouse
		)
		so.append("items", {"item_code": serialized_item, "qty": 10, "rate": 100, "warehouse": warehouse})
		so.append("items", {"item_code": batched_item, "qty": 10, "rate": 100, "warehouse": warehouse})
		so.set_missing_values()
		so.save()
		so.submit()

		pl = create_pick_list(so.name)
		pl.pick_manually = 1

		for row in pl.locations:
			row.qty = row.qty + 10
			row.picked_qty = row.qty

		self.assertRaises(frappe.ValidationError, pl.save)

	def test_over_allowance_picking(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Over Allowance Picking Item",
			properties={
				"is_stock_item": 1,
			},
		).name

		make_stock_entry(item=item, to_warehouse=warehouse, qty=100)

		so = make_sales_order(item_code=item, qty=10, rate=100)

		pl_doc = create_pick_list(so.name)
		pl_doc.save()
		self.assertEqual(pl_doc.locations[0].qty, 10)

		pl_doc.locations[0].qty = 15
		pl_doc.locations[0].stock_qty = 15
		pl_doc.save()

		self.assertEqual(pl_doc.locations[0].qty, 15)
		self.assertRaises(frappe.ValidationError, pl_doc.submit)

		frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 50)

		pl_doc.reload()
		pl_doc.submit()

		frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0)

	@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
	def test_ignore_pricing_rule_in_pick_list(self):
		frappe.flags.print_stmt = False
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"batch_number_series": "IPR-PICKLT-.######",
				"create_new_batch": 1,
			}
		).name

		make_stock_entry(
			item=item,
			to_warehouse=warehouse,
			qty=2,
			basic_rate=100,
		)

		pricing_rule = frappe.get_doc(
			{
				"doctype": "Pricing Rule",
				"title": "Same Free Item",
				"price_or_product_discount": "Product",
				"selling": 1,
				"apply_on": "Item Code",
				"items": [
					{
						"item_code": item,
					}
				],
				"same_item": 1,
				"is_recursive": 1,
				"recurse_for": 2,
				"free_qty": 1,
				"dont_enforce_free_item_qty": 0,
				"company": "_Test Company",
				"customer": "_Test Customer",
			}
		)

		pricing_rule.save()
		frappe.flags.print_stmt = True

		so = make_sales_order(item_code=item, qty=2, rate=100, do_not_save=True)
		so.set_warehouse = warehouse
		so.submit()

		self.assertEqual(len(so.items), 2)
		self.assertTrue(so.items[1].is_free_item)

		pl = create_pick_list(so.name)
		pl.ignore_pricing_rule = 1
		pl.save()
		pl.submit()

		self.assertEqual(len(pl.locations), 1)

		delivery_note = create_delivery_note(pl.name)

		self.assertEqual(len(delivery_note.items), 1)

	def test_pick_list_not_reset_batch(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item(
			"Test Do Not Reset Picked Item",
			properties={
				"is_stock_item": 1,
				"has_batch_no": 1,
				"create_new_batch": 1,
				"batch_number_series": "BTH-PICKLT-.######",
			},
		).name

		se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
		batch1 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
		se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
		batch2 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)

		so = make_sales_order(item_code=item, qty=10, rate=100)

		pl = create_pick_list(so.name)
		pl.save()

		for loc in pl.locations:
			self.assertEqual(loc.batch_no, batch1)
			loc.batch_no = batch2
			loc.picked_qty = 0.0

		pl.save()

		for loc in pl.locations:
			self.assertEqual(loc.batch_no, batch1)
			loc.batch_no = batch2
			loc.picked_qty = 10.0

		pl.save()

		for loc in pl.locations:
			self.assertEqual(loc.batch_no, batch2)

	@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
	def test_multiple_pick_lists_delivery_note(self):
		from erpnext.stock.doctype.pick_list.pick_list import create_dn_for_pick_lists

		item_code = make_item().name
		warehouse = "_Test Warehouse - _TC"

		stock_entry = make_stock_entry(item=item_code, to_warehouse=warehouse, qty=500, basic_rate=100)

		def create_pick_list(qty):
			pick_list = frappe.get_doc(
				{
					"doctype": "Pick List",
					"company": "_Test Company",
					"customer": "_Test Customer",
					"purpose": "Delivery",
					"locations": [
						{
							"item_code": item_code,
							"warehouse": warehouse,
							"qty": qty,
							"stock_qty": qty,
							"picked_qty": 0,
							"sales_order": sales_order.name,
							"sales_order_item": sales_order.items[0].name,
						},
					],
				}
			)
			pick_list.submit()
			return pick_list

		sales_order = make_sales_order(item_code=item_code, qty=50, rate=100)
		pick_list_1 = create_pick_list(10)
		pick_list_2 = create_pick_list(20)

		delivery_note = create_dn_for_pick_lists(pick_list_1.name)
		delivery_note = create_dn_for_pick_lists(pick_list_2.name, delivery_note)
		delivery_note.items[0].qty = 5
		delivery_note.submit()

		sales_order.reload()
		pick_list_1.reload()
		pick_list_2.reload()

		self.assertEqual(sales_order.items[0].picked_qty, 30)
		self.assertEqual(pick_list_1.locations[0].delivered_qty, delivery_note.items[0].qty)
		self.assertEqual(pick_list_1.status, "Partly Delivered")
		self.assertEqual(pick_list_2.status, "Completed")

		pick_list_1.cancel()
		pick_list_2.cancel()
		delivery_note.cancel()
		sales_order.reload()
		sales_order.cancel()
		stock_entry.cancel()

	def test_packed_item_in_pick_list(self):
		warehouse_1 = "_Test Warehouse - _TC"
		warehouse_2 = "_Test Warehouse 2 - _TC"
		item_1 = make_item(properties={"is_stock_item": 0}).name
		item_2 = make_item().name
		item_3 = make_item().name

		make_product_bundle(item_1, items=[item_2, item_3])

		stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=10, basic_rate=100)
		stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=4, basic_rate=100)
		stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=6, basic_rate=100)

		sales_order = make_sales_order(item_code=item_1, qty=10, rate=100)

		pick_list = create_pick_list(sales_order.name)
		pick_list.submit()
		self.assertEqual(len(pick_list.locations), 3)
		delivery_note = create_delivery_note(pick_list.name)

		self.assertEqual(delivery_note.items[0].qty, 10)
		self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
		self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_2)

		pick_list.cancel()
		sales_order.cancel()
		stock_entry_1.cancel()
		stock_entry_2.cancel()
		stock_entry_3.cancel()

	@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
	def test_packed_item_multiple_times_in_so(self):
		frappe.db.delete("Item Price")
		warehouse_1 = "_Test Warehouse - _TC"
		warehouse_2 = "_Test Warehouse 2 - _TC"
		warehouse = "_Test Warehouse - _TC"
		item_1 = make_item(properties={"is_stock_item": 0}).name
		item_2 = make_item().name
		item_3 = make_item().name

		make_product_bundle(item_1, items=[item_2, item_3])

		stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=20, basic_rate=100)
		stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=8, basic_rate=100)
		stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=12, basic_rate=100)

		sales_order = make_sales_order(
			item_list=[
				{"item_code": item_1, "qty": 8, "rate": 100, "warehouse": warehouse},
				{"item_code": item_1, "qty": 12, "rate": 100, "warehouse": warehouse},
			]
		)

		pick_list = create_pick_list(sales_order.name)
		pick_list.submit()
		self.assertEqual(len(pick_list.locations), 4)
		delivery_note = create_delivery_note(pick_list.name)

		self.assertEqual(delivery_note.items[0].qty, 8)
		self.assertEqual(delivery_note.items[1].qty, 12)

		self.assertEqual(delivery_note.packed_items[0].qty, 8)
		self.assertEqual(delivery_note.packed_items[2].qty, 12)

		self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
		self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_1)
		self.assertEqual(delivery_note.packed_items[2].warehouse, warehouse_1)
		self.assertEqual(delivery_note.packed_items[3].warehouse, warehouse_2)

		pick_list.cancel()
		sales_order.cancel()
		stock_entry_1.cancel()
		stock_entry_2.cancel()
		stock_entry_3.cancel()

	def test_pick_list_with_and_without_so(self):
		warehouse = "_Test Warehouse - _TC"
		item = make_item().name

		sales_order = make_sales_order(item_code=item, qty=20, rate=100)
		stock_entry = make_stock_entry(item=item, to_warehouse=warehouse, qty=500, basic_rate=100)

		pick_list = create_pick_list(sales_order.name)
		pick_list.append(
			"locations",
			{
				"item_code": item,
				"qty": 10,
				"stock_qty": 10,
				"warehouse": warehouse,
				"picked_qty": 0,
			},
		)
		pick_list.submit()

		delivery_note = create_dn_for_pick_lists(pick_list.name)

		self.assertEqual(delivery_note.items[0].against_pick_list, pick_list.name)
		self.assertEqual(delivery_note.items[0].against_sales_order, sales_order.name)
		self.assertEqual(delivery_note.items[0].qty, 20)

		self.assertEqual(delivery_note.items[1].against_pick_list, pick_list.name)
		self.assertEqual(delivery_note.items[1].qty, 10)

		pick_list.cancel()
		sales_order.cancel()
		stock_entry.cancel()

	def test_creating_dn_from_so_with_different_addresses(self):
		warehouse = "_Test Warehouse - _TC"
		item1 = make_item().name
		item2 = make_item().name
		item3 = make_item().name

		customer_shipping_address_1 = frappe.get_doc(
			{
				"doctype": "Address",
				"address_title": "Customer Shipping Address",
				"address_type": "Shipping",
				"address_line1": "123, ABC Street",
				"city": "Mumbai",
				"country": "India",
				"links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}],
				"is_shipping_address": 1,
			}
		).insert()

		sales_order1 = make_sales_order(item_code=item1, qty=1)
		make_stock_entry(item=item1, to_warehouse=warehouse, qty=1)
		sales_order2 = make_sales_order(item_code=item2, qty=1)
		make_stock_entry(item=item2, to_warehouse=warehouse, qty=1)
		sales_order3 = make_sales_order(item_code=item3, qty=1, do_not_submit=True)

		customer_shipping_address_2 = frappe.get_doc(
			{
				"doctype": "Address",
				"address_title": "Customer Shipping Address",
				"address_type": "Shipping",
				"address_line1": "123, ABC Street",
				"city": "Mumbai",
				"country": "India",
				"links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}],
			}
		).insert()
		sales_order3.shipping_address_name = customer_shipping_address_2.name
		sales_order3.submit()
		make_stock_entry(item=item3, to_warehouse=warehouse, qty=1)

		from json import dumps

		from frappe.model.mapper import map_docs

		pick_list = frappe.new_doc("Pick List")
		map_docs(
			"erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
			dumps([sales_order1.name, sales_order2.name, sales_order3.name]),
			pick_list,
		)
		pick_list.submit()

		create_delivery_note(pick_list.name)
		delivery_notes = frappe.get_all(
			"Delivery Note Item", filters={"against_pick_list": pick_list.name}, pluck="parent", distinct=True
		)
		self.assertEqual(len(delivery_notes), 2)

		for delivery_note in delivery_notes:
			doc = frappe.get_doc("Delivery Note", delivery_note)
			if len(doc.items) == 1:
				self.assertEqual(doc.items[0].item_code, item3)
				self.assertEqual(doc.shipping_address_name, customer_shipping_address_2.name)
			else:
				self.assertEqual(doc.shipping_address_name, customer_shipping_address_1.name)
				item_codes = [item.item_code for item in doc.items]
				self.assertTrue(item1 in item_codes)
				self.assertTrue(item2 in item_codes)
