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

import frappe
from frappe import _dict
from frappe.tests.utils import FrappeTestCase

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_note, create_dn_for_pick_lists
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,
)

test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]


class TestPickList(FrappeTestCase):
	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,
						"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_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)

	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)

	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 test_records.json
		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_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_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)

	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)

	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 = "RJ 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()

	def test_packed_item_multiple_times_in_so(self):
		frappe.db.delete("Item Price")
		warehouse_1 = "RJ 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()
