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

import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.utils import nowdate, nowtime

from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
	CanNotBeChildDoc,
	CanNotBeDefaultDimension,
	DoNotChangeError,
	delete_dimension,
	get_inventory_dimensions,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import InventoryDimensionNegativeStockError
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite


class TestInventoryDimension(ERPNextTestSuite):
	def test_validate_inventory_dimension(self):
		# Can not be child doc
		inv_dim1 = create_inventory_dimension(
			reference_document="Stock Entry Detail",
			type_of_transaction="Outward",
			dimension_name="Stock Entry",
			apply_to_all_doctypes=0,
			istable=0,
			document_type="Stock Entry",
			do_not_save=True,
		)

		self.assertRaises(CanNotBeChildDoc, inv_dim1.insert)

		inv_dim1 = create_inventory_dimension(
			reference_document="Batch",
			type_of_transaction="Outward",
			dimension_name="Batch",
			apply_to_all_doctypes=0,
			document_type="Stock Entry Detail",
			do_not_save=True,
		)

		self.assertRaises(CanNotBeDefaultDimension, inv_dim1.insert)

	def test_delete_inventory_dimension(self):
		inv_dim1 = create_inventory_dimension(
			reference_document="Shelf",
			type_of_transaction="Outward",
			dimension_name="From Shelf",
			apply_to_all_doctypes=0,
			document_type="Stock Entry Detail",
			condition="parent.purpose == 'Material Issue'",
		)

		inv_dim1.save()

		custom_field = frappe.db.get_value(
			"Custom Field", {"fieldname": "from_shelf", "dt": "Stock Entry Detail"}, "name"
		)

		self.assertTrue(custom_field)

		delete_dimension(inv_dim1.name)

		custom_field = frappe.db.get_value(
			"Custom Field", {"fieldname": "from_shelf", "dt": "Stock Entry Detail"}, "name"
		)

		self.assertFalse(custom_field)

	def test_inventory_dimension(self):
		create_warehouse("Shelf Warehouse")
		warehouse = "Shelf Warehouse - _TC"
		item_code = "_Test Item"

		inv_dim1 = create_inventory_dimension(
			reference_document="Shelf",
			type_of_transaction="Outward",
			dimension_name="Shelf",
			apply_to_all_doctypes=0,
			document_type="Stock Entry Detail",
			condition="parent.purpose == 'Material Issue'",
		)

		inv_dim1.reqd = 0
		inv_dim1.save()

		create_inventory_dimension(
			reference_document="Shelf",
			type_of_transaction="Inward",
			dimension_name="To Shelf",
			apply_to_all_doctypes=0,
			document_type="Stock Entry Detail",
			condition="parent.purpose == 'Material Receipt'",
		)

		inward = make_stock_entry(
			item_code=item_code,
			target=warehouse,
			qty=5,
			basic_rate=10,
			do_not_save=True,
			purpose="Material Receipt",
		)

		inward.items[0].to_shelf = "Shelf 1"
		inward.save()
		inward.submit()
		inward.load_from_db()

		sle_data = frappe.db.get_value(
			"Stock Ledger Entry", {"voucher_no": inward.name}, ["to_shelf", "warehouse"], as_dict=1
		)

		self.assertEqual(inward.items[0].to_shelf, "Shelf 1")
		self.assertEqual(sle_data.warehouse, warehouse)
		self.assertEqual(sle_data.to_shelf, "Shelf 1")

		outward = make_stock_entry(
			item_code=item_code,
			source=warehouse,
			qty=3,
			basic_rate=10,
			do_not_save=True,
			purpose="Material Issue",
		)

		outward.items[0].shelf = "Shelf 1"
		outward.save()
		outward.submit()
		outward.load_from_db()

		sle_shelf = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": outward.name}, "shelf")
		self.assertEqual(sle_shelf, "Shelf 1")

		inv_dim1.load_from_db()
		inv_dim1.apply_to_all_doctypes = 1

		self.assertTrue(inv_dim1.has_stock_ledger())
		self.assertRaises(DoNotChangeError, inv_dim1.save)

	def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self):
		inv_dimension = create_inventory_dimension(
			reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
		)

		inv_dimension.db_set("fetch_from_parent", "Rack")

		self.assertEqual(inv_dimension.type_of_transaction, "Both")
		self.assertEqual(inv_dimension.fetch_from_parent, "Rack")

		create_custom_field(
			"Purchase Receipt", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack")
		)

		create_custom_field(
			"Delivery Note", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack")
		)

		pr_doc = make_purchase_receipt(qty=2, do_not_submit=True)
		pr_doc.rack = "Rack 1"
		pr_doc.save()
		pr_doc.submit()

		pr_doc.load_from_db()

		self.assertEqual(pr_doc.items[0].rack, "Rack 1")
		sle_rack = frappe.db.get_value(
			"Stock Ledger Entry",
			{"voucher_detail_no": pr_doc.items[0].name, "voucher_type": pr_doc.doctype},
			"rack",
		)

		self.assertEqual(sle_rack, "Rack 1")

		dn_doc = create_delivery_note(qty=2, do_not_submit=True)
		dn_doc.rack = "Rack 1"
		dn_doc.save()
		dn_doc.submit()

		dn_doc.load_from_db()

		self.assertEqual(dn_doc.items[0].rack, "Rack 1")
		sle_rack = frappe.db.get_value(
			"Stock Ledger Entry",
			{"voucher_detail_no": dn_doc.items[0].name, "voucher_type": dn_doc.doctype},
			"rack",
		)

		self.assertEqual(sle_rack, "Rack 1")

	def test_check_standard_dimensions(self):
		create_inventory_dimension(
			reference_document="Project",
			type_of_transaction="Outward",
			dimension_name="Project",
			apply_to_all_doctypes=0,
			document_type="Stock Ledger Entry",
		)

		self.assertFalse(
			frappe.db.get_value("Custom Field", {"fieldname": "project", "dt": "Stock Ledger Entry"}, "name")
		)

	def test_check_mandatory_dimensions(self):
		doc = create_inventory_dimension(
			reference_document="Pallet",
			type_of_transaction="Outward",
			dimension_name="Pallet 75",
			apply_to_all_doctypes=0,
			document_type="Delivery Note Item",
		)

		doc.reqd = 1
		doc.save()

		# Mandatory enforcement is now done server-side, so the custom field must NOT be `reqd`.
		self.assertFalse(
			frappe.db.get_value(
				"Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item"}, "reqd"
			)
		)

		item_code = "Test Mandatory Dimension Item"
		create_item(item_code)
		warehouse = create_warehouse("Mandatory Dimension Warehouse")

		dn_doc = create_delivery_note(item_code=item_code, qty=5, warehouse=warehouse, do_not_save=True)

		# Dimension value missing -> server-side validation should block the document.
		self.assertRaises(frappe.ValidationError, dn_doc.save)

		if not frappe.db.exists("Pallet", "Pallet 75 Value"):
			frappe.get_doc({"doctype": "Pallet", "pallet_name": "Pallet 75 Value"}).insert(
				ignore_permissions=True
			)

		dn_doc.items[0].pallet_75 = "Pallet 75 Value"
		dn_doc.save()

		doc.reqd = 0
		doc.save()

	def test_check_mandatory_depends_on_backend(self):
		doc = create_inventory_dimension(
			reference_document="Pallet",
			type_of_transaction="Outward",
			dimension_name="Pallet Backend",
			apply_to_all_doctypes=0,
			document_type="Delivery Note Item",
		)

		doc.reqd = 0
		doc.mandatory_depends_on_backend = "doc.qty > 0"
		doc.save()

		# The condition is enforced server-side, the custom field must not carry field-level `reqd`.
		self.assertFalse(
			frappe.db.get_value(
				"Custom Field", {"fieldname": "pallet_backend", "dt": "Delivery Note Item"}, "reqd"
			)
		)

		item_code = "Test Backend Dimension Item"
		create_item(item_code)
		warehouse = create_warehouse("Backend Dimension Warehouse")

		dn_doc = create_delivery_note(item_code=item_code, qty=5, warehouse=warehouse, do_not_save=True)

		# qty > 0 -> backend condition is met, so the dimension is mandatory and blocks the save.
		self.assertRaises(frappe.ValidationError, dn_doc.save)

		if not frappe.db.exists("Pallet", "Pallet Backend Value"):
			frappe.get_doc({"doctype": "Pallet", "pallet_name": "Pallet Backend Value"}).insert(
				ignore_permissions=True
			)

		dn_doc.items[0].pallet_backend = "Pallet Backend Value"
		dn_doc.save()

		# Reset so the always-true condition does not make the dimension mandatory for
		# subsequent Delivery Note tests sharing the same test database.
		doc.mandatory_depends_on_backend = ""
		doc.save()

	def test_for_purchase_sales_and_stock_transaction(self):
		from erpnext.controllers.sales_and_purchase_return import make_return_doc

		create_inventory_dimension(
			reference_document="Store",
			type_of_transaction="Outward",
			dimension_name="Store",
			apply_to_all_doctypes=1,
		)

		item_code = "Test Inventory Dimension Item"
		create_item(item_code)
		warehouse = create_warehouse("Store Warehouse")
		rj_warehouse = create_warehouse("RJ Warehouse")

		if not frappe.db.exists("Store", "Rejected Store"):
			frappe.get_doc({"doctype": "Store", "store_name": "Rejected Store"}).insert(
				ignore_permissions=True
			)

		# Purchase Receipt -> Inward in Store 1
		pr_doc = make_purchase_receipt(
			item_code=item_code,
			warehouse=warehouse,
			qty=10,
			rejected_qty=5,
			rate=100,
			rejected_warehouse=rj_warehouse,
			do_not_submit=True,
		)

		pr_doc.items[0].store = "Store 1"
		pr_doc.items[0].rejected_store = "Rejected Store"
		pr_doc.save()
		pr_doc.submit()

		entries = frappe.get_all(
			"Stock Ledger Entry",
			filters={"voucher_no": pr_doc.name, "warehouse": warehouse},
			fields=["store"],
			order_by="creation",
		)

		self.assertEqual(entries[0].store, "Store 1")

		entries = frappe.get_all(
			"Stock Ledger Entry",
			filters={"voucher_no": pr_doc.name, "warehouse": rj_warehouse},
			fields=["store"],
			order_by="creation",
		)

		self.assertEqual(entries[0].store, "Rejected Store")

		# Stock Entry -> Transfer from Store 1 to Store 2
		se_doc = make_stock_entry(
			item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True
		)

		se_doc.items[0].store = "Store 1"
		se_doc.items[0].to_store = "Store 2"

		se_doc.save()
		se_doc.submit()

		entries = get_voucher_sl_entries(se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"])

		for entry in entries:
			self.assertEqual(entry.warehouse, warehouse)
			if entry.actual_qty > 0:
				self.assertEqual(entry.store, "Store 2")
				self.assertEqual(entry.incoming_rate, 100.0)
			else:
				self.assertEqual(entry.store, "Store 1")

		# Delivery Note -> Outward from Store 2

		dn_doc = create_delivery_note(item_code=item_code, qty=10, warehouse=warehouse, do_not_save=True)

		dn_doc.items[0].store = "Store 2"
		dn_doc.save()
		dn_doc.submit()

		entries = get_voucher_sl_entries(dn_doc.name, ["warehouse", "store", "actual_qty"])

		self.assertEqual(entries[0].warehouse, warehouse)
		self.assertEqual(entries[0].store, "Store 2")
		self.assertEqual(entries[0].actual_qty, -10.0)

		return_dn = make_return_doc("Delivery Note", dn_doc.name)
		return_dn.submit()
		entries = get_voucher_sl_entries(return_dn.name, ["warehouse", "store", "actual_qty"])

		self.assertEqual(entries[0].warehouse, warehouse)
		self.assertEqual(entries[0].store, "Store 2")
		self.assertEqual(entries[0].actual_qty, 10.0)

		se_doc = make_stock_entry(
			item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True
		)

		se_doc.items[0].store = "Store 2"
		se_doc.items[0].to_store = "Store 1"

		se_doc.save()
		se_doc.submit()

		return_pr = make_return_doc("Purchase Receipt", pr_doc.name)
		return_pr.submit()
		entries = get_voucher_sl_entries(return_pr.name, ["warehouse", "store", "actual_qty"])

		self.assertEqual(entries[0].warehouse, warehouse)
		self.assertEqual(entries[0].store, "Store 1")
		self.assertEqual(entries[0].actual_qty, -10.0)

	def test_inter_transfer_return_against_inventory_dimension(self):
		from erpnext.controllers.sales_and_purchase_return import make_return_doc
		from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt

		data = prepare_data_for_internal_transfer()

		dn_doc = create_delivery_note(
			customer=data.customer,
			company=data.company,
			warehouse=data.from_warehouse,
			target_warehouse=data.to_warehouse,
			qty=5,
			cost_center=data.cost_center,
			expense_account=data.expense_account,
			do_not_submit=True,
		)

		dn_doc.items[0].store = "Inter Transfer Store 1"
		dn_doc.items[0].to_store = "Inter Transfer Store 2"
		dn_doc.save()
		dn_doc.submit()

		for d in get_voucher_sl_entries(dn_doc.name, ["store", "actual_qty"]):
			if d.actual_qty > 0:
				self.assertEqual(d.store, "Inter Transfer Store 2")
			else:
				self.assertEqual(d.store, "Inter Transfer Store 1")

		pr_doc = make_inter_company_purchase_receipt(dn_doc.name)
		pr_doc.items[0].warehouse = data.store_warehouse
		pr_doc.items[0].from_store = "Inter Transfer Store 2"
		pr_doc.items[0].store = "Inter Transfer Store 3"
		pr_doc.save()
		pr_doc.submit()

		for d in get_voucher_sl_entries(pr_doc.name, ["store", "actual_qty"]):
			if d.actual_qty > 0:
				self.assertEqual(d.store, "Inter Transfer Store 3")
			else:
				self.assertEqual(d.store, "Inter Transfer Store 2")

		return_doc = make_return_doc("Purchase Receipt", pr_doc.name)
		return_doc.submit()

		for d in get_voucher_sl_entries(return_doc.name, ["store", "actual_qty"]):
			if d.actual_qty > 0:
				self.assertEqual(d.store, "Inter Transfer Store 2")
			else:
				self.assertEqual(d.store, "Inter Transfer Store 3")

		dn_doc.load_from_db()

		return_doc1 = make_return_doc("Delivery Note", dn_doc.name)
		return_doc1.posting_date = nowdate()
		return_doc1.posting_time = nowtime()
		return_doc1.items[0].target_warehouse = dn_doc.items[0].target_warehouse
		return_doc1.items[0].warehouse = dn_doc.items[0].warehouse
		return_doc1.save()
		return_doc1.submit()

		for d in get_voucher_sl_entries(return_doc1.name, ["store", "actual_qty"]):
			if d.actual_qty > 0:
				self.assertEqual(d.store, "Inter Transfer Store 1")
			else:
				self.assertEqual(d.store, "Inter Transfer Store 2")

	def test_validate_negative_stock_for_inventory_dimension(self):
		item_code = "Test Negative Inventory Dimension Item"
		frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
		create_item(item_code)

		inv_dimension = create_inventory_dimension(
			apply_to_all_doctypes=1,
			dimension_name="Inv Site",
			reference_document="Inv Site",
			document_type="Inv Site",
			validate_negative_stock=1,
		)

		warehouse = create_warehouse("Negative Stock Warehouse")

		# Try issuing 10 qty, more than available stock against inventory dimension
		doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True)
		doc.items[0].inv_site = "Site 1"
		self.assertRaises(InventoryDimensionNegativeStockError, doc.submit)

		# cancel the stock entry
		doc.reload()
		if doc.docstatus == 1:
			doc.cancel()

		# Receive 10 qty against inventory dimension
		doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True)
		doc.items[0].to_inv_site = "Site 1"
		doc.submit()

		# check inventory dimension value in stock ledger entry
		site_name = frappe.get_all(
			"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
		)[0].inv_site

		self.assertEqual(site_name, "Site 1")

		# Receive another 100 qty without inventory dimension
		doc = make_stock_entry(item_code=item_code, target=warehouse, qty=100)

		# Try issuing 100 qty, more than available stock against inventory dimension
		# Note: total available qty for the item is 110, but against inventory dimension, only 10 qty is available
		doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
		doc.items[0].inv_site = "Site 1"
		self.assertRaises(InventoryDimensionNegativeStockError, doc.submit)

		# disable validate_negative_stock for inventory dimension
		inv_dimension.reload()
		inv_dimension.db_set("validate_negative_stock", 0)
		frappe.clear_cache(doctype="Inventory Dimension")

		# Try issuing 100 qty, more than available stock against inventory dimension
		doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True)
		doc.items[0].inv_site = "Site 1"
		doc.submit()
		self.assertEqual(doc.docstatus, 1)

		# check inventory dimension value in stock ledger entry
		site_name = frappe.get_all(
			"Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"]
		)[0].inv_site

		self.assertEqual(site_name, "Site 1")

	@ERPNextTestSuite.change_settings("Stock Settings", {"allow_negative_stock": 0})
	def test_validate_negative_stock_with_multiple_dimension(self):
		item_code = "Test Negative Multi Inventory Dimension Item"
		create_item(item_code)

		inv_dimension_1 = create_inventory_dimension(
			apply_to_all_doctypes=1,
			dimension_name="Inv Site",
			reference_document="Inv Site",
			document_type="Inv Site",
			validate_negative_stock=1,
		)
		inv_dimension_1.db_set("validate_negative_stock", 1)

		inv_dimension_2 = create_inventory_dimension(
			apply_to_all_doctypes=1,
			dimension_name="Rack",
			reference_document="Rack",
			document_type="Rack",
			validate_negative_stock=1,
		)
		inv_dimension_2.db_set("validate_negative_stock", 1)

		pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
		pr_doc.items[0].inv_site = "Site 1"
		pr_doc.items[0].rack = "Rack 1"
		pr_doc.save()
		pr_doc.submit()

		pr_doc = make_purchase_receipt(item_code=item_code, qty=15, do_not_submit=True)
		pr_doc.items[0].inv_site = "Site 1"
		pr_doc.items[0].rack = "Rack 2"
		pr_doc.save()
		pr_doc.submit()

		pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
		pr_doc.items[0].inv_site = "Site 2"
		pr_doc.items[0].rack = "Rack 1"
		pr_doc.save()
		pr_doc.submit()

		pr_doc = make_purchase_receipt(item_code=item_code, qty=25, do_not_submit=True)
		pr_doc.items[0].inv_site = "Site 2"
		pr_doc.items[0].rack = "Rack 2"
		pr_doc.save()
		pr_doc.submit()

		dn_doc = create_delivery_note(item_code=item_code, qty=35, do_not_submit=True)
		dn_doc.items[0].inv_site = "Site 2"
		dn_doc.items[0].rack = "Rack 1"
		dn_doc.save()
		self.assertRaises(InventoryDimensionNegativeStockError, dn_doc.submit)


def get_voucher_sl_entries(voucher_no, fields):
	return frappe.get_all(
		"Stock Ledger Entry", filters={"voucher_no": voucher_no}, fields=fields, order_by="creation"
	)


def create_inventory_dimension(**args):
	args = frappe._dict(args)

	if frappe.db.exists("Inventory Dimension", args.dimension_name):
		return frappe.get_doc("Inventory Dimension", args.dimension_name)

	doc = frappe.new_doc("Inventory Dimension")
	doc.update(args)

	if not args.do_not_save:
		doc.insert(ignore_permissions=True)

	return doc


def prepare_data_for_internal_transfer():
	from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
	from erpnext.selling.doctype.customer.test_customer import create_internal_customer
	from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

	company = "_Test Company with perpetual inventory"

	customer = create_internal_customer(
		"_Test Internal Customer 2",
		company,
		company,
	)

	supplier = create_internal_supplier(
		"_Test Internal Supplier 2",
		company,
		company,
	)

	for store in ["Inter Transfer Store 1", "Inter Transfer Store 2", "Inter Transfer Store 3"]:
		if not frappe.db.exists("Store", store):
			frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True)

	warehouse = create_warehouse("_Test Internal Warehouse New A", company=company)

	to_warehouse = create_warehouse("_Test Internal Warehouse GIT A", company=company)

	pr_doc = make_purchase_receipt(company=company, warehouse=warehouse, qty=10, rate=100, do_not_submit=True)
	pr_doc.items[0].store = "Inter Transfer Store 1"
	pr_doc.submit()

	if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"):
		account = "Unrealized Profit and Loss - TCP1"
		if not frappe.db.exists("Account", account):
			frappe.get_doc(
				{
					"doctype": "Account",
					"account_name": "Unrealized Profit and Loss",
					"parent_account": "Direct Income - TCP1",
					"company": company,
					"is_group": 0,
					"account_type": "Income Account",
				}
			).insert()

		frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account)

	cost_center = frappe.db.get_value("Company", company, "cost_center") or frappe.db.get_value(
		"Cost Center", {"company": company}, "name"
	)

	expene_account = frappe.db.get_value(
		"Company", company, "stock_adjustment_account"
	) or frappe.db.get_value("Account", {"company": company, "account_type": "Expense Account"}, "name")

	return frappe._dict(
		{
			"from_warehouse": warehouse,
			"to_warehouse": to_warehouse,
			"customer": customer,
			"supplier": supplier,
			"company": company,
			"cost_center": cost_center,
			"expene_account": expene_account,
			"store_warehouse": frappe.db.get_value(
				"Warehouse", {"name": ("like", "Store%"), "company": company}, "name"
			),
		}
	)
