Merge pull request #37395 from ruthra-kumar/exception_on_exporting_errored_rows_in_bank_statement_import

fix: exception on exporting errored rows
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
index c62b711..df232a5 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -9,6 +9,7 @@
   "disabled",
   "service_provider",
   "api_endpoint",
+  "access_key",
   "url",
   "column_break_3",
   "help",
@@ -84,12 +85,18 @@
    "fieldname": "disabled",
    "fieldtype": "Check",
    "label": "Disabled"
+  },
+  {
+   "depends_on": "eval:doc.service_provider == 'exchangerate.host';",
+   "fieldname": "access_key",
+   "fieldtype": "Data",
+   "label": "Access Key"
   }
  ],
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-01-09 12:19:03.955906",
+ "modified": "2023-10-04 15:30:25.333860",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Currency Exchange Settings",
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
index d618c5c..117d5ff 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py
@@ -18,11 +18,21 @@
 
 	def set_parameters_and_result(self):
 		if self.service_provider == "exchangerate.host":
+
+			if not self.access_key:
+				frappe.throw(
+					_("Access Key is required for Service Provider: {0}").format(
+						frappe.bold(self.service_provider)
+					)
+				)
+
 			self.set("result_key", [])
 			self.set("req_params", [])
 
 			self.api_endpoint = "https://api.exchangerate.host/convert"
 			self.append("result_key", {"key": "result"})
+			self.append("req_params", {"key": "access_key", "value": self.access_key})
+			self.append("req_params", {"key": "amount", "value": "1"})
 			self.append("req_params", {"key": "date", "value": "{transaction_date}"})
 			self.append("req_params", {"key": "from", "value": "{from_currency}"})
 			self.append("req_params", {"key": "to", "value": "{to_currency}"})
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index cdd1203..22b6880 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -53,7 +53,15 @@
 
 		erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
 	},
-
+	before_save: function(frm) {
+		if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
+			let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
+			if (payment_entry_references.length > 0) {
+				let rows = payment_entry_references.map(x => "#"+x.idx);
+				frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
+			}
+		}
+	},
 	make_inter_company_journal_entry: function(frm) {
 		var d = new frappe.ui.Dialog({
 			title: __("Select Company"),
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 52ae951..6c959ba 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -48,6 +48,20 @@
 
 
 def get_report_pdf(doc, consolidated=True):
+	statement_dict = get_statement_dict(doc)
+	if not bool(statement_dict):
+		return False
+	elif consolidated:
+		delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
+		result = delimiter.join(list(statement_dict.values()))
+		return get_pdf(result, {"orientation": doc.orientation})
+	else:
+		for customer, statement_html in statement_dict.items():
+			statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
+		return statement_dict
+
+
+def get_statement_dict(doc, get_statement_dict=False):
 	statement_dict = {}
 	ageing = ""
 
@@ -78,18 +92,11 @@
 			if not res:
 				continue
 
-		statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
+		statement_dict[entry.customer] = (
+			[res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
+		)
 
-	if not bool(statement_dict):
-		return False
-	elif consolidated:
-		delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
-		result = delimiter.join(list(statement_dict.values()))
-		return get_pdf(result, {"orientation": doc.orientation})
-	else:
-		for customer, statement_html in statement_dict.items():
-			statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
-		return statement_dict
+	return statement_dict
 
 
 def set_ageing(doc, entry):
@@ -102,7 +109,8 @@
 			"range2": 60,
 			"range3": 90,
 			"range4": 120,
-			"customer": entry.customer,
+			"party_type": "Customer",
+			"party": [entry.customer],
 		}
 	)
 	col1, ageing = get_ageing(ageing_filters)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
index fb0d8d1..a3a74df 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py
@@ -4,39 +4,107 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_days, getdate, today
 
 from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
+	get_statement_dict,
 	send_emails,
 )
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
 
 
-class TestProcessStatementOfAccounts(unittest.TestCase):
+class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
 	def setUp(self):
+		self.create_company()
+		self.create_customer()
+		self.create_customer(customer_name="Other Customer")
+		self.clear_old_entries()
 		self.si = create_sales_invoice()
-		self.process_soa = create_process_soa()
+		create_sales_invoice(customer="Other Customer")
+
+	def test_process_soa_for_gl(self):
+		"""Tests the utils for Statement of Accounts(General Ledger)"""
+		process_soa = create_process_soa(
+			name="_Test Process SOA for GL",
+			customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
+		)
+		statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
+
+		# Checks if the statements are filtered based on the Customer
+		self.assertIn("Other Customer", statement_dict)
+		self.assertIn("_Test Customer", statement_dict)
+
+		# Checks if the correct number of receivable entries exist
+		# 3 rows for opening and closing and 1 row for SI
+		receivable_entries = statement_dict["_Test Customer"][0]
+		self.assertEqual(len(receivable_entries), 4)
+
+		# Checks the amount for the receivable entry
+		self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
+		self.assertEqual(receivable_entries[1].balance, 100)
+
+	def test_process_soa_for_ar(self):
+		"""Tests the utils for Statement of Accounts(Accounts Receivable)"""
+		process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
+		statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
+
+		# Checks if the statements are filtered based on the Customer
+		self.assertNotIn("Other Customer", statement_dict)
+		self.assertIn("_Test Customer", statement_dict)
+
+		# Checks if the correct number of receivable entries exist
+		receivable_entries = statement_dict["_Test Customer"][0]
+		self.assertEqual(len(receivable_entries), 1)
+
+		# Checks the amount for the receivable entry
+		self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
+		self.assertEqual(receivable_entries[0].total_due, 100)
+
+		# Checks the ageing summary for AR
+		ageing_summary = statement_dict["_Test Customer"][1][0]
+		expected_summary = frappe._dict(
+			range1=100,
+			range2=0,
+			range3=0,
+			range4=0,
+			range5=0,
+		)
+		self.check_ageing_summary(ageing_summary, expected_summary)
 
 	def test_auto_email_for_process_soa_ar(self):
-		send_emails(self.process_soa.name, from_scheduler=True)
-		self.process_soa.load_from_db()
-		self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7)))
+		process_soa = create_process_soa(
+			name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
+		)
+		send_emails(process_soa.name, from_scheduler=True)
+		process_soa.load_from_db()
+		self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
+
+	def check_ageing_summary(self, ageing, expected_ageing):
+		for age_range in expected_ageing:
+			self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
 
 	def tearDown(self):
-		frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
+		frappe.db.rollback()
 
 
-def create_process_soa():
-	frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
+def create_process_soa(**args):
+	args = frappe._dict(args)
+	frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
 	process_soa = frappe.new_doc("Process Statement Of Accounts")
-	soa_dict = {
-		"name": "Test Process SOA",
-		"company": "_Test Company",
-	}
+	soa_dict = frappe._dict(
+		name=args.name,
+		company=args.company or "_Test Company",
+		customers=args.customers or [{"customer": "_Test Customer"}],
+		enable_auto_email=1 if args.enable_auto_email else 0,
+		frequency=args.frequency or "Weekly",
+		report=args.report or "General Ledger",
+		from_date=args.from_date or getdate(today()),
+		to_date=args.to_date or getdate(today()),
+		posting_date=args.posting_date or getdate(today()),
+		include_ageing=1,
+	)
 	process_soa.update(soa_dict)
-	process_soa.set("customers", [{"customer": "_Test Customer"}])
-	process_soa.enable_auto_email = 1
-	process_soa.frequency = "Weekly"
-	process_soa.report = "Accounts Receivable"
 	process_soa.save()
 	return process_soa
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 9014662..418a56f 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -288,7 +288,9 @@
 			last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
 
 			if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
-				throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate")
+				throw_message(
+					item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)"
+				)
 
 	def get_item_list(self):
 		il = []
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 62d4c53..95bf0e4 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -190,7 +190,9 @@
 
 				item.net_rate = item.rate
 
-				if not item.qty and self.doc.get("is_return"):
+				if (
+					not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt"
+				):
 					item.amount = flt(-1 * item.rate, item.precision("amount"))
 				elif not item.qty and self.doc.get("is_debit_note"):
 					item.amount = flt(item.rate, item.precision("amount"))
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 105c58d..e897ba4 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -379,7 +379,7 @@
 		}
 	)
 
-	set_address_details(out, lead, "Lead")
+	set_address_details(out, lead, "Lead", company=company)
 
 	taxes_and_charges = set_taxes(
 		None,
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index eeb09cb..70b70c3 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -135,7 +135,15 @@
 				}
 				else {
 					// allow for '0' qty on Credit/Debit notes
-					let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
+					let qty = flt(item.qty);
+					if (!qty) {
+						qty = (me.frm.doc.is_debit_note ? 1 : -1);
+						if (me.frm.doc.doctype !== "Purchase Receipt" && me.frm.doc.is_return === 1) {
+							// In case of Purchase Receipt, qty can be 0 if all items are rejected
+							qty = flt(item.qty);
+						}
+					}
+
 					item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
 				}
 
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 3b48c2b..8477984 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -121,6 +121,7 @@
 		# Update Currency Exchange Rate
 		settings = frappe.get_single("Currency Exchange Settings")
 		settings.service_provider = "exchangerate.host"
+		settings.access_key = "12345667890"
 		settings.save()
 
 		# Update exchange
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 04eff54..6afa86e 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -956,6 +956,10 @@
 
 		total_amount += total_billable_amount
 		total_billed_amount += flt(item.billed_amt)
+
+		if pr_doc.get("is_return") and not total_amount and total_billed_amount:
+			total_amount = total_billed_amount
+
 		if adjust_incoming_rate:
 			adjusted_amt = 0.0
 			if item.billed_amt and item.amount:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index b7712ee..a8ef5e8 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2060,6 +2060,32 @@
 		company.enable_provisional_accounting_for_non_stock_items = 0
 		company.save()
 
+	def test_purchase_return_status_with_debit_note(self):
+		pr = make_purchase_receipt(rejected_qty=10, received_qty=10, rate=100, do_not_save=1)
+		pr.items[0].qty = 0
+		pr.items[0].stock_qty = 0
+		pr.submit()
+
+		return_pr = make_purchase_receipt(
+			is_return=1,
+			return_against=pr.name,
+			qty=0,
+			rejected_qty=10 * -1,
+			received_qty=10 * -1,
+			do_not_save=1,
+		)
+		return_pr.items[0].qty = 0.0
+		return_pr.items[0].stock_qty = 0.0
+		return_pr.submit()
+
+		self.assertEqual(return_pr.status, "To Bill")
+
+		pi = make_purchase_invoice(return_pr.name)
+		pi.submit()
+
+		return_pr.reload()
+		self.assertEqual(return_pr.status, "Completed")
+
 
 def prepare_data_for_internal_transfer():
 	from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier