Merge branch 'develop' into early-payment-loss
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index d67d59b..a4f6a74 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -211,8 +211,7 @@
 			# Handle Accounts with '0' balance in Account/Base Currency
 			for d in [x for x in account_details if x.zero_balance]:
 
-				# TODO: Set new balance in Base/Account currency
-				if d.balance > 0:
+				if d.balance != 0:
 					current_exchange_rate = new_exchange_rate = 0
 
 					new_balance_in_account_currency = 0  # this will be '0'
@@ -399,6 +398,9 @@
 
 		journal_entry_accounts = []
 		for d in accounts:
+			if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
+				continue
+
 			dr_or_cr = (
 				"debit_in_account_currency"
 				if d.get("balance_in_account_currency") > 0
@@ -448,7 +450,13 @@
 				}
 			)
 
-		journal_entry_accounts.append(
+		journal_entry.set("accounts", journal_entry_accounts)
+		journal_entry.set_amounts_in_company_currency()
+		journal_entry.set_total_debit_credit()
+
+		self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
+		journal_entry.append(
+			"accounts",
 			{
 				"account": unrealized_exchange_gain_loss_account,
 				"balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@
 				"exchange_rate": 1,
 				"reference_type": "Exchange Rate Revaluation",
 				"reference_name": self.name,
-			}
+			},
 		)
 
-		journal_entry.set("accounts", journal_entry_accounts)
 		journal_entry.set_amounts_in_company_currency()
 		journal_entry.set_total_debit_credit()
 		journal_entry.save()
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 2f43914..7005c17 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -495,26 +495,22 @@
 	"""get amount based on doctype"""
 	dt = ref_doc.doctype
 	if dt in ["Sales Order", "Purchase Order"]:
-		grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid)
-
+		grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
 	elif dt in ["Sales Invoice", "Purchase Invoice"]:
 		if ref_doc.party_account_currency == ref_doc.currency:
 			grand_total = flt(ref_doc.outstanding_amount)
 		else:
 			grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
-
 	elif dt == "POS Invoice":
 		for pay in ref_doc.payments:
 			if pay.type == "Phone" and pay.account == payment_account:
 				grand_total = pay.amount
 				break
-
 	elif dt == "Fees":
 		grand_total = ref_doc.outstanding_amount
 
 	if grand_total > 0:
 		return grand_total
-
 	else:
 		frappe.throw(_("Payment Entry is already created"))
 
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 477c726..4279aa4 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -45,7 +45,10 @@
 				frappe.get_doc(method).insert(ignore_permissions=True)
 
 	def test_payment_request_linkings(self):
-		so_inr = make_sales_order(currency="INR")
+		so_inr = make_sales_order(currency="INR", do_not_save=True)
+		so_inr.disable_rounded_total = 1
+		so_inr.save()
+
 		pr = make_payment_request(
 			dt="Sales Order",
 			dn=so_inr.name,
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 8b4d28b..fc16a91 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -136,7 +136,7 @@
 			self.in_words = money_in_words(amount, self.currency)
 
 	def calculate_commission(self):
-		if not self.meta.get_field("commission_rate"):
+		if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
 			return
 
 		self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 1f76a1a..b261795 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -19,10 +19,6 @@
 				}
 			}
 		});
-
-		if (frm.doc.opportunity_from && frm.doc.party_name){
-			frm.trigger('set_contact_link');
-		}
 	},
 
 	validate: function(frm) {
@@ -130,6 +126,10 @@
 		} else {
 			frappe.contacts.clear_address_and_contact(frm);
 		}
+
+		if (frm.doc.opportunity_from && frm.doc.party_name) {
+			frm.trigger('set_contact_link');
+		}
 	},
 
 	set_contact_link: function(frm) {
@@ -137,6 +137,8 @@
 			frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
 		} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
 			frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
+		} else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) {
+			frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'}
 		}
 	},
 
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 3133628..e82f379 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -561,7 +561,34 @@
 		)
 
 	def set_transferred_qty_in_job_card_item(self, ste_doc):
-		from frappe.query_builder.functions import Sum
+		def _get_job_card_items_transferred_qty(ste_doc):
+			from frappe.query_builder.functions import Sum
+
+			job_card_items_transferred_qty = {}
+			job_card_items = [
+				x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
+			]
+
+			if job_card_items:
+				se = frappe.qb.DocType("Stock Entry")
+				sed = frappe.qb.DocType("Stock Entry Detail")
+
+				query = (
+					frappe.qb.from_(sed)
+					.join(se)
+					.on(sed.parent == se.name)
+					.select(sed.job_card_item, Sum(sed.qty))
+					.where(
+						(sed.job_card_item.isin(job_card_items))
+						& (se.docstatus == 1)
+						& (se.purpose == "Material Transfer for Manufacture")
+					)
+					.groupby(sed.job_card_item)
+				)
+
+				job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
+
+			return job_card_items_transferred_qty
 
 		def _validate_over_transfer(row, transferred_qty):
 			"Block over transfer of items if not allowed in settings."
@@ -578,29 +605,23 @@
 					exc=JobCardOverTransferError,
 				)
 
-		for row in ste_doc.items:
-			if not row.job_card_item:
-				continue
+		job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
 
-			sed = frappe.qb.DocType("Stock Entry Detail")
-			se = frappe.qb.DocType("Stock Entry")
-			transferred_qty = (
-				frappe.qb.from_(sed)
-				.join(se)
-				.on(sed.parent == se.name)
-				.select(Sum(sed.qty))
-				.where(
-					(sed.job_card_item == row.job_card_item)
-					& (se.docstatus == 1)
-					& (se.purpose == "Material Transfer for Manufacture")
-				)
-			).run()[0][0]
-
+		if job_card_items_transferred_qty:
 			allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
-			if not allow_excess:
-				_validate_over_transfer(row, transferred_qty)
 
-			frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
+			for row in ste_doc.items:
+				if not row.job_card_item:
+					continue
+
+				transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
+
+				if not allow_excess:
+					_validate_over_transfer(row, transferred_qty)
+
+				frappe.db.set_value(
+					"Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
+				)
 
 	def set_transferred_qty(self, update_status=False):
 		"Set total FG Qty in Job Card for which RM was transferred."
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 4aff42c..97480b2 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -506,7 +506,7 @@
 				callback: function(r) {
 					if (r.message) {
 						frappe.model.set_value(cdt, cdn, {
-							"required_qty": 1,
+							"required_qty": row.required_qty || 1,
 							"item_name": r.message.item_name,
 							"description": r.message.description,
 							"source_warehouse": r.message.default_warehouse,
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index cdf1541..3573a3a 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -4,7 +4,8 @@
 
 import frappe
 from frappe import _
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Floor, Sum
+from frappe.utils import cint
 from pypika.terms import ExistsCriterion
 
 
@@ -34,57 +35,55 @@
 
 
 def get_bom_stock(filters):
-	qty_to_produce = filters.get("qty_to_produce") or 1
-	if int(qty_to_produce) < 0:
-		frappe.throw(_("Quantity to Produce can not be less than Zero"))
+	qty_to_produce = filters.get("qty_to_produce")
+	if cint(qty_to_produce) <= 0:
+		frappe.throw(_("Quantity to Produce should be greater than zero."))
 
 	if filters.get("show_exploded_view"):
 		bom_item_table = "BOM Explosion Item"
 	else:
 		bom_item_table = "BOM Item"
 
-	bin = frappe.qb.DocType("Bin")
-	bom = frappe.qb.DocType("BOM")
-	bom_item = frappe.qb.DocType(bom_item_table)
-
-	query = (
-		frappe.qb.from_(bom)
-		.inner_join(bom_item)
-		.on(bom.name == bom_item.parent)
-		.left_join(bin)
-		.on(bom_item.item_code == bin.item_code)
-		.select(
-			bom_item.item_code,
-			bom_item.description,
-			bom_item.stock_qty,
-			bom_item.stock_uom,
-			(bom_item.stock_qty / bom.quantity) * qty_to_produce,
-			Sum(bin.actual_qty),
-			Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
-		)
-		.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
-		.groupby(bom_item.item_code)
+	warehouse_details = frappe.db.get_value(
+		"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
 	)
 
-	if filters.get("warehouse"):
-		warehouse_details = frappe.db.get_value(
-			"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
-		)
+	BOM = frappe.qb.DocType("BOM")
+	BOM_ITEM = frappe.qb.DocType(bom_item_table)
+	BIN = frappe.qb.DocType("Bin")
+	WH = frappe.qb.DocType("Warehouse")
+	CONDITIONS = ()
 
-		if warehouse_details:
-			wh = frappe.qb.DocType("Warehouse")
-			query = query.where(
-				ExistsCriterion(
-					frappe.qb.from_(wh)
-					.select(wh.name)
-					.where(
-						(wh.lft >= warehouse_details.lft)
-						& (wh.rgt <= warehouse_details.rgt)
-						& (bin.warehouse == wh.name)
-					)
-				)
+	if warehouse_details:
+		CONDITIONS = ExistsCriterion(
+			frappe.qb.from_(WH)
+			.select(WH.name)
+			.where(
+				(WH.lft >= warehouse_details.lft)
+				& (WH.rgt <= warehouse_details.rgt)
+				& (BIN.warehouse == WH.name)
 			)
-		else:
-			query = query.where(bin.warehouse == filters.get("warehouse"))
+		)
+	else:
+		CONDITIONS = BIN.warehouse == filters.get("warehouse")
 
-	return query.run()
+	QUERY = (
+		frappe.qb.from_(BOM)
+		.inner_join(BOM_ITEM)
+		.on(BOM.name == BOM_ITEM.parent)
+		.left_join(BIN)
+		.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
+		.select(
+			BOM_ITEM.item_code,
+			BOM_ITEM.description,
+			BOM_ITEM.stock_qty,
+			BOM_ITEM.stock_uom,
+			BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
+			Sum(BIN.actual_qty).as_("actual_qty"),
+			Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
+		)
+		.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
+		.groupby(BOM_ITEM.item_code)
+	)
+
+	return QUERY.run()
diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
new file mode 100644
index 0000000..1c56ebe
--- /dev/null
+++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe.exceptions import ValidationError
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import floor
+
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
+	get_bom_stock as bom_stock_report,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+
+class TestBomStockReport(FrappeTestCase):
+	def setUp(self):
+		self.warehouse = "_Test Warehouse - _TC"
+		self.fg_item, self.rm_items = create_items()
+		make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
+		make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
+		self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
+
+	def test_bom_stock_report(self):
+		# Test 1: When `qty_to_produce` is 0.
+		filters = frappe._dict(
+			{
+				"bom": self.bom.name,
+				"warehouse": "Stores - _TC",
+				"qty_to_produce": 0,
+			}
+		)
+		self.assertRaises(ValidationError, bom_stock_report, filters)
+
+		# Test 2: When stock is not available.
+		data = bom_stock_report(
+			frappe._dict(
+				{
+					"bom": self.bom.name,
+					"warehouse": "Stores - _TC",
+					"qty_to_produce": 1,
+				}
+			)
+		)
+		expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
+		self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+		# Test 3: When stock is available.
+		data = bom_stock_report(
+			frappe._dict(
+				{
+					"bom": self.bom.name,
+					"warehouse": self.warehouse,
+					"qty_to_produce": 1,
+				}
+			)
+		)
+		expected_data = get_expected_data(self.bom, self.warehouse, 1)
+		self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
+
+
+def create_items():
+	fg_item = make_item(properties={"is_stock_item": 1}).name
+	rm_item1 = make_item(
+		properties={
+			"is_stock_item": 1,
+			"standard_rate": 100,
+			"opening_stock": 100,
+			"last_purchase_rate": 100,
+		}
+	).name
+	rm_item2 = make_item(
+		properties={
+			"is_stock_item": 1,
+			"standard_rate": 200,
+			"opening_stock": 200,
+			"last_purchase_rate": 200,
+		}
+	).name
+
+	return fg_item, [rm_item1, rm_item2]
+
+
+def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
+	expected_data = []
+
+	for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
+		in_stock_qty = frappe.get_cached_value(
+			"Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
+		)
+
+		expected_data.append(
+			[
+				item.item_code,
+				item.description,
+				item.stock_qty,
+				item.stock_uom,
+				item.stock_qty * qty_to_produce / bom.quantity,
+				in_stock_qty,
+				floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
+				if in_stock_qty
+				else None,
+			]
+		)
+
+	return expected_data
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2d29454..8d69ea0 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1884,11 +1884,13 @@
 
 	get_advances() {
 		if(!this.frm.is_return) {
+			var me = this;
 			return this.frm.call({
 				method: "set_advances",
 				doc: this.frm.doc,
 				callback: function(r, rt) {
 					refresh_field("advances");
+					me.frm.dirty();
 				}
 			})
 		}
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 5ce6e9c..f1df3a1 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -253,7 +253,7 @@
 	}
 
 	calculate_commission() {
-		if(!this.frm.fields_dict.commission_rate) return;
+		if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
 
 		if(this.frm.doc.commission_rate > 100) {
 			this.frm.set_value("commission_rate", 100);
diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py
index fb1a28d..0c24d3c 100644
--- a/erpnext/stock/doctype/item_alternative/item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/item_alternative.py
@@ -54,7 +54,7 @@
 		if not item_data.allow_alternative_item:
 			frappe.throw(alternate_item_check_msg.format(self.item_code))
 		if self.two_way and not alternative_item_data.allow_alternative_item:
-			frappe.throw(alternate_item_check_msg.format(self.item_code))
+			frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
 
 	def validate_duplicate(self):
 		if frappe.db.get_value(
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index dcbc460..8aeb751 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -10,6 +10,7 @@
 import frappe
 from frappe import _, msgprint
 from frappe.model.mapper import get_mapped_doc
+from frappe.query_builder.functions import Sum
 from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
 
 from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
@@ -180,6 +181,34 @@
 		self.update_requested_qty()
 		self.update_requested_qty_in_production_plan()
 
+	def get_mr_items_ordered_qty(self, mr_items):
+		mr_items_ordered_qty = {}
+		mr_items = [d.name for d in self.get("items") if d.name in mr_items]
+
+		doctype = qty_field = None
+		if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
+			doctype = frappe.qb.DocType("Stock Entry Detail")
+			qty_field = doctype.transfer_qty
+		elif self.material_request_type == "Manufacture":
+			doctype = frappe.qb.DocType("Work Order")
+			qty_field = doctype.qty
+
+		if doctype and qty_field:
+			query = (
+				frappe.qb.from_(doctype)
+				.select(doctype.material_request_item, Sum(qty_field))
+				.where(
+					(doctype.material_request == self.name)
+					& (doctype.material_request_item.isin(mr_items))
+					& (doctype.docstatus == 1)
+				)
+				.groupby(doctype.material_request_item)
+			)
+
+			mr_items_ordered_qty = frappe._dict(query.run())
+
+		return mr_items_ordered_qty
+
 	def update_completed_qty(self, mr_items=None, update_modified=True):
 		if self.material_request_type == "Purchase":
 			return
@@ -187,18 +216,13 @@
 		if not mr_items:
 			mr_items = [d.name for d in self.get("items")]
 
+		mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
+		mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+
 		for d in self.get("items"):
 			if d.name in mr_items:
 				if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
-					d.ordered_qty = flt(
-						frappe.db.sql(
-							"""select sum(transfer_qty)
-						from `tabStock Entry Detail` where material_request = %s
-						and material_request_item = %s and docstatus = 1""",
-							(self.name, d.name),
-						)[0][0]
-					)
-					mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
+					d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
 
 					if mr_qty_allowance:
 						allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
@@ -217,14 +241,7 @@
 						)
 
 				elif self.material_request_type == "Manufacture":
-					d.ordered_qty = flt(
-						frappe.db.sql(
-							"""select sum(qty)
-						from `tabWork Order` where material_request = %s
-						and material_request_item = %s and docstatus = 1""",
-							(self.name, d.name),
-						)[0][0]
-					)
+					d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
 
 				frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
 
diff --git a/pyproject.toml b/pyproject.toml
index 1c93eed..0718e5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,9 +28,6 @@
 requires = ["flit_core >=3.4,<4"]
 build-backend = "flit_core.buildapi"
 
-[tool.bench.dev-dependencies]
-hypothesis = "~=6.31.0"
-
 [tool.black]
 line-length = 99