# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt


from frappe.permissions import add_user_permission, remove_user_permission
from frappe.utils import add_days, cstr, flt, get_time, getdate, nowtime, today

from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.exceptions import QualityInspectionRequiredError
from erpnext.stock.doctype.item.test_item import (
	create_item,
	make_item,
	make_item_variant,
	set_item_variant_settings,
)
from erpnext.stock.doctype.material_request.material_request import (
	make_in_transit_stock_entry,
)
from erpnext.stock.doctype.material_request.test_material_request import (
	get_in_transit_warehouse,
	make_material_request,
)
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.serial_no.serial_no import *
from erpnext.stock.doctype.stock_entry.stock_entry import FinishedGoodError, make_stock_in_entry
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
	OpeningEntryAccountError,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
	create_stock_reconciliation,
)
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
from erpnext.tests.utils import ERPNextTestSuite


def get_sle(**args):
	condition, values = "", []
	for key, value in args.items():
		condition += " and " if condition else " where "
		condition += f"`{key}`=%s"
		values.append(value)

	return frappe.db.sql(
		"""select * from `tabStock Ledger Entry` %s
		order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""
		% condition,
		values,
		as_dict=1,
	)


class TestStockEntry(ERPNextTestSuite):
	def setUp(self):
		self.load_test_records("Stock Entry")
		frappe.local.flags.dont_execute_stock_reposts = False

	def test_stock_entry_qty(self):
		item_code = "_Test Item 2"
		warehouse = "_Test Warehouse - _TC"
		se = make_stock_entry(item_code=item_code, target=warehouse, qty=0, do_not_save=True)
		with self.assertRaises(InvalidQtyError):
			se.save()

		# No error with qty=1
		se.items[0].qty = 1
		se.save()
		self.assertEqual(se.items[0].qty, 1)

	def test_fifo(self):
		frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
		item_code = "_Test Item 2"
		warehouse = "_Test Warehouse - _TC"

		create_stock_reconciliation(
			item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", qty=0, rate=100
		)

		make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=10)
		sle = get_sle(item_code=item_code, warehouse=warehouse)[0]

		self.assertEqual([[1, 10]], frappe.safe_eval(sle.stock_queue))

		# negative qty
		make_stock_entry(item_code=item_code, source=warehouse, qty=2, basic_rate=10)
		sle = get_sle(item_code=item_code, warehouse=warehouse)[0]

		self.assertEqual([[-1, 10]], frappe.safe_eval(sle.stock_queue))

		# further negative
		make_stock_entry(item_code=item_code, source=warehouse, qty=1)
		sle = get_sle(item_code=item_code, warehouse=warehouse)[0]

		self.assertEqual([[-2, 10]], frappe.safe_eval(sle.stock_queue))

		# move stock to positive
		make_stock_entry(item_code=item_code, target=warehouse, qty=3, basic_rate=20)
		sle = get_sle(item_code=item_code, warehouse=warehouse)[0]
		self.assertEqual([[1, 20]], frappe.safe_eval(sle.stock_queue))

		# incoming entry with diff rate
		make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=30)
		sle = get_sle(item_code=item_code, warehouse=warehouse)[0]

		self.assertEqual([[1, 20], [1, 30]], frappe.safe_eval(sle.stock_queue))

		frappe.db.set_default("allow_negative_stock", 0)

	def test_auto_material_request(self):
		make_item_variant()
		self._test_auto_material_request("_Test Item")
		self._test_auto_material_request("_Test Item", material_request_type="Transfer")

	def test_barcode_item_stock_entry(self):
		item_code = make_item("_Test Item Stock Entry For Barcode", barcode="BDD-1234567890")

		se = make_stock_entry(item_code=item_code.name, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
		self.assertEqual(se.items[0].barcode, "BDD-1234567890")

	def test_auto_material_request_for_variant(self):
		fields = [{"field_name": "reorder_levels"}]
		set_item_variant_settings(fields)
		make_item_variant()
		template = frappe.get_doc("Item", "_Test Variant Item")

		if not template.reorder_levels:
			template.append(
				"reorder_levels",
				{
					"material_request_type": "Purchase",
					"warehouse": "_Test Warehouse - _TC",
					"warehouse_reorder_level": 20,
					"warehouse_reorder_qty": 20,
				},
			)

		template.save()
		self._test_auto_material_request("_Test Variant Item-S")

	def test_auto_material_request_for_warehouse_group(self):
		self._test_auto_material_request(
			"_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC"
		)

	def _test_auto_material_request(
		self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC"
	):
		variant = frappe.get_doc("Item", item_code)

		projected_qty, actual_qty = frappe.db.get_value(
			"Bin", {"item_code": item_code, "warehouse": warehouse}, ["projected_qty", "actual_qty"]
		) or [0, 0]

		# stock entry reqd for auto-reorder
		create_stock_reconciliation(
			item_code=item_code, warehouse=warehouse, qty=actual_qty + abs(projected_qty) + 10, rate=100
		)

		projected_qty = (
			frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") or 0
		)

		frappe.db.set_single_value("Stock Settings", "auto_indent", 1)

		# update re-level qty so that it is more than projected_qty
		if projected_qty >= variant.reorder_levels[0].warehouse_reorder_level:
			variant.reorder_levels[0].warehouse_reorder_level += projected_qty
			variant.reorder_levels[0].material_request_type = material_request_type
			variant.save()

		from erpnext.stock.reorder_item import reorder_item

		mr_list = reorder_item()

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

		items = []
		for mr in mr_list:
			for d in mr.items:
				items.append(d.item_code)

		self.assertTrue(item_code in items)

	def test_add_to_transit_entry(self):
		from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

		item_code = "_Test Transit Item"
		company = "_Test Company"

		create_warehouse("Test From Warehouse")
		create_warehouse("Test Transit Warehouse")
		create_warehouse("Test To Warehouse")

		create_item(
			item_code=item_code,
			is_stock_item=1,
			is_purchase_item=1,
			company=company,
		)

		# create inward stock entry
		make_stock_entry(
			item_code=item_code,
			target="Test From Warehouse - _TC",
			qty=10,
			basic_rate=100,
			expense_account="Stock Adjustment - _TC",
			cost_center="Main - _TC",
		)

		transit_entry = make_stock_entry(
			item_code=item_code,
			source="Test From Warehouse - _TC",
			target="Test Transit Warehouse - _TC",
			add_to_transit=1,
			stock_entry_type="Material Transfer",
			purpose="Material Transfer",
			qty=10,
			basic_rate=100,
			expense_account="Stock Adjustment - _TC",
			cost_center="Main - _TC",
		)

		end_transit_entry = make_stock_in_entry(transit_entry.name)

		self.assertEqual(end_transit_entry.stock_entry_type, "Material Transfer")
		self.assertEqual(end_transit_entry.purpose, "Material Transfer")
		self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry)
		self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry)
		self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail)

		transit_entry.reload()
		self.assertEqual(transit_entry.per_transferred, 0)

		end_transit_entry.to_warehouse = "Test To Warehouse - _TC"
		end_transit_entry.items[0].t_warehouse = "Test To Warehouse - _TC"
		end_transit_entry.save().submit()

		transit_entry.reload()
		self.assertEqual(transit_entry.per_transferred, 100)

	def test_material_receipt_gl_entry(self):
		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")

		mr = make_stock_entry(
			item_code="_Test Item",
			target="Stores - TCP1",
			company=company,
			qty=50,
			basic_rate=100,
			expense_account="Stock Adjustment - TCP1",
		)

		stock_in_hand_account = get_inventory_account(mr.company, mr.get("items")[0].t_warehouse)
		self.check_stock_ledger_entries("Stock Entry", mr.name, [["_Test Item", "Stores - TCP1", 50.0]])

		self.check_gl_entries(
			"Stock Entry",
			mr.name,
			sorted([[stock_in_hand_account, 5000.0, 0.0], ["Stock Adjustment - TCP1", 0.0, 5000.0]]),
		)

		mr.cancel()

		self.assertTrue(
			frappe.db.sql(
				"""select * from `tabStock Ledger Entry`
			where voucher_type='Stock Entry' and voucher_no=%s""",
				mr.name,
			)
		)

		self.assertTrue(
			frappe.db.sql(
				"""select * from `tabGL Entry`
			where voucher_type='Stock Entry' and voucher_no=%s""",
				mr.name,
			)
		)

	def test_material_issue_gl_entry(self):
		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
		make_stock_entry(
			item_code="_Test Item",
			target="Stores - TCP1",
			company=company,
			qty=50,
			basic_rate=100,
			expense_account="Stock Adjustment - TCP1",
		)

		mi = make_stock_entry(
			item_code="_Test Item",
			source="Stores - TCP1",
			company=company,
			qty=40,
			expense_account="Stock Adjustment - TCP1",
		)

		self.check_stock_ledger_entries("Stock Entry", mi.name, [["_Test Item", "Stores - TCP1", -40.0]])

		stock_in_hand_account = get_inventory_account(mi.company, "Stores - TCP1")
		stock_value_diff = abs(
			frappe.db.get_value(
				"Stock Ledger Entry",
				{"voucher_type": "Stock Entry", "voucher_no": mi.name},
				"stock_value_difference",
			)
		)

		self.check_gl_entries(
			"Stock Entry",
			mi.name,
			sorted(
				[
					[stock_in_hand_account, 0.0, stock_value_diff],
					["Stock Adjustment - TCP1", stock_value_diff, 0.0],
				]
			),
		)
		mi.cancel()

	def test_material_transfer_gl_entry(self):
		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")

		item_code = "Hand Sanitizer - 001"
		create_item(
			item_code=item_code,
			is_stock_item=1,
			is_purchase_item=1,
			opening_stock=1000,
			valuation_rate=10,
			company=company,
			warehouse="Stores - TCP1",
		)

		mtn = make_stock_entry(
			item_code=item_code,
			source="Stores - TCP1",
			target="Finished Goods - TCP1",
			qty=45,
			company=company,
		)

		self.check_stock_ledger_entries(
			"Stock Entry",
			mtn.name,
			[[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]],
		)

		source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse)

		target_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse)

		if source_warehouse_account == target_warehouse_account:
			# no gl entry as both source and target warehouse has linked to same account.
			self.assertFalse(
				frappe.db.sql(
					"""select * from `tabGL Entry`
				where voucher_type='Stock Entry' and voucher_no=%s""",
					mtn.name,
					as_dict=1,
				)
			)

		else:
			stock_value_diff = abs(
				frappe.db.get_value(
					"Stock Ledger Entry",
					{"voucher_type": "Stock Entry", "voucher_no": mtn.name, "warehouse": "Stores - TCP1"},
					"stock_value_difference",
				)
			)

			self.check_gl_entries(
				"Stock Entry",
				mtn.name,
				sorted(
					[
						[source_warehouse_account, 0.0, stock_value_diff],
						[target_warehouse_account, stock_value_diff, 0.0],
					]
				),
			)

		mtn.cancel()

	def test_repack_multiple_fg(self):
		"Test `is_finished_item` for one item repacked into two items."
		make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)

		repack = frappe.copy_doc(self.globalTestRecords["Stock Entry"][3])
		repack.posting_date = nowdate()
		repack.posting_time = nowtime()

		repack.items[0].qty = 100.0
		repack.items[0].transfer_qty = 100.0
		repack.items[1].qty = 50.0

		repack.append(
			"items",
			{
				"conversion_factor": 1.0,
				"cost_center": "_Test Cost Center - _TC",
				"doctype": "Stock Entry Detail",
				"expense_account": "Stock Adjustment - _TC",
				"basic_rate": 150,
				"item_code": "_Test Item 2",
				"parentfield": "items",
				"qty": 50.0,
				"stock_uom": "_Test UOM",
				"t_warehouse": "_Test Warehouse - _TC",
				"transfer_qty": 50.0,
				"uom": "_Test UOM",
			},
		)
		repack.set_stock_entry_type()
		for row in repack.items:
			if row.t_warehouse:
				row.set_basic_rate_manually = 1

		repack.insert()

		self.assertEqual(repack.items[1].is_finished_item, 1)
		self.assertEqual(repack.items[2].is_finished_item, 1)

		repack.items[1].is_finished_item = 0
		repack.items[2].is_finished_item = 0

		# must raise error if 0 fg in repack entry
		self.assertRaises(FinishedGoodError, repack.validate_finished_goods)

		repack.delete()  # teardown

	def test_repack_no_change_in_valuation(self):
		make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
		make_stock_entry(
			item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100
		)

		repack = frappe.copy_doc(self.globalTestRecords["Stock Entry"][3])
		repack.posting_date = nowdate()
		repack.posting_time = nowtime()
		repack.set_stock_entry_type()
		repack.insert()
		repack.submit()

		self.check_stock_ledger_entries(
			"Stock Entry",
			repack.name,
			[
				["_Test Item", "_Test Warehouse - _TC", -50.0],
				["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1],
			],
		)

		gl_entries = frappe.db.sql(
			"""select account, debit, credit
			from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s
			order by account desc""",
			repack.name,
			as_dict=1,
		)
		self.assertFalse(gl_entries)

	def test_repack_with_additional_costs(self):
		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")

		make_stock_entry(
			item_code="_Test Item",
			target="Stores - TCP1",
			company=company,
			qty=50,
			basic_rate=100,
			expense_account="Stock Adjustment - TCP1",
		)

		repack = make_stock_entry(company=company, purpose="Repack", do_not_save=True)
		repack.posting_date = nowdate()
		repack.posting_time = nowtime()

		default_expense_account = frappe.get_value("Company", company, "default_expense_account")

		items = get_multiple_items()
		repack.items = []
		for item in items:
			repack.append("items", item)

		repack.set(
			"additional_costs",
			[
				{
					"expense_account": default_expense_account,
					"description": "Actual Operating Cost",
					"amount": 1000,
				},
				{
					"expense_account": default_expense_account,
					"description": "Additional Operating Cost",
					"amount": 200,
				},
			],
		)

		repack.set_stock_entry_type()
		repack.insert()
		repack.submit()

		stock_in_hand_account = get_inventory_account(repack.company, repack.get("items")[1].t_warehouse)
		rm_stock_value_diff = abs(
			frappe.db.get_value(
				"Stock Ledger Entry",
				{"voucher_type": "Stock Entry", "voucher_no": repack.name, "item_code": "_Test Item"},
				"stock_value_difference",
			)
		)

		fg_stock_value_diff = abs(
			frappe.db.get_value(
				"Stock Ledger Entry",
				{
					"voucher_type": "Stock Entry",
					"voucher_no": repack.name,
					"item_code": "_Test Item Home Desktop 100",
				},
				"stock_value_difference",
			)
		)

		stock_value_diff = flt(fg_stock_value_diff - rm_stock_value_diff, 2)

		self.assertEqual(stock_value_diff, 1200)

		self.check_gl_entries(
			"Stock Entry",
			repack.name,
			sorted([[stock_in_hand_account, 1200, 0.0], ["Cost of Goods Sold - TCP1", 0.0, 1200.0]]),
		)

	def test_additional_cost_no_rounding_residual_on_stock_adjustment(self):
		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
		warehouse = "Stores - TCP1"
		items = [
			make_item(f"_Test Addl Cost Rounding {x}", {"is_stock_item": 1}).name for x in ("A", "B", "C")
		]

		for item_code in items:
			make_stock_entry(item_code=item_code, target=warehouse, company=company, qty=100, basic_rate=10)

		transfer = make_stock_entry(company=company, purpose="Material Transfer", do_not_save=True)
		transfer.from_warehouse = warehouse
		transfer.to_warehouse = warehouse
		transfer.items = []
		for item_code in items:
			transfer.append(
				"items",
				{
					"item_code": item_code,
					"qty": 100,
					"s_warehouse": warehouse,
					"t_warehouse": warehouse,
					"uom": "Nos",
					"conversion_factor": 1,
				},
			)
		transfer.append(
			"additional_costs",
			{
				"expense_account": "Expenses Included In Valuation - TCP1",
				"description": "freight",
				"amount": 100,
			},
		)
		transfer.insert()
		transfer.submit()

		gl_entries = frappe.get_all(
			"GL Entry",
			filters={"voucher_type": "Stock Entry", "voucher_no": transfer.name},
			fields=["account", "debit", "credit"],
		)
		gl_map = {}
		for row in gl_entries:
			account = gl_map.setdefault(row.account, frappe._dict(debit=0.0, credit=0.0))
			account.debit += row.debit
			account.credit += row.credit

		self.assertNotIn("Stock Adjustment - TCP1", gl_map)

		stock_in_hand_account = get_inventory_account(company, warehouse)
		self.assertEqual(flt(gl_map[stock_in_hand_account].debit, 2), 99.99)
		self.assertEqual(flt(gl_map["Expenses Included In Valuation - TCP1"].credit, 2), 99.99)

	def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle):
		expected_sle.sort(key=lambda x: x[1])

		# check stock ledger entries
		sle = frappe.db.sql(
			"""select item_code, warehouse, actual_qty
			from `tabStock Ledger Entry` where voucher_type = %s
			and voucher_no = %s order by item_code, warehouse, actual_qty""",
			(voucher_type, voucher_no),
			as_list=1,
		)
		self.assertTrue(sle)
		sle.sort(key=lambda x: x[1])

		for i, sle_value in enumerate(sle):
			self.assertEqual(expected_sle[i][0], sle_value[0])
			self.assertEqual(expected_sle[i][1], sle_value[1])
			self.assertEqual(expected_sle[i][2], sle_value[2])

	def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries):
		expected_gl_entries.sort(key=lambda x: x[0])

		gl_entries = frappe.db.sql(
			"""select account, debit, credit
			from `tabGL Entry` where voucher_type=%s and voucher_no=%s
			order by account asc, debit asc""",
			(voucher_type, voucher_no),
			as_list=1,
		)

		self.assertTrue(gl_entries)
		gl_entries.sort(key=lambda x: x[0])
		for i, gle in enumerate(gl_entries):
			self.assertEqual(expected_gl_entries[i][0], gle[0])
			self.assertEqual(expected_gl_entries[i][1], gle[1])
			self.assertEqual(expected_gl_entries[i][2], gle[2])

	def test_serial_no_not_reqd(self):
		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		se.get("items")[0].serial_no = "ABCD"

		bundle_id = make_serial_batch_bundle(
			frappe._dict(
				{
					"item_code": se.get("items")[0].item_code,
					"warehouse": se.get("items")[0].t_warehouse,
					"company": se.company,
					"qty": 2,
					"voucher_type": "Stock Entry",
					"serial_nos": ["ABCD"],
					"posting_date": se.posting_date,
					"posting_time": se.posting_time,
					"do_not_save": True,
				}
			)
		)

		self.assertRaises(frappe.ValidationError, bundle_id.make_serial_and_batch_bundle)

	def test_serial_no_reqd(self):
		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		se.get("items")[0].item_code = "_Test Serialized Item"
		se.get("items")[0].qty = 2
		se.get("items")[0].transfer_qty = 2

		bundle_id = make_serial_batch_bundle(
			frappe._dict(
				{
					"item_code": se.get("items")[0].item_code,
					"warehouse": se.get("items")[0].t_warehouse,
					"company": se.company,
					"qty": 2,
					"voucher_type": "Stock Entry",
					"posting_date": se.posting_date,
					"posting_time": se.posting_time,
					"do_not_save": True,
				}
			)
		)

		self.assertRaises(frappe.ValidationError, bundle_id.make_serial_and_batch_bundle)

	def test_serial_no_qty_less(self):
		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		se.get("items")[0].item_code = "_Test Serialized Item"
		se.get("items")[0].qty = 2
		se.get("items")[0].serial_no = "ABCD"
		se.get("items")[0].transfer_qty = 2

		bundle_id = make_serial_batch_bundle(
			frappe._dict(
				{
					"item_code": se.get("items")[0].item_code,
					"warehouse": se.get("items")[0].t_warehouse,
					"company": se.company,
					"qty": 2,
					"serial_nos": ["ABCD"],
					"voucher_type": "Stock Entry",
					"posting_date": se.posting_date,
					"posting_time": se.posting_time,
					"do_not_save": True,
				}
			)
		)

		self.assertRaises(frappe.ValidationError, bundle_id.make_serial_and_batch_bundle)

	def test_serial_no_transfer_in(self):
		serial_nos = ["ABCD1", "EFGH1"]
		for serial_no in serial_nos:
			if not frappe.db.exists("Serial No", serial_no):
				doc = frappe.new_doc("Serial No")
				doc.serial_no = serial_no
				doc.item_code = "_Test Serialized Item"
				doc.company = "_Test Company"
				doc.insert(ignore_permissions=True)

		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		se.get("items")[0].item_code = "_Test Serialized Item"
		se.get("items")[0].qty = 2
		se.get("items")[0].transfer_qty = 2
		se.set_stock_entry_type()

		se.get("items")[0].serial_and_batch_bundle = make_serial_batch_bundle(
			frappe._dict(
				{
					"item_code": se.get("items")[0].item_code,
					"warehouse": se.get("items")[0].t_warehouse,
					"company": se.company,
					"qty": 2,
					"voucher_type": "Stock Entry",
					"serial_nos": serial_nos,
					"posting_date": se.posting_date,
					"posting_time": se.posting_time,
					"do_not_submit": True,
				}
			)
		).name

		se.insert()
		se.submit()

		self.assertTrue(frappe.db.get_value("Serial No", "ABCD1", "warehouse"))
		self.assertTrue(frappe.db.get_value("Serial No", "EFGH1", "warehouse"))

		se.cancel()
		self.assertFalse(frappe.db.get_value("Serial No", "ABCD1", "warehouse"))

	def test_serial_by_series(self):
		se = make_serialized_item(self)

		serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)

		self.assertTrue(frappe.db.exists("Serial No", serial_nos[0]))
		self.assertTrue(frappe.db.exists("Serial No", serial_nos[1]))

		return se, serial_nos

	def test_serial_move(self):
		se = make_serialized_item(self)
		serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
		frappe.flags.use_serial_and_batch_fields = True

		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		se.purpose = "Material Transfer"
		se.get("items")[0].item_code = "_Test Serialized Item With Series"
		se.get("items")[0].qty = 1
		se.get("items")[0].transfer_qty = 1
		se.get("items")[0].serial_no = [serial_no]
		se.get("items")[0].s_warehouse = "_Test Warehouse - _TC"
		se.get("items")[0].t_warehouse = "_Test Warehouse 1 - _TC"
		se.set_stock_entry_type()
		se.insert()
		se.submit()
		self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC")

		se.cancel()
		self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC")
		frappe.flags.use_serial_and_batch_fields = False

	def test_serial_cancel(self):
		se, serial_nos = self.test_serial_by_series()
		se.load_from_db()
		serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]

		se.cancel()
		self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))

	def test_serial_batch_item_stock_entry(self):
		"""
		Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
		2) Cancel same Stock Entry
		Expected Result: 1) Batch is created with Reference in Serial No
		2) Batch is deleted and Serial No is Inactive
		"""
		from erpnext.stock.doctype.batch.batch import get_batch_qty

		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"})

		se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
		batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
		serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0]
		batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)

		batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
		self.assertEqual(batch_in_serial_no, batch_no)

		self.assertEqual(batch_qty, 1)

		se.cancel()

		batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
		self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), None)

	def test_warehouse_company_validation(self):
		frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company")
		frappe.get_doc("User", "test2@example.com").add_roles(
			"Sales User", "Sales Manager", "Stock User", "Stock Manager"
		)
		with self.set_user("test2@example.com"):
			from erpnext.stock.utils import InvalidWarehouseCompany

			st1 = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
			st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1"
			st1.set_stock_entry_type()
			st1.insert()
			self.assertRaises(InvalidWarehouseCompany, st1.submit)

	# permission tests
	def test_warehouse_user(self):
		add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
		add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
		add_user_permission("Company", "_Test Company 1", "test2@example.com")
		test_user = frappe.get_doc("User", "test@example.com")
		test_user.add_roles("Sales User", "Sales Manager", "Stock User")
		test_user.remove_roles("Stock Manager", "System Manager")

		frappe.get_doc("User", "test2@example.com").add_roles(
			"Sales User", "Sales Manager", "Stock User", "Stock Manager"
		)

		st1 = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		st1.company = "_Test Company 1"

		with self.set_user("test@example.com"):
			st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1"
			self.assertRaises(frappe.PermissionError, st1.insert)

		test_user.add_roles("System Manager")

		with self.set_user("test2@example.com"):
			st1 = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
			st1.company = "_Test Company 1"
			st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1"
			st1.get("items")[0].expense_account = "Stock Adjustment - _TC1"
			st1.get("items")[0].cost_center = "Main - _TC1"
			st1.set_stock_entry_type()
			st1.insert()
			st1.submit()
			st1.cancel()

		remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
		remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
		remove_user_permission("Company", "_Test Company 1", "test2@example.com")

	def test_freeze_stocks(self):
		frappe.db.set_single_value("Stock Settings", "stock_auth_role", "")

		# test freeze_stocks_upto
		frappe.db.set_single_value("Stock Settings", "stock_frozen_upto", add_days(nowdate(), 5))
		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0]).insert()
		self.assertRaises(StockFreezeError, se.submit)

		frappe.db.set_single_value("Stock Settings", "stock_frozen_upto", "")

		# test freeze_stocks_upto_days
		frappe.db.set_single_value("Stock Settings", "stock_frozen_upto_days", -1)
		se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])
		se.set_posting_time = 1
		se.posting_date = nowdate()
		se.set_stock_entry_type()
		se.insert()
		self.assertRaises(StockFreezeError, se.submit)
		frappe.db.set_single_value("Stock Settings", "stock_frozen_upto_days", 0)

	def test_work_order(self):
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as _make_stock_entry,
		)

		bom_no, bom_operation_cost = frappe.db.get_value(
			"BOM", {"item": "_Test FG Item 2", "is_default": 1, "docstatus": 1}, ["name", "operating_cost"]
		)

		work_order = frappe.new_doc("Work Order")
		work_order.update(
			{
				"company": "_Test Company",
				"fg_warehouse": "_Test Warehouse 1 - _TC",
				"production_item": "_Test FG Item 2",
				"bom_no": bom_no,
				"qty": 1.0,
				"stock_uom": "_Test UOM",
				"wip_warehouse": "_Test Warehouse - _TC",
				"additional_operating_cost": 1000,
			}
		)
		work_order.insert()
		work_order.submit()

		make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
		make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)

		stock_entry = _make_stock_entry(work_order.name, "Manufacture", 1)

		rm_cost = 0
		for d in stock_entry.get("items"):
			if d.item_code != "_Test FG Item 2":
				rm_cost += flt(d.amount)
		fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items"))).amount
		self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))

	@ERPNextTestSuite.change_settings("System Settings", {"float_precision": 3})
	@ERPNextTestSuite.change_settings("Manufacturing Settings", {"backflush_raw_materials_based_on": "BOM"})
	def test_material_transfer_for_manufacture_qty_precision(self):
		work_order = frappe.new_doc("Work Order")
		work_order.append(
			"required_items",
			{
				"item_code": "_Test Item",
				"required_qty": 33.876,
				"transferred_qty": 33.875,
			},
		)

		stock_entry = frappe.new_doc("Stock Entry")
		stock_entry.work_order = "Test Work Order"
		stock_entry.append(
			"items",
			{
				"item_code": "_Test Item",
				"s_warehouse": "_Test Warehouse - _TC",
				"qty": 0.001,
				"uom": "Nos",
			},
		)

		stock_entry.pro_doc = work_order
		stock_entry._validate_no_excess_transfer()

		stock_entry.items[0].qty = 0.002
		with self.assertRaises(frappe.ValidationError):
			stock_entry._validate_no_excess_transfer()

	@ERPNextTestSuite.change_settings("Manufacturing Settings", {"material_consumption": 1})
	def test_work_order_manufacture_with_material_consumption(self):
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as _make_stock_entry,
		)

		bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_default": 1, "docstatus": 1})

		work_order = frappe.new_doc("Work Order")
		work_order.update(
			{
				"company": "_Test Company",
				"fg_warehouse": "_Test Warehouse 1 - _TC",
				"production_item": "_Test FG Item",
				"bom_no": bom_no,
				"qty": 1.0,
				"stock_uom": "_Test UOM",
				"wip_warehouse": "_Test Warehouse - _TC",
			}
		)
		work_order.insert()
		work_order.submit()

		make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0)
		make_stock_entry(
			item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0
		)

		s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
		for d in s.get("items"):
			d.s_warehouse = "Stores - _TC"
		s.insert()
		s.submit()

		# When Stock Entry has RM and FG
		s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
		s.save()
		rm_cost = 0
		for d in s.get("items"):
			if d.s_warehouse:
				rm_cost += d.amount
		fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount
		secondary_item_cost = next(filter(lambda x: x.type or x.is_legacy_scrap_item, s.get("items"))).amount
		self.assertEqual(fg_cost, flt(rm_cost - secondary_item_cost, 2))

		# When Stock Entry has only FG + Scrap
		s.items.pop(0)
		s.items.pop(0)
		s.submit()

		rm_cost = 0
		for d in s.get("items"):
			if d.s_warehouse:
				rm_cost += d.amount
		self.assertEqual(rm_cost, 0)
		expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
		fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item", s.get("items"))).amount
		self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))

	def test_variant_work_order(self):
		bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1})

		make_item_variant()  # make variant of _Test Variant Item if absent

		work_order = frappe.new_doc("Work Order")
		work_order.update(
			{
				"company": "_Test Company",
				"fg_warehouse": "_Test Warehouse 1 - _TC",
				"production_item": "_Test Variant Item-S",
				"bom_no": bom_no,
				"qty": 1.0,
				"stock_uom": "_Test UOM",
				"wip_warehouse": "_Test Warehouse - _TC",
				"skip_transfer": 1,
			}
		)
		work_order.insert()
		work_order.submit()

		from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry

		stock_entry = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
		stock_entry.insert()
		self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.items])

	def test_nagative_stock_for_batch(self):
		item = make_item(
			"_Test Batch Negative Item",
			{
				"has_batch_no": 1,
				"create_new_batch": 1,
				"batch_number_series": "B-BATCH-.##",
				"is_stock_item": 1,
			},
		)

		make_stock_entry(item_code=item.name, target="_Test Warehouse - _TC", qty=50, basic_rate=100)

		ste = frappe.new_doc("Stock Entry")
		ste.purpose = "Material Issue"
		ste.company = "_Test Company"
		for qty in [50, 20, 30]:
			ste.append(
				"items",
				{
					"item_code": item.name,
					"s_warehouse": "_Test Warehouse - _TC",
					"qty": qty,
					"uom": item.stock_uom,
					"stock_uom": item.stock_uom,
					"conversion_factor": 1,
					"transfer_qty": qty,
				},
			)

		ste.set_stock_entry_type()
		ste.insert()
		make_stock_entry(item_code=item.name, target="_Test Warehouse - _TC", qty=50, basic_rate=100)

		self.assertRaises(frappe.ValidationError, ste.submit)

	def test_quality_check_for_secondary_item(self):
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as _make_stock_entry,
		)

		secondary_item = "_Test Scrap Item 1"
		make_item(secondary_item, {"is_stock_item": 1, "is_purchase_item": 0})

		bom_name = frappe.db.get_value("BOM Secondary Item", {"docstatus": 1}, "parent")
		production_item = frappe.db.get_value("BOM", bom_name, "item")

		work_order = frappe.new_doc("Work Order")
		work_order.production_item = production_item
		work_order.update(
			{
				"company": "_Test Company",
				"fg_warehouse": "_Test Warehouse 1 - _TC",
				"production_item": production_item,
				"bom_no": bom_name,
				"qty": 1.0,
				"stock_uom": frappe.db.get_value("Item", production_item, "stock_uom"),
				"skip_transfer": 1,
			}
		)

		work_order.get_items_and_operations_from_bom()
		work_order.submit()

		stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
		for row in stock_entry.items:
			if row.s_warehouse:
				make_stock_entry(
					item_code=row.item_code,
					target=row.s_warehouse,
					qty=row.qty,
					basic_rate=row.basic_rate or 100,
				)

			if row.type or row.is_legacy_scrap_item:
				row.item_code = secondary_item
				row.uom = frappe.db.get_value("Item", secondary_item, "stock_uom")
				row.stock_uom = frappe.db.get_value("Item", secondary_item, "stock_uom")

		stock_entry.inspection_required = 1
		stock_entry.save()

		self.assertTrue([row.item_code for row in stock_entry.items if row.type or row.is_legacy_scrap_item])

		for row in stock_entry.items:
			if not row.type and not row.is_legacy_scrap_item:
				qc = frappe.get_doc(
					{
						"doctype": "Quality Inspection",
						"reference_name": stock_entry.name,
						"inspected_by": "Administrator",
						"reference_type": "Stock Entry",
						"inspection_type": "In Process",
						"status": "Accepted",
						"sample_size": 1,
						"item_code": row.item_code,
					}
				)

				qc_name = qc.submit()
				row.quality_inspection = qc_name

		stock_entry.reload()
		stock_entry.submit()
		for row in stock_entry.items:
			if row.type or row.is_legacy_scrap_item:
				self.assertFalse(row.quality_inspection)
			else:
				self.assertTrue(row.quality_inspection)

	def test_quality_check(self):
		item_code = "_Test Item For QC"
		if not frappe.db.exists("Item", item_code):
			create_item(item_code)

		repack = frappe.copy_doc(self.globalTestRecords["Stock Entry"][3])
		repack.inspection_required = 1
		for d in repack.items:
			if not d.s_warehouse and d.t_warehouse:
				d.item_code = item_code
				d.qty = 1
				d.uom = "Nos"
				d.stock_uom = "Nos"
				d.basic_rate = 5000

		repack.insert()
		self.assertRaises(frappe.ValidationError, repack.submit)

	def test_check_item_quality_inspection_returns_items_for_stock_entry(self):
		from erpnext.controllers.stock_controller import check_item_quality_inspection

		items = [
			{"item_code": "_Test Item", "qty": 1},
			{"item_code": "_Test Item Home Desktop 100", "qty": 1},
		]

		se_result = check_item_quality_inspection("Stock Entry", 0, items)
		self.assertEqual(len(se_result), 2)

		# a doctype not in INSPECTION_FIELDNAME_MAP and not a Stock Entry returns nothing
		self.assertEqual(check_item_quality_inspection("Material Request", 0, items), [])

	@ERPNextTestSuite.change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
	def test_quality_inspection_across_stock_entry_purposes(self):
		from erpnext.controllers.stock_controller import check_item_quality_inspection
		from erpnext.exceptions import QualityInspectionRejectedError, QualityInspectionRequiredError
		from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
			create_quality_inspection,
		)

		item_code = "_Test Item For QI Purposes"
		if not frappe.db.exists("Item", item_code):
			create_item(item_code, is_stock_item=1)

		s_wh = "Stores - _TC"
		t_wh = "_Test Warehouse - _TC"
		# stock the source warehouse for transfer / issue purposes
		make_stock_entry(item_code=item_code, target=s_wh, qty=100, basic_rate=100)

		# purpose -> warehouses for the moved row and the direction QI is required on:
		# Material Receipt inspects the inward row, Transfer/Issue inspect the outgoing row.
		purposes = {
			"Material Receipt": {"warehouses": {"to_warehouse": t_wh}, "inspection_type": "Incoming"},
			"Material Transfer": {
				"warehouses": {"from_warehouse": s_wh, "to_warehouse": t_wh},
				"inspection_type": "Outgoing",
			},
			"Material Issue": {"warehouses": {"from_warehouse": s_wh}, "inspection_type": "Outgoing"},
		}

		for purpose, config in purposes.items():
			with self.subTest(purpose=purpose):
				warehouses = config["warehouses"]
				inspection_type = config["inspection_type"]

				se = make_stock_entry(
					item_code=item_code,
					qty=5,
					basic_rate=100,
					purpose=purpose,
					inspection_required=True,
					do_not_submit=True,
					**warehouses,
				)

				# QI can be created from the Stock Entry for any purpose
				allowed = check_item_quality_inspection("Stock Entry", 0, se.as_dict().get("items"))
				self.assertTrue(any(row.get("item_code") == item_code for row in allowed))

				# entry without QI must block submission
				self.assertRaises(QualityInspectionRequiredError, se.submit)

				# a rejected QI must also block submission
				se_rej = make_stock_entry(
					item_code=item_code,
					qty=5,
					basic_rate=100,
					purpose=purpose,
					inspection_required=True,
					do_not_submit=True,
					**warehouses,
				)
				create_quality_inspection(
					reference_type="Stock Entry",
					reference_name=se_rej.name,
					item_code=item_code,
					inspection_type=inspection_type,
					status="Rejected",
				)
				se_rej.reload()
				self.assertRaises(QualityInspectionRejectedError, se_rej.submit)

				# a submitted, accepted QI links itself to the inspected row; submission then succeeds
				se_ok = make_stock_entry(
					item_code=item_code,
					qty=5,
					basic_rate=100,
					purpose=purpose,
					inspection_required=True,
					do_not_submit=True,
					**warehouses,
				)
				create_quality_inspection(
					reference_type="Stock Entry",
					reference_name=se_ok.name,
					item_code=item_code,
					inspection_type=inspection_type,
					status="Accepted",
				)
				se_ok.reload()
				se_ok.submit()
				self.assertEqual(se_ok.docstatus, 1)

	@ERPNextTestSuite.change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
	def test_quality_inspection_required_for_manufacture(self):
		from erpnext.exceptions import QualityInspectionRejectedError, QualityInspectionRequiredError
		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_wo_stock_entry,
		)
		from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
			create_quality_inspection,
		)

		wo = make_wo_order_test_record(qty=1)
		make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
		make_stock_entry(
			item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100
		)

		# transfer raw materials to WIP (no inspection on the transfer)
		transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
		for d in transfer.get("items"):
			d.s_warehouse = "Stores - _TC"
		transfer.insert()
		transfer.submit()

		# manufacture with inspection required
		mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1))
		mfg.inspection_required = 1
		mfg.insert()
		self.assertRaises(QualityInspectionRequiredError, mfg.submit)

		# a rejected QI on the finished-good row must also block submission
		qi = create_quality_inspection(
			reference_type="Stock Entry",
			reference_name=mfg.name,
			item_code=wo.production_item,
			inspection_type="Incoming",
			status="Rejected",
		)
		mfg.reload()
		self.assertRaises(QualityInspectionRejectedError, mfg.submit)

		# accepting the QI then allows submission
		frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
		mfg.reload()
		mfg.submit()
		self.assertEqual(mfg.docstatus, 1)

	@ERPNextTestSuite.change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
	def test_quality_inspection_required_for_material_transfer_for_manufacture(self):
		from erpnext.exceptions import QualityInspectionRejectedError, QualityInspectionRequiredError
		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_wo_stock_entry,
		)
		from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
			create_quality_inspection,
		)

		wo = make_wo_order_test_record(qty=1)
		make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
		make_stock_entry(
			item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100
		)

		transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
		for d in transfer.get("items"):
			d.s_warehouse = "Stores - _TC"
		transfer.inspection_required = 1
		transfer.insert()
		self.assertRaises(QualityInspectionRequiredError, transfer.submit)

		# a rejected QI on any row moved into WIP must block submission;
		# every raw-material row moved into WIP needs a QI
		qis = []
		for item_code in {d.item_code for d in transfer.items if d.t_warehouse}:
			qis.append(
				create_quality_inspection(
					reference_type="Stock Entry",
					reference_name=transfer.name,
					item_code=item_code,
					inspection_type="Incoming",
					status="Rejected",
				)
			)
		transfer.reload()
		self.assertRaises(QualityInspectionRejectedError, transfer.submit)

		# accepting every QI then allows submission
		for qi in qis:
			frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
		transfer.reload()
		transfer.submit()
		self.assertEqual(transfer.docstatus, 1)

	def test_quality_inspection_required_for_send_to_subcontractor(self):
		from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
		from erpnext.controllers.tests.test_subcontracting_controller import (
			get_subcontracting_order,
			make_service_item,
		)
		from erpnext.exceptions import QualityInspectionRequiredError
		from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
			create_quality_inspection,
		)

		make_service_item("Subcontracted Service Item 1")
		sco = get_subcontracting_order(
			service_items=[
				{
					"warehouse": "_Test Warehouse - _TC",
					"item_code": "Subcontracted Service Item 1",
					"qty": 10,
					"rate": 500,
					"fg_item": "_Test FG Item",
					"fg_item_qty": 10,
				}
			]
		)
		make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
		make_stock_entry(
			item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100
		)

		se = frappe.get_doc(make_rm_stock_entry(sco.name))
		se.from_warehouse = "_Test Warehouse - _TC"
		se.to_warehouse = "_Test Warehouse - _TC"
		se.stock_entry_type = "Send to Subcontractor"
		se.inspection_required = 1
		se.insert()
		self.assertRaises(QualityInspectionRequiredError, se.submit)

		for item_code in {row.item_code for row in se.items if row.t_warehouse}:
			create_quality_inspection(
				reference_type="Stock Entry",
				reference_name=se.name,
				item_code=item_code,
				inspection_type="Outgoing",
				status="Accepted",
			)
		se.reload()
		se.submit()
		self.assertEqual(se.docstatus, 1)

	@ERPNextTestSuite.change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
	def test_quality_inspection_required_for_disassemble(self):
		from erpnext.exceptions import QualityInspectionRejectedError, QualityInspectionRequiredError
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_wo_stock_entry,
		)
		from erpnext.stock.doctype.item.test_item import make_item
		from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
			create_quality_inspection,
		)

		source_warehouse = "Stores - _TC"
		fg_item = make_item("Test Disassemble FG QI", {"is_stock_item": 1}).name
		raw_materials = ["Test Disassemble RM QI 1", "Test Disassemble RM QI 2"]
		for item in raw_materials:
			make_item(item, {"is_stock_item": 1})
			make_stock_entry(item_code=item, target=source_warehouse, qty=5, basic_rate=100)

		make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)

		wo = make_wo_order_test_record(
			item=fg_item, qty=1, source_warehouse=source_warehouse, skip_transfer=1
		)

		# manufacture the FG so there is something to disassemble
		mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1))
		for row in mfg.items:
			if row.item_code in raw_materials:
				row.s_warehouse = source_warehouse
		mfg.submit()

		# disassemble with inspection required -> the consumed (outgoing) rows need a QI
		dis = frappe.get_doc(make_wo_stock_entry(wo.name, "Disassemble", 1))
		dis.inspection_required = 1
		dis.insert()
		self.assertRaises(QualityInspectionRequiredError, dis.submit)

		# a rejected QI on any consumed (outgoing) row must also block submission
		qis = []
		for item_code in {row.item_code for row in dis.items if row.s_warehouse}:
			qis.append(
				create_quality_inspection(
					reference_type="Stock Entry",
					reference_name=dis.name,
					item_code=item_code,
					inspection_type="Outgoing",
					status="Rejected",
				)
			)
		dis.reload()
		self.assertRaises(QualityInspectionRejectedError, dis.submit)

		# accepting every QI then allows submission
		for qi in qis:
			frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
		dis.reload()
		dis.submit()
		self.assertEqual(dis.docstatus, 1)

	def test_customer_provided_parts_se(self):
		create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
		se = make_stock_entry(
			item_code="CUST-0987", purpose="Material Receipt", qty=4, to_warehouse="_Test Warehouse - _TC"
		)
		self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
		self.assertEqual(se.get("items")[0].amount, 0)

	def test_zero_incoming_rate(self):
		"""Make sure incoming rate of 0 is allowed while consuming.

		qty  | rate | valuation rate
		 1   | 100  | 100
		 1   | 0    | 50
		-1   | 100  | 0
		-1   | 0  <--- assert this
		"""
		item_code = "_TestZeroVal"
		warehouse = "_Test Warehouse - _TC"
		create_item("_TestZeroVal")
		_receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100)
		receipt2 = make_stock_entry(
			item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True
		)
		receipt2.items[0].allow_zero_valuation_rate = 1
		receipt2.save()
		receipt2.submit()

		issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)

		value_diff = frappe.db.get_value(
			"Stock Ledger Entry",
			{"voucher_no": issue.name, "voucher_type": "Stock Entry"},
			"stock_value_difference",
		)
		self.assertEqual(value_diff, -100)

		issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
		value_diff = frappe.db.get_value(
			"Stock Ledger Entry",
			{"voucher_no": issue2.name, "voucher_type": "Stock Entry"},
			"stock_value_difference",
		)
		self.assertEqual(value_diff, 0)

	def test_gle_for_opening_stock_entry(self):
		mr = make_stock_entry(
			item_code="_Test Item",
			target="Stores - TCP1",
			company="_Test Company with perpetual inventory",
			qty=50,
			basic_rate=100,
			expense_account="Stock Adjustment - TCP1",
			is_opening="Yes",
			do_not_save=True,
		)

		self.assertRaises(OpeningEntryAccountError, mr.save)

		mr.items[0].expense_account = "Temporary Opening - TCP1"

		mr.save()
		mr.submit()

		is_opening = frappe.db.get_value(
			"GL Entry",
			filters={"voucher_type": "Stock Entry", "voucher_no": mr.name},
			fieldname="is_opening",
		)
		self.assertEqual(is_opening, "Yes")

	def test_total_basic_amount_zero(self):
		se = frappe.get_doc(
			{
				"doctype": "Stock Entry",
				"purpose": "Material Receipt",
				"stock_entry_type": "Material Receipt",
				"posting_date": nowdate(),
				"company": "_Test Company with perpetual inventory",
				"items": [
					{
						"item_code": "_Test Item",
						"description": "_Test Item",
						"qty": 1,
						"basic_rate": 0,
						"uom": "Nos",
						"t_warehouse": "Stores - TCP1",
						"allow_zero_valuation_rate": 1,
						"cost_center": "Main - TCP1",
					},
					{
						"item_code": "_Test Item",
						"description": "_Test Item",
						"qty": 2,
						"basic_rate": 0,
						"uom": "Nos",
						"t_warehouse": "Stores - TCP1",
						"allow_zero_valuation_rate": 1,
						"cost_center": "Main - TCP1",
					},
				],
				"additional_costs": [
					{
						"expense_account": "Miscellaneous Expenses - TCP1",
						"amount": 100,
						"description": "miscellanous",
					}
				],
			}
		)
		se.insert()
		se.submit()

		self.check_gl_entries(
			"Stock Entry",
			se.name,
			sorted([["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]]),
		)

	def test_conversion_factor_change(self):
		frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
		repack_entry = frappe.copy_doc(self.globalTestRecords["Stock Entry"][3])
		repack_entry.posting_date = nowdate()
		repack_entry.posting_time = nowtime()
		repack_entry.set_stock_entry_type()
		repack_entry.insert()

		# check current uom and conversion factor
		self.assertTrue(repack_entry.items[0].uom, "_Test UOM")
		self.assertTrue(repack_entry.items[0].conversion_factor, 1)

		# change conversion factor
		repack_entry.items[0].uom = "_Test UOM 1"
		repack_entry.items[0].stock_uom = "_Test UOM 1"
		repack_entry.items[0].conversion_factor = 2
		repack_entry.save()
		repack_entry.submit()

		self.assertEqual(repack_entry.items[0].conversion_factor, 2)
		self.assertEqual(repack_entry.items[0].uom, "_Test UOM 1")
		self.assertEqual(repack_entry.items[0].qty, 50)
		self.assertEqual(repack_entry.items[0].transfer_qty, 100)

		frappe.db.set_default("allow_negative_stock", 0)

	def test_additional_cost_distribution_manufacture(self):
		se = frappe.get_doc(
			doctype="Stock Entry",
			purpose="Manufacture",
			additional_costs=[frappe._dict(base_amount=100)],
			items=[
				frappe._dict(item_code="RM", basic_amount=10),
				frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1),
				frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X"),
			],
		)

		se.distribute_additional_costs()

		distributed_costs = [d.additional_cost for d in se.items]
		self.assertEqual([0.0, 100.0, 0.0], distributed_costs)

	def test_additional_cost_distribution_non_manufacture(self):
		se = frappe.get_doc(
			doctype="Stock Entry",
			purpose="Material Receipt",
			additional_costs=[frappe._dict(base_amount=100)],
			items=[
				frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"),
				frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X"),
			],
		)

		se.distribute_additional_costs()

		distributed_costs = [d.additional_cost for d in se.items]
		self.assertEqual([40.0, 60.0], distributed_costs)

	def test_independent_manufacture_entry(self):
		"Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked."
		se = frappe.get_doc(
			doctype="Stock Entry",
			purpose="Manufacture",
			stock_entry_type="Manufacture",
			company="_Test Company",
			items=[
				frappe._dict(
					item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"
				),
				frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC"),
			],
		)
		frappe.db.set_value("Company", "_Test Company", "stock_adjustment_account", "Stock Adjustment - _TC")
		# SE must have atleast one FG
		self.assertRaises(FinishedGoodError, se.save)

		se.items[0].is_finished_item = 1
		se.items[1].is_finished_item = 1
		# SE cannot have multiple FGs
		self.assertRaises(FinishedGoodError, se.save)

		se.items[0].is_finished_item = 0
		se.save()

		# Check if FG cost is calculated based on RM total cost
		# RM total cost = 200, FG rate = 200/4(FG qty) =  50
		self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate / 4))
		self.assertEqual(se.value_difference, 0.0)
		self.assertEqual(se.total_incoming_value, se.total_outgoing_value)

		self.assertEqual(se.items[0].expense_account, "_Test Account Cost for Goods Sold - _TC")
		self.assertEqual(se.items[1].expense_account, "_Test Account Cost for Goods Sold - _TC")

	@ERPNextTestSuite.change_settings("Stock Settings", {"allow_negative_stock": 0})
	def test_future_negative_sle(self):
		# Initialize item, batch, warehouse, opening qty
		item_code = "_Test Future Neg Item"
		batch_no = "_Test Future Neg Batch"
		warehouses = ["_Test Future Neg Warehouse Source", "_Test Future Neg Warehouse Destination"]
		warehouse_names = initialize_records_for_future_negative_sle_test(
			item_code, batch_no, warehouses, opening_qty=2, posting_date="2021-07-01"
		)

		# Executing an illegal sequence should raise an error
		sequence_of_entries = [
			dict(
				item_code=item_code,
				qty=2,
				from_warehouse=warehouse_names[0],
				to_warehouse=warehouse_names[1],
				batch_no=batch_no,
				posting_date="2021-07-03",
				purpose="Material Transfer",
			),
			dict(
				item_code=item_code,
				qty=2,
				from_warehouse=warehouse_names[1],
				to_warehouse=warehouse_names[0],
				batch_no=batch_no,
				posting_date="2021-07-04",
				purpose="Material Transfer",
			),
			dict(
				item_code=item_code,
				qty=2,
				from_warehouse=warehouse_names[0],
				to_warehouse=warehouse_names[1],
				batch_no=batch_no,
				posting_date="2021-07-02",  # Illegal SE
				purpose="Material Transfer",
			),
		]

		self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)

	@ERPNextTestSuite.change_settings("Stock Settings", {"allow_negative_stock": 0})
	def test_future_negative_sle_batch(self):
		from erpnext.stock.doctype.batch.test_batch import TestBatch

		# Initialize item, batch, warehouse, opening qty
		item_code = "_Test MultiBatch Item"
		TestBatch.make_batch_item(item_code)

		batch_nos = []  # store generate batches
		warehouse = "_Test Warehouse - _TC"

		se1 = make_stock_entry(
			item_code=item_code,
			qty=2,
			to_warehouse=warehouse,
			posting_date="2021-09-01",
			purpose="Material Receipt",
		)
		batch_nos.append(get_batch_from_bundle(se1.items[0].serial_and_batch_bundle))
		se2 = make_stock_entry(
			item_code=item_code,
			qty=2,
			to_warehouse=warehouse,
			posting_date="2021-09-03",
			purpose="Material Receipt",
		)
		batch_nos.append(get_batch_from_bundle(se2.items[0].serial_and_batch_bundle))

		with self.assertRaises(frappe.ValidationError):
			make_stock_entry(
				item_code=item_code,
				qty=1,
				from_warehouse=warehouse,
				batch_no=batch_nos[1],
				posting_date="2021-09-02",  # backdated consumption of 2nd batch
				purpose="Material Issue",
			)

	def test_multi_batch_value_diff(self):
		"""Test value difference on stock entry in case of multi-batch.
		| Stock entry | batch | qty | rate | value diff on SE             |
		| ---         | ---   | --- | ---  | ---                          |
		| receipt     | A     | 1   | 10   | 30                           |
		| receipt     | B     | 1   | 20   |                              |
		| issue       | A     | -1  | 10   | -30 (to assert after submit) |
		| issue       | B     | -1  | 20   |                              |
		"""
		from erpnext.stock.doctype.batch.test_batch import TestBatch

		item_code = "_TestMultibatchFifo"
		TestBatch.make_batch_item(item_code)
		warehouse = "_Test Warehouse - _TC"
		receipt = make_stock_entry(
			item_code=item_code,
			qty=1,
			rate=10,
			to_warehouse=warehouse,
			purpose="Material Receipt",
			do_not_save=True,
		)
		receipt.append(
			"items", frappe.copy_doc(receipt.items[0], ignore_no_copy=False).update({"basic_rate": 20})
		)
		receipt.save()
		receipt.submit()
		receipt.load_from_db()

		batches = frappe._dict(
			{get_batch_from_bundle(row.serial_and_batch_bundle): row.qty for row in receipt.items}
		)

		self.assertEqual(receipt.value_difference, 30)

		issue = make_stock_entry(
			item_code=item_code,
			qty=2,
			from_warehouse=warehouse,
			purpose="Material Issue",
			do_not_save=True,
			batches=batches,
		)

		issue.save()
		issue.submit()
		issue.reload()  # reload because reposting current voucher updates rate
		self.assertEqual(issue.value_difference, -30)

	def test_transfer_qty_validation(self):
		se = make_stock_entry(item_code="_Test Item", do_not_save=True, qty=0.001, rate=100)
		se.items[0].uom = "Kg"
		se.items[0].conversion_factor = 0.002

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

	def test_mapped_stock_entry(self):
		"Check if rate and stock details are populated in mapped SE given warehouse."
		from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_stock_entry
		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

		item_code = "_TestMappedItem"
		create_item(item_code, is_stock_item=True)

		pr = make_purchase_receipt(
			item_code=item_code, qty=2, rate=100, company="_Test Company", warehouse="Stores - _TC"
		)

		mapped_se = make_stock_entry(pr.name)

		self.assertEqual(mapped_se.items[0].s_warehouse, "Stores - _TC")
		self.assertEqual(mapped_se.items[0].actual_qty, 2)
		self.assertEqual(mapped_se.items[0].basic_rate, 100)
		self.assertEqual(mapped_se.items[0].basic_amount, 200)

	def test_stock_entry_item_details(self):
		item = make_item()

		se = make_stock_entry(
			item_code=item.name, qty=1, to_warehouse="_Test Warehouse - _TC", do_not_submit=True
		)

		self.assertEqual(se.items[0].item_name, item.item_name)
		se.items[0].item_name = "wat"
		se.items[0].stock_uom = "Kg"
		se.save()

		self.assertEqual(se.items[0].item_name, item.item_name)
		self.assertEqual(se.items[0].stock_uom, item.stock_uom)

	@ERPNextTestSuite.change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
	def test_reposting_for_depedent_warehouse(self):
		from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
		from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

		# Inward at WH1 warehouse (Component)
		# 1st Repack (Component (WH1) - Subcomponent (WH2))
		# 2nd Repack (Subcomponent (WH2) - FG Item (WH3))
		# Material Transfer of FG Item -> WH 3 -> WH2 -> Wh1 (Two transfer entries)
		# Backdated transction which should update valuation rate in repack as well trasfer entries

		for item_code in ["FG Item 1", "Sub Component 1", "Component 1"]:
			create_item(item_code)

		for warehouse in ["WH 1", "WH 2", "WH 3"]:
			create_warehouse(warehouse)

		make_stock_entry(
			item_code="Component 1",
			rate=100,
			purpose="Material Receipt",
			qty=10,
			to_warehouse="WH 1 - _TC",
			posting_date=add_days(nowdate(), -10),
		)

		repack1 = make_stock_entry(
			item_code="Component 1",
			purpose="Repack",
			do_not_save=True,
			qty=10,
			from_warehouse="WH 1 - _TC",
			posting_date=add_days(nowdate(), -9),
		)

		repack1.append(
			"items",
			{
				"item_code": "Sub Component 1",
				"qty": 10,
				"t_warehouse": "WH 2 - _TC",
				"transfer_qty": 10,
				"uom": "Nos",
				"stock_uom": "Nos",
				"conversion_factor": 1.0,
			},
		)

		repack1.save()
		repack1.submit()

		self.assertEqual(repack1.items[1].basic_rate, 100)
		self.assertEqual(repack1.items[1].amount, 1000)

		repack2 = make_stock_entry(
			item_code="Sub Component 1",
			purpose="Repack",
			do_not_save=True,
			qty=10,
			from_warehouse="WH 2 - _TC",
			posting_date=add_days(nowdate(), -8),
		)

		repack2.append(
			"items",
			{
				"item_code": "FG Item 1",
				"qty": 10,
				"t_warehouse": "WH 3 - _TC",
				"transfer_qty": 10,
				"uom": "Nos",
				"stock_uom": "Nos",
				"conversion_factor": 1.0,
			},
		)

		repack2.save()
		repack2.submit()

		self.assertEqual(repack2.items[1].basic_rate, 100)
		self.assertEqual(repack2.items[1].amount, 1000)

		transfer1 = make_stock_entry(
			item_code="FG Item 1",
			purpose="Material Transfer",
			qty=10,
			from_warehouse="WH 3 - _TC",
			to_warehouse="WH 2 - _TC",
			posting_date=add_days(nowdate(), -7),
		)

		self.assertEqual(transfer1.items[0].basic_rate, 100)
		self.assertEqual(transfer1.items[0].amount, 1000)

		transfer2 = make_stock_entry(
			item_code="FG Item 1",
			purpose="Material Transfer",
			qty=10,
			from_warehouse="WH 2 - _TC",
			to_warehouse="WH 1 - _TC",
			posting_date=add_days(nowdate(), -6),
		)

		self.assertEqual(transfer2.items[0].basic_rate, 100)
		self.assertEqual(transfer2.items[0].amount, 1000)

		# Backdated transaction
		receipt2 = make_stock_entry(
			item_code="Component 1",
			rate=200,
			purpose="Material Receipt",
			qty=10,
			to_warehouse="WH 1 - _TC",
			posting_date=add_days(nowdate(), -15),
		)

		self.assertEqual(receipt2.items[0].basic_rate, 200)
		self.assertEqual(receipt2.items[0].amount, 2000)

		repost_name = frappe.db.get_value(
			"Repost Item Valuation", {"voucher_no": receipt2.name, "docstatus": 1}, "name"
		)

		doc = frappe.get_doc("Repost Item Valuation", repost_name)
		repost_sl_entries(doc)

		for obj in [repack1, repack2, transfer1, transfer2]:
			obj.load_from_db()

			index = 1 if obj.purpose == "Repack" else 0
			self.assertEqual(obj.items[index].basic_rate, 200)
			self.assertEqual(obj.items[index].basic_amount, 2000)

	def test_batch_expiry(self):
		from erpnext.controllers.stock_controller import BatchExpiredError
		from erpnext.stock.doctype.batch.test_batch import make_new_batch

		item_code = "Test Batch Expiry Test Item - 001"
		item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)

		item_doc.has_batch_no = 1
		item_doc.save()

		batch = make_new_batch(
			batch_id=frappe.generate_hash("", 5), item_code=item_doc.name, expiry_date=add_days(today(), -1)
		)

		se = make_stock_entry(
			item_code=item_code,
			purpose="Material Receipt",
			qty=4,
			to_warehouse="_Test Warehouse - _TC",
			batch_no=batch.name,
			use_serial_batch_fields=1,
			do_not_save=True,
		)

		self.assertRaises(BatchExpiredError, se.save)

	def test_negative_stock_reco(self):
		frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)

		item_code = "Test Negative Item - 001"
		create_item(item_code=item_code, is_stock_item=1, valuation_rate=10)

		make_stock_entry(
			item_code=item_code,
			posting_date=add_days(today(), -3),
			posting_time="00:00:00",
			target="_Test Warehouse - _TC",
			qty=10,
			to_warehouse="_Test Warehouse - _TC",
		)

		make_stock_entry(
			item_code=item_code,
			posting_date=today(),
			posting_time="00:00:00",
			source="_Test Warehouse - _TC",
			qty=8,
			from_warehouse="_Test Warehouse - _TC",
		)

		sr_doc = create_stock_reconciliation(
			purpose="Stock Reconciliation",
			posting_date=add_days(today(), -3),
			posting_time="00:00:00",
			item_code=item_code,
			warehouse="_Test Warehouse - _TC",
			valuation_rate=10,
			qty=7,
			do_not_submit=True,
		)

		self.assertRaises(frappe.ValidationError, sr_doc.submit)

	def test_negative_batch(self):
		item_code = "Test Negative Batch Item - 001"
		make_item(
			item_code,
			{"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"},
		)

		se1 = make_stock_entry(
			item_code=item_code,
			purpose="Material Receipt",
			qty=100,
			target="_Test Warehouse - _TC",
		)

		se1.reload()

		batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)

		se2 = make_stock_entry(
			item_code=item_code,
			purpose="Material Issue",
			batch_no=batch_no,
			qty=10,
			source="_Test Warehouse - _TC",
		)

		se2.reload()

		se3 = make_stock_entry(
			item_code=item_code,
			purpose="Material Receipt",
			qty=100,
			target="_Test Warehouse - _TC",
		)

		se3.reload()

		self.assertRaises(frappe.ValidationError, se1.cancel)

	def test_auto_reorder_level(self):
		from erpnext.stock.reorder_item import reorder_item

		item_doc = make_item(
			"Test Auto Reorder Item - 001",
			properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1},
			uoms=[{"uom": "Nos", "conversion_factor": 5}],
		)

		if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}):
			item_doc.append(
				"reorder_levels",
				{
					"warehouse_reorder_level": 0,
					"warehouse_reorder_qty": 10,
					"warehouse": "_Test Warehouse - _TC",
					"material_request_type": "Purchase",
				},
			)

		item_doc.save(ignore_permissions=True)

		frappe.db.set_single_value("Stock Settings", "auto_indent", 1)

		mr_list = reorder_item()

		frappe.db.set_single_value("Stock Settings", "auto_indent", 0)
		mrs = frappe.get_all(
			"Material Request Item",
			fields=["qty", "stock_uom", "stock_qty"],
			filters={"item_code": item_doc.name, "uom": "Nos"},
		)

		for mri in mrs:
			self.assertEqual(mri.stock_uom, "Kg")
			self.assertEqual(mri.stock_qty, 10)
			self.assertEqual(mri.qty, 2)

		for mr in mr_list:
			mr.cancel()
			mr.delete()

	def test_auto_reorder_level_with_lead_time_days(self):
		from erpnext.stock.reorder_item import reorder_item

		item_doc = make_item(
			"Test Auto Reorder Item - 002",
			properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1, "lead_time_days": 2},
			uoms=[{"uom": "Nos", "conversion_factor": 5}],
		)

		if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}):
			item_doc.append(
				"reorder_levels",
				{
					"warehouse_reorder_level": 0,
					"warehouse_reorder_qty": 10,
					"warehouse": "_Test Warehouse - _TC",
					"material_request_type": "Purchase",
				},
			)

		item_doc.save(ignore_permissions=True)

		frappe.db.set_single_value("Stock Settings", "auto_indent", 1)

		mr_list = reorder_item()

		frappe.db.set_single_value("Stock Settings", "auto_indent", 0)
		mrs = frappe.get_all(
			"Material Request Item",
			fields=["schedule_date"],
			filters={"item_code": item_doc.name, "uom": "Nos"},
		)

		for mri in mrs:
			self.assertEqual(getdate(mri.schedule_date), getdate(add_days(today(), 2)))

		for mr in mr_list:
			mr.cancel()
			mr.delete()

	def test_use_serial_and_batch_fields(self):
		item = make_item(
			"Test Use Serial and Batch Item SN Item",
			{"has_serial_no": 1, "is_stock_item": 1},
		)

		serial_nos = [
			"Test Use Serial and Batch Item SN Item - SN 001",
			"Test Use Serial and Batch Item SN Item - SN 002",
		]

		se = make_stock_entry(
			item_code=item.name,
			qty=2,
			to_warehouse="_Test Warehouse - _TC",
			use_serial_batch_fields=1,
			serial_no="\n".join(serial_nos),
		)

		self.assertTrue(se.items[0].use_serial_batch_fields)
		self.assertTrue(se.items[0].serial_no)
		self.assertTrue(se.items[0].serial_and_batch_bundle)

		for serial_no in serial_nos:
			self.assertTrue(frappe.db.exists("Serial No", serial_no))
			self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")

		se1 = make_stock_entry(
			item_code=item.name,
			qty=2,
			from_warehouse="_Test Warehouse - _TC",
			use_serial_batch_fields=1,
			serial_no="\n".join(serial_nos),
		)

		se1.reload()

		self.assertTrue(se1.items[0].use_serial_batch_fields)
		self.assertTrue(se1.items[0].serial_no)
		self.assertTrue(se1.items[0].serial_and_batch_bundle)

		for serial_no in serial_nos:
			self.assertTrue(frappe.db.exists("Serial No", serial_no))
			self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Consumed")

	def test_serial_batch_bundle_type_of_transaction(self):
		item = make_item(
			"Test Use Serial and Batch Item SN Item",
			{
				"has_batch_no": 1,
				"is_stock_item": 1,
				"create_new_batch": 1,
				"batch_naming_series": "Test-SBBTYT-NNS.#####",
			},
		).name

		se = make_stock_entry(
			item_code=item,
			qty=2,
			target="_Test Warehouse - _TC",
			use_serial_batch_fields=1,
		)

		batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)

		se = make_stock_entry(
			item_code=item,
			qty=2,
			source="_Test Warehouse - _TC",
			target="Stores - _TC",
			use_serial_batch_fields=0,
			batch_no=batch_no,
			do_not_submit=True,
		)

		se.reload()
		sbb = se.items[0].serial_and_batch_bundle
		frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward")
		self.assertRaises(frappe.ValidationError, se.submit)

	def test_stock_entry_for_same_posting_date_and_time(self):
		warehouse = "_Test Warehouse - _TC"
		item_code = "Test Stock Entry For Same Posting Datetime 1"
		make_item(item_code, {"is_stock_item": 1})
		posting_date = nowdate()
		posting_time = nowtime()

		for index in range(25):
			se = make_stock_entry(
				item_code=item_code,
				qty=1,
				to_warehouse=warehouse,
				posting_date=posting_date,
				posting_time=posting_time,
				do_not_submit=True,
				purpose="Material Receipt",
				basic_rate=100,
			)

			se.append(
				"items",
				{
					"item_code": item_code,
					"item_name": se.items[0].item_name,
					"description": se.items[0].description,
					"t_warehouse": se.items[0].t_warehouse,
					"basic_rate": 100,
					"qty": 1,
					"stock_qty": 1,
					"conversion_factor": 1,
					"expense_account": se.items[0].expense_account,
					"cost_center": se.items[0].cost_center,
					"uom": se.items[0].uom,
					"stock_uom": se.items[0].stock_uom,
				},
			)

			se.remarks = f"The current number is {cstr(index)}"

			se.submit()

		sles = frappe.get_all(
			"Stock Ledger Entry",
			fields=[
				"posting_date",
				"posting_time",
				"actual_qty",
				"qty_after_transaction",
				"incoming_rate",
				"stock_value_difference",
				"stock_value",
			],
			filters={"item_code": item_code, "warehouse": warehouse},
			order_by="creation",
		)

		self.assertEqual(len(sles), 50)
		i = 0
		for sle in sles:
			i += 1
			self.assertEqual(getdate(sle.posting_date), getdate(posting_date))
			self.assertEqual(get_time(sle.posting_time), get_time(posting_time))
			self.assertEqual(sle.actual_qty, 1)
			self.assertEqual(sle.qty_after_transaction, i)
			self.assertEqual(sle.incoming_rate, 100)
			self.assertEqual(sle.stock_value_difference, 100)
			self.assertEqual(sle.stock_value, 100 * i)

	def test_stock_entry_amount(self):
		warehouse = "_Test Warehouse - _TC"
		rm_item_code = "Test Stock Entry Amount 1"
		make_item(rm_item_code, {"is_stock_item": 1})

		fg_item_code = "Test Repack Stock Entry Amount 1"
		make_item(fg_item_code, {"is_stock_item": 1})

		make_stock_entry(
			item_code=rm_item_code,
			qty=1,
			to_warehouse=warehouse,
			basic_rate=200,
			posting_date=nowdate(),
		)

		se = make_stock_entry(
			item_code=rm_item_code,
			qty=1,
			purpose="Repack",
			basic_rate=100,
			do_not_save=True,
		)

		se.items[0].s_warehouse = warehouse
		se.append(
			"items",
			{
				"item_code": fg_item_code,
				"qty": 1,
				"t_warehouse": warehouse,
				"uom": "Nos",
				"conversion_factor": 1.0,
			},
		)
		se.set_stock_entry_type()
		se.submit()

		self.assertEqual(se.items[0].amount, 200)
		self.assertEqual(se.items[0].basic_amount, 200)

		make_stock_entry(
			item_code=rm_item_code,
			qty=1,
			to_warehouse=warehouse,
			basic_rate=300,
			posting_date=add_days(nowdate(), -1),
		)

		se.reload()
		self.assertEqual(se.items[0].amount, 300)
		self.assertEqual(se.items[0].basic_amount, 300)

	def test_use_batch_wise_valuation_for_moving_average_item(self):
		item_code = "_Test Use Batch Wise MA Valuation Item"

		make_item(
			item_code,
			{
				"is_stock_item": 1,
				"valuation_method": "Moving Average",
				"has_batch_no": 1,
				"create_new_batch": 1,
				"batch_naming_series": "Test-UBWVMAV-T-NNS.#####",
			},
		)

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

		batches = []
		se = make_stock_entry(
			item_code=item_code,
			qty=10,
			to_warehouse="_Test Warehouse - _TC",
			basic_rate=100,
			posting_date=add_days(nowdate(), -2),
		)

		batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle))

		se = make_stock_entry(
			item_code=item_code,
			qty=10,
			to_warehouse="_Test Warehouse - _TC",
			basic_rate=300,
			posting_date=add_days(nowdate(), -1),
		)

		batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle))

		se = make_stock_entry(
			item_code=item_code,
			qty=5,
			from_warehouse="_Test Warehouse - _TC",
			batch_no=batches[1],
			posting_date=nowdate(),
		)

		self.assertEqual(se.items[0].basic_rate, 300)

	def test_periodic_accounting_entries(self):
		item_code = "_Test Periodic Accounting Item"
		make_item(item_code, {"is_stock_item": 1})

		company = "_Test Periodic Accounting Company"

		frappe.get_doc(
			{
				"doctype": "Company",
				"company_name": company,
				"abbr": "_TPC",
				"default_currency": "INR",
				"enable_perpetual_inventory": 0,
				"country": "India",
			}
		).insert(ignore_permissions=True)

		warehouse = frappe.db.get_value("Warehouse", {"company": company, "is_group": 0}, "name")

		make_stock_entry(
			item_code=item_code,
			qty=10,
			to_warehouse=warehouse,
			basic_rate=100,
			posting_date=add_days(nowdate(), -2),
		)

		jv = frappe.new_doc("Journal Entry")
		jv.voucher_type = "Periodic Accounting Entry"
		jv.posting_date = add_days(nowdate(), -1)
		jv.posting_time = nowtime()
		jv.company = company
		jv.for_all_stock_asset_accounts = 1
		jv.periodic_entry_difference_account = "Stock Adjustment - _TPC"
		jv.get_balance_for_periodic_accounting()
		jv.save()
		jv.submit()

		self.assertEqual(len(jv.accounts), 2)
		self.assertEqual(jv.accounts[0].debit_in_account_currency, 1000)
		self.assertEqual(jv.accounts[1].credit_in_account_currency, 1000)
		self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC")
		self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC")

		make_stock_entry(
			item_code=item_code,
			qty=5,
			from_warehouse=warehouse,
			company=company,
			posting_date=nowdate(),
			posting_time=nowtime(),
		)

		jv = frappe.new_doc("Journal Entry")
		jv.voucher_type = "Periodic Accounting Entry"
		jv.posting_date = nowdate()
		jv.posting_time = nowtime()
		jv.company = company
		jv.for_all_stock_asset_accounts = 1
		jv.periodic_entry_difference_account = "Stock Adjustment - _TPC"
		jv.get_balance_for_periodic_accounting()
		jv.save()
		jv.submit()

		self.assertEqual(len(jv.accounts), 2)
		self.assertEqual(jv.accounts[0].credit_in_account_currency, 500)
		self.assertEqual(jv.accounts[1].debit_in_account_currency, 500)
		self.assertEqual(jv.accounts[0].account, "Stock In Hand - _TPC")
		self.assertEqual(jv.accounts[1].account, "Stock Adjustment - _TPC")

	def test_batch_item_additional_cost_for_material_transfer_entry(self):
		item_code = "_Test Batch Item Additional Cost MTE"
		make_item(
			item_code,
			{
				"is_stock_item": 1,
				"has_batch_no": 1,
				"create_new_batch": 1,
				"batch_naming_series": "BT-MTE.#####",
			},
		)

		se = make_stock_entry(
			item_code=item_code,
			target="_Test Warehouse - _TC",
			qty=2,
			basic_rate=100,
			use_serial_batch_fields=1,
		)

		batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)

		se = make_stock_entry(
			item_code=item_code,
			source="_Test Warehouse - _TC",
			target="_Test Warehouse 1 - _TC",
			batch_no=batch_no,
			use_serial_batch_fields=1,
			qty=2,
			purpose="Material Transfer",
			do_not_save=True,
		)

		se.append(
			"additional_costs",
			{
				"cost_center": "Main - _TC",
				"amount": 50,
				"expense_account": "Stock Adjustment - _TC",
				"description": "Test Additional Cost",
			},
		)
		se.save()
		self.assertEqual(se.additional_costs[0].amount, 50)
		self.assertEqual(se.items[0].basic_rate, 100)
		self.assertEqual(se.items[0].valuation_rate, 125)

		se.submit()
		self.assertEqual(se.items[0].basic_rate, 100)
		self.assertEqual(se.items[0].valuation_rate, 125)

		incoming_rate = frappe.db.get_value(
			"Stock Ledger Entry",
			{
				"item_code": item_code,
				"warehouse": "_Test Warehouse 1 - _TC",
				"voucher_type": "Stock Entry",
				"voucher_no": se.name,
			},
			"incoming_rate",
		)

		self.assertEqual(incoming_rate, 125.0)

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

		item = "Test Prevent Reuse Delivered Serial No"
		warehouse = "_Test Warehouse - _TC"

		item_doc = make_item(item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SHGJ.####"})

		make_stock_entry(item_code="_Test Item", target=warehouse, qty=2, rate=100)
		make_stock_entry(item_code=item, target=warehouse, qty=2, rate=100)

		dn = create_delivery_note(item_code=item, qty=2)
		delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0]

		se = make_stock_entry(
			item_code="_Test Item", source=warehouse, qty=1, purpose="Repack", do_not_save=True
		)
		se.append(
			"items",
			{
				"item_code": item_doc.name,
				"item_name": item_doc.item_name,
				"s_warehouse": None,
				"t_warehouse": warehouse,
				"description": item_doc.description,
				"uom": item_doc.stock_uom,
				"qty": 1,
				"use_serial_batch_fields": 1,
				"serial_no": delivered_serial_no,
			},
		)

		se.save()
		status = frappe.db.get_value("Serial No", delivered_serial_no, "status")

		self.assertEqual(status, "Delivered")
		self.assertEqual(se.purpose, "Repack")
		self.assertRaises(frappe.ValidationError, se.submit)

	def test_transferred_qty_in_material_transfer(self):
		item_code = "_Test Item"
		source_warehouse = "_Test Warehouse - _TC"
		target_warehouse = "_Test Warehouse 1 - _TC"

		if not frappe.db.get_value("UOM Conversion Detail", {"parent": item_code, "uom": "Box"}):
			item_doc = frappe.get_doc("Item", item_code)
			item_doc.append("uoms", {"uom": "Box", "conversion_factor": 12})
			item_doc.save(ignore_permissions=True)

		make_stock_entry(item_code=item_code, target=source_warehouse, qty=12, rate=100)

		# Create a Material Request for Material Transfer
		material_request = make_material_request(
			material_request_type="Material Transfer",
			qty=1,
			item_code=item_code,
			uom="Box",
			conversion_factor=12,
			from_warehouse=source_warehouse,
			warehouse=target_warehouse,
		)
		in_transit_wh = get_in_transit_warehouse(material_request.company)

		# Create first Stock Entry (Source -> In-Transit)
		stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
		stock_entry_1.items[0].update(
			{
				"qty": 1,
				"s_warehouse": source_warehouse,
			}
		)
		stock_entry_1.save().submit()

		# Validate transfer status after first transfer
		material_request.reload()
		self.assertEqual(material_request.transfer_status, "In Transit")

		# Create final Stock Entry (In-Transit -> Target)
		end_transit_1 = make_stock_in_entry(stock_entry_1.name)
		end_transit_1.save().submit()
		end_transit_1.reload()

		# Validate quantities
		stock_entry_1.reload()
		self.assertEqual(stock_entry_1.items[0].qty, 1)
		self.assertEqual(stock_entry_1.items[0].transfer_qty, 12)
		self.assertEqual(stock_entry_1.items[0].transferred_qty, 12)

		# Validate transfer status after final transfer
		material_request.reload()
		self.assertEqual(material_request.transfer_status, "Completed")

	def test_manufacture_entry_without_wo(self):
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom

		fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name
		rm_item1 = make_item("_Temper Glass", properties={"is_stock_item": 1}).name
		rm_item2 = make_item("_Battery", properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=rm_item1, target=warehouse, qty=5, purpose="Material Receipt")
		make_stock_entry(item_code=rm_item2, target=warehouse, qty=5, purpose="Material Receipt")

		bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
		se = make_stock_entry(item_code=fg_item, qty=1, purpose="Manufacture", do_not_save=True)
		se.from_bom = 1
		se.use_multi_level_bom = 1
		se.bom_no = bom_no
		se.fg_completed_qty = 1
		se.from_warehouse = warehouse
		se.to_warehouse = warehouse

		se.get_items()
		rm_items = {d.item_code: d.qty for d in se.items if d.item_code != fg_item}
		self.assertEqual(rm_items[rm_item1], 1)
		self.assertEqual(rm_items[rm_item2], 1)
		se.calculate_rate_and_amount()
		se.save()
		se.submit()

	def test_manufacture_with_zero_valued_raw_material(self):
		# A finished good produced from free inputs is worth nothing. Falling back to the item's
		# own valuation would create value out of nothing and inflate it on every production run.
		fg_item = make_item(properties={"is_stock_item": 1}).name
		rm_item = make_item(properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		fg_warehouse = "Finished Goods - _TC"

		rm_receipt = make_stock_entry(item_code=rm_item, target=warehouse, qty=100, rate=0, do_not_save=True)
		rm_receipt.items[0].allow_zero_valuation_rate = 1
		rm_receipt.save()
		rm_receipt.submit()

		# the finished good already carries a valuation in the target warehouse
		make_stock_entry(item_code=fg_item, target=fg_warehouse, qty=10, rate=100)

		se = frappe.new_doc("Stock Entry")
		se.purpose = se.stock_entry_type = "Manufacture"
		se.company = "_Test Company"
		se.append(
			"items",
			{"item_code": rm_item, "s_warehouse": warehouse, "qty": 10, "conversion_factor": 1},
		)
		se.append(
			"items",
			{
				"item_code": fg_item,
				"t_warehouse": fg_warehouse,
				"qty": 10,
				"is_finished_item": 1,
				"conversion_factor": 1,
			},
		)
		se.save()

		self.assertEqual(se.items[0].basic_amount, 0)
		self.assertEqual(se.items[1].basic_rate, 0)
		self.assertEqual(se.items[1].basic_amount, 0)

		se.submit()

		fg_sle = frappe.db.get_value(
			"Stock Ledger Entry",
			{"voucher_no": se.name, "item_code": fg_item, "is_cancelled": 0},
			["incoming_rate", "stock_value_difference"],
			as_dict=True,
		)

		self.assertEqual(fg_sle.incoming_rate, 0)
		self.assertEqual(fg_sle.stock_value_difference, 0)

	def test_manufacture_balances_secondary_item_added_without_a_bom(self):
		"""A secondary item with no BOM link is costed out of the finished good, as legacy scrap was."""
		rm_item = make_item(properties={"is_stock_item": 1}).name
		fg_item = make_item(properties={"is_stock_item": 1}).name
		scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
		warehouse = "_Test Warehouse - _TC"

		make_stock_entry(item_code=rm_item, target=warehouse, qty=10, basic_rate=100)

		se = frappe.new_doc("Stock Entry")
		se.purpose = se.stock_entry_type = "Manufacture"
		se.company = "_Test Company"
		se.append(
			"items", {"item_code": rm_item, "s_warehouse": warehouse, "qty": 10, "conversion_factor": 1}
		)
		se.append(
			"items",
			{
				"item_code": fg_item,
				"t_warehouse": warehouse,
				"qty": 10,
				"is_finished_item": 1,
				"conversion_factor": 1,
			},
		)
		se.append(
			"items",
			{
				"item_code": scrap_item,
				"t_warehouse": warehouse,
				"qty": 5,
				"type": "Scrap",
				"conversion_factor": 1,
			},
		)
		se.save()

		scrap_row = se.items[2]
		self.assertEqual(flt(scrap_row.basic_rate), 20.0)
		self.assertEqual(flt(scrap_row.basic_amount), 100.0)

		fg_row = se.items[1]
		self.assertEqual(flt(fg_row.basic_rate), 90.0)
		self.assertEqual(flt(fg_row.basic_amount), 900.0)

		self.assertEqual(flt(se.total_incoming_value), 1000.0)
		self.assertEqual(flt(se.total_outgoing_value), 1000.0)
		self.assertEqual(flt(se.value_difference), 0.0)

	def test_repack_allocates_cost_to_secondary_item(self):
		"""A Repack secondary item takes its own BOM share, not the finished good's."""
		rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
		fg_item = make_item(properties={"is_stock_item": 1}).name
		scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
		warehouse = "_Test Warehouse - _TC"

		bom = frappe.get_doc(
			{
				"doctype": "BOM",
				"item": fg_item,
				"currency": "INR",
				"quantity": 10,
				"company": "_Test Company",
			}
		)
		bom.append("items", {"item_code": rm_item, "qty": 10})
		bom.append(
			"secondary_items",
			{
				"type": "Scrap",
				"item_code": scrap_item,
				"item_name": scrap_item,
				"qty": 5,
				"cost_allocation_per": 25,
				"process_loss_per": 0,
			},
		)
		bom.insert()
		bom.submit()
		self.assertEqual(flt(bom.cost_allocation_per), 75.0)

		make_stock_entry(item_code=rm_item, target=warehouse, qty=100, basic_rate=100)

		se = frappe.new_doc("Stock Entry")
		se.purpose = se.stock_entry_type = "Repack"
		se.company = "_Test Company"
		se.from_bom = 1
		se.bom_no = bom.name
		se.fg_completed_qty = 10
		se.from_warehouse = warehouse
		se.to_warehouse = warehouse
		se.get_items()
		se.save()

		fg_row = next(d for d in se.items if d.is_finished_item)
		scrap_row = next(d for d in se.items if d.type)

		self.assertFalse(scrap_row.is_finished_item)
		self.assertEqual(flt(scrap_row.basic_amount), 250.0)
		self.assertEqual(flt(fg_row.basic_amount), 750.0)

		self.assertEqual(flt(se.total_incoming_value), 1000.0)
		self.assertEqual(flt(se.total_outgoing_value), 1000.0)
		self.assertEqual(flt(se.value_difference), 0.0)

	def test_secondary_item_with_zero_cost_allocation_carries_no_value(self):
		"""A BOM that allocates 0% to a secondary item gives the finished good everything."""
		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_stock_entry_from_wo,
		)

		rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
		fg_item = make_item(properties={"is_stock_item": 1}).name
		scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
		warehouse = "_Test Warehouse - _TC"

		bom = frappe.get_doc(
			{
				"doctype": "BOM",
				"item": fg_item,
				"currency": "INR",
				"quantity": 10,
				"company": "_Test Company",
			}
		)
		bom.append("items", {"item_code": rm_item, "qty": 10})
		bom.append(
			"secondary_items",
			{
				"type": "Scrap",
				"item_code": scrap_item,
				"item_name": scrap_item,
				"qty": 5,
				"cost_allocation_per": 0,
				"process_loss_per": 0,
			},
		)
		bom.insert()
		bom.submit()
		self.assertEqual(flt(bom.cost_allocation_per), 100.0)

		make_stock_entry(item_code=rm_item, target=warehouse, qty=100, basic_rate=100)
		wo = make_wo_order_test_record(
			production_item=fg_item, bom_no=bom.name, qty=10, skip_transfer=1, source_warehouse=warehouse
		)

		se = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Manufacture", 10))
		se.save()

		scrap_row = next(d for d in se.items if d.type)
		fg_row = next(d for d in se.items if d.is_finished_item)

		self.assertEqual(flt(scrap_row.basic_rate), 0.0)
		self.assertEqual(flt(scrap_row.basic_amount), 0.0)
		self.assertEqual(flt(fg_row.basic_amount), 1000.0)
		self.assertEqual(flt(se.value_difference), 0.0)

	def test_secondary_item_type_does_not_waive_inspection_outside_manufacturing(self):
		"""A stray secondary item type must not let a QI-required item through a receipt."""
		item = make_item(
			properties={
				"is_stock_item": 1,
				"valuation_rate": 50,
				"inspection_required_before_purchase": 1,
			}
		).name

		def receipt(secondary_item_type):
			se = frappe.new_doc("Stock Entry")
			se.purpose = se.stock_entry_type = "Material Receipt"
			se.company = "_Test Company"
			se.inspection_required = 1
			se.append(
				"items",
				{
					"item_code": item,
					"t_warehouse": "_Test Warehouse - _TC",
					"qty": 10,
					"conversion_factor": 1,
					"type": secondary_item_type,
				},
			)
			return se

		self.assertRaises(QualityInspectionRequiredError, receipt("").submit)
		self.assertRaises(QualityInspectionRequiredError, receipt("Scrap").submit)

	@ERPNextTestSuite.change_settings(
		"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
	)
	def test_secondary_item_allocation_uses_consumption_entry_cost(self):
		"""A BOM allocation splits the consumption entry's cost, not an empty set of consumed rows."""
		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_stock_entry_from_wo,
		)

		rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
		fg_item = make_item(properties={"is_stock_item": 1}).name
		scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
		warehouse = "_Test Warehouse - _TC"

		bom = frappe.get_doc(
			{
				"doctype": "BOM",
				"item": fg_item,
				"currency": "INR",
				"quantity": 10,
				"company": "_Test Company",
			}
		)
		bom.append("items", {"item_code": rm_item, "qty": 10})
		bom.append(
			"secondary_items",
			{
				"type": "Scrap",
				"item_code": scrap_item,
				"item_name": scrap_item,
				"qty": 5,
				"cost_allocation_per": 25,
				"process_loss_per": 0,
			},
		)
		bom.insert()
		bom.submit()

		make_stock_entry(item_code=rm_item, target=warehouse, qty=100, basic_rate=100)
		wo = make_wo_order_test_record(
			production_item=fg_item, bom_no=bom.name, qty=10, skip_transfer=1, source_warehouse=warehouse
		)

		consumption = frappe.get_doc(
			make_stock_entry_from_wo(wo.name, "Material Consumption for Manufacture", 10)
		)
		consumption.submit()
		self.assertEqual(flt(consumption.total_outgoing_value), 1000.0)

		se = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Manufacture", 10))
		se.save()

		scrap_row = next(d for d in se.items if d.type)
		fg_row = next(d for d in se.items if d.is_finished_item)

		self.assertEqual(flt(fg_row.basic_amount), 750.0)
		self.assertEqual(flt(scrap_row.basic_amount), 250.0)
		self.assertEqual(flt(se.total_incoming_value), 1000.0)

	def _make_wo_for_free_raw_material(self, rm_item, fg_item, bom_no):
		from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_stock_entry_from_wo,
		)

		receipt = make_stock_entry(item_code=rm_item, target="Stores - _TC", qty=10, rate=0, do_not_save=True)
		receipt.items[0].allow_zero_valuation_rate = 1
		receipt.save()
		receipt.submit()

		wo = make_wo_order_test_record(production_item=fg_item, bom_no=bom_no, qty=10)

		transfer = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Material Transfer for Manufacture", 10))
		transfer.items[0].s_warehouse = "Stores - _TC"
		transfer.insert().submit()

		return wo

	@ERPNextTestSuite.change_settings(
		"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 0}
	)
	def test_manufacture_does_not_fall_back_to_bom_cost_for_free_raw_material(self):
		# The BOM is only an estimate for when nothing was consumed. Items that were consumed and
		# cost nothing are a real cost, so a BOM rate must not stand in for them.
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_stock_entry_from_wo,
		)

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

		frappe.get_doc(
			{
				"doctype": "Item Price",
				"item_code": rm_item,
				"price_list": "_Test Price List India",
				"price_list_rate": 150,
				"buying": 1,
			}
		).insert()

		# price the BOM off the price list so that it carries a rate the free stock does not
		bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_save=True)
		bom.rm_cost_as_per = "Price List"
		bom.buying_price_list = "_Test Price List India"
		bom.currency = "INR"
		bom.save()
		bom.submit()

		wo = self._make_wo_for_free_raw_material(rm_item, fg_item, bom.name)

		manufacture = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Manufacture", 10))
		manufacture.save()

		fg_row = next(d for d in manufacture.items if d.is_finished_item)
		self.assertEqual(fg_row.basic_rate, 0)
		self.assertEqual(fg_row.basic_amount, 0)

	@ERPNextTestSuite.change_settings(
		"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
	)
	def test_manufacture_with_zero_valued_consumption_entry(self):
		# The raw material is consumed by a separate entry, so the Manufacture entry carries no
		# consumed rows of its own. Its cost is still known, and it is zero.
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.manufacturing.doctype.work_order.work_order import (
			make_stock_entry as make_stock_entry_from_wo,
		)

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

		# the finished good already carries a valuation in the work order's target warehouse
		make_stock_entry(item_code=fg_item, target="_Test Warehouse 1 - _TC", qty=10, rate=100)

		bom = make_bom(item=fg_item, raw_materials=[rm_item]).name
		wo = self._make_wo_for_free_raw_material(rm_item, fg_item, bom)

		consumption = frappe.get_doc(
			make_stock_entry_from_wo(wo.name, "Material Consumption for Manufacture", 10)
		)
		consumption.insert().submit()

		manufacture = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Manufacture", 10))
		manufacture.save()

		fg_row = next(d for d in manufacture.items if d.is_finished_item)
		self.assertEqual(fg_row.basic_rate, 0)
		self.assertEqual(fg_row.basic_amount, 0)

	def test_disassemble_entry_without_wo(self):
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom

		fg_item = make_item("_Disassemble Mobile", properties={"is_stock_item": 1}).name
		rm_item1 = make_item("_Disassemble Temper Glass", properties={"is_stock_item": 1}).name
		rm_item2 = make_item("_Disassemble Battery", properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"

		# Stock up the FG item (what we'll disassemble)
		make_stock_entry(item_code=fg_item, target=warehouse, qty=5, purpose="Material Receipt")

		bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name

		se = make_stock_entry(item_code=fg_item, qty=1, purpose="Disassemble", do_not_save=True)
		se.from_bom = 1
		se.use_multi_level_bom = 1
		se.bom_no = bom_no
		se.fg_completed_qty = 1
		se.from_warehouse = warehouse
		se.to_warehouse = warehouse

		se.get_items()

		# Verify FG as source (being consumed)
		fg_items = [d for d in se.items if d.is_finished_item]
		self.assertEqual(len(fg_items), 1)
		self.assertEqual(fg_items[0].item_code, fg_item)
		self.assertEqual(fg_items[0].qty, 1)
		self.assertEqual(fg_items[0].s_warehouse, warehouse)
		self.assertFalse(fg_items[0].t_warehouse)

		# Verify RM as target (being received)
		rm_items = {d.item_code: d for d in se.items if not d.is_finished_item}
		self.assertEqual(len(rm_items), 2)
		self.assertIn(rm_item1, rm_items)
		self.assertIn(rm_item2, rm_items)
		self.assertEqual(rm_items[rm_item1].qty, 1)
		self.assertEqual(rm_items[rm_item2].qty, 1)
		self.assertEqual(rm_items[rm_item1].t_warehouse, warehouse)
		self.assertFalse(rm_items[rm_item1].s_warehouse)

		se.calculate_rate_and_amount()
		se.save()
		se.submit()

	def test_disassemble_blocks_finished_good_qty_tampering(self):
		# A disassembly consuming N finished goods must consume exactly N (in stock UOM).
		# Switching the finished-good row to a larger UOM with a tiny conversion_factor previously
		# let a user consume ~0 finished goods while still producing the full raw materials --
		# minting inventory. The quantity invariant must reject this.
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom

		fg_item = make_item("_Disassemble Mint FG", properties={"is_stock_item": 1}).name
		rm_item1 = make_item("_Disassemble Mint RM1", properties={"is_stock_item": 1}).name
		rm_item2 = make_item("_Disassemble Mint RM2", properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"

		# Give the finished good a non-stock UOM. When uom == stock_uom the system resets the
		# conversion_factor to 1, so the tamper is only possible (and worth guarding) on a
		# non-stock UOM, where the user-supplied conversion_factor is preserved.
		if not frappe.db.get_value("UOM Conversion Detail", {"parent": fg_item, "uom": "Box"}):
			item_doc = frappe.get_doc("Item", fg_item)
			item_doc.append("uoms", {"uom": "Box", "conversion_factor": 0.01})
			item_doc.save(ignore_permissions=True)

		make_stock_entry(item_code=fg_item, target=warehouse, qty=100, purpose="Material Receipt")
		bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name

		se = make_stock_entry(item_code=fg_item, qty=100, purpose="Disassemble", do_not_save=True)
		se.from_bom = 1
		se.use_multi_level_bom = 1
		se.bom_no = bom_no
		se.fg_completed_qty = 100
		se.from_warehouse = warehouse
		se.to_warehouse = warehouse
		se.get_items()

		# Tamper the finished-good row: a tiny conversion factor on the larger UOM means only
		# 100 * 0.01 = 1 unit is actually consumed, while raw materials are still produced at full
		# quantity. The finished-good consumption invariant must reject the save.
		fg_row = next(d for d in se.items if d.is_finished_item)
		fg_row.uom = "Box"
		fg_row.conversion_factor = 0.01

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

	@ERPNextTestSuite.change_settings(
		"Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"}
	)
	def test_sample_retention_stock_entry(self):
		from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse

		warehouse = "_Test Warehouse - _TC"
		retain_sample_item = make_item(
			"Retain Sample Item",
			properties={
				"is_stock_item": 1,
				"retain_sample": 1,
				"sample_quantity": 2,
				"has_batch_no": 1,
				"has_serial_no": 1,
				"create_new_batch": 1,
				"batch_number_series": "SAMPLE-RET-.#####",
				"serial_no_series": "SAMPLE-RET-SN-.#####",
			},
		)
		material_receipt = make_stock_entry(
			item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt"
		)

		source_sabb = frappe.get_doc(
			"Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle
		)
		batch = source_sabb.entries[0].batch_no
		serial_nos = [entry.serial_no for entry in source_sabb.entries]

		sample_entry = frappe.get_doc(
			move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items)
		)
		sample_entry.submit()
		target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle)

		self.assertEqual(sample_entry.items[0].transfer_qty, 2)
		self.assertEqual(target_sabb.entries[0].batch_no, batch)
		self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2])

	@ERPNextTestSuite.change_settings("Manufacturing Settings", {"material_consumption": 0})
	def test_raw_material_missing_validation(self):
		stock_entry = make_stock_entry(
			item_code="_Test Item",
			qty=1,
			target="_Test Warehouse - _TC",
			do_not_save=True,
		)

		stock_entry.purpose = "Manufacture"
		stock_entry.stock_entry_type = "Manufacture"
		stock_entry.items[0].is_finished_item = 1

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

	@ERPNextTestSuite.change_settings(
		"Manufacturing Settings",
		{
			"material_consumption": 1,
			"backflush_raw_materials_based_on": "BOM",
			"validate_components_quantities_per_bom": 1,
		},
	)
	def test_validation_as_per_bom_with_continuous_raw_material_consumption(self):
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as _make_stock_entry
		from erpnext.manufacturing.doctype.work_order.work_order import make_work_order

		fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name
		rm_item1 = make_item("_Battery", properties={"is_stock_item": 1}).name
		warehouse = "Stores - WP"
		bom_no = make_bom(item=fg_item, raw_materials=[rm_item1]).name
		se = make_stock_entry(
			item_code=rm_item1, target=warehouse, qty=5, rate=10, purpose="Material Receipt"
		)

		work_order = make_work_order(bom_no, fg_item, 5)
		work_order.company = se.company
		work_order.skip_transfer = 1
		work_order.source_warehouse = warehouse
		work_order.fg_warehouse = warehouse
		work_order.submit()

		frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit()
		frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit()

	@ERPNextTestSuite.change_settings(
		"Manufacturing Settings",
		{"material_consumption": 1, "backflush_raw_materials_based_on": "BOM"},
	)
	def test_qi_not_required_for_material_consumption_for_manufacture(self):
		"""An inspection_required BOM inspects the finished good (the Manufacture rule),
		not each consumed raw material, so Material Consumption for Manufacture (whose
		rows are outgoing only) must still submit without a Quality Inspection."""
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as _make_stock_entry
		from erpnext.manufacturing.doctype.work_order.work_order import make_work_order

		fg_item = make_item("_Test QI Consumption FG", properties={"is_stock_item": 1}).name
		rm_item = make_item("_Test QI Consumption RM", properties={"is_stock_item": 1}).name
		warehouse = "Stores - WP"

		bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True)
		bom.inspection_required = 1
		bom.submit()

		se = make_stock_entry(item_code=rm_item, target=warehouse, qty=5, rate=10, purpose="Material Receipt")

		work_order = make_work_order(bom.name, fg_item, 5)
		work_order.company = se.company
		work_order.skip_transfer = 1
		work_order.source_warehouse = warehouse
		work_order.fg_warehouse = warehouse
		work_order.submit()

		consumption = frappe.get_doc(
			_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)
		)
		# the mapper copies inspection_required from the BOM ...
		self.assertEqual(consumption.inspection_required, 1)
		# ... but the consumed rows are outgoing-only, so no QI is required and submit succeeds
		consumption.submit()
		self.assertEqual(consumption.docstatus, 1)

	def test_qi_creation_with_naming_rule_company_condition(self):
		"""
		Unit test case to check the document naming rule with company condition
		For Quality Inspection, when created from Stock Entry.
		"""
		from erpnext.controllers.stock_controller import make_quality_inspections
		from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

		# create a separate company to handle document naming rule with company condition
		qc_company = "Test Quality Company"

		# create document naming rule based on that for Quality Inspection Doctype
		qc_naming_rule = frappe.new_doc(
			"Document Naming Rule", document_type="Quality Inspection", prefix="NQC.-ST-", prefix_digits=5
		)
		qc_naming_rule.append("conditions", {"field": "company", "condition": "=", "value": qc_company})
		qc_naming_rule.save()

		warehouse = create_warehouse(warehouse_name="Test QI Warehouse", company=qc_company)
		item = create_item(
			item_code="Test QI DNR Item",
			is_stock_item=1,
		)

		# create inward stock entry
		stock_entry = make_stock_entry(
			item_code=item.item_code,
			target=warehouse,
			qty=10,
			basic_rate=100,
			inspection_required=True,
			do_not_submit=True,
		)

		# create QI from Stock Entry and check the naming series generated.
		qi = make_quality_inspections(
			stock_entry.company,
			stock_entry.doctype,
			stock_entry.name,
			stock_entry.as_dict().get("items"),
			"Incoming",
		)
		self.assertEqual(qi[0], "NQC-ST-00001")

		# delete naming rule
		frappe.delete_doc("Document Naming Rule", qc_naming_rule.name)

	def test_co_by_product(self):
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom

		frappe.set_value("UOM", "Nos", "must_be_whole_number", 0)

		fg_item = make_item("FG Item", properties={"is_stock_item": 1}).name
		rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name
		scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=rm_item, target=warehouse, qty=5, rate=10, purpose="Material Receipt")

		bom_no = make_bom(
			item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10
		).name
		se = make_stock_entry(item_code=fg_item, qty=5, purpose="Manufacture", do_not_save=True)
		se.from_bom = 1
		se.bom_no = bom_no
		se.fg_completed_qty = 5
		se.from_warehouse = warehouse
		se.to_warehouse = "_Test Warehouse 1 - _TC"
		se.get_items()
		se.save()
		se.reload()

		self.assertEqual(se.items[1].qty, 4.5)
		self.assertEqual(se.items[1].amount, 45)
		self.assertEqual(se.items[2].qty, 4.5)
		self.assertEqual(se.items[2].amount, 5)

	def test_process_loss_percentage_resyncs_from_qty(self):
		# changing fg qty recomputes process_loss_qty
		se = frappe.new_doc("Stock Entry")
		se.purpose = "Manufacture"
		se.fg_completed_qty = 200
		se.process_loss_qty = 100
		se.process_loss_percentage = 80

		se.set_process_loss_qty()

		self.assertEqual(se.process_loss_percentage, 50)

	def test_process_loss_qty_derived_from_percentage_when_qty_blank(self):
		se = frappe.new_doc("Stock Entry")
		se.purpose = "Manufacture"
		se.fg_completed_qty = 200
		se.process_loss_percentage = 25

		se.set_process_loss_qty()

		self.assertEqual(se.process_loss_qty, 50)


def make_serialized_item(self, **args):
	args = frappe._dict(args)
	se = frappe.copy_doc(self.globalTestRecords["Stock Entry"][0])

	if args.company:
		se.company = args.company

	if args.target_warehouse:
		se.get("items")[0].t_warehouse = args.target_warehouse

	se.get("items")[0].item_code = args.item_code or "_Test Serialized Item With Series"

	if args.serial_no:
		serial_nos = args.serial_no
		if isinstance(serial_nos, str):
			serial_nos = [serial_nos]

		se.get("items")[0].serial_and_batch_bundle = make_serial_batch_bundle(
			frappe._dict(
				{
					"item_code": se.get("items")[0].item_code,
					"warehouse": se.get("items")[0].t_warehouse,
					"company": se.company,
					"qty": 2,
					"voucher_type": "Stock Entry",
					"serial_nos": serial_nos,
					"posting_date": today(),
					"posting_time": nowtime(),
					"do_not_submit": True,
				}
			)
		).name

	if args.cost_center:
		se.get("items")[0].cost_center = args.cost_center

	if args.expense_account:
		se.get("items")[0].expense_account = args.expense_account

	se.get("items")[0].qty = 2
	se.get("items")[0].transfer_qty = 2

	se.set_stock_entry_type()
	se.insert()
	se.submit()

	se.load_from_db()
	return se


def get_qty_after_transaction(**args):
	args = frappe._dict(args)
	last_sle = get_previous_sle(
		{
			"item_code": args.item_code or "_Test Item",
			"warehouse": args.warehouse or "_Test Warehouse - _TC",
			"posting_date": args.posting_date or nowdate(),
			"posting_time": args.posting_time or nowtime(),
		}
	)
	return flt(last_sle.get("qty_after_transaction"))


def get_multiple_items():
	return [
		{
			"conversion_factor": 1.0,
			"cost_center": "Main - TCP1",
			"doctype": "Stock Entry Detail",
			"expense_account": "Stock Adjustment - TCP1",
			"basic_rate": 100,
			"item_code": "_Test Item",
			"qty": 50.0,
			"s_warehouse": "Stores - TCP1",
			"stock_uom": "_Test UOM",
			"transfer_qty": 50.0,
			"uom": "_Test UOM",
		},
		{
			"conversion_factor": 1.0,
			"cost_center": "Main - TCP1",
			"doctype": "Stock Entry Detail",
			"expense_account": "Stock Adjustment - TCP1",
			"basic_rate": 5000,
			"item_code": "_Test Item Home Desktop 100",
			"qty": 1,
			"stock_uom": "_Test UOM",
			"t_warehouse": "Stores - TCP1",
			"transfer_qty": 1,
			"uom": "_Test UOM",
		},
	]


def initialize_records_for_future_negative_sle_test(
	item_code, batch_no, warehouses, opening_qty, posting_date
):
	from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
	from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
		create_stock_reconciliation,
	)
	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

	TestBatch.make_batch_item(item_code)
	make_new_batch(item_code=item_code, batch_id=batch_no)
	warehouse_names = [create_warehouse(w) for w in warehouses]
	create_stock_reconciliation(
		purpose="Opening Stock",
		posting_date=posting_date,
		posting_time="20:00:20",
		item_code=item_code,
		warehouse=warehouse_names[0],
		valuation_rate=100,
		qty=opening_qty,
		batch_no=batch_no,
	)
	return warehouse_names


def create_stock_entries(sequence_of_entries):
	for entry_detail in sequence_of_entries:
		make_stock_entry(**entry_detail)
