Merge branch 'develop' into optimize-get-item-details
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index f492a03..e11fe13 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1087,8 +1087,6 @@
 
 		actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
 
-		frappe.db.commit()
-
 		self.assertEqual(actual_qty_0 - 5, actual_qty_1)
 
 		# outgoing_rate
diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
index a5b0947..fd23795 100644
--- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
@@ -45,7 +45,6 @@
 		pr = make_purchase_receipt(po.name)
 		pr.get("items")[0].cost_center = "Main - _TPC"
 		pr.submit()
-		frappe.db.commit()
 		date_obj = datetime.date(datetime.now())
 
 		po.load_from_db()
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 78a6e52..4697205 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -591,7 +591,7 @@
 
 	data = frappe.db.sql("""
 		select item_code, warehouse, count(name) as total_row
-		from `tabStock Ledger Entry`
+		from `tabStock Ledger Entry` force index (item_warehouse)
 		where
 			({})
 			and timestamp(posting_date, posting_time)
diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
index bed12e3..8a23682 100644
--- a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
+++ b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
@@ -74,7 +74,6 @@
 			from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
 			where q.name = r.parent""", as_dict=1)
 
-		frappe.db.commit()
 
 	def setup_groups(self, hour=None):
 		# setup email to trigger at this hour
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index c9f23ca..5979992 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -334,7 +334,6 @@
   },
   {
    "depends_on": "eval:doc.is_secured_loan",
-   "fetch_from": "loan_application.maximum_loan_amount",
    "fieldname": "maximum_loan_amount",
    "fieldtype": "Currency",
    "label": "Maximum Loan Amount",
@@ -360,7 +359,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-04-19 18:10:32.360818",
+ "modified": "2021-10-12 18:10:32.360818",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 7dbd422..0f2c3cf 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -137,16 +137,23 @@
 			frappe.throw(_("Loan amount is mandatory"))
 
 	def link_loan_security_pledge(self):
-		if self.is_secured_loan:
-			loan_security_pledge = frappe.db.get_value('Loan Security Pledge', {'loan_application': self.loan_application},
-				'name')
+		if self.is_secured_loan and self.loan_application:
+			maximum_loan_value = frappe.db.get_value('Loan Security Pledge',
+				{
+					'loan_application': self.loan_application,
+					'status': 'Requested'
+				},
+				'sum(maximum_loan_value)'
+			)
 
-			if loan_security_pledge:
-				frappe.db.set_value('Loan Security Pledge', loan_security_pledge, {
-					'loan': self.name,
-					'status': 'Pledged',
-					'pledge_time': now_datetime()
-				})
+			if maximum_loan_value:
+				frappe.db.sql("""
+					UPDATE `tabLoan Security Pledge`
+					SET loan = %s, pledge_time = %s, status = 'Pledged'
+					WHERE status = 'Requested' and loan_application = %s
+				""", (self.name, now_datetime(), self.loan_application))
+
+				self.db_set('maximum_loan_amount', maximum_loan_value)
 
 	def unlink_loan_security_pledge(self):
 		pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index e492920..ede0467 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -130,10 +130,11 @@
 def create_loan(source_name, target_doc=None, submit=0):
 	def update_accounts(source_doc, target_doc, source_parent):
 		account_details = frappe.get_all("Loan Type",
-		 fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
-		 filters = {'name': source_doc.loan_type}
-		)[0]
+			fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
+			filters = {'name': source_doc.loan_type})[0]
 
+		if source_doc.is_secured_loan:
+			target_doc.maximum_loan_amount = 0
 
 		target_doc.mode_of_payment = account_details.mode_of_payment
 		target_doc.payment_account = account_details.payment_account
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 6d9d4f4..99f0d25 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -198,7 +198,7 @@
 		security_value = get_total_pledged_security_value(loan)
 
 	if loan_details.is_secured_loan and not on_current_security_price:
-		security_value = flt(loan_details.maximum_loan_amount)
+		security_value = get_maximum_amount_as_per_pledged_security(loan)
 
 	if not security_value and not loan_details.is_secured_loan:
 		security_value = flt(loan_details.loan_amount)
@@ -209,3 +209,6 @@
 		disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
 
 	return disbursal_amount
+
+def get_maximum_amount_as_per_pledged_security(loan):
+	return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)'))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 13b7357..40bb581 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -411,7 +411,7 @@
 		if due_date and not final_due_date:
 			final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
 
-	if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
+	if against_loan_doc.status in ('Disbursed', 'Closed') or against_loan_doc.disbursed_amount >= against_loan_doc.loan_amount:
 		pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
 			- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
 	else:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index c55fb0a..f15c65e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -299,7 +299,7 @@
 erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
 erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
 erpnext.patches.v13_0.create_custom_field_for_finance_book
-erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
+erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries #2
 erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
 erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
 erpnext.patches.v13_0.add_default_interview_notification_templates
diff --git a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
index fa8a864..3af7dac 100644
--- a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
+++ b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
@@ -17,7 +17,7 @@
 		where
 			ref_exchange_rate = 1
 			and docstatus = 1
-			and ifnull(exchange_gain_loss, '') != ''
+			and ifnull(exchange_gain_loss, 0) != 0
 		group by
 			parent
 	""", as_dict=1)
@@ -30,7 +30,7 @@
 		where
 			ref_exchange_rate = 1
 			and docstatus = 1
-			and ifnull(exchange_gain_loss, '') != ''
+			and ifnull(exchange_gain_loss, 0) != 0
 		group by
 			parent
 	""", as_dict=1)
@@ -38,12 +38,24 @@
 	if purchase_invoices + sales_invoices:
 		frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
 
+	acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
+	if acc_frozen_upto:
+		frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+
 	for invoice in purchase_invoices + sales_invoices:
-		doc = frappe.get_doc(invoice.type, invoice.name)
-		doc.docstatus = 2
-		doc.make_gl_entries()
-		for advance in doc.advances:
-			if advance.ref_exchange_rate == 1:
-				advance.db_set('exchange_gain_loss', 0, False)
-		doc.docstatus = 1
-		doc.make_gl_entries()
\ No newline at end of file
+		try:
+			doc = frappe.get_doc(invoice.type, invoice.name)
+			doc.docstatus = 2
+			doc.make_gl_entries()
+			for advance in doc.advances:
+				if advance.ref_exchange_rate == 1:
+					advance.db_set('exchange_gain_loss', 0, False)
+			doc.docstatus = 1
+			doc.make_gl_entries()
+			frappe.db.commit()
+		except Exception:
+			frappe.db.rollback()
+			print(f'Failed to correct gl entries of {invoice.name}')
+
+	if acc_frozen_upto:
+		frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', acc_frozen_upto)
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index bbfe7c0..222e74e 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1382,7 +1382,6 @@
 
 	frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True)
 	frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True)
-	frappe.db.commit()
 	frappe.cache().hdel('roles', frappe.session.user)
 
 	workflow = frappe.get_doc({
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
index f8a22b0..1164a5d 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
@@ -44,7 +44,6 @@
 
 	def test_tax_rule_validation(self):
 		frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
-		frappe.db.commit()
 
 		cart_settings = self.get_cart_settings()
 		cart_settings.enabled = 1
diff --git a/erpnext/shopping_cart/utils.py b/erpnext/shopping_cart/utils.py
index f412e61..5f0c792 100644
--- a/erpnext/shopping_cart/utils.py
+++ b/erpnext/shopping_cart/utils.py
@@ -1,8 +1,5 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-
 import frappe
 
 from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
@@ -18,10 +15,19 @@
 	return False
 
 def set_cart_count(login_manager):
-	role, parties = check_customer_or_supplier()
-	if role == 'Supplier': return
+	# since this is run only on hooks login event
+	# make sure user is already a customer
+	# before trying to set cart count
+	user_is_customer = is_customer()
+	if not user_is_customer:
+		return
+
 	if show_cart_count():
 		from erpnext.shopping_cart.cart import set_cart_count
+
+		# set_cart_count will try to fetch existing cart quotation
+		# or create one if non existent (and create a customer too)
+		# cart count is calculated from this quotation's items
 		set_cart_count()
 
 def clear_cart_count(login_manager):
@@ -32,13 +38,13 @@
 	cart_enabled = is_cart_enabled()
 	context["shopping_cart_enabled"] = cart_enabled
 
-def check_customer_or_supplier():
-	if frappe.session.user:
+def is_customer():
+	if frappe.session.user and frappe.session.user != "Guest":
 		contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
 		if contact_name:
 			contact = frappe.get_doc('Contact', contact_name)
 			for link in contact.links:
-				if link.link_doctype in ('Customer', 'Supplier'):
-					return link.link_doctype, link.link_name
+				if link.link_doctype == 'Customer':
+					return True
 
-		return 'Customer', None
+		return False
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 7998930..0a663c2 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -1,8 +1,5 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
-import unittest
 
 import frappe
 from frappe.exceptions import ValidationError
@@ -11,9 +8,10 @@
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
 from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
 from erpnext.stock.get_item_details import get_item_details
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestBatch(unittest.TestCase):
+class TestBatch(ERPNextTestCase):
 	def test_item_has_batch_enabled(self):
 		self.assertRaises(ValidationError, frappe.get_doc({
 			"doctype": "Batch",
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 5fbc2d8..4be0415 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -14,51 +14,6 @@
 			self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom')
 		self.set_projected_qty()
 
-	def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
-		'''Called from erpnext.stock.utils.update_bin'''
-		self.update_qty(args)
-
-		if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
-			from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
-
-			if not args.get("posting_date"):
-				args["posting_date"] = nowdate()
-
-			if args.get("is_cancelled") and via_landed_cost_voucher:
-				return
-
-			# Reposts only current voucher SL Entries
-			# Updates valuation rate, stock value, stock queue for current transaction
-			update_entries_after({
-				"item_code": self.item_code,
-				"warehouse": self.warehouse,
-				"posting_date": args.get("posting_date"),
-				"posting_time": args.get("posting_time"),
-				"voucher_type": args.get("voucher_type"),
-				"voucher_no": args.get("voucher_no"),
-				"sle_id": args.name,
-				"creation": args.creation
-			}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
-
-			# update qty in future ale and Validate negative qty
-			update_qty_in_future_sle(args, allow_negative_stock)
-
-
-	def update_qty(self, args):
-		# update the stock values (for current quantities)
-		if args.get("voucher_type")=="Stock Reconciliation":
-			self.actual_qty = args.get("qty_after_transaction")
-		else:
-			self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
-
-		self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty"))
-		self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
-		self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
-		self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
-		self.set_projected_qty()
-		self.db_update()
-
 	def set_projected_qty(self):
 		self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty)
 			+ flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
@@ -143,3 +98,67 @@
 
 def on_doctype_update():
 	frappe.db.add_index("Bin", ["item_code", "warehouse"])
+
+
+def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
+	'''Called from erpnext.stock.utils.update_bin'''
+	update_qty(bin_name, args)
+
+	if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
+		from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
+
+		if not args.get("posting_date"):
+			args["posting_date"] = nowdate()
+
+		if args.get("is_cancelled") and via_landed_cost_voucher:
+			return
+
+		# Reposts only current voucher SL Entries
+		# Updates valuation rate, stock value, stock queue for current transaction
+		update_entries_after({
+			"item_code": args.get('item_code'),
+			"warehouse": args.get('warehouse'),
+			"posting_date": args.get("posting_date"),
+			"posting_time": args.get("posting_time"),
+			"voucher_type": args.get("voucher_type"),
+			"voucher_no": args.get("voucher_no"),
+			"sle_id": args.get('name'),
+			"creation": args.get('creation')
+		}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+		# update qty in future sle and Validate negative qty
+		update_qty_in_future_sle(args, allow_negative_stock)
+
+def get_bin_details(bin_name):
+	return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
+	'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production',
+	'reserved_qty_for_sub_contract'], as_dict=1)
+
+def update_qty(bin_name, args):
+	bin_details = get_bin_details(bin_name)
+
+	# update the stock values (for current quantities)
+	if args.get("voucher_type")=="Stock Reconciliation":
+		actual_qty = args.get('qty_after_transaction')
+	else:
+		actual_qty = bin_details.actual_qty + flt(args.get("actual_qty"))
+
+	ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
+	reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
+	indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty"))
+	planned_qty = flt(bin_details.planned_qty) + flt(args.get("planned_qty"))
+
+
+	# compute projected qty
+	projected_qty = (flt(actual_qty) + flt(ordered_qty)
+		+ flt(indented_qty) + flt(planned_qty) - flt(reserved_qty)
+		- flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract))
+
+	frappe.db.set_value('Bin', bin_name, {
+		'actual_qty': actual_qty,
+		'ordered_qty': ordered_qty,
+		'reserved_qty': reserved_qty,
+		'indented_qty': indented_qty,
+		'planned_qty': planned_qty,
+		'projected_qty': projected_qty
+	})
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 7fda94b..f58b586 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -5,7 +5,6 @@
 from __future__ import unicode_literals
 
 import json
-import unittest
 
 import frappe
 from frappe.utils import cstr, flt, nowdate, nowtime
@@ -37,9 +36,10 @@
 )
 from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
 from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestDeliveryNote(unittest.TestCase):
+class TestDeliveryNote(ERPNextTestCase):
 	def test_over_billing_against_dn(self):
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
 
diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
index c9081c9..c6ff73e 100644
--- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
@@ -14,11 +14,12 @@
 	make_expense_claim,
 	notify_customers,
 )
-from erpnext.tests.utils import create_test_contact_and_address
+from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
 
 
-class TestDeliveryTrip(unittest.TestCase):
+class TestDeliveryTrip(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		driver = create_driver()
 		create_vehicle()
 		create_delivery_notification()
@@ -32,6 +33,7 @@
 		frappe.db.sql("delete from `tabVehicle`")
 		frappe.db.sql("delete from `tabEmail Template`")
 		frappe.db.sql("delete from `tabDelivery Trip`")
+		return super().tearDown()
 
 	def test_expense_claim_fields_are_fetched_properly(self):
 		expense_claim = make_expense_claim(self.delivery_trip.name)
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index e911d35..9198272 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -4,7 +4,6 @@
 from __future__ import unicode_literals
 
 import json
-import unittest
 
 import frappe
 from frappe.test_runner import make_test_objects
@@ -25,7 +24,7 @@
 )
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 from erpnext.stock.get_item_details import get_item_details
-from erpnext.tests.utils import change_settings
+from erpnext.tests.utils import ERPNextTestCase, change_settings
 
 test_ignore = ["BOM"]
 test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
@@ -53,8 +52,9 @@
 
 	return item
 
-class TestItem(unittest.TestCase):
+class TestItem(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		frappe.flags.attribute_values = None
 
 	def get_item(self, idx):
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index 2be8ef7..af6cc47 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -4,7 +4,6 @@
 from __future__ import unicode_literals
 
 import json
-import unittest
 
 import frappe
 from frappe.utils import flt
@@ -21,10 +20,12 @@
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestItemAlternative(unittest.TestCase):
+class TestItemAlternative(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		make_items()
 
 	def test_alternative_item_for_subcontract_rm(self):
diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
index fc809f4..2cd711b 100644
--- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
@@ -3,17 +3,17 @@
 
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 
 test_records = frappe.get_test_records('Item Attribute')
 
 from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestItemAttribute(unittest.TestCase):
+class TestItemAttribute(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		if frappe.db.exists("Item Attribute", "_Test_Length"):
 			frappe.delete_doc("Item Attribute", "_Test_Length")
 
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 5ed8092..3a51fbb 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -3,17 +3,17 @@
 
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 from frappe.test_runner import make_test_records_for_doctype
 
 from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem
 from erpnext.stock.get_item_details import get_price_list_rate_for, process_args
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestItemPrice(unittest.TestCase):
+class TestItemPrice(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		frappe.db.sql("delete from `tabItem Price`")
 		make_test_records_for_doctype("Item Price", force=True)
 
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 58a72f7..339eaaa 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -4,8 +4,6 @@
 
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 from frappe.utils import flt
 
@@ -16,9 +14,10 @@
 	get_gl_entries,
 	make_purchase_receipt,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestLandedCostVoucher(unittest.TestCase):
+class TestLandedCostVoucher(ERPNextTestCase):
 	def test_landed_cost_voucher(self):
 		frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
 
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 5c2ac25..f66a228 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -6,8 +6,6 @@
 
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 from frappe.utils import flt, today
 
@@ -18,9 +16,10 @@
 	make_supplier_quotation,
 	raise_work_orders,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestMaterialRequest(unittest.TestCase):
+class TestMaterialRequest(ERPNextTestCase):
 	def test_make_purchase_order(self):
 		mr = frappe.copy_doc(test_records[0]).insert()
 
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index 193adfc..c70cba6 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -6,6 +6,8 @@
 import unittest
 
 # test_records = frappe.get_test_records('Packing Slip')
+from erpnext.tests.utils import ERPNextTestCase
+
 
 class TestPackingSlip(unittest.TestCase):
 	pass
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index aa710ad..fd0b368 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -3,8 +3,6 @@
 # See license.txt
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 
 test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
@@ -15,9 +13,10 @@
 from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
 	EmptyStockReconciliationItemsError,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestPickList(unittest.TestCase):
+class TestPickList(ERPNextTestCase):
 
 	def test_pick_list_picks_warehouse_for_each_item(self):
 		try:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 044856c..de17744 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -17,9 +17,10 @@
 from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestPurchaseReceipt(unittest.TestCase):
+class TestPurchaseReceipt(ERPNextTestCase):
 	def setUp(self):
 		frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
 
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index 0aa7610..c25bca9 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -3,8 +3,6 @@
 # See license.txt
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 
 from erpnext.stock.doctype.batch.test_batch import make_new_batch
@@ -13,9 +11,10 @@
 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.get_item_details import get_conversion_factor
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestPutawayRule(unittest.TestCase):
+class TestPutawayRule(ERPNextTestCase):
 	def setUp(self):
 		if not frappe.db.exists("Item", "_Rice"):
 			make_item("_Rice", {
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index f5d076a..308c628 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -1,8 +1,6 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
 # See license.txt
 
-import unittest
-
 import frappe
 from frappe.utils import nowdate
 
@@ -15,12 +13,14 @@
 from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
 
 # test_records = frappe.get_test_records('Quality Inspection')
 
 
-class TestQualityInspection(unittest.TestCase):
+class TestQualityInspection(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		create_item("_Test Item with QA")
 		frappe.db.set_value(
 			"Item", "_Test Item with QA", "inspection_required_before_delivery", 1
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 818c163..546e21b 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -6,8 +6,6 @@
 
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 
 from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -20,9 +18,10 @@
 test_records = frappe.get_test_records('Serial No')
 
 from erpnext.stock.doctype.serial_no.serial_no import *
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestSerialNo(unittest.TestCase):
+class TestSerialNo(ERPNextTestCase):
 	def test_cannot_create_direct(self):
 		frappe.delete_doc_if_exists("Serial No", "_TCSER0001")
 
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index 9914cf8..dcd0b7c 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -3,15 +3,15 @@
 # See license.txt
 from __future__ import unicode_literals
 
-import unittest
 from datetime import date, timedelta
 
 import frappe
 
 from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestShipment(unittest.TestCase):
+class TestShipment(ERPNextTestCase):
 	def test_shipment_from_delivery_note(self):
 		delivery_note = create_test_delivery_note()
 		delivery_note.submit()
@@ -47,7 +47,6 @@
 		}
 	)
 	delivery_note.insert()
-	frappe.db.commit()
 	return delivery_note
 
 
@@ -91,7 +90,6 @@
 		}
 	)
 	shipment.insert()
-	frappe.db.commit()
 	return shipment
 
 
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index 40ae340..2651407 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -317,7 +317,7 @@
  "in_create": 1,
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-10-08 12:42:51.857631",
+ "modified": "2021-10-08 13:42:51.857631",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 382fdfa..2cf71ac 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -181,3 +181,4 @@
 
 	frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
 	frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
+	frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 61bae49..ff33c27 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -3,8 +3,6 @@
 # See license.txt
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 from frappe.core.page.permission_manager.permission_manager import reset
 from frappe.utils import add_days, today
@@ -21,9 +19,10 @@
 	create_stock_reconciliation,
 )
 from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestStockLedgerEntry(unittest.TestCase):
+class TestStockLedgerEntry(ERPNextTestCase):
 	def setUp(self):
 		items = create_items()
 		reset('Stock Entry')
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 8647bee..415ac5e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,8 +6,6 @@
 
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 from frappe.utils import add_days, flt, nowdate, nowtime, random_string
 
@@ -22,12 +20,13 @@
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
 from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
-from erpnext.tests.utils import change_settings
+from erpnext.tests.utils import ERPNextTestCase, change_settings
 
 
-class TestStockReconciliation(unittest.TestCase):
+class TestStockReconciliation(ERPNextTestCase):
 	@classmethod
 	def setUpClass(self):
+		super().setUpClass()
 		create_batch_or_serial_no_items()
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
 
@@ -372,7 +371,6 @@
 		"""
 		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 		from erpnext.stock.stock_ledger import NegativeStockError
-		frappe.db.commit()
 
 		item_code = "Backdated-Reco-Cancellation-Item"
 		warehouse = "_Test Warehouse - _TC"
@@ -395,10 +393,6 @@
 		repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name}))
 		self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation")
 
-		# teardown
-		frappe.db.rollback()
-
-
 	def test_valid_batch(self):
 		create_batch_item_with_batch("Testing Batch Item 1", "001")
 		create_batch_item_with_batch("Testing Batch Item 2", "002")
diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
index 7e80904..bf8ac5d 100644
--- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
@@ -7,9 +7,12 @@
 
 import frappe
 
+from erpnext.tests.utils import ERPNextTestCase
 
-class TestStockSettings(unittest.TestCase):
+
+class TestStockSettings(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		frappe.db.set_value("Stock Settings", None, "clean_description_html", 0)
 
 	def test_settings(self):
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 1ca7181..98317ec 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -2,8 +2,6 @@
 # License: GNU General Public License v3. See license.txt
 from __future__ import unicode_literals
 
-import unittest
-
 import frappe
 from frappe.test_runner import make_test_records
 from frappe.utils import cint
@@ -12,11 +10,13 @@
 from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
 from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
 
 test_records = frappe.get_test_records('Warehouse')
 
-class TestWarehouse(unittest.TestCase):
+class TestWarehouse(ERPNextTestCase):
 	def setUp(self):
+		super().setUp()
 		if not frappe.get_value('Item', '_Test Item'):
 			make_test_records('Item')
 
diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py
index 21e1205..32df585 100644
--- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py
+++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py
@@ -5,9 +5,10 @@
 
 from erpnext.accounts.utils import get_fiscal_year
 from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
+from erpnext.tests.utils import ERPNextTestCase
 
 
-class TestStockAnalyticsReport(unittest.TestCase):
+class TestStockAnalyticsReport(ERPNextTestCase):
 	def test_get_period_date_ranges(self):
 
 		filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 2b6eefb..bdbec52 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -13,8 +13,8 @@
 
 import erpnext
 from erpnext.stock.utils import (
-	get_bin,
 	get_incoming_outgoing_rate_for_cancel,
+	get_or_make_bin,
 	get_valuation_method,
 )
 
@@ -804,14 +804,13 @@
 	def update_bin(self):
 		# update bin for each warehouse
 		for warehouse, data in iteritems(self.data):
-			bin_doc = get_bin(self.item_code, warehouse)
-			bin_doc.update({
+			bin_record = get_or_make_bin(self.item_code, warehouse)
+
+			frappe.db.set_value('Bin', bin_record, {
 				"valuation_rate": data.valuation_rate,
 				"actual_qty": data.qty_after_transaction,
 				"stock_value": data.stock_value
 			})
-			bin_doc.flags.via_stock_ledger_entry = True
-			bin_doc.save(ignore_permissions=True)
 
 
 def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
@@ -917,7 +916,7 @@
 		company = erpnext.get_default_company()
 
 	last_valuation_rate = frappe.db.sql("""select valuation_rate
-		from `tabStock Ledger Entry`
+		from `tabStock Ledger Entry` force index (item_warehouse)
 		where
 			item_code = %s
 			AND warehouse = %s
@@ -928,7 +927,7 @@
 	if not last_valuation_rate:
 		# Get valuation rate from last sle for the item against any warehouse
 		last_valuation_rate = frappe.db.sql("""select valuation_rate
-			from `tabStock Ledger Entry`
+			from `tabStock Ledger Entry` force index (item_code)
 			where
 				item_code = %s
 				AND valuation_rate > 0
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index aeb06e9..c4a0497 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -180,12 +180,27 @@
 	bin_obj.flags.ignore_permissions = True
 	return bin_obj
 
+def get_or_make_bin(item_code, warehouse) -> str:
+	bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
+
+	if not bin_record:
+		bin_obj = frappe.get_doc({
+			"doctype": "Bin",
+			"item_code": item_code,
+			"warehouse": warehouse,
+		})
+		bin_obj.flags.ignore_permissions = 1
+		bin_obj.insert()
+		bin_record = bin_obj.name
+
+	return bin_record
+
 def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+	from erpnext.stock.doctype.bin.bin import update_stock
 	is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
 	if is_stock_item:
-		bin = get_bin(args.get("item_code"), args.get("warehouse"))
-		bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher)
-		return bin
+		bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+		update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
 	else:
 		frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
 
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index a3cab4b..91df548 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -2,6 +2,7 @@
 # License: GNU General Public License v3. See license.txt
 
 import copy
+import unittest
 from contextlib import contextmanager
 from typing import Any, Dict, NewType, Optional
 
@@ -12,6 +13,21 @@
 ReportName = NewType("ReportName", str)
 
 
+class ERPNextTestCase(unittest.TestCase):
+	"""A sane default test class for ERPNext tests."""
+
+
+	@classmethod
+	def setUpClass(cls) -> None:
+		frappe.db.commit()
+		return super().setUpClass()
+
+	@classmethod
+	def tearDownClass(cls) -> None:
+		frappe.db.rollback()
+		return super().tearDownClass()
+
+
 def create_test_contact_and_address():
 	frappe.db.sql('delete from tabContact')
 	frappe.db.sql('delete from `tabContact Email`')