Merge pull request #33722 from rohitwaghchaure/purchase-invoice-performance-issue

fix: purchase invoice performance issue
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index 059e1d3..35d606b 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -118,6 +118,10 @@
 	}
 
 	plaid_success(token, response) {
-		frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
+		frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
+			response: response,
+		}).then(() => {
+			frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
+		});
 	}
 };
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index c083189..ae84154 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -155,7 +155,7 @@
 		}
 	},
 
-	render_chart: frappe.utils.debounce((frm) => {
+	render_chart(frm) {
 		frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
 			{
 				$reconciliation_tool_cards: frm.get_field(
@@ -167,7 +167,7 @@
 				currency: frm.currency,
 			}
 		);
-	}, 500),
+	},
 
 	render(frm) {
 		if (frm.doc.bank_account) {
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 4ba6146..c4a23a6 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -10,7 +10,7 @@
 from frappe.query_builder.custom import ConstantColumn
 from frappe.utils import cint, flt
 
-from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
+from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
 from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
 	get_amounts_not_reflected_in_system,
 	get_entries,
@@ -28,7 +28,7 @@
 	filters = []
 	filters.append(["bank_account", "=", bank_account])
 	filters.append(["docstatus", "=", 1])
-	filters.append(["unallocated_amount", ">", 0])
+	filters.append(["unallocated_amount", ">", 0.0])
 	if to_date:
 		filters.append(["date", "<=", to_date])
 	if from_date:
@@ -58,7 +58,7 @@
 @frappe.whitelist()
 def get_account_balance(bank_account, till_date):
 	# returns account balance till the specified date
-	account = frappe.get_cached_value("Bank Account", bank_account, "account")
+	account = frappe.db.get_value("Bank Account", bank_account, "account")
 	filters = frappe._dict(
 		{"account": account, "report_date": till_date, "include_pos_transactions": 1}
 	)
@@ -66,7 +66,7 @@
 
 	balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
 
-	total_debit, total_credit = 0, 0
+	total_debit, total_credit = 0.0, 0.0
 	for d in data:
 		total_debit += flt(d.debit)
 		total_credit += flt(d.credit)
@@ -131,10 +131,8 @@
 		fieldname=["name", "deposit", "withdrawal", "bank_account"],
 		as_dict=True,
 	)[0]
-	company_account = frappe.get_cached_value(
-		"Bank Account", bank_transaction.bank_account, "account"
-	)
-	account_type = frappe.get_cached_value("Account", second_account, "account_type")
+	company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+	account_type = frappe.db.get_value("Account", second_account, "account_type")
 	if account_type in ["Receivable", "Payable"]:
 		if not (party_type and party):
 			frappe.throw(
@@ -147,10 +145,8 @@
 	accounts.append(
 		{
 			"account": second_account,
-			"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
-			"debit_in_account_currency": bank_transaction.withdrawal
-			if bank_transaction.withdrawal > 0
-			else 0,
+			"credit_in_account_currency": bank_transaction.deposit,
+			"debit_in_account_currency": bank_transaction.withdrawal,
 			"party_type": party_type,
 			"party": party,
 		}
@@ -160,14 +156,12 @@
 		{
 			"account": company_account,
 			"bank_account": bank_transaction.bank_account,
-			"credit_in_account_currency": bank_transaction.withdrawal
-			if bank_transaction.withdrawal > 0
-			else 0,
-			"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
+			"credit_in_account_currency": bank_transaction.withdrawal,
+			"debit_in_account_currency": bank_transaction.deposit,
 		}
 	)
 
-	company = frappe.get_cached_value("Account", company_account, "company")
+	company = frappe.get_value("Account", company_account, "company")
 
 	journal_entry_dict = {
 		"voucher_type": entry_type,
@@ -187,16 +181,22 @@
 	journal_entry.insert()
 	journal_entry.submit()
 
-	if bank_transaction.deposit > 0:
+	if bank_transaction.deposit > 0.0:
 		paid_amount = bank_transaction.deposit
 	else:
 		paid_amount = bank_transaction.withdrawal
 
 	vouchers = json.dumps(
-		[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
+		[
+			{
+				"payment_doctype": "Journal Entry",
+				"payment_name": journal_entry.name,
+				"amount": paid_amount,
+			}
+		]
 	)
 
-	return reconcile_vouchers(bank_transaction.name, vouchers)
+	return reconcile_vouchers(bank_transaction_name, vouchers)
 
 
 @frappe.whitelist()
@@ -220,12 +220,10 @@
 		as_dict=True,
 	)[0]
 	paid_amount = bank_transaction.unallocated_amount
-	payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
+	payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
 
-	company_account = frappe.get_cached_value(
-		"Bank Account", bank_transaction.bank_account, "account"
-	)
-	company = frappe.get_cached_value("Account", company_account, "company")
+	company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+	company = frappe.get_value("Account", company_account, "company")
 	payment_entry_dict = {
 		"company": company,
 		"payment_type": payment_type,
@@ -261,9 +259,15 @@
 
 	payment_entry.submit()
 	vouchers = json.dumps(
-		[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
+		[
+			{
+				"payment_doctype": "Payment Entry",
+				"payment_name": payment_entry.name,
+				"amount": paid_amount,
+			}
+		]
 	)
-	return reconcile_vouchers(bank_transaction.name, vouchers)
+	return reconcile_vouchers(bank_transaction_name, vouchers)
 
 
 @frappe.whitelist()
@@ -345,59 +349,7 @@
 	# updated clear date of all the vouchers based on the bank transaction
 	vouchers = json.loads(vouchers)
 	transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
-	company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
-
-	if transaction.unallocated_amount == 0:
-		frappe.throw(_("This bank transaction is already fully reconciled"))
-	total_amount = 0
-	for voucher in vouchers:
-		voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
-		total_amount += get_paid_amount(
-			frappe._dict(
-				{
-					"payment_document": voucher["payment_doctype"],
-					"payment_entry": voucher["payment_name"],
-				}
-			),
-			transaction.currency,
-			company_account,
-		)
-
-	if total_amount > transaction.unallocated_amount:
-		frappe.throw(
-			_(
-				"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
-			)
-		)
-	account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
-
-	for voucher in vouchers:
-		gl_entry = frappe.db.get_value(
-			"GL Entry",
-			dict(
-				account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
-			),
-			["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
-			as_dict=1,
-		)
-		gl_amount, transaction_amount = (
-			(gl_entry.credit, transaction.deposit)
-			if gl_entry.credit > 0
-			else (gl_entry.debit, transaction.withdrawal)
-		)
-		allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
-
-		transaction.append(
-			"payment_entries",
-			{
-				"payment_document": voucher["payment_entry"].doctype,
-				"payment_entry": voucher["payment_entry"].name,
-				"allocated_amount": allocated_amount,
-			},
-		)
-
-	transaction.save()
-	transaction.update_allocations()
+	transaction.add_payment_entries(vouchers)
 	return frappe.get_doc("Bank Transaction", bank_transaction_name)
 
 
@@ -416,9 +368,9 @@
 	bank_account = frappe.db.get_values(
 		"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
 	)[0]
-	(account, company) = (bank_account.account, bank_account.company)
+	(gl_account, company) = (bank_account.account, bank_account.company)
 	matching = check_matching(
-		account,
+		gl_account,
 		company,
 		transaction,
 		document_types,
@@ -428,7 +380,27 @@
 		from_reference_date,
 		to_reference_date,
 	)
-	return matching
+	return subtract_allocations(gl_account, matching)
+
+
+def subtract_allocations(gl_account, vouchers):
+	"Look up & subtract any existing Bank Transaction allocations"
+	copied = []
+	for voucher in vouchers:
+		rows = get_total_allocated_amount(voucher[1], voucher[2])
+		amount = None
+		for row in rows:
+			if row["gl_account"] == gl_account:
+				amount = row["total"]
+				break
+
+		if amount:
+			l = list(voucher)
+			l[3] -= amount
+			copied.append(tuple(l))
+		else:
+			copied.append(voucher)
+	return copied
 
 
 def check_matching(
@@ -442,6 +414,7 @@
 	from_reference_date,
 	to_reference_date,
 ):
+	exact_match = True if "exact_match" in document_types else False
 	# combine all types of vouchers
 	subquery = get_queries(
 		bank_account,
@@ -453,10 +426,11 @@
 		filter_by_reference_date,
 		from_reference_date,
 		to_reference_date,
+		exact_match,
 	)
 	filters = {
 		"amount": transaction.unallocated_amount,
-		"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
+		"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
 		"reference_no": transaction.reference_number,
 		"party_type": transaction.party_type,
 		"party": transaction.party,
@@ -465,7 +439,9 @@
 
 	matching_vouchers = []
 
-	matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
+	matching_vouchers.extend(
+		get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
+	)
 
 	for query in subquery:
 		matching_vouchers.extend(
@@ -487,10 +463,10 @@
 	filter_by_reference_date,
 	from_reference_date,
 	to_reference_date,
+	exact_match,
 ):
 	# get queries to get matching vouchers
-	amount_condition = "=" if "exact_match" in document_types else "<="
-	account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
+	account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
 	queries = []
 
 	# get matching queries from all the apps
@@ -501,7 +477,7 @@
 				company,
 				transaction,
 				document_types,
-				amount_condition,
+				exact_match,
 				account_from_to,
 				from_date,
 				to_date,
@@ -520,7 +496,7 @@
 	company,
 	transaction,
 	document_types,
-	amount_condition,
+	exact_match,
 	account_from_to,
 	from_date,
 	to_date,
@@ -530,8 +506,8 @@
 ):
 	queries = []
 	if "payment_entry" in document_types:
-		pe_amount_matching = get_pe_matching_query(
-			amount_condition,
+		query = get_pe_matching_query(
+			exact_match,
 			account_from_to,
 			transaction,
 			from_date,
@@ -540,11 +516,11 @@
 			from_reference_date,
 			to_reference_date,
 		)
-		queries.extend([pe_amount_matching])
+		queries.append(query)
 
 	if "journal_entry" in document_types:
-		je_amount_matching = get_je_matching_query(
-			amount_condition,
+		query = get_je_matching_query(
+			exact_match,
 			transaction,
 			from_date,
 			to_date,
@@ -552,34 +528,70 @@
 			from_reference_date,
 			to_reference_date,
 		)
-		queries.extend([je_amount_matching])
+		queries.append(query)
 
-	if transaction.deposit > 0 and "sales_invoice" in document_types:
-		si_amount_matching = get_si_matching_query(amount_condition)
-		queries.extend([si_amount_matching])
+	if transaction.deposit > 0.0 and "sales_invoice" in document_types:
+		query = get_si_matching_query(exact_match)
+		queries.append(query)
 
-	if transaction.withdrawal > 0:
+	if transaction.withdrawal > 0.0:
 		if "purchase_invoice" in document_types:
-			pi_amount_matching = get_pi_matching_query(amount_condition)
-			queries.extend([pi_amount_matching])
+			query = get_pi_matching_query(exact_match)
+			queries.append(query)
+
+	if "bank_transaction" in document_types:
+		query = get_bt_matching_query(exact_match, transaction)
+		queries.append(query)
 
 	return queries
 
 
-def get_loan_vouchers(bank_account, transaction, document_types, filters):
+def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
 	vouchers = []
-	amount_condition = True if "exact_match" in document_types else False
 
-	if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
-		vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
+	if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
+		vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
 
-	if transaction.deposit > 0 and "loan_repayment" in document_types:
-		vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+	if transaction.deposit > 0.0 and "loan_repayment" in document_types:
+		vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
 
 	return vouchers
 
 
-def get_ld_matching_query(bank_account, amount_condition, filters):
+def get_bt_matching_query(exact_match, transaction):
+	# get matching bank transaction query
+	# find bank transactions in the same bank account with opposite sign
+	# same bank account must have same company and currency
+	field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
+
+	return f"""
+
+		SELECT
+			(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+			+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+			+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+			+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+			+ 1) AS rank,
+			'Bank Transaction' AS doctype,
+			name,
+			unallocated_amount AS paid_amount,
+			reference_number AS reference_no,
+			date AS reference_date,
+			party,
+			party_type,
+			date AS posting_date,
+			currency
+		FROM
+			`tabBank Transaction`
+		WHERE
+			status != 'Reconciled'
+			AND name != '{transaction.name}'
+			AND bank_account = '{transaction.bank_account}'
+			AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
+	"""
+
+
+def get_ld_matching_query(bank_account, exact_match, filters):
 	loan_disbursement = frappe.qb.DocType("Loan Disbursement")
 	matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
 	matching_party = loan_disbursement.applicant_type == filters.get(
@@ -607,17 +619,17 @@
 		.where(loan_disbursement.disbursement_account == bank_account)
 	)
 
-	if amount_condition:
+	if exact_match:
 		query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
 	else:
-		query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
+		query.where(loan_disbursement.disbursed_amount > 0.0)
 
 	vouchers = query.run(as_list=True)
 
 	return vouchers
 
 
-def get_lr_matching_query(bank_account, amount_condition, filters):
+def get_lr_matching_query(bank_account, exact_match, filters):
 	loan_repayment = frappe.qb.DocType("Loan Repayment")
 	matching_reference = loan_repayment.reference_number == filters.get("reference_number")
 	matching_party = loan_repayment.applicant_type == filters.get(
@@ -648,10 +660,10 @@
 	if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
 		query = query.where((loan_repayment.repay_from_salary == 0))
 
-	if amount_condition:
+	if exact_match:
 		query.where(loan_repayment.amount_paid == filters.get("amount"))
 	else:
-		query.where(loan_repayment.amount_paid <= filters.get("amount"))
+		query.where(loan_repayment.amount_paid > 0.0)
 
 	vouchers = query.run()
 
@@ -659,7 +671,7 @@
 
 
 def get_pe_matching_query(
-	amount_condition,
+	exact_match,
 	account_from_to,
 	transaction,
 	from_date,
@@ -669,7 +681,7 @@
 	to_reference_date,
 ):
 	# get matching payment entries query
-	if transaction.deposit > 0:
+	if transaction.deposit > 0.0:
 		currency_field = "paid_to_account_currency as currency"
 	else:
 		currency_field = "paid_from_account_currency as currency"
@@ -684,7 +696,8 @@
 	return f"""
 		SELECT
 			(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
-			+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0  END
+			+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+			+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
 			+ 1 ) AS rank,
 			'Payment Entry' as doctype,
 			name,
@@ -698,20 +711,19 @@
 		FROM
 			`tabPayment Entry`
 		WHERE
-			paid_amount {amount_condition} %(amount)s
-			AND docstatus = 1
+			docstatus = 1
 			AND payment_type IN (%(payment_type)s, 'Internal Transfer')
 			AND ifnull(clearance_date, '') = ""
 			AND {account_from_to} = %(bank_account)s
+			AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
 			{filter_by_date}
 			{filter_by_reference_no}
 		order by{order_by}
-
 	"""
 
 
 def get_je_matching_query(
-	amount_condition,
+	exact_match,
 	transaction,
 	from_date,
 	to_date,
@@ -723,7 +735,7 @@
 	# We have mapping at the bank level
 	# So one bank could have both types of bank accounts like asset and liability
 	# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
-	cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+	cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
 	filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
 	order_by = " je.posting_date"
 	filter_by_reference_no = ""
@@ -735,26 +747,29 @@
 	return f"""
 		SELECT
 			(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+			+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
 			+ 1) AS rank ,
-			'Journal Entry' as doctype,
+			'Journal Entry' AS doctype,
 			je.name,
-			jea.{cr_or_dr}_in_account_currency as paid_amount,
-			je.cheque_no as reference_no,
-			je.cheque_date as reference_date,
-			je.pay_to_recd_from as party,
+			jea.{cr_or_dr}_in_account_currency AS paid_amount,
+			je.cheque_no AS reference_no,
+			je.cheque_date AS reference_date,
+			je.pay_to_recd_from AS party,
 			jea.party_type,
 			je.posting_date,
-			jea.account_currency as currency
+			jea.account_currency AS currency
 		FROM
-			`tabJournal Entry Account` as jea
+			`tabJournal Entry Account` AS jea
 		JOIN
-			`tabJournal Entry` as je
+			`tabJournal Entry` AS je
 		ON
 			jea.parent = je.name
 		WHERE
-			(je.clearance_date is null or je.clearance_date='0000-00-00')
+			je.docstatus = 1
+			AND je.voucher_type NOT IN ('Opening Entry')
+			AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
 			AND jea.account = %(bank_account)s
-			AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
+			AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
 			AND je.docstatus = 1
 			{filter_by_date}
 			{filter_by_reference_no}
@@ -762,11 +777,12 @@
 	"""
 
 
-def get_si_matching_query(amount_condition):
-	# get matchin sales invoice query
+def get_si_matching_query(exact_match):
+	# get matching sales invoice query
 	return f"""
 		SELECT
-			( CASE WHEN si.customer = %(party)s  THEN 1 ELSE 0  END
+			( CASE WHEN si.customer = %(party)s  THEN 1 ELSE 0 END
+			+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
 			+ 1 ) AS rank,
 			'Sales Invoice' as doctype,
 			si.name,
@@ -784,18 +800,20 @@
 			`tabSales Invoice` as si
 		ON
 			sip.parent = si.name
-		WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
+		WHERE
+			si.docstatus = 1
+			AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
 			AND sip.account = %(bank_account)s
-			AND sip.amount {amount_condition} %(amount)s
-			AND si.docstatus = 1
+			AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
 	"""
 
 
-def get_pi_matching_query(amount_condition):
-	# get matching purchase invoice query
+def get_pi_matching_query(exact_match):
+	# get matching purchase invoice query when they are also used as payment entries (is_paid)
 	return f"""
 		SELECT
 			( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+			+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
 			+ 1 ) AS rank,
 			'Purchase Invoice' as doctype,
 			name,
@@ -809,9 +827,9 @@
 		FROM
 			`tabPurchase Invoice`
 		WHERE
-			paid_amount {amount_condition} %(amount)s
-			AND docstatus = 1
+			docstatus = 1
 			AND is_paid = 1
 			AND ifnull(clearance_date, '') = ""
-			AND cash_bank_account  = %(bank_account)s
+			AND cash_bank_account = %(bank_account)s
+			AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
 	"""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
index 6f2900a..e548b4c 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
@@ -12,8 +12,13 @@
 			};
 		});
 	},
-
-	bank_account: function(frm) {
+	refresh(frm) {
+		frm.add_custom_button(__('Unreconcile Transaction'), () => {
+			frm.call('remove_payment_entries')
+			.then( () => frm.refresh() );
+		});
+	},
+	bank_account: function (frm) {
 		set_bank_statement_filter(frm);
 	},
 
@@ -34,6 +39,7 @@
 			"Journal Entry",
 			"Sales Invoice",
 			"Purchase Invoice",
+			"Bank Transaction",
 		];
 	}
 });
@@ -49,7 +55,7 @@
 		frappe
 			.xcall(
 				"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
-				{ doctype: cdt, docname: cdn }
+				{ doctype: cdt, docname: cdn, bt_name: frm.doc.name }
 			)
 			.then((e) => {
 				if (e == "success") {
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 2bdaa10..768d2f0 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -20,9 +20,11 @@
   "currency",
   "section_break_10",
   "description",
-  "section_break_14",
   "reference_number",
+  "column_break_10",
   "transaction_id",
+  "transaction_type",
+  "section_break_14",
   "payment_entries",
   "section_break_18",
   "allocated_amount",
@@ -190,11 +192,21 @@
    "label": "Withdrawal",
    "oldfieldname": "credit",
    "options": "currency"
+  },
+  {
+   "fieldname": "column_break_10",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "transaction_type",
+   "fieldtype": "Data",
+   "label": "Transaction Type",
+   "length": 50
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2022-03-21 19:05:04.208222",
+ "modified": "2022-05-29 18:36:50.475964",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Bank Transaction",
@@ -248,4 +260,4 @@
  "states": [],
  "title_field": "bank_account",
  "track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 9b36c93..1516237 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -1,9 +1,6 @@
 # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
-
-from functools import reduce
-
 import frappe
 from frappe.utils import flt
 
@@ -18,72 +15,137 @@
 		self.clear_linked_payment_entries()
 		self.set_status()
 
+	_saving_flag = False
+
+	# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
 	def on_update_after_submit(self):
-		self.update_allocations()
-		self.clear_linked_payment_entries()
-		self.set_status(update=True)
+		"Run on save(). Avoid recursion caused by multiple saves"
+		if not self._saving_flag:
+			self._saving_flag = True
+			self.clear_linked_payment_entries()
+			self.update_allocations()
+			self._saving_flag = False
 
 	def on_cancel(self):
 		self.clear_linked_payment_entries(for_cancel=True)
 		self.set_status(update=True)
 
 	def update_allocations(self):
+		"The doctype does not allow modifications after submission, so write to the db direct"
 		if self.payment_entries:
-			allocated_amount = reduce(
-				lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
-			)
+			allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
 		else:
-			allocated_amount = 0
+			allocated_amount = 0.0
 
-		if allocated_amount:
-			frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
-			frappe.db.set_value(
-				self.doctype,
-				self.name,
-				"unallocated_amount",
-				abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
-			)
+		amount = abs(flt(self.withdrawal) - flt(self.deposit))
+		self.db_set("allocated_amount", flt(allocated_amount))
+		self.db_set("unallocated_amount", amount - flt(allocated_amount))
+		self.reload()
+		self.set_status(update=True)
 
-		else:
-			frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
-			frappe.db.set_value(
-				self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
-			)
+	def add_payment_entries(self, vouchers):
+		"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
+		if 0.0 >= self.unallocated_amount:
+			frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
 
-		amount = self.deposit or self.withdrawal
-		if amount == self.allocated_amount:
-			frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
+		added = False
+		for voucher in vouchers:
+			# Can't add same voucher twice
+			found = False
+			for pe in self.payment_entries:
+				if (
+					pe.payment_document == voucher["payment_doctype"]
+					and pe.payment_entry == voucher["payment_name"]
+				):
+					found = True
+
+			if not found:
+				pe = {
+					"payment_document": voucher["payment_doctype"],
+					"payment_entry": voucher["payment_name"],
+					"allocated_amount": 0.0,  # Temporary
+				}
+				child = self.append("payment_entries", pe)
+				added = True
+
+		# runs on_update_after_submit
+		if added:
+			self.save()
+
+	def allocate_payment_entries(self):
+		"""Refactored from bank reconciliation tool.
+		Non-zero allocations must be amended/cleared manually
+		Get the bank transaction amount (b) and remove as we allocate
+		For each payment_entry if allocated_amount == 0:
+		- get the amount already allocated against all transactions (t), need latest date
+		- get the voucher amount (from gl) (v)
+		- allocate (a = v - t)
+		    - a = 0: should already be cleared, so clear & remove payment_entry
+		    - 0 < a <= u: allocate a & clear
+		    - 0 < a, a > u: allocate u
+		    - 0 > a: Error: already over-allocated
+		- clear means: set the latest transaction date as clearance date
+		"""
+		gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
+		remaining_amount = self.unallocated_amount
+		for payment_entry in self.payment_entries:
+			if payment_entry.allocated_amount == 0.0:
+				unallocated_amount, should_clear, latest_transaction = get_clearance_details(
+					self, payment_entry
+				)
+
+				if 0.0 == unallocated_amount:
+					if should_clear:
+						latest_transaction.clear_linked_payment_entry(payment_entry)
+					self.db_delete_payment_entry(payment_entry)
+
+				elif remaining_amount <= 0.0:
+					self.db_delete_payment_entry(payment_entry)
+
+				elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
+					payment_entry.db_set("allocated_amount", unallocated_amount)
+					remaining_amount -= unallocated_amount
+					if should_clear:
+						latest_transaction.clear_linked_payment_entry(payment_entry)
+
+				elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
+					payment_entry.db_set("allocated_amount", remaining_amount)
+					remaining_amount = 0.0
+
+				elif 0.0 > unallocated_amount:
+					self.db_delete_payment_entry(payment_entry)
+					frappe.throw(
+						frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
+					)
 
 		self.reload()
 
-	def clear_linked_payment_entries(self, for_cancel=False):
+	def db_delete_payment_entry(self, payment_entry):
+		frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
+
+	@frappe.whitelist()
+	def remove_payment_entries(self):
 		for payment_entry in self.payment_entries:
-			if payment_entry.payment_document == "Sales Invoice":
-				self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
-			elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
-				self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
+			self.remove_payment_entry(payment_entry)
+		# runs on_update_after_submit
+		self.save()
 
-	def clear_simple_entry(self, payment_entry, for_cancel=False):
-		if payment_entry.payment_document == "Payment Entry":
-			if (
-				frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
-				== "Internal Transfer"
-			):
-				if len(get_reconciled_bank_transactions(payment_entry)) < 2:
-					return
+	def remove_payment_entry(self, payment_entry):
+		"Clear payment entry and clearance"
+		self.clear_linked_payment_entry(payment_entry, for_cancel=True)
+		self.remove(payment_entry)
 
-		clearance_date = self.date if not for_cancel else None
-		frappe.db.set_value(
-			payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
-		)
+	def clear_linked_payment_entries(self, for_cancel=False):
+		if for_cancel:
+			for payment_entry in self.payment_entries:
+				self.clear_linked_payment_entry(payment_entry, for_cancel)
+		else:
+			self.allocate_payment_entries()
 
-	def clear_sales_invoice(self, payment_entry, for_cancel=False):
-		clearance_date = self.date if not for_cancel else None
-		frappe.db.set_value(
-			"Sales Invoice Payment",
-			dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
-			"clearance_date",
-			clearance_date,
+	def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
+		clearance_date = None if for_cancel else self.date
+		set_voucher_clearance(
+			payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
 		)
 
 
@@ -93,38 +155,112 @@
 	return frappe.get_hooks("bank_reconciliation_doctypes")
 
 
-def get_reconciled_bank_transactions(payment_entry):
-	reconciled_bank_transactions = frappe.get_all(
-		"Bank Transaction Payments",
-		filters={"payment_entry": payment_entry.payment_entry},
-		fields=["parent"],
+def get_clearance_details(transaction, payment_entry):
+	"""
+	There should only be one bank gle for a voucher.
+	Could be none for a Bank Transaction.
+	But if a JE, could affect two banks.
+	Should only clear the voucher if all bank gles are allocated.
+	"""
+	gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
+	gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
+	bt_allocations = get_total_allocated_amount(
+		payment_entry.payment_document, payment_entry.payment_entry
 	)
 
-	return reconciled_bank_transactions
+	unallocated_amount = min(
+		transaction.unallocated_amount,
+		get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
+	)
+	unmatched_gles = len(gles)
+	latest_transaction = transaction
+	for gle in gles:
+		if gle["gl_account"] == gl_bank_account:
+			if gle["amount"] <= 0.0:
+				frappe.throw(
+					frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
+				)
+
+			unmatched_gles -= 1
+			unallocated_amount = gle["amount"]
+			for a in bt_allocations:
+				if a["gl_account"] == gle["gl_account"]:
+					unallocated_amount = gle["amount"] - a["total"]
+					if frappe.utils.getdate(transaction.date) < a["latest_date"]:
+						latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
+		else:
+			# Must be a Journal Entry affecting more than one bank
+			for a in bt_allocations:
+				if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
+					unmatched_gles -= 1
+
+	return unallocated_amount, unmatched_gles == 0, latest_transaction
 
 
-def get_total_allocated_amount(payment_entry):
-	return frappe.db.sql(
+def get_related_bank_gl_entries(doctype, docname):
+	# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
+	result = frappe.db.sql(
 		"""
 		SELECT
-			SUM(btp.allocated_amount) as allocated_amount,
-			bt.name
+			ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
+			gle.account AS gl_account
 		FROM
-			`tabBank Transaction Payments` as btp
+			`tabGL Entry` gle
 		LEFT JOIN
-			`tabBank Transaction` bt ON bt.name=btp.parent
+			`tabAccount` ac ON ac.name=gle.account
 		WHERE
-			btp.payment_document = %s
-		AND
-			btp.payment_entry = %s
-		AND
-			bt.docstatus = 1""",
-		(payment_entry.payment_document, payment_entry.payment_entry),
+			ac.account_type = 'Bank'
+			AND gle.voucher_type = %(doctype)s
+			AND gle.voucher_no = %(docname)s
+			AND is_cancelled = 0
+		""",
+		dict(doctype=doctype, docname=docname),
 		as_dict=True,
 	)
+	return result
 
 
-def get_paid_amount(payment_entry, currency, bank_account):
+def get_total_allocated_amount(doctype, docname):
+	"""
+	Gets the sum of allocations for a voucher on each bank GL account
+	along with the latest bank transaction name & date
+	NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
+	"""
+	# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
+	result = frappe.db.sql(
+		"""
+		SELECT total, latest_name, latest_date, gl_account FROM (
+			SELECT
+				ROW_NUMBER() OVER w AS rownum,
+				SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
+				FIRST_VALUE(bt.name) OVER w AS latest_name,
+				FIRST_VALUE(bt.date) OVER w AS latest_date,
+				ba.account AS gl_account
+			FROM
+				`tabBank Transaction Payments` btp
+			LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
+			LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
+			WHERE
+				btp.payment_document = %(doctype)s
+				AND btp.payment_entry = %(docname)s
+				AND bt.docstatus = 1
+			WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
+		) temp
+		WHERE
+			rownum = 1
+		""",
+		dict(doctype=doctype, docname=docname),
+		as_dict=True,
+	)
+	for row in result:
+		# Why is this *sometimes* a byte string?
+		if isinstance(row["latest_name"], bytes):
+			row["latest_name"] = row["latest_name"].decode()
+		row["latest_date"] = frappe.utils.getdate(row["latest_date"])
+	return result
+
+
+def get_paid_amount(payment_entry, currency, gl_bank_account):
 	if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
 
 		paid_amount_field = "paid_amount"
@@ -147,7 +283,7 @@
 	elif payment_entry.payment_document == "Journal Entry":
 		return frappe.db.get_value(
 			"Journal Entry Account",
-			{"parent": payment_entry.payment_entry, "account": bank_account},
+			{"parent": payment_entry.payment_entry, "account": gl_bank_account},
 			"sum(credit_in_account_currency)",
 		)
 
@@ -166,6 +302,12 @@
 			payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
 		)
 
+	elif payment_entry.payment_document == "Bank Transaction":
+		dep, wth = frappe.db.get_value(
+			"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
+		)
+		return abs(flt(wth) - flt(dep))
+
 	else:
 		frappe.throw(
 			"Please reconcile {0}: {1} manually".format(
@@ -174,18 +316,55 @@
 		)
 
 
-@frappe.whitelist()
-def unclear_reference_payment(doctype, docname):
-	if frappe.db.exists(doctype, docname):
-		doc = frappe.get_doc(doctype, docname)
-		if doctype == "Sales Invoice":
-			frappe.db.set_value(
-				"Sales Invoice Payment",
-				dict(parenttype=doc.payment_document, parent=doc.payment_entry),
-				"clearance_date",
-				None,
-			)
-		else:
-			frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
+def set_voucher_clearance(doctype, docname, clearance_date, self):
+	if doctype in [
+		"Payment Entry",
+		"Journal Entry",
+		"Purchase Invoice",
+		"Expense Claim",
+		"Loan Repayment",
+		"Loan Disbursement",
+	]:
+		if (
+			doctype == "Payment Entry"
+			and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
+			and len(get_reconciled_bank_transactions(doctype, docname)) < 2
+		):
+			return
+		frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
 
-		return doc.payment_entry
+	elif doctype == "Sales Invoice":
+		frappe.db.set_value(
+			"Sales Invoice Payment",
+			dict(parenttype=doctype, parent=docname),
+			"clearance_date",
+			clearance_date,
+		)
+
+	elif doctype == "Bank Transaction":
+		# For when a second bank transaction has fixed another, e.g. refund
+		bt = frappe.get_doc(doctype, docname)
+		if clearance_date:
+			vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
+			bt.add_payment_entries(vouchers)
+		else:
+			for pe in bt.payment_entries:
+				if pe.payment_document == self.doctype and pe.payment_entry == self.name:
+					bt.remove(pe)
+					bt.save()
+					break
+
+
+def get_reconciled_bank_transactions(doctype, docname):
+	return frappe.get_all(
+		"Bank Transaction Payments",
+		filters={"payment_document": doctype, "payment_entry": docname},
+		pluck="parent",
+	)
+
+
+@frappe.whitelist()
+def unclear_reference_payment(doctype, docname, bt_name):
+	bt = frappe.get_doc("Bank Transaction", bt_name)
+	set_voucher_clearance(doctype, docname, None, bt)
+	return docname
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 21f27ae..089f20b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@
 frappe.ui.form.on("Journal Entry", {
 	setup: function(frm) {
 		frm.add_fetch("bank_account", "account", "account");
-		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry'];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
 	},
 
 	refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 1de1b38..db399b7 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -89,7 +89,13 @@
 		from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
 
 		unlink_ref_doc_from_payment_entries(self)
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
+		self.ignore_linked_doctypes = (
+			"GL Entry",
+			"Stock Ledger Entry",
+			"Payment Ledger Entry",
+			"Repost Payment Ledger",
+			"Repost Payment Ledger Items",
+		)
 		self.make_gl_entries(1)
 		self.update_advance_paid()
 		self.unlink_advance_entry_reference()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 6039bdf..91374ae 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -7,7 +7,7 @@
 
 frappe.ui.form.on('Payment Entry', {
 	onload: function(frm) {
-		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
 
 		if(frm.doc.__islocal) {
 			if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 1cccbd9..cd5b6d5 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -92,7 +92,13 @@
 		self.set_status()
 
 	def on_cancel(self):
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
+		self.ignore_linked_doctypes = (
+			"GL Entry",
+			"Stock Ledger Entry",
+			"Payment Ledger Entry",
+			"Repost Payment Ledger",
+			"Repost Payment Ledger Items",
+		)
 		self.make_gl_entries(cancel=1)
 		self.update_outstanding_amounts()
 		self.update_advance_paid()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 675a328..e3d9c26 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -368,6 +368,7 @@
 				"exchange_rate": 1,
 				"cost_center": erpnext.get_default_cost_center(self.company),
 				reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
+				reverse_dr_or_cr: flt(row.difference_amount),
 			}
 		)
 
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 52eb29b..2f43914 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -495,7 +495,7 @@
 	"""get amount based on doctype"""
 	dt = ref_doc.doctype
 	if dt in ["Sales Order", "Purchase Order"]:
-		grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
+		grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid)
 
 	elif dt in ["Sales Invoice", "Purchase Invoice"]:
 		if ref_doc.party_account_currency == ref_doc.currency:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index a098e8d..e2b4a1a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -31,7 +31,7 @@
 		super.onload();
 
 		// Ignore linked advances
-		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
+		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
 
 		if(!this.frm.doc.__islocal) {
 			// show credit_to in print format
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c95dafa..21addab 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1417,6 +1417,8 @@
 			"GL Entry",
 			"Stock Ledger Entry",
 			"Repost Item Valuation",
+			"Repost Payment Ledger",
+			"Repost Payment Ledger Items",
 			"Payment Ledger Entry",
 			"Tax Withheld Vouchers",
 		)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 7abf3f3..47e3f9b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -34,7 +34,7 @@
 		super.onload();
 
 		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
-			'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
+							  'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
 
 		if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
 			// show debit_to in print format
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 31cf120..5cda276 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -397,6 +397,8 @@
 			"GL Entry",
 			"Stock Ledger Entry",
 			"Repost Item Valuation",
+			"Repost Payment Ledger",
+			"Repost Payment Ledger Items",
 			"Payment Ledger Entry",
 		)
 
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 34417f7..652dcf0 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -21,6 +21,7 @@
   "allow_multiple_items",
   "bill_for_rejected_quantity_in_purchase_invoice",
   "disable_last_purchase_rate",
+  "show_pay_button",
   "subcontract",
   "backflush_raw_materials_of_subcontract_based_on",
   "column_break_11",
@@ -140,6 +141,12 @@
    "fieldname": "disable_last_purchase_rate",
    "fieldtype": "Check",
    "label": "Disable Last Purchase Rate"
+  },
+  {
+   "default": "1",
+   "fieldname": "show_pay_button",
+   "fieldtype": "Check",
+   "label": "Show Pay Button in Purchase Order Portal"
   }
  ],
  "icon": "fa fa-cog",
@@ -147,7 +154,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-01-09 17:08:28.828173",
+ "modified": "2023-02-15 14:42:10.200679",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Buying Settings",
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 66eafe9..1bf7f58 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -23,7 +23,6 @@
   "default_bank_account",
   "column_break_10",
   "default_price_list",
-  "payment_terms",
   "internal_supplier_section",
   "is_internal_supplier",
   "represents_company",
@@ -53,6 +52,7 @@
   "supplier_primary_address",
   "primary_address",
   "accounting_tab",
+  "payment_terms",
   "accounts",
   "settings_tab",
   "allow_purchase_invoice_creation_without_purchase_order",
@@ -457,11 +457,10 @@
    "link_fieldname": "party"
   }
  ],
- "modified": "2022-11-09 18:02:59.075203",
+ "modified": "2023-02-18 11:05:50.592270",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Supplier",
- "name_case": "Title Case",
  "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6fa44c9..3705fcf 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -204,6 +204,12 @@
 		validate_einvoice_fields(self)
 
 	def on_trash(self):
+		# delete references in 'Repost Payment Ledger'
+		rpi = frappe.qb.DocType("Repost Payment Ledger Items")
+		frappe.qb.from_(rpi).delete().where(
+			(rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
+		).run()
+
 		# delete sl and gl entries on deletion of transaction
 		if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
 			ple = frappe.qb.DocType("Payment Ledger Entry")
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 38d6993..f44fad3 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -12,7 +12,7 @@
 	def __init__(self, access_token=None):
 		self.access_token = access_token
 		self.settings = frappe.get_single("Plaid Settings")
-		self.products = ["auth", "transactions"]
+		self.products = ["transactions"]
 		self.client_name = frappe.local.site
 		self.client = plaid.Client(
 			client_id=self.settings.plaid_client_id,
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index 3740d04..3ba6bb9 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -47,7 +47,7 @@
 	}
 
 	async init_config() {
-		this.product = ["auth", "transactions"];
+		this.product = ["transactions"];
 		this.plaid_env = this.frm.doc.plaid_env;
 		this.client_name = frappe.boot.sitename;
 		this.token = await this.get_link_token();
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 62ea85f..f3aa6a3 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -70,7 +70,8 @@
 	except TypeError:
 		pass
 
-	bank = json.loads(bank)
+	if isinstance(bank, str):
+		bank = json.loads(bank)
 	result = []
 
 	default_gl_account = get_default_bank_cash_account(company, "Bank")
@@ -177,16 +178,15 @@
 		)
 
 		result = []
-		for transaction in reversed(transactions):
-			result += new_bank_transaction(transaction)
+		if transactions:
+			for transaction in reversed(transactions):
+				result += new_bank_transaction(transaction)
 
 		if result:
 			last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
 
 			frappe.logger().info(
-				"Plaid added {} new Bank Transactions from '{}' between {} and {}".format(
-					len(result), bank_account, start_date, end_date
-				)
+				f"Plaid added {len(result)} new Bank Transactions from '{bank_account}' between {start_date} and {end_date}"
 			)
 
 			frappe.db.set_value(
@@ -230,19 +230,20 @@
 
 	bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
 
-	if float(transaction["amount"]) >= 0:
-		debit = 0
-		credit = float(transaction["amount"])
+	amount = float(transaction["amount"])
+	if amount >= 0.0:
+		deposit = 0.0
+		withdrawal = amount
 	else:
-		debit = abs(float(transaction["amount"]))
-		credit = 0
+		deposit = abs(amount)
+		withdrawal = 0.0
 
 	status = "Pending" if transaction["pending"] == "True" else "Settled"
 
 	tags = []
 	try:
 		tags += transaction["category"]
-		tags += ["Plaid Cat. {}".format(transaction["category_id"])]
+		tags += [f'Plaid Cat. {transaction["category_id"]}']
 	except KeyError:
 		pass
 
@@ -254,11 +255,18 @@
 					"date": getdate(transaction["date"]),
 					"status": status,
 					"bank_account": bank_account,
-					"deposit": debit,
-					"withdrawal": credit,
+					"deposit": deposit,
+					"withdrawal": withdrawal,
 					"currency": transaction["iso_currency_code"],
 					"transaction_id": transaction["transaction_id"],
-					"reference_number": transaction["payment_meta"]["reference_number"],
+					"transaction_type": (
+						transaction["transaction_code"] or transaction["payment_meta"]["payment_method"]
+					),
+					"reference_number": (
+						transaction["check_number"]
+						or transaction["payment_meta"]["reference_number"]
+						or transaction["name"]
+					),
 					"description": transaction["name"],
 				}
 			)
@@ -271,7 +279,7 @@
 			result.append(new_transaction.name)
 
 		except Exception:
-			frappe.throw(title=_("Bank transaction creation error"))
+			frappe.throw(_("Bank transaction creation error"))
 
 	return result
 
@@ -300,3 +308,26 @@
 def get_link_token_for_update(access_token):
 	plaid = PlaidConnector(access_token)
 	return plaid.get_link_token(update_mode=True)
+
+
+def get_company(bank_account_name):
+	from frappe.defaults import get_user_default
+
+	company_names = frappe.db.get_all("Company", pluck="name")
+	if len(company_names) == 1:
+		return company_names[0]
+	if frappe.db.exists("Bank Account", bank_account_name):
+		return frappe.db.get_value("Bank Account", bank_account_name, "company")
+	company_default = get_user_default("Company")
+	if company_default:
+		return company_default
+	frappe.throw(_("Could not detect the Company for updating Bank Accounts"))
+
+
+@frappe.whitelist()
+def update_bank_account_ids(response):
+	data = json.loads(response)
+	institution_name = data["institution"]["name"]
+	bank = frappe.get_doc("Bank", institution_name).as_dict()
+	bank_account_name = f"{data['account']['name']} - {institution_name}"
+	return add_bank_accounts(response, bank, get_company(bank_account_name))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index e8dc3e2..6d34a20 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
@@ -125,6 +125,8 @@
 			"unofficial_currency_code": None,
 			"name": "INTRST PYMNT",
 			"transaction_type": "place",
+			"transaction_code": "direct debit",
+			"check_number": "3456789",
 			"amount": -4.22,
 			"location": {
 				"city": None,
diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py
index 987f53f..60621fb 100644
--- a/erpnext/patches/v13_0/delete_old_purchase_reports.py
+++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py
@@ -17,10 +17,11 @@
 
 	for report in reports_to_delete:
 		if frappe.db.exists("Report", report):
+			delete_links_from_desktop_icons(report)
 			delete_auto_email_reports(report)
 			check_and_delete_linked_reports(report)
 
-			frappe.delete_doc("Report", report)
+			frappe.delete_doc("Report", report, force=True)
 
 
 def delete_auto_email_reports(report):
@@ -28,3 +29,10 @@
 	auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
 	for auto_email_report in auto_email_reports:
 		frappe.delete_doc("Auto Email Report", auto_email_report[0])
+
+
+def delete_links_from_desktop_icons(report):
+	"""Check for one or multiple Desktop Icons and delete"""
+	desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
+	for desktop_icon in desktop_icons:
+		frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True)
diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
index f7c19a1..0cda938 100644
--- a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
@@ -182,6 +182,9 @@
 			);
 		} else {
 			this.transactions.splice(transaction_index, 1);
+			for (const [k, v] of Object.entries(this.transaction_dt_map)) {
+				if (v > transaction_index) this.transaction_dt_map[k] = v - 1;
+			}
 		}
 		this.datatable.refresh(this.transactions, this.columns);
 
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 911343d..321b812 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -20,7 +20,7 @@
 				doctype: "Bank Transaction",
 				filters: { name: this.bank_transaction_name },
 				fieldname: [
-					"date as reference_date",
+					"date",
 					"deposit",
 					"withdrawal",
 					"currency",
@@ -33,6 +33,7 @@
 					"party",
 					"unallocated_amount",
 					"allocated_amount",
+					"transaction_type",
 				],
 			},
 			callback: (r) => {
@@ -41,11 +42,23 @@
 					r.message.payment_entry = 1;
 					r.message.journal_entry = 1;
 					this.dialog.set_values(r.message);
+					this.copy_data_to_voucher();
 					this.dialog.show();
 				}
 			},
 		});
 	}
+
+	copy_data_to_voucher() {
+		let copied = {
+			reference_number: this.bank_transaction.reference_number || this.bank_transaction.description,
+			posting_date: this.bank_transaction.date,
+			reference_date: this.bank_transaction.date,
+			mode_of_payment: this.bank_transaction.transaction_type,
+		};
+		this.dialog.set_values(copied);
+	}
+
 	get_linked_vouchers(document_types) {
 		frappe.call({
 			method:
@@ -75,10 +88,9 @@
 							row[1],
 							row[2],
 							reference_date,
-							row[8],
 							format_currency(row[3], row[9]),
-							row[6],
 							row[4],
+							row[6],
 						]);
 					});
 					this.get_dt_columns();
@@ -104,7 +116,7 @@
 			{
 				name: __("Document Name"),
 				editable: false,
-				width: 150,
+				width: 1,
 			},
 			{
 				name: __("Reference Date"),
@@ -112,25 +124,19 @@
 				width: 120,
 			},
 			{
-				name: "Posting Date",
-				editable: false,
-				width: 120,
-			},
-			{
-				name: __("Amount"),
+				name: __("Remaining"),
 				editable: false,
 				width: 100,
 			},
 			{
-				name: __("Party"),
-				editable: false,
-				width: 120,
-			},
-
-			{
 				name: __("Reference Number"),
 				editable: false,
-				width: 140,
+				width: 200,
+			},
+			{
+				name: __("Party"),
+				editable: false,
+				width: 100,
 			},
 		];
 	}
@@ -225,6 +231,16 @@
 				onchange: () => this.update_options(),
 			},
 			{
+				fieldname: "column_break_5",
+				fieldtype: "Column Break",
+			},
+			{
+				fieldtype: "Check",
+				label: "Bank Transaction",
+				fieldname: "bank_transaction",
+				onchange: () => this.update_options(),
+			},
+			{
 				fieldtype: "Section Break",
 				fieldname: "section_break_1",
 				label: __("Select Vouchers to Match"),
@@ -289,7 +305,7 @@
 				fieldtype: "Column Break",
 			},
 			{
-				default: "Journal Entry Type",
+				default: "Bank Entry",
 				fieldname: "journal_entry_type",
 				fieldtype: "Select",
 				label: "Journal Entry Type",
@@ -364,7 +380,12 @@
 				fieldtype: "Section Break",
 				fieldname: "details_section",
 				label: "Transaction Details",
-				collapsible: 1,
+			},
+			{
+				fieldname: "date",
+				fieldtype: "Date",
+				label: "Date",
+				read_only: 1,
 			},
 			{
 				fieldname: "deposit",
@@ -381,14 +402,14 @@
 				read_only: 1,
 			},
 			{
-				fieldname: "description",
-				fieldtype: "Small Text",
-				label: "Description",
+				fieldname: "column_break_17",
+				fieldtype: "Column Break",
 				read_only: 1,
 			},
 			{
-				fieldname: "column_break_17",
-				fieldtype: "Column Break",
+				fieldname: "description",
+				fieldtype: "Small Text",
+				label: "Description",
 				read_only: 1,
 			},
 			{
@@ -398,7 +419,6 @@
 				options: "Currency",
 				read_only: 1,
 			},
-
 			{
 				fieldname: "unallocated_amount",
 				fieldtype: "Currency",
@@ -593,4 +613,4 @@
 		}
 	}
 
-};
\ No newline at end of file
+};
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 7482a33..c133cd3 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -24,10 +24,10 @@
   "account_manager",
   "image",
   "defaults_tab",
-  "default_price_list",
+  "default_currency",
   "default_bank_account",
   "column_break_14",
-  "default_currency",
+  "default_price_list",
   "internal_customer_section",
   "is_internal_customer",
   "represents_company",
@@ -568,11 +568,10 @@
    "link_fieldname": "party"
   }
  ],
- "modified": "2022-11-08 15:52:34.462657",
+ "modified": "2023-02-18 11:04:46.343527",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Customer",
- "name_case": "Title Case",
  "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 686e6cb..c06700a 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -358,7 +358,7 @@
 					check_list.append(d.item_tax_template)
 
 	def validate_barcode(self):
-		from stdnum import ean
+		import barcodenumber
 
 		if len(self.barcodes) > 0:
 			for item_barcode in self.barcodes:
@@ -376,19 +376,16 @@
 					item_barcode.barcode_type = (
 						"" if item_barcode.barcode_type not in options else item_barcode.barcode_type
 					)
-					if item_barcode.barcode_type and item_barcode.barcode_type.upper() in (
-						"EAN",
-						"UPC-A",
-						"EAN-13",
-						"EAN-8",
-					):
-						if not ean.is_valid(item_barcode.barcode):
-							frappe.throw(
-								_("Barcode {0} is not a valid {1} code").format(
-									item_barcode.barcode, item_barcode.barcode_type
-								),
-								InvalidBarcode,
-							)
+					if item_barcode.barcode_type:
+						barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper())
+						if barcode_type in barcodenumber.barcodes():
+							if not barcodenumber.check_code(barcode_type, item_barcode.barcode):
+								frappe.throw(
+									_("Barcode {0} is not a valid {1} code").format(
+										item_barcode.barcode, item_barcode.barcode_type
+									),
+									InvalidBarcode,
+								)
 
 	def validate_warehouse_for_reorder(self):
 		"""Validate Reorder level table for duplicate and conditional mandatory"""
@@ -985,6 +982,22 @@
 				)
 
 
+def convert_erpnext_to_barcodenumber(erpnext_number):
+	convert = {
+		"UPC-A": "UPCA",
+		"CODE-39": "CODE39",
+		"EAN": "EAN13",
+		"EAN-12": "EAN",
+		"EAN-8": "EAN8",
+		"ISBN-10": "ISBN10",
+		"ISBN-13": "ISBN13",
+	}
+	if erpnext_number in convert:
+		return convert[erpnext_number]
+	else:
+		return erpnext_number
+
+
 def make_item_price(item, price_list_name, item_price):
 	frappe.get_doc(
 		{
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 53f6b7f..67ed90d 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -579,6 +579,19 @@
 			{
 				"barcode": "ARBITRARY_TEXT",
 			},
+			{"barcode": "72527273070", "barcode_type": "UPC-A"},
+			{"barcode": "123456", "barcode_type": "CODE-39"},
+			{"barcode": "401268452363", "barcode_type": "EAN-12"},
+			{"barcode": "90311017", "barcode_type": "EAN-8"},
+			{"barcode": "0123456789012", "barcode_type": "GS1"},
+			{"barcode": "2211564566668", "barcode_type": "GTIN"},
+			{"barcode": "0256480249", "barcode_type": "ISBN"},
+			{"barcode": "0192552570", "barcode_type": "ISBN-10"},
+			{"barcode": "9781234567897", "barcode_type": "ISBN-13"},
+			{"barcode": "9771234567898", "barcode_type": "ISSN"},
+			{"barcode": "4581171967072", "barcode_type": "JAN"},
+			{"barcode": "12345678", "barcode_type": "PZN"},
+			{"barcode": "725272730706", "barcode_type": "UPC"},
 		]
 		create_item(item_code)
 		for barcode_properties in barcode_properties_list:
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index bda1218..d9a8347 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -25,7 +25,7 @@
    "fieldtype": "Select",
    "in_list_view": 1,
    "label": "Barcode Type",
-   "options": "\nEAN\nUPC-A"
+   "options": "\nEAN\nUPC-A\nCODE-39\nEAN-12\nEAN-8\nGS1\nGTIN\nISBN\nISBN-10\nISBN-13\nISSN\nJAN\nPZN\nUPC"
   },
   {
    "fieldname": "uom",
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
index 42d0723..5f81679 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
@@ -2,7 +2,22 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Stock Reposting Settings', {
-	// refresh: function(frm) {
+	refresh: function(frm) {
+		frm.trigger('convert_to_item_based_reposting');
+	},
 
-	// }
+	convert_to_item_based_reposting: function(frm) {
+		frm.add_custom_button(__('Convert to Item Based Reposting'), function() {
+			frm.call({
+				method: 'convert_to_item_wh_reposting',
+				frezz: true,
+				doc: frm.doc,
+				callback: function(r) {
+					if (!r.exc) {
+						frm.reload_doc();
+					}
+				}
+			})
+		})
+	}
 });
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
index e0c8ed1..51fb5ac 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
@@ -1,6 +1,8 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
+import frappe
+from frappe import _
 from frappe.model.document import Document
 from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
 
@@ -24,3 +26,62 @@
 
 		if diff < 10:
 			self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
+
+	@frappe.whitelist()
+	def convert_to_item_wh_reposting(self):
+		"""Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled."""
+
+		reposting_data = get_reposting_entries()
+
+		vouchers = [d.voucher_no for d in reposting_data]
+
+		item_warehouses = {}
+
+		for ledger in get_stock_ledgers(vouchers):
+			key = (ledger.item_code, ledger.warehouse)
+			if key not in item_warehouses:
+				item_warehouses[key] = ledger.posting_date
+			elif frappe.utils.getdate(item_warehouses.get(key)) > frappe.utils.getdate(ledger.posting_date):
+				item_warehouses[key] = ledger.posting_date
+
+		for key, posting_date in item_warehouses.items():
+			item_code, warehouse = key
+			create_repost_item_valuation(item_code, warehouse, posting_date)
+
+		for row in reposting_data:
+			frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped")
+
+		self.db_set("item_based_reposting", 1)
+		frappe.msgprint(_("Item Warehouse based reposting has been enabled."))
+
+
+def get_reposting_entries():
+	return frappe.get_all(
+		"Repost Item Valuation",
+		fields=["voucher_no", "name"],
+		filters={"status": ("in", ["Queued", "In Progress"]), "docstatus": 1, "based_on": "Transaction"},
+	)
+
+
+def get_stock_ledgers(vouchers):
+	return frappe.get_all(
+		"Stock Ledger Entry",
+		fields=["item_code", "warehouse", "posting_date"],
+		filters={"voucher_no": ("in", vouchers)},
+	)
+
+
+def create_repost_item_valuation(item_code, warehouse, posting_date):
+	frappe.get_doc(
+		{
+			"doctype": "Repost Item Valuation",
+			"company": frappe.get_cached_value("Warehouse", warehouse, "company"),
+			"posting_date": posting_date,
+			"based_on": "Item and Warehouse",
+			"posting_time": "00:00:01",
+			"item_code": item_code,
+			"warehouse": warehouse,
+			"allow_negative_stock": True,
+			"status": "Queued",
+		}
+	).submit()
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 6b354b2..bc34ad5 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -34,16 +34,18 @@
 				</a>
 			</ul>
 		</div>
-		<div class="form-column col-sm-6">
-			<div class="page-header-actions-block" data-html-block="header-actions">
-				<p>
-					<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
-						class="btn btn-primary btn-sm" id="pay-for-order">
-						{{ _("Pay") }} {{doc.get_formatted("grand_total") }}
-					</a>
-				</p>
+		{% if show_pay_button %}
+			<div class="form-column col-sm-6">
+				<div class="page-header-actions-block" data-html-block="header-actions">
+					<p>
+						<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
+							class="btn btn-primary btn-sm" id="pay-for-order">
+							{{ _("Pay") }} {{doc.get_formatted("grand_total") }}
+						</a>
+					</p>
+				</div>
 			</div>
-		</div>
+		{% endif %}
 	</div>
 {% endblock %}
 
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py
index 185ec66..13772d3 100644
--- a/erpnext/templates/pages/order.py
+++ b/erpnext/templates/pages/order.py
@@ -55,6 +55,7 @@
 			)
 			context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
 
+	context.show_pay_button = frappe.db.get_single_value("Buying Settings", "show_pay_button")
 	context.show_make_pi_button = False
 	if context.doc.get("supplier"):
 		# show Make Purchase Invoice button based on permission
diff --git a/pyproject.toml b/pyproject.toml
index 1b342a5..1c93eed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,8 +10,8 @@
 dependencies = [
     # Core dependencies
     "pycountry~=20.7.3",
-    "python-stdnum~=1.16",
     "Unidecode~=1.2.0",
+    "barcodenumber~=0.5.0",
 
     # integration dependencies
     "gocardless-pro~=1.22.0",