Merge pull request #36811 from s-aga-r/SCR-SCRAP-ITEMS

feat: provision to add scrap items in Subcontracting Receipt
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
index ec55e60..ced04ce 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
@@ -3,6 +3,296 @@
 
 import unittest
 
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, today
 
-class TestExchangeRateRevaluation(unittest.TestCase):
-	pass
+from erpnext import get_default_cost_center
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.party import get_party_account
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
+	def setUp(self):
+		self.create_company()
+		self.create_usd_receivable_account()
+		self.create_item()
+		self.create_customer()
+		self.clear_old_entries()
+		self.set_system_and_company_settings()
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	def set_system_and_company_settings(self):
+		# set number and currency precision
+		system_settings = frappe.get_doc("System Settings")
+		system_settings.float_precision = 2
+		system_settings.currency_precision = 2
+		system_settings.save()
+
+		# Using Exchange Gain/Loss account for unrealized as well.
+		company_doc = frappe.get_doc("Company", self.company)
+		company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
+		company_doc.save()
+
+	@change_settings(
+		"Accounts Settings",
+		{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+	)
+	def test_01_revaluation_of_forex_balance(self):
+		"""
+		Test Forex account balance and Journal creation post Revaluation
+		"""
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debtors_usd,
+			posting_date=today(),
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+			price_list_rate=100,
+			do_not_submit=1,
+		)
+		si.currency = "USD"
+		si.conversion_rate = 80
+		si.save().submit()
+
+		err = frappe.new_doc("Exchange Rate Revaluation")
+		err.company = (self.company,)
+		err.posting_date = today()
+		accounts = err.get_accounts_data()
+		err.extend("accounts", accounts)
+		row = err.accounts[0]
+		row.new_exchange_rate = 85
+		row.new_balance_in_base_currency = flt(
+			row.new_exchange_rate * flt(row.balance_in_account_currency)
+		)
+		row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
+		err.set_total_gain_loss()
+		err = err.save().submit()
+
+		# Create JV for ERR
+		err_journals = err.make_jv_entries()
+		je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
+		je = je.submit()
+
+		je.reload()
+		self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
+		self.assertEqual(je.total_debit, 8500.0)
+		self.assertEqual(je.total_credit, 8500.0)
+
+		acc_balance = frappe.db.get_all(
+			"GL Entry",
+			filters={"account": self.debtors_usd, "is_cancelled": 0},
+			fields=["sum(debit)-sum(credit) as balance"],
+		)[0]
+		self.assertEqual(acc_balance.balance, 8500.0)
+
+	@change_settings(
+		"Accounts Settings",
+		{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+	)
+	def test_02_accounts_only_with_base_currency_balance(self):
+		"""
+		Test Revaluation on Forex account with balance only in base currency
+		"""
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debtors_usd,
+			posting_date=today(),
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+			price_list_rate=100,
+			do_not_submit=1,
+		)
+		si.currency = "USD"
+		si.conversion_rate = 80
+		si.save().submit()
+
+		pe = get_payment_entry(si.doctype, si.name)
+		pe.source_exchange_rate = 85
+		pe.received_amount = 8500
+		pe.save().submit()
+
+		# Cancel the auto created gain/loss JE to simulate balance only in base currency
+		je = frappe.db.get_all(
+			"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
+		)[0]
+		frappe.get_doc("Journal Entry", je).cancel()
+
+		err = frappe.new_doc("Exchange Rate Revaluation")
+		err.company = (self.company,)
+		err.posting_date = today()
+		err.fetch_and_calculate_accounts_data()
+		err = err.save().submit()
+
+		# Create JV for ERR
+		self.assertTrue(err.check_journal_entry_condition())
+		err_journals = err.make_jv_entries()
+		je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
+		je = je.submit()
+
+		je.reload()
+		self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
+		self.assertEqual(len(je.accounts), 2)
+		# Only base currency fields will be posted to
+		for acc in je.accounts:
+			self.assertEqual(acc.debit_in_account_currency, 0)
+			self.assertEqual(acc.credit_in_account_currency, 0)
+
+		self.assertEqual(je.total_debit, 500.0)
+		self.assertEqual(je.total_credit, 500.0)
+
+		acc_balance = frappe.db.get_all(
+			"GL Entry",
+			filters={"account": self.debtors_usd, "is_cancelled": 0},
+			fields=[
+				"sum(debit)-sum(credit) as balance",
+				"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
+			],
+		)[0]
+		# account shouldn't have balance in base and account currency
+		self.assertEqual(acc_balance.balance, 0.0)
+		self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
+
+	@change_settings(
+		"Accounts Settings",
+		{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+	)
+	def test_03_accounts_only_with_account_currency_balance(self):
+		"""
+		Test Revaluation on Forex account with balance only in account currency
+		"""
+		precision = frappe.db.get_single_value("System Settings", "currency_precision")
+
+		# posting on previous date to make sure that ERR picks up the Payment entry's exchange
+		# rate while calculating gain/loss for account currency balance
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debtors_usd,
+			posting_date=add_days(today(), -1),
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+			price_list_rate=100,
+			do_not_submit=1,
+		)
+		si.currency = "USD"
+		si.conversion_rate = 80
+		si.save().submit()
+
+		pe = get_payment_entry(si.doctype, si.name)
+		pe.paid_amount = 95
+		pe.source_exchange_rate = 84.211
+		pe.received_amount = 8000
+		pe.references = []
+		pe.save().submit()
+
+		acc_balance = frappe.db.get_all(
+			"GL Entry",
+			filters={"account": self.debtors_usd, "is_cancelled": 0},
+			fields=[
+				"sum(debit)-sum(credit) as balance",
+				"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
+			],
+		)[0]
+		# account should have balance only in account currency
+		self.assertEqual(flt(acc_balance.balance, precision), 0.0)
+		self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0)  # in USD
+
+		err = frappe.new_doc("Exchange Rate Revaluation")
+		err.company = (self.company,)
+		err.posting_date = today()
+		err.fetch_and_calculate_accounts_data()
+		err.set_total_gain_loss()
+		err = err.save().submit()
+
+		# Create JV for ERR
+		self.assertTrue(err.check_journal_entry_condition())
+		err_journals = err.make_jv_entries()
+		je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
+		je = je.submit()
+
+		je.reload()
+		self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
+		self.assertEqual(len(je.accounts), 2)
+		# Only account currency fields will be posted to
+		for acc in je.accounts:
+			self.assertEqual(flt(acc.debit, precision), 0.0)
+			self.assertEqual(flt(acc.credit, precision), 0.0)
+
+		row = [x for x in je.accounts if x.account == self.debtors_usd][0]
+		self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0)  # in USD
+		row = [x for x in je.accounts if x.account != self.debtors_usd][0]
+		self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06)  # in INR
+
+		# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
+		self.assertEqual(flt(je.total_debit, precision), 0.0)
+		self.assertEqual(flt(je.total_credit, precision), 0.0)
+
+		acc_balance = frappe.db.get_all(
+			"GL Entry",
+			filters={"account": self.debtors_usd, "is_cancelled": 0},
+			fields=[
+				"sum(debit)-sum(credit) as balance",
+				"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
+			],
+		)[0]
+		# account shouldn't have balance in base and account currency post revaluation
+		self.assertEqual(flt(acc_balance.balance, precision), 0.0)
+		self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
+
+	@change_settings(
+		"Accounts Settings",
+		{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+	)
+	def test_04_get_account_details_function(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debtors_usd,
+			posting_date=today(),
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+			price_list_rate=100,
+			do_not_submit=1,
+		)
+		si.currency = "USD"
+		si.conversion_rate = 80
+		si.save().submit()
+
+		from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
+			get_account_details,
+		)
+
+		account_details = get_account_details(
+			self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
+		)
+		# not checking for new exchange rate and balances as it is dependent on live exchange rates
+		expected_data = {
+			"account_currency": "USD",
+			"balance_in_base_currency": 8000.0,
+			"balance_in_account_currency": 100.0,
+			"current_exchange_rate": 80.0,
+			"zero_balance": False,
+			"new_balance_in_account_currency": 100.0,
+		}
+
+		for key, val in expected_data.items():
+			self.assertEqual(expected_data.get(key), account_details.get(key))
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index ac31e8a..9ed3d32 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -999,14 +999,14 @@
 		if self.payment_type == "Internal Transfer":
 			remarks = [
 				_("Amount {0} {1} transferred from {2} to {3}").format(
-					self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to
+					_(self.paid_from_account_currency), self.paid_amount, self.paid_from, self.paid_to
 				)
 			]
 		else:
 
 			remarks = [
 				_("Amount {0} {1} {2} {3}").format(
-					self.party_account_currency,
+					_(self.party_account_currency),
 					self.paid_amount if self.payment_type == "Receive" else self.received_amount,
 					_("received from") if self.payment_type == "Receive" else _("to"),
 					self.party,
@@ -1023,14 +1023,14 @@
 				if d.allocated_amount:
 					remarks.append(
 						_("Amount {0} {1} against {2} {3}").format(
-							self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name
+							_(self.party_account_currency), d.allocated_amount, d.reference_doctype, d.reference_name
 						)
 					)
 
 		for d in self.get("deductions"):
 			if d.amount:
 				remarks.append(
-					_("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account)
+					_("Amount {0} {1} deducted against {2}").format(_(self.company_currency), d.amount, d.account)
 				)
 
 		self.set("remarks", "\n".join(remarks))
@@ -1993,10 +1993,15 @@
 		if not total_amount:
 			if party_account_currency == company_currency:
 				# for handling cases that don't have multi-currency (base field)
-				total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
+				total_amount = (
+					ref_doc.get("base_rounded_total")
+					or ref_doc.get("rounded_total")
+					or ref_doc.get("base_grand_total")
+					or ref_doc.get("grand_total")
+				)
 				exchange_rate = 1
 			else:
-				total_amount = ref_doc.get("grand_total")
+				total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
 		if not exchange_rate:
 			# Get the exchange rate from the original ref doc
 			# or get it based on the posting date of the ref doc.
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index c8bf664..edfec41 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -1244,6 +1244,24 @@
 		template.allocate_payment_based_on_payment_terms = 1
 		template.save()
 
+	def test_allocation_validation_for_sales_order(self):
+		so = make_sales_order(do_not_save=True)
+		so.items[0].rate = 99.55
+		so.save().submit()
+		self.assertGreater(so.rounded_total, 0.0)
+		pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
+		pe.paid_from = "Debtors - _TC"
+		pe.paid_amount = 45.55
+		pe.references[0].allocated_amount = 45.55
+		pe.save().submit()
+		pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
+		pe.paid_from = "Debtors - _TC"
+		# No validation error should be thrown here.
+		pe.save().submit()
+
+		so.reload()
+		self.assertEqual(so.advance_paid, so.rounded_total)
+
 
 def create_payment_entry(**args):
 	payment_entry = frappe.new_doc("Payment Entry")
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 955ebef..1d50639 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -201,9 +201,9 @@
 			# apply tax withholding only if checked and applicable
 			self.set_tax_withholding()
 
-		validate_regional(self)
-
-		validate_einvoice_fields(self)
+		with temporary_flag("company", self.company):
+			validate_regional(self)
+			validate_einvoice_fields(self)
 
 		if self.doctype != "Material Request" and not self.ignore_pricing_rule:
 			apply_pricing_rule_on_transaction(self)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 6f1a50d..9771f60 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -388,7 +388,7 @@
 		for d in self.get("items"):
 			if d.get(ref_fieldname):
 				status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status")
-				if status in ("Closed", "On Hold"):
+				if status in ("Closed", "On Hold") and not self.is_return:
 					frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status))
 
 	def update_reserved_qty(self):
@@ -404,7 +404,9 @@
 			if so and so_item_rows:
 				sales_order = frappe.get_doc("Sales Order", so)
 
-				if sales_order.status in ["Closed", "Cancelled"]:
+				if (sales_order.status == "Closed" and not self.is_return) or sales_order.status in [
+					"Cancelled"
+				]:
 					frappe.throw(
 						_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError
 					)
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 4c82393..85d9a65 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -517,6 +517,8 @@
 			}
 		)
 
+		customer.append("portal_users", {"user": user})
+
 		if debtors_account:
 			customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]})
 
diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
index 6d977e0..2b2da7b 100644
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
@@ -41,7 +41,10 @@
 	if frappe.flags.woocomm_test_order_data:
 		order = frappe.flags.woocomm_test_order_data
 		event = "created"
-
+	# Ignore the test ping issued during WooCommerce webhook configuration
+	# Ref: https://github.com/woocommerce/woocommerce/issues/15642
+	if frappe.request.data.decode("utf-8").startswith("webhook_id="):
+		return "success"
 	elif frappe.request and frappe.request.data:
 		verify_request()
 		try:
@@ -81,7 +84,9 @@
 	customer.save()
 
 	if customer_exists:
-		frappe.rename_doc("Customer", old_name, customer_name)
+		# Fixes https://github.com/frappe/erpnext/issues/33708
+		if old_name != customer_name:
+			frappe.rename_doc("Customer", old_name, customer_name)
 		for address_type in (
 			"Billing",
 			"Shipping",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 131f438..34e9423 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -53,7 +53,7 @@
 		data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
 
 		title = _("Production Plan Already Submitted")
-		if not data:
+		if not data and sales_orders:
 			msg = _("No items are available in the sales order {0} for production").format(sales_orders[0])
 			if len(sales_orders) > 1:
 				sales_orders = ", ".join(sales_orders)
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
index b3b2e9f..582b487 100644
--- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -383,7 +383,7 @@
 
 		frappe.views.trees["BOM Configurator"].tree.load_children(node);
 
-		while (true) {
+		while (node) {
 			item_row = response.message.items.filter(item => item.name === node.data.name);
 
 			if (item_row?.length) {
@@ -402,12 +402,8 @@
 			);
 
 			$($(parent_dom).find(".fg-item-amt")[0]).html(total_amount);
-
-			if (node.is_root) {
-				break;
-			}
-
 			node = node.parent_node;
+
 		}
 
 	}