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

# ERPNext - web based ERP (http://erpnext.com)
# For license information, please see license.txt


import frappe
from frappe.utils import add_days, flt, getdate, today

from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import (
	create_pick_list,
	make_in_transit_stock_entry,
	make_purchase_order,
	make_stock_entry,
	make_supplier_quotation,
	raise_work_orders,
)
from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite


class TestMaterialRequest(ERPNextTestSuite):
	def setUp(self):
		self.load_test_records("Material Request")

	def test_material_request_qty(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.items[0].qty = 0
		with self.assertRaises(InvalidQtyError):
			mr.insert()

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

	def test_make_purchase_order(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()

		self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.submit()
		po = make_purchase_order(mr.name)

		self.assertEqual(po.doctype, "Purchase Order")
		self.assertEqual(len(po.get("items")), len(mr.get("items")))

	def test_make_subcontracted_purchase_order(self):
		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
		from erpnext.stock.doctype.item.test_item import create_item, make_item
		from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
			create_subcontracting_bom,
		)

		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()
		mr.material_request_type = "Subcontracting"
		mr.submit()

		frappe.db.set_value("Item", mr.items[0].item_code, "is_sub_contracted_item", 1)

		raw_materials = ["Raw Material Item 1", "Raw Material Item 2"]
		for item in raw_materials:
			create_item(item)

		frappe.new_doc("UOM").update({"uom_name": "Test UOM"}).save()
		service_item = make_item(
			properties={"is_stock_item": 0}, uoms=[{"uom": "Test UOM", "conversion_factor": 3}]
		)

		mr.items[0].default_bom = make_bom(item=mr.items[0].item_code, raw_materials=raw_materials)
		mr.reload()

		create_subcontracting_bom(
			finished_good=mr.items[0].item_code,
			service_item=service_item.name,
			finished_good_qty=2,
			service_item_qty=1,
			service_item_uom="Test UOM",
		)

		po = make_purchase_order(mr.name)
		po.supplier = "_Test Supplier"
		po.items[0].schedule_date = today()
		po.items.pop(1)

		# Test 1 - Test if items stock qty, qty and finished good qty are calculated correctly based on provided UOMs
		self.assertEqual(po.items[0].stock_qty, 81)
		self.assertEqual(po.items[0].qty, 27)
		self.assertEqual(po.items[0].fg_item_qty, 54)

		po.submit()
		mr.reload()

		# Test 2 - MR items ordered qty should be updated based on PO items qty when submitted
		self.assertEqual(mr.items[0].ordered_qty, 54)

	def test_make_supplier_quotation(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()

		self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.submit()
		sq = make_supplier_quotation(mr.name)

		self.assertEqual(sq.doctype, "Supplier Quotation")
		self.assertEqual(len(sq.get("items")), len(mr.get("items")))

	def test_make_stock_entry(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()

		self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.material_request_type = "Material Transfer"
		mr.submit()
		se = make_stock_entry(mr.name)

		self.assertEqual(se.stock_entry_type, "Material Transfer")
		self.assertEqual(se.purpose, "Material Transfer")
		self.assertEqual(se.doctype, "Stock Entry")
		self.assertEqual(len(se.get("items")), len(mr.get("items")))

	def test_partial_make_stock_entry(self):
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry as _make_stock_entry

		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()

		source_wh = create_warehouse(
			warehouse_name="_Test Source Warehouse",
			properties={"parent_warehouse": "All Warehouses - _TC"},
			company="_Test Company",
		)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.material_request_type = "Material Transfer"

		for row in mr.items:
			_make_stock_entry(
				item_code=row.item_code,
				qty=10,
				to_warehouse=source_wh,
				company="_Test Company",
				rate=100,
			)

			row.from_warehouse = source_wh
			row.qty = 10

		mr.save()
		mr.submit()

		se = make_stock_entry(mr.name)
		se.get("items")[0].qty = 5
		se.insert()
		se.submit()

		mr.reload()
		self.assertEqual(mr.status, "Partially Received")

	def test_in_transit_make_stock_entry(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()

		self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.material_request_type = "Material Transfer"
		mr.submit()

		in_transit_warehouse = get_in_transit_warehouse(mr.company)
		se = make_in_transit_stock_entry(mr.name, in_transit_warehouse)

		self.assertEqual(se.stock_entry_type, "Material Transfer")
		self.assertEqual(se.purpose, "Material Transfer")
		self.assertEqual(se.doctype, "Stock Entry")
		for row in se.get("items"):
			self.assertEqual(row.t_warehouse, in_transit_warehouse)

	def _insert_stock_entry(self, qty1, qty2, warehouse=None):
		se = frappe.get_doc(
			{
				"company": "_Test Company",
				"doctype": "Stock Entry",
				"posting_date": "2013-03-01",
				"posting_time": "00:00:00",
				"purpose": "Material Receipt",
				"items": [
					{
						"conversion_factor": 1.0,
						"doctype": "Stock Entry Detail",
						"item_code": "_Test Item Home Desktop 100",
						"parentfield": "items",
						"basic_rate": 100,
						"qty": qty1,
						"stock_uom": "_Test UOM 1",
						"transfer_qty": qty1,
						"uom": "_Test UOM 1",
						"t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
						"cost_center": "_Test Cost Center - _TC",
					},
					{
						"conversion_factor": 1.0,
						"doctype": "Stock Entry Detail",
						"item_code": "_Test Item Home Desktop 200",
						"parentfield": "items",
						"basic_rate": 100,
						"qty": qty2,
						"stock_uom": "_Test UOM 1",
						"transfer_qty": qty2,
						"uom": "_Test UOM 1",
						"t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
						"cost_center": "_Test Cost Center - _TC",
					},
				],
			}
		)

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

	def test_cannot_stop_cancelled_material_request(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()

		mr.load_from_db()
		mr.cancel()
		self.assertRaises(frappe.ValidationError, mr.update_status, "Stopped")

	def test_mr_changes_from_stopped_to_pending_after_reopen(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()
		self.assertEqual("Pending", mr.status)

		mr.update_status("Stopped")
		self.assertEqual("Stopped", mr.status)

		mr.update_status("Submitted")
		self.assertEqual("Pending", mr.status)

	def test_cannot_submit_cancelled_mr(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()
		mr.load_from_db()
		mr.cancel()
		self.assertRaises(frappe.ValidationError, mr.submit)

	def test_mr_changes_from_pending_to_cancelled_after_cancel(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()
		mr.cancel()
		self.assertEqual("Cancelled", mr.status)

	def test_cannot_change_cancelled_mr(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()
		mr.load_from_db()
		mr.cancel()

		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Draft")
		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped")
		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Ordered")
		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Issued")
		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Transferred")
		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Pending")

	def test_cannot_submit_deleted_material_request(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.delete()

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

	def test_cannot_delete_submitted_mr(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()

		self.assertRaises(frappe.ValidationError, mr.delete)

	def test_stopped_mr_changes_to_pending_after_reopen(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()
		mr.load_from_db()

		mr.update_status("Stopped")
		mr.update_status("Submitted")
		self.assertEqual(mr.status, "Pending")

	def test_pending_mr_changes_to_stopped_after_stop(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()
		mr.load_from_db()

		mr.update_status("Stopped")
		self.assertEqual(mr.status, "Stopped")

	def test_cannot_stop_unsubmitted_mr(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped")

	def test_completed_qty_for_purchase(self):
		existing_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		existing_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		# submit material request of type Purchase
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.insert()
		mr.submit()

		# map a purchase order
		po_doc = make_purchase_order(mr.name)
		po_doc.supplier = "_Test Supplier"
		po_doc.transaction_date = "2013-07-07"
		po_doc.schedule_date = "2013-07-09"
		po_doc.get("items")[0].qty = 27.0
		po_doc.get("items")[1].qty = 1.5
		po_doc.get("items")[0].schedule_date = "2013-07-09"
		po_doc.get("items")[1].schedule_date = "2013-07-09"

		# check for stopped status of Material Request
		po = frappe.copy_doc(po_doc)
		po.insert()
		po.load_from_db()
		mr.update_status("Stopped")
		self.assertRaises(frappe.InvalidStatusError, po.submit)
		po.db_set("docstatus", 1)
		self.assertRaises(frappe.InvalidStatusError, po.cancel)

		# resubmit and check for per complete
		mr.load_from_db()
		mr.update_status("Submitted")
		po = frappe.copy_doc(po_doc)
		po.insert()
		po.submit()

		# check if per complete is as expected
		mr.load_from_db()
		self.assertEqual(mr.per_ordered, 50)
		self.assertEqual(mr.get("items")[0].ordered_qty, 27.0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 1.5)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)

		po.cancel()
		# check if per complete is as expected
		mr.load_from_db()
		self.assertEqual(mr.per_ordered, 0)
		self.assertEqual(mr.get("items")[0].ordered_qty, 0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 0)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)

	def test_completed_qty_for_transfer(self):
		existing_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		existing_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		# submit material request of type Purchase
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.material_request_type = "Material Transfer"
		mr.insert()
		mr.submit()

		# check if per complete is None
		mr.load_from_db()
		self.assertEqual(mr.per_ordered, 0)
		self.assertEqual(mr.get("items")[0].ordered_qty, 0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 0)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)

		# map a stock entry
		se_doc = make_stock_entry(mr.name)
		se_doc.update(
			{
				"posting_date": "2013-03-01",
				"posting_time": "01:00",
				"fiscal_year": "_Test Fiscal Year 2013",
			}
		)
		se_doc.get("items")[0].update(
			{"qty": 27.0, "transfer_qty": 27.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
		)
		se_doc.get("items")[1].update(
			{"qty": 1.5, "transfer_qty": 1.5, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
		)

		# make available the qty in _Test Warehouse 1 before transfer
		self._insert_stock_entry(27.0, 1.5)

		# check for stopped status of Material Request
		se = frappe.copy_doc(se_doc)
		se.insert()
		mr.update_status("Stopped")
		self.assertRaises(frappe.InvalidStatusError, se.submit)

		mr.update_status("Submitted")

		se.flags.ignore_validate_update_after_submit = True
		se.submit()
		mr.update_status("Stopped")
		self.assertRaises(frappe.InvalidStatusError, se.cancel)

		mr.update_status("Submitted")
		se = frappe.copy_doc(se_doc)
		se.insert()
		se.submit()

		# check if per complete is as expected
		mr.load_from_db()
		self.assertEqual(mr.per_ordered, 50)
		self.assertEqual(mr.get("items")[0].ordered_qty, 27.0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 1.5)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)

		# check if per complete is as expected for Stock Entry cancelled
		se.cancel()
		mr.load_from_db()
		self.assertEqual(mr.per_ordered, 0)
		self.assertEqual(mr.get("items")[0].ordered_qty, 0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 0)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)

	def test_over_transfer_qty_allowance(self):
		mr = frappe.new_doc("Material Request")
		mr.company = "_Test Company"
		mr.scheduled_date = today()
		mr.append(
			"items",
			{
				"item_code": "_Test FG Item",
				"item_name": "_Test FG Item",
				"qty": 10,
				"schedule_date": today(),
				"uom": "_Test UOM 1",
				"warehouse": "_Test Warehouse - _TC",
			},
		)

		mr.material_request_type = "Material Transfer"
		mr.insert()
		mr.submit()

		frappe.db.set_single_value("Stock Settings", "mr_qty_allowance", 20)

		# map a stock entry

		se_doc = make_stock_entry(mr.name)
		se_doc.update(
			{
				"posting_date": today(),
				"posting_time": "00:00",
			}
		)
		se_doc.get("items")[0].update(
			{
				"qty": 13,
				"transfer_qty": 12.0,
				"s_warehouse": "_Test Warehouse - _TC",
				"t_warehouse": "_Test Warehouse 1 - _TC",
				"basic_rate": 1.0,
			}
		)

		# make available the qty in _Test Warehouse 1 before transfer
		sr = frappe.new_doc("Stock Reconciliation")
		sr.company = "_Test Company"
		sr.purpose = "Opening Stock"
		sr.append(
			"items",
			{
				"item_code": "_Test FG Item",
				"warehouse": "_Test Warehouse - _TC",
				"qty": 20,
				"valuation_rate": 0.01,
			},
		)
		sr.insert()
		sr.submit()
		se = frappe.copy_doc(se_doc)
		se.insert()
		self.assertRaises(frappe.ValidationError)
		se.items[0].qty = 12
		se.submit()

	def test_completed_qty_for_over_transfer(self):
		existing_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		existing_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		# submit material request of type Purchase
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.material_request_type = "Material Transfer"
		mr.insert()
		mr.submit()

		# map a stock entry

		se_doc = make_stock_entry(mr.name)
		se_doc.update(
			{
				"posting_date": "2013-03-01",
				"posting_time": "00:00",
				"fiscal_year": "_Test Fiscal Year 2013",
			}
		)
		se_doc.get("items")[0].update(
			{"qty": 54.0, "transfer_qty": 54.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
		)
		se_doc.get("items")[1].update(
			{"qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0}
		)

		# make available the qty in _Test Warehouse 1 before transfer
		self._insert_stock_entry(60.0, 3.0)

		# check for stopped status of Material Request
		se = frappe.copy_doc(se_doc)
		se.set_stock_entry_type()
		se.insert()
		mr.update_status("Stopped")
		self.assertRaises(frappe.InvalidStatusError, se.submit)
		self.assertRaises(frappe.InvalidStatusError, se.cancel)

		mr.update_status("Submitted")
		se = frappe.copy_doc(se_doc)
		se.set_stock_entry_type()
		se.insert()
		se.submit()

		# check if per complete is as expected
		mr.load_from_db()

		self.assertEqual(mr.per_ordered, 100)
		self.assertEqual(mr.get("items")[0].ordered_qty, 54.0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 3.0)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2)

		# check if per complete is as expected for Stock Entry cancelled
		se.cancel()
		mr.load_from_db()
		self.assertEqual(mr.per_ordered, 0)
		self.assertEqual(mr.get("items")[0].ordered_qty, 0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 0)

		current_requested_qty_item1 = self._get_requested_qty(
			"_Test Item Home Desktop 100", "_Test Warehouse - _TC"
		)
		current_requested_qty_item2 = self._get_requested_qty(
			"_Test Item Home Desktop 200", "_Test Warehouse - _TC"
		)

		self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
		self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)

	def test_incorrect_mapping_of_stock_entry(self):
		# submit material request of type Transfer
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.material_request_type = "Material Transfer"
		mr.insert()
		mr.submit()

		se_doc = make_stock_entry(mr.name)
		se_doc.update(
			{
				"posting_date": "2013-03-01",
				"posting_time": "00:00",
				"fiscal_year": "_Test Fiscal Year 2013",
			}
		)
		se_doc.get("items")[0].update(
			{
				"qty": 60.0,
				"transfer_qty": 60.0,
				"s_warehouse": "_Test Warehouse - _TC",
				"t_warehouse": "_Test Warehouse 1 - _TC",
				"basic_rate": 1.0,
			}
		)
		se_doc.get("items")[1].update(
			{
				"item_code": "_Test Item Home Desktop 100",
				"qty": 3.0,
				"transfer_qty": 3.0,
				"s_warehouse": "_Test Warehouse 1 - _TC",
				"basic_rate": 1.0,
			}
		)

		# check for stopped status of Material Request
		se = frappe.copy_doc(se_doc)
		self.assertRaises(frappe.MappingMismatchError, se.insert)

		# submit material request of type Transfer
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.material_request_type = "Material Issue"
		mr.insert()
		mr.submit()

		se_doc = make_stock_entry(mr.name)
		self.assertEqual(se_doc.get("items")[0].s_warehouse, "_Test Warehouse - _TC")

	def test_warehouse_company_validation(self):
		from erpnext.stock.utils import InvalidWarehouseCompany

		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.company = "_Test Company 1"
		self.assertRaises(InvalidWarehouseCompany, mr.insert)

	def _get_requested_qty(self, item_code, warehouse):
		return flt(
			frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")
		)

	def test_make_stock_entry_for_material_issue(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()

		self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.material_request_type = "Material Issue"
		mr.submit()
		se = make_stock_entry(mr.name)

		self.assertEqual(se.doctype, "Stock Entry")
		self.assertEqual(len(se.get("items")), len(mr.get("items")))

	def test_completed_qty_for_issue(self):
		def _get_requested_qty():
			return flt(
				frappe.db.get_value(
					"Bin",
					{"item_code": "_Test Item Home Desktop 100", "warehouse": "_Test Warehouse - _TC"},
					"indented_qty",
				)
			)

		existing_requested_qty = _get_requested_qty()

		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.material_request_type = "Material Issue"
		mr.submit()
		frappe.db.value_cache.clear()

		# testing bin value after material request is submitted
		self.assertEqual(_get_requested_qty(), existing_requested_qty - 54.0)

		# receive items to allow issue
		self._insert_stock_entry(60, 6, "_Test Warehouse - _TC")

		# make stock entry against MR

		se_doc = make_stock_entry(mr.name)
		se_doc.fiscal_year = "_Test Fiscal Year 2014"
		se_doc.get("items")[0].qty = 54.0
		se_doc.insert()
		se_doc.submit()

		# check if per complete is as expected
		mr.load_from_db()
		self.assertEqual(mr.get("items")[0].ordered_qty, 54.0)
		self.assertEqual(mr.get("items")[1].ordered_qty, 3.0)

		# testing bin requested qty after issuing stock against material request
		self.assertEqual(_get_requested_qty(), existing_requested_qty)

	def test_material_request_type_manufacture(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][1]).insert()
		mr = frappe.get_doc("Material Request", mr.name)
		mr.submit()
		completed_qty = mr.items[0].ordered_qty
		requested_qty = frappe.db.sql(
			"""select indented_qty from `tabBin` where \
			item_code= %s and warehouse= %s """,
			(mr.items[0].item_code, mr.items[0].warehouse),
		)[0][0]

		prod_order = raise_work_orders(mr.name, mr.company)
		po = frappe.get_doc("Work Order", prod_order[0])
		po.wip_warehouse = "_Test Warehouse 1 - _TC"
		po.submit()

		mr = frappe.get_doc("Material Request", mr.name)
		self.assertEqual(completed_qty + po.qty, mr.items[0].ordered_qty)

		new_requested_qty = frappe.db.sql(
			"""select indented_qty from `tabBin` where \
			item_code= %s and warehouse= %s """,
			(mr.items[0].item_code, mr.items[0].warehouse),
		)[0][0]

		self.assertEqual(requested_qty - po.qty, new_requested_qty)

		po.cancel()

		mr = frappe.get_doc("Material Request", mr.name)
		self.assertEqual(completed_qty, mr.items[0].ordered_qty)

		new_requested_qty = frappe.db.sql(
			"""select indented_qty from `tabBin` where \
			item_code= %s and warehouse= %s """,
			(mr.items[0].item_code, mr.items[0].warehouse),
		)[0][0]
		self.assertEqual(requested_qty, new_requested_qty)

	def test_requested_qty_multi_uom(self):
		existing_requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")

		mr = make_material_request(
			item_code="_Test FG Item",
			material_request_type="Manufacture",
			uom="_Test UOM 1",
			conversion_factor=12,
		)

		requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")

		self.assertEqual(requested_qty, existing_requested_qty + 120)

		work_order = raise_work_orders(mr.name, mr.company)
		wo = frappe.get_doc("Work Order", work_order[0])
		wo.qty = 50
		wo.wip_warehouse = "_Test Warehouse 1 - _TC"
		wo.submit()

		requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
		self.assertEqual(requested_qty, existing_requested_qty + 70)

		wo.cancel()

		requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
		self.assertEqual(requested_qty, existing_requested_qty + 120)

		mr.reload()
		mr.cancel()
		requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC")
		self.assertEqual(requested_qty, existing_requested_qty)

	def test_multi_uom_for_purchase(self):
		mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0])
		mr.material_request_type = "Purchase"
		item = mr.items[0]
		mr.schedule_date = today()

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

		item.uom = "Kg"
		for item in mr.items:
			item.schedule_date = mr.schedule_date

		mr.insert()
		self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name)

		mr = frappe.get_doc("Material Request", mr.name)
		mr.submit()
		item = mr.items[0]

		self.assertEqual(item.uom, "Kg")
		self.assertEqual(item.conversion_factor, 5.0)
		self.assertEqual(item.stock_qty, flt(item.qty * 5))

		po = make_purchase_order(mr.name)
		self.assertEqual(po.doctype, "Purchase Order")
		self.assertEqual(len(po.get("items")), len(mr.get("items")))

		po.supplier = "_Test Supplier"
		po.insert()
		po.submit()
		mr = frappe.get_doc("Material Request", mr.name)
		self.assertEqual(mr.per_ordered, 100)

	def test_fractional_conversion_factor_for_purchase(self):
		item = create_item("_Test Fractional Conversion Item", stock_uom="Kg", is_purchase_item=1)
		conversion_factor = 0.453592292

		mr = make_material_request(
			item_code=item.name,
			qty=1000,
			uom="Pound",
			conversion_factor=conversion_factor,
		)
		mr.reload()

		self.assertEqual(mr.items[0].conversion_factor, conversion_factor)

		po = make_purchase_order(mr.name)
		po.supplier = "_Test Supplier"
		po.insert()
		po.reload()

		self.assertEqual(po.items[0].conversion_factor, conversion_factor)
		self.assertEqual(po.items[0].stock_qty, mr.items[0].stock_qty)

	def test_customer_provided_parts_mr(self):
		create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
		existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")

		mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided")
		se = make_stock_entry(mr.name)
		se.insert()
		se.submit()
		self.assertEqual(se.get("items")[0].amount, 0)
		self.assertEqual(se.get("items")[0].material_request, mr.name)
		mr = frappe.get_doc("Material Request", mr.name)
		mr.submit()
		current_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")

		self.assertEqual(mr.per_ordered, 100)
		self.assertEqual(existing_requested_qty, current_requested_qty)

	def test_auto_email_users_with_company_user_permissions(self):
		from erpnext.stock.reorder_item import get_email_list

		comapnywise_users = {
			"_Test Company": "test_auto_email_@example.com",
			"_Test Company 1": "test_auto_email_1@example.com",
		}

		permissions = []

		for company, user in comapnywise_users.items():
			if not frappe.db.exists("User", user):
				frappe.get_doc(
					{
						"doctype": "User",
						"email": user,
						"first_name": user,
						"send_notifications": 0,
						"enabled": 1,
						"user_type": "System User",
						"roles": [{"role": "Purchase Manager"}],
					}
				).insert(ignore_permissions=True)

			if not frappe.db.exists(
				"User Permission", {"user": user, "allow": "Company", "for_value": company}
			):
				perm_doc = frappe.get_doc(
					{
						"doctype": "User Permission",
						"user": user,
						"allow": "Company",
						"for_value": company,
						"apply_to_all_doctypes": 1,
					}
				).insert(ignore_permissions=True)

				permissions.append(perm_doc)

		comapnywise_mr_list = frappe._dict({})
		mr1 = make_material_request()
		comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name)

		mr2 = make_material_request(
			company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1"
		)
		comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name)

		for company, _mr_list in comapnywise_mr_list.items():
			emails = get_email_list(company)

			self.assertTrue(comapnywise_users[company] in emails)

		for perm in permissions:
			perm.delete()

	def test_manufacture_type_status_over_wo(self):
		from erpnext.stock.doctype.material_request.material_request import raise_work_orders

		mr = make_material_request(
			item_code="_Test FG Item", material_request_type="Manufacture", do_not_submit=False
		)

		work_order = raise_work_orders(mr.name, mr.company)
		wo = frappe.get_doc("Work Order", work_order[0])
		wo.wip_warehouse = "_Test Warehouse 1 - _TC"
		wo.submit()

		mr.reload()

		self.assertEqual(mr.per_ordered, 100)
		self.assertEqual(mr.status, "Ordered")

	def test_customer_provided_received_status(self):
		create_item("CUST-0989", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)

		mr = make_material_request(item_code="CUST-0989", material_request_type="Customer Provided")
		se = make_stock_entry(mr.name)
		se.insert()
		se.submit()

		mr.reload()

		self.assertEqual(mr.per_ordered, 100)
		self.assertEqual(mr.status, "Received")

	def test_customer_provided_partially_received_status(self):
		create_item("CUST-0990", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)

		mr = make_material_request(item_code="CUST-0990", qty=10, material_request_type="Customer Provided")
		se = make_stock_entry(mr.name)
		se.get("items")[0].qty = 5
		se.insert()
		se.submit()

		mr.reload()

		self.assertEqual(mr.per_ordered, 50)
		self.assertEqual(mr.status, "Partially Received")

	def test_material_request_qty_over_sales_order_limit(self):
		from erpnext.controllers.status_updater import OverAllowanceError
		from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

		so = make_sales_order()
		mr = make_material_request(qty=100, do_not_submit=True)
		mr.items[0].sales_order = so.name
		mr.items[0].sales_order_item = so.items[0].name
		mr.save()

		self.assertRaises(OverAllowanceError, mr.submit)

	def test_get_remaining_qty_from_sales_order(self):
		from frappe.utils import add_to_date, today

		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
		from erpnext.selling.doctype.sales_order.sales_order import make_material_request
		from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

		sub_item_a = "_Test Bundle ItemA"
		create_item(sub_item_a, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)

		sub_item_b = "_Test Bundle ItemB"
		create_item(sub_item_b, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)

		bundle_item = "_Test Bundle"
		create_item(
			bundle_item,
			is_stock_item=0,
			is_customer_provided_item=1,
			customer="_Test Customer",
			is_purchase_item=0,
		)

		make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])

		so = make_sales_order(item_code=bundle_item)
		so.submit()

		mr = make_material_request(so.name)
		mr.schedule_date = add_to_date(today(), days=1, as_string=True)
		mr.get("items")[0].qty = 5
		mr.get("items")[1].qty = 5
		mr.insert()
		mr.submit()

		mr = make_material_request(so.name)

		self.assertEqual(mr.items[0].qty, 5)
		self.assertEqual(mr.items[1].qty, 5)

	def test_pending_qty_in_pick_list(self):
		"""Test for pick list mapped doc qty from partially received Material Request Transfer"""
		import json

		from erpnext.stock.doctype.pick_list.pick_list import create_stock_entry
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		new_item = create_item("_Test Pick List Item", is_stock_item=1)
		item_code = new_item.name

		make_stock_entry(
			item_code=item_code,
			target="_Test Warehouse - _TC",
			qty=10,
			do_not_save=False,
			do_not_submit=False,
		)

		mr = make_material_request(item_code=item_code, material_request_type="Material Transfer")
		pl = create_pick_list(mr.name)
		pl.save()
		pl.locations[0].qty = 5
		pl.locations[0].stock_qty = 5
		pl.submit()

		to_warehouse = create_warehouse("_Test Warehouse - _TC")

		se_data = create_stock_entry(json.dumps(pl.as_dict()))
		se = frappe.get_doc(se_data)
		se.items[0].t_warehouse = to_warehouse
		se.save()
		se.submit()

		pl.load_from_db()
		self.assertEqual(pl.locations[0].picked_qty, se.items[0].qty)

		mr.load_from_db()
		self.assertEqual(mr.status, "Partially Received")

		pl_for_pending = create_pick_list(mr.name)
		self.assertEqual(pl_for_pending.locations[0].qty, 5)

	def test_mr_pick_list_qty_validation(self):
		"""Test for checking pick list qty validation from Material Request"""
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		make_stock_entry(
			item_code="_Test Item",
			target="_Test Warehouse - _TC",
			qty=10,
			do_not_save=False,
			do_not_submit=False,
		)

		mr = make_material_request(material_request_type="Material Transfer")
		pl = create_pick_list(mr.name)
		pl.locations[0].qty = 9
		pl.locations[0].stock_qty = 9
		pl.submit()

		mr.reload()
		self.assertEqual(mr.items[0].picked_qty, 9)

		pl = create_pick_list(mr.name)
		self.assertEqual(pl.locations[0].qty, 1)

		pl.locations[0].qty = 2
		pl.locations[0].stock_qty = 2

		# System should allow picking qty for excess transfer
		pl.submit()

	def test_mr_status_with_partial_and_excess_end_transit(self):
		material_request = make_material_request(
			material_request_type="Material Transfer",
			item_code="_Test Item Home Desktop 100",
		)

		in_transit_wh = get_in_transit_warehouse(material_request.company)

		# Make sure stock is available in source warehouse
		self._insert_stock_entry(20.0, 20.0)

		# Stock Entry (Transfer to In-Transit)
		stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
		stock_entry_1.items[0].update(
			{
				"qty": 5,
				"s_warehouse": "_Test Warehouse 1 - _TC",
			}
		)
		stock_entry_1.save().submit()

		stock_entry_2 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
		stock_entry_2.items[0].update(
			{
				"qty": 5,
				"s_warehouse": "_Test Warehouse 1 - _TC",
			}
		)
		stock_entry_2.save().submit()

		end_transit_1 = make_stock_in_entry(stock_entry_1.name)
		end_transit_1.save().submit()

		# Material Request Transfer Status should still be In Transit
		material_request.load_from_db()
		self.assertEqual(material_request.transfer_status, "In Transit")

		end_transit_2 = make_stock_in_entry(stock_entry_2.name)
		end_transit_2.items[0].update({"qty": 6})  # More than transferred
		end_transit_2.save()

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

	def test_make_stock_entry_material_issue_warehouse_mapping(self):
		"""Test to ensure while making stock entry from material request of type Material Issue, warehouse is mapped correctly"""
		mr = make_material_request(material_request_type="Material Issue", do_not_submit=True)
		mr.set_warehouse = "_Test Warehouse - _TC"
		mr.save()
		mr.submit()

		se = make_stock_entry(mr.name)
		self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
		self.assertIsNone(se.to_warehouse)
		se.save()
		se.submit()

	def test_mr_status_for_mixed_direct_and_transit_transfer(self):
		material_request = make_material_request(
			material_request_type="Material Transfer",
			item_code="_Test Item Home Desktop 100",
			qty=5,
		)

		in_transit_wh = get_in_transit_warehouse(material_request.company)

		# Make stock available
		self._insert_stock_entry(20.0, 20.0)

		# Direct Transfer for 3 Qty
		direct_transfer = make_stock_entry(material_request.name)
		direct_transfer.items[0].update(
			{
				"qty": 3,
				"transfer_qty": 3,
				"s_warehouse": "_Test Warehouse 1 - _TC",
			}
		)
		direct_transfer.save()
		direct_transfer.submit()

		# In Transit Transfer for remaining 2 Qty
		transit_transfer = make_in_transit_stock_entry(material_request.name, in_transit_wh)
		transit_transfer.items[0].update(
			{
				"qty": 2,
				"s_warehouse": "_Test Warehouse 1 - _TC",
			}
		)
		transit_transfer.save()
		transit_transfer.submit()

		# Complete End Transit
		end_transit = make_stock_in_entry(transit_transfer.name)
		end_transit.save()
		end_transit.submit()

		material_request.reload()

		self.assertEqual(material_request.per_ordered, 100)
		self.assertEqual(material_request.status, "Transferred")
		self.assertEqual(material_request.transfer_status, "Completed")

	def test_get_item_default_suppliers(self):
		from erpnext.stock.doctype.material_request.material_request import get_item_default_suppliers

		with_supplier = create_item_with_default_supplier("_Test MR Item Supplier A", "_Test Supplier")
		without_supplier = create_item("_Test MR Item Without Supplier").name

		mr = make_material_request_for_items([with_supplier, without_supplier])
		items = get_item_default_suppliers(mr.name)

		self.assertEqual([d["item_code"] for d in items], [with_supplier, without_supplier])
		self.assertEqual(items[0]["supplier"], "_Test Supplier")
		self.assertFalse(items[1]["supplier"])
		self.assertEqual(items[0]["pending_qty"], 10)

	def test_make_purchase_order_sets_supplier(self):
		mr = make_material_request_for_items(["_Test Item"])
		po = make_purchase_order(mr.name, args={"supplier": "_Test Supplier"})

		self.assertEqual(po.supplier, "_Test Supplier")

	def test_make_purchase_orders_by_supplier(self):
		from erpnext.stock.doctype.material_request.material_request import make_purchase_orders_by_supplier

		item_codes = [create_item(f"_Test MR Grouped Item {index}").name for index in range(1, 4)]
		mr = make_material_request_for_items(item_codes)
		suppliers = ["_Test Supplier", "_Test Supplier", "_Test Supplier 1"]

		purchase_orders = make_purchase_orders_by_supplier(
			mr.name,
			[
				{
					"material_request_item": item.name,
					"item_code": item.item_code,
					"qty": qty,
					"supplier": supplier,
				}
				for item, supplier, qty in zip(mr.items, suppliers, [10, 10, 4], strict=True)
			],
		)

		self.assertEqual(len(purchase_orders), 2)

		first, second = (frappe.get_doc("Purchase Order", name) for name in purchase_orders)
		self.assertEqual(first.supplier, "_Test Supplier")
		self.assertEqual([d.item_code for d in first.items], item_codes[:2])
		self.assertEqual(second.supplier, "_Test Supplier 1")
		self.assertEqual([d.item_code for d in second.items], item_codes[2:])
		self.assertEqual(second.items[0].qty, 4)
		self.assertEqual(second.items[0].stock_qty, 4)

	def test_make_purchase_orders_by_supplier_sets_schedule_date(self):
		from erpnext.stock.doctype.material_request.material_request import make_purchase_orders_by_supplier

		mr = make_material_request_for_items(["_Test Item"])
		frappe.db.set_value("Material Request Item", mr.items[0].name, "schedule_date", add_days(today(), -1))

		purchase_orders = make_purchase_orders_by_supplier(
			mr.name,
			[
				{
					"material_request_item": mr.items[0].name,
					"item_code": "_Test Item",
					"qty": 10,
					"supplier": "_Test Supplier",
				}
			],
		)

		po = frappe.get_doc("Purchase Order", purchase_orders[0])
		self.assertEqual(po.schedule_date, getdate(today()))

		alerts = [m for m in frappe.get_message_log() if m.get("alert")]
		self.assertTrue(any("was set to today" in m.get("message") for m in alerts))

	def test_make_purchase_orders_by_supplier_invalid_rows(self):
		from erpnext.stock.doctype.material_request.material_request import make_purchase_orders_by_supplier

		mr = make_material_request_for_items(["_Test Item"])
		row = {
			"material_request_item": mr.items[0].name,
			"item_code": "_Test Item",
			"qty": 10,
			"supplier": "_Test Supplier",
		}

		for invalid in [{"supplier": None}, {"qty": 0}, {"qty": -5}, {"qty": 11}]:
			self.assertRaises(
				frappe.ValidationError, make_purchase_orders_by_supplier, mr.name, [row | invalid]
			)

		self.assertRaises(frappe.ValidationError, make_purchase_orders_by_supplier, mr.name, [])

		self.assertRaises(
			frappe.ValidationError,
			make_purchase_orders_by_supplier,
			mr.name,
			[row, row | {"supplier": "_Test Supplier 1"}],
		)


def create_item_with_default_supplier(item_code, supplier):
	item = create_item(item_code)
	item.set("item_defaults", [])
	item.append(
		"item_defaults",
		{
			"company": "_Test Company",
			"default_warehouse": "_Test Warehouse - _TC",
			"default_supplier": supplier,
		},
	)
	item.save()

	return item.name


def make_material_request_for_items(item_codes, **args):
	args = frappe._dict(args)
	mr = frappe.new_doc("Material Request")
	mr.material_request_type = args.material_request_type or "Purchase"
	mr.company = args.company or "_Test Company"
	mr.schedule_date = today()
	for item_code in item_codes:
		mr.append(
			"items",
			{
				"item_code": item_code,
				"qty": args.qty or 10,
				"schedule_date": today(),
				"warehouse": args.warehouse or "_Test Warehouse - _TC",
			},
		)

	mr.insert()
	mr.submit()

	return mr


def get_in_transit_warehouse(company):
	if not frappe.db.exists("Warehouse Type", "Transit"):
		frappe.get_doc(
			{
				"doctype": "Warehouse Type",
				"name": "Transit",
			}
		).insert()

	in_transit_warehouse = frappe.db.exists("Warehouse", {"warehouse_type": "Transit", "company": company})

	if not in_transit_warehouse:
		in_transit_warehouse = (
			frappe.get_doc(
				{
					"doctype": "Warehouse",
					"warehouse_name": "Transit",
					"warehouse_type": "Transit",
					"company": company,
				}
			)
			.insert()
			.name
		)

	return in_transit_warehouse


def make_material_request(**args):
	args = frappe._dict(args)
	mr = frappe.new_doc("Material Request")
	mr.material_request_type = args.material_request_type or "Purchase"
	mr.company = args.company or "_Test Company"
	if mr.material_request_type == "Customer Provided":
		mr.customer = args.customer or "_Test Customer"
	mr.append(
		"items",
		{
			"item_code": args.item_code or "_Test Item",
			"qty": args.qty or 10,
			"uom": args.uom or "_Test UOM",
			"conversion_factor": args.conversion_factor or 1,
			"schedule_date": args.schedule_date or today(),
			"from_warehouse": args.from_warehouse,
			"warehouse": args.warehouse or "_Test Warehouse - _TC",
			"cost_center": args.cost_center or "_Test Cost Center - _TC",
		},
	)
	mr.insert()
	if not args.do_not_submit:
		mr.submit()
	return mr
