Merge branch 'develop' into psoa_account_filter
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 4211bd0..f3351dd 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,6 +7,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
 from frappe.utils import flt
 
 from erpnext import get_company_currency
@@ -275,6 +276,10 @@
 		}
 
 	matching_vouchers = []
+
+	matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
+		document_types, filters))
+
 	for query in subquery:
 		matching_vouchers.extend(
 			frappe.db.sql(query, filters,)
@@ -311,6 +316,114 @@
 
 	return queries
 
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+	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.deposit > 0 and "loan_repayment" in document_types:
+		vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+
+	return vouchers
+
+def get_ld_matching_query(bank_account, amount_condition, 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("party_type") and \
+			loan_disbursement.applicant == filters.get("party")
+
+	rank = (
+			frappe.qb.terms.Case()
+			.when(matching_reference, 1)
+			.else_(0)
+		)
+
+	rank1 = (
+			frappe.qb.terms.Case()
+			.when(matching_party, 1)
+			.else_(0)
+		)
+
+	query = frappe.qb.from_(loan_disbursement).select(
+		rank + rank1 + 1,
+		ConstantColumn("Loan Disbursement").as_("doctype"),
+		loan_disbursement.name,
+		loan_disbursement.disbursed_amount,
+		loan_disbursement.reference_number,
+		loan_disbursement.reference_date,
+		loan_disbursement.applicant_type,
+		loan_disbursement.disbursement_date
+	).where(
+		loan_disbursement.docstatus == 1
+	).where(
+		loan_disbursement.clearance_date.isnull()
+	).where(
+		loan_disbursement.disbursement_account == bank_account
+	)
+
+	if amount_condition:
+		query.where(
+			loan_disbursement.disbursed_amount == filters.get('amount')
+		)
+	else:
+		query.where(
+			loan_disbursement.disbursed_amount <= filters.get('amount')
+		)
+
+	vouchers = query.run(as_list=True)
+
+	return vouchers
+
+def get_lr_matching_query(bank_account, amount_condition, 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("party_type") and \
+			loan_repayment.applicant == filters.get("party")
+
+	rank = (
+			frappe.qb.terms.Case()
+			.when(matching_reference, 1)
+			.else_(0)
+		)
+
+	rank1 = (
+			frappe.qb.terms.Case()
+			.when(matching_party, 1)
+			.else_(0)
+		)
+
+	query = frappe.qb.from_(loan_repayment).select(
+		rank + rank1 + 1,
+		ConstantColumn("Loan Repayment").as_("doctype"),
+		loan_repayment.name,
+		loan_repayment.amount_paid,
+		loan_repayment.reference_number,
+		loan_repayment.reference_date,
+		loan_repayment.applicant_type,
+		loan_repayment.posting_date
+	).where(
+		loan_repayment.docstatus == 1
+	).where(
+		loan_repayment.clearance_date.isnull()
+	).where(
+		loan_repayment.payment_account == bank_account
+	)
+
+	if amount_condition:
+		query.where(
+			loan_repayment.amount_paid == filters.get('amount')
+		)
+	else:
+		query.where(
+			loan_repayment.amount_paid <= filters.get('amount')
+		)
+
+	vouchers = query.run()
+
+	return vouchers
+
 def get_pe_matching_query(amount_condition, account_from_to, transaction):
 	# get matching payment entries query
 	if transaction.deposit > 0:
@@ -348,7 +461,6 @@
 	# 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
-	company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
 	cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
 
 	return f"""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 51e1d6e..a476cab 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -49,7 +49,8 @@
 
 	def clear_linked_payment_entries(self, for_cancel=False):
 		for payment_entry in self.payment_entries:
-			if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+			if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
+				"Loan Disbursement"]:
 				self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
 
 			elif payment_entry.payment_document == "Sales Invoice":
@@ -116,11 +117,18 @@
 			payment_entry.payment_entry, paid_amount_field)
 
 	elif payment_entry.payment_document == "Journal Entry":
-		return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
+		return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
+			"sum(credit_in_account_currency)")
 
 	elif payment_entry.payment_document == "Expense Claim":
 		return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
 
+	elif payment_entry.payment_document == "Loan Disbursement":
+		return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
+
+	elif payment_entry.payment_document == "Loan Repayment":
+		return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
+
 	else:
 		frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
 
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 72b6893..d84b8e0 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -109,7 +109,7 @@
 		frappe.get_doc({
 			"doctype": "Bank",
 			"bank_name":bank_name,
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 
@@ -119,7 +119,7 @@
 			"account_name":"Checking Account",
 			"bank": bank_name,
 			"account": account_name
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 
@@ -184,7 +184,7 @@
 			"supplier_group":"All Supplier Groups",
 			"supplier_type": "Company",
 			"supplier_name": "Conrad Electronic"
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 
 	except frappe.DuplicateEntryError:
 		pass
@@ -203,7 +203,7 @@
 			"supplier_group":"All Supplier Groups",
 			"supplier_type": "Company",
 			"supplier_name": "Mr G"
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 
@@ -227,7 +227,7 @@
 			"supplier_group":"All Supplier Groups",
 			"supplier_type": "Company",
 			"supplier_name": "Poore Simon's"
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 
@@ -237,7 +237,7 @@
 			"customer_group":"All Customer Groups",
 			"customer_type": "Company",
 			"customer_name": "Poore Simon's"
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 
@@ -266,7 +266,7 @@
 			"customer_group":"All Customer Groups",
 			"customer_type": "Company",
 			"customer_name": "Fayva"
-		}).insert()
+		}).insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index ade7f81..6e7b80e 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -166,7 +166,7 @@
 			frappe.scrub(row.party_type): row.party,
 			"is_pos": 0,
 			"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
-			"update_stock": 0,
+			"update_stock": 0,   # important: https://github.com/frappe/erpnext/pull/23559
 			"invoice_number": row.invoice_number,
 			"disable_rounded_total": 1
 		})
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 6700e9b..3eaf6a2 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -1,11 +1,7 @@
 # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 
-import unittest
-
 import frappe
-from frappe.cache_manager import clear_doctype_cache
-from frappe.custom.doctype.property_setter.property_setter import make_property_setter
 
 from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
 	create_dimension,
@@ -14,14 +10,17 @@
 from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
 	get_temporary_opening_account,
 )
+from erpnext.tests.utils import ERPNextTestCase
 
 test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
 
-class TestOpeningInvoiceCreationTool(unittest.TestCase):
-	def setUp(self):
+class TestOpeningInvoiceCreationTool(ERPNextTestCase):
+	@classmethod
+	def setUpClass(self):
 		if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
 			make_company()
 		create_dimension()
+		return super().setUpClass()
 
 	def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
 		doc = frappe.get_single("Opening Invoice Creation Tool")
@@ -31,26 +30,20 @@
 		return doc.make_invoices()
 
 	def test_opening_sales_invoice_creation(self):
-		property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
-		try:
-			invoices = self.make_invoices(company="_Test Opening Invoice Company")
+		invoices = self.make_invoices(company="_Test Opening Invoice Company")
 
-			self.assertEqual(len(invoices), 2)
-			expected_value = {
-				"keys": ["customer", "outstanding_amount", "status"],
-				0: ["_Test Customer", 300, "Overdue"],
-				1: ["_Test Customer 1", 250, "Overdue"],
-			}
-			self.check_expected_values(invoices, expected_value)
+		self.assertEqual(len(invoices), 2)
+		expected_value = {
+			"keys": ["customer", "outstanding_amount", "status"],
+			0: ["_Test Customer", 300, "Overdue"],
+			1: ["_Test Customer 1", 250, "Overdue"],
+		}
+		self.check_expected_values(invoices, expected_value)
 
-			si = frappe.get_doc("Sales Invoice", invoices[0])
+		si = frappe.get_doc("Sales Invoice", invoices[0])
 
-			# Check if update stock is not enabled
-			self.assertEqual(si.update_stock, 0)
-
-		finally:
-			property_setter.delete()
-			clear_doctype_cache("Sales Invoice")
+		# Check if update stock is not enabled
+		self.assertEqual(si.update_stock, 0)
 
 	def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
 		doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 02a144d..0d8f079 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1077,7 +1077,7 @@
 		if d.voucher_type in ("Purchase Invoice"):
 			d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
 
-	# Get all SO / PO which are not fully billed or aginst which full advance not paid
+	# Get all SO / PO which are not fully billed or against which full advance not paid
 	orders_to_be_billed = []
 	if (args.get("party_type") != "Student"):
 		orders_to_be_billed =  get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 5229d87..9b3b3aa 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -439,7 +439,6 @@
 			self.paid_amount = 0
 
 	def set_account_for_mode_of_payment(self):
-		self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
 		for pay in self.payments:
 			if not pay.account:
 				pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index b590944..1d30934 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -46,7 +46,7 @@
 
 	for tax in doc.get("taxes"):
 		validate_taxes_and_charges(tax)
-		validate_account_head(tax, doc)
+		validate_account_head(tax.idx, tax.account_head, doc.company)
 		validate_cost_center(tax, doc)
 		validate_inclusive_tax(tax, doc)
 
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index c13bc23..d6f6c5b 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -307,7 +307,7 @@
 			.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
 
 def validate_party_accounts(doc):
-
+	from erpnext.controllers.accounts_controller import validate_account_head
 	companies = []
 
 	for account in doc.get("accounts"):
@@ -330,6 +330,9 @@
 			if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
 				frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
 
+		# validate if account is mapped for same company
+		validate_account_head(account.idx, account.account, account.company)
+
 
 @frappe.whitelist()
 def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
index 6c401fb..b72d266 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -4,7 +4,12 @@
 
 import frappe
 from frappe import _
-from frappe.utils import flt, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Sum
+from frappe.utils import flt, getdate
+from pypika import CustomFunction
+
+from erpnext.accounts.utils import get_balance_on
 
 
 def execute(filters=None):
@@ -18,7 +23,6 @@
 
 	data = get_entries(filters)
 
-	from erpnext.accounts.utils import get_balance_on
 	balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
 
 	total_debit, total_credit = 0,0
@@ -118,7 +122,21 @@
 	]
 
 def get_entries(filters):
-	journal_entries = frappe.db.sql("""
+	journal_entries = get_journal_entries(filters)
+
+	payment_entries = get_payment_entries(filters)
+
+	loan_entries = get_loan_entries(filters)
+
+	pos_entries = []
+	if filters.include_pos_transactions:
+		pos_entries = get_pos_entries(filters)
+
+	return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
+			key=lambda k: getdate(k['posting_date']))
+
+def get_journal_entries(filters):
+	return frappe.db.sql("""
 		select "Journal Entry" as payment_document, jv.posting_date,
 			jv.name as payment_entry, jvd.debit_in_account_currency as debit,
 			jvd.credit_in_account_currency as credit, jvd.against_account,
@@ -130,7 +148,8 @@
 			and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
 			and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
 
-	payment_entries = frappe.db.sql("""
+def get_payment_entries(filters):
+	return frappe.db.sql("""
 		select
 			"Payment Entry" as payment_document, name as payment_entry,
 			reference_no, reference_date as ref_date,
@@ -145,9 +164,8 @@
 			and ifnull(clearance_date, '4000-01-01') > %(report_date)s
 	""", filters, as_dict=1)
 
-	pos_entries = []
-	if filters.include_pos_transactions:
-		pos_entries = frappe.db.sql("""
+def get_pos_entries(filters):
+	return frappe.db.sql("""
 			select
 				"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
 				si.posting_date, si.debit_to as against_account, sip.clearance_date,
@@ -161,8 +179,42 @@
 				si.posting_date ASC, si.name DESC
 		""", filters, as_dict=1)
 
-	return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
-			key=lambda k: k['posting_date'] or getdate(nowdate()))
+def get_loan_entries(filters):
+	loan_docs = []
+	for doctype in ["Loan Disbursement", "Loan Repayment"]:
+		loan_doc = frappe.qb.DocType(doctype)
+		ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+		if doctype == "Loan Disbursement":
+			amount_field = (loan_doc.disbursed_amount).as_("credit")
+			posting_date = (loan_doc.disbursement_date).as_("posting_date")
+			account = loan_doc.disbursement_account
+		else:
+			amount_field = (loan_doc.amount_paid).as_("debit")
+			posting_date = (loan_doc.posting_date).as_("posting_date")
+			account = loan_doc.payment_account
+
+		entries = frappe.qb.from_(loan_doc).select(
+			ConstantColumn(doctype).as_("payment_document"),
+			(loan_doc.name).as_("payment_entry"),
+			(loan_doc.reference_number).as_("reference_no"),
+			(loan_doc.reference_date).as_("ref_date"),
+			amount_field,
+			posting_date,
+		).where(
+			loan_doc.docstatus == 1
+		).where(
+			account == filters.get('account')
+		).where(
+			posting_date <= getdate(filters.get('report_date'))
+		).where(
+			ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date'))
+		).run(as_dict=1)
+
+		loan_docs.extend(entries)
+
+	return loan_docs
+
 
 def get_amounts_not_reflected_in_system(filters):
 	je_amount = frappe.db.sql("""
@@ -182,7 +234,40 @@
 
 	pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
 
-	return je_amount + pe_amount
+	loan_amount = get_loan_amount(filters)
+
+	return je_amount + pe_amount + loan_amount
+
+def get_loan_amount(filters):
+	total_amount = 0
+	for doctype in ["Loan Disbursement", "Loan Repayment"]:
+		loan_doc = frappe.qb.DocType(doctype)
+		ifnull = CustomFunction('IFNULL', ['value', 'default'])
+
+		if doctype == "Loan Disbursement":
+			amount_field = Sum(loan_doc.disbursed_amount)
+			posting_date = (loan_doc.disbursement_date).as_("posting_date")
+			account = loan_doc.disbursement_account
+		else:
+			amount_field = Sum(loan_doc.amount_paid)
+			posting_date = (loan_doc.posting_date).as_("posting_date")
+			account = loan_doc.payment_account
+
+		amount = frappe.qb.from_(loan_doc).select(
+			amount_field
+		).where(
+			loan_doc.docstatus == 1
+		).where(
+			account == filters.get('account')
+		).where(
+			posting_date > getdate(filters.get('report_date'))
+		).where(
+			ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date'))
+		).run()[0][0]
+
+		total_amount += flt(amount)
+
+	return amount
 
 def get_balance_row(label, amount, account_currency):
 	if amount > 0:
diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py
index bf668ab..621de82 100644
--- a/erpnext/accounts/report/tax_detail/test_tax_detail.py
+++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py
@@ -61,7 +61,7 @@
 					# Create GL Entries:
 					db_doc.submit()
 				else:
-					db_doc.insert()
+					db_doc.insert(ignore_if_duplicate=True)
 			except frappe.exceptions.DuplicateEntryError:
 				pass
 
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index 78c109a..4ed966d 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -39,10 +39,11 @@
 	def test_execute_all_accounts_reports(self):
 		"""Test that all script report in stock modules are executable with supported filters"""
 		for report, filter in REPORT_FILTER_TEST_CASES:
-			execute_script_report(
-				report_name=report,
-				module="Accounts",
-				filters=filter,
-				default_filters=DEFAULT_FILTERS,
-				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
-			)
+			with self.subTest(report=report):
+				execute_script_report(
+					report_name=report,
+					module="Accounts",
+					filters=filter,
+					default_filters=DEFAULT_FILTERS,
+					optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+				)
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 39e84e3..b17b90b 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -847,7 +847,7 @@
 			"payment_account": bank_account.name,
 			"currency": bank_account.account_currency,
 			"payment_channel": payment_channel
-		}).insert(ignore_permissions=True)
+		}).insert(ignore_permissions=True, ignore_if_duplicate=True)
 
 	except frappe.DuplicateEntryError:
 		# already exists, due to a reinstall?
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 6e87426..ea473fa 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -417,11 +417,12 @@
 	def validate_asset_finance_books(self, row):
 		if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
 			frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
-				.format(row.idx))
+				.format(row.idx), title=_("Invalid Schedule"))
 
 		if not row.depreciation_start_date:
 			if not self.available_for_use_date:
-				frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
+				frappe.throw(_("Row {0}: Depreciation Start Date is required")
+					.format(row.idx), title=_("Invalid Schedule"))
 			row.depreciation_start_date = get_last_day(self.available_for_use_date)
 
 		if not self.is_existing_asset:
@@ -439,8 +440,9 @@
 			else:
 				self.number_of_depreciations_booked = 0
 
-			if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
-				frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
+			if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
+				frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
+					.format(row.idx), title=_("Invalid Schedule"))
 
 		if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
 			frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index c08dc21..ffd1065 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -873,8 +873,9 @@
 		self.assertRaises(frappe.ValidationError, asset.save)
 
 	def test_number_of_depreciations(self):
-		"""Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
+		"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
 
+		# number_of_depreciations_booked > total_number_of_depreciations
 		asset = create_asset(
 			item_code = "Macbook Pro",
 			calculate_depreciation = 1,
@@ -889,6 +890,21 @@
 
 		self.assertRaises(frappe.ValidationError, asset.save)
 
+		# number_of_depreciations_booked = total_number_of_depreciations
+		asset_2 = create_asset(
+			item_code = "Macbook Pro",
+			calculate_depreciation = 1,
+			available_for_use_date = "2019-12-31",
+			total_number_of_depreciations = 5,
+			expected_value_after_useful_life = 10000,
+			depreciation_start_date = "2020-07-01",
+			opening_accumulated_depreciation = 10000,
+			number_of_depreciations_booked = 5,
+			do_not_save = 1
+		)
+
+		self.assertRaises(frappe.ValidationError, asset_2.save)
+
 	def test_depreciation_start_date_is_before_purchase_date(self):
 		asset = create_asset(
 			item_code = "Macbook Pro",
@@ -1264,7 +1280,7 @@
 
 	if not args.do_not_save:
 		try:
-			asset.save()
+			asset.insert(ignore_if_duplicate=True)
 		except frappe.DuplicateEntryError:
 			pass
 
@@ -1305,7 +1321,7 @@
 			"is_grouped_asset": is_grouped_asset,
 			"asset_naming_series": naming_series
 		})
-		item.insert()
+		item.insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 	return item
diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py
index 3d19fa3..2f52248 100644
--- a/erpnext/assets/doctype/asset_category/test_asset_category.py
+++ b/erpnext/assets/doctype/asset_category/test_asset_category.py
@@ -23,7 +23,7 @@
 		})
 
 		try:
-			asset_category.insert()
+			asset_category.insert(ignore_if_duplicate=True)
 		except frappe.DuplicateEntryError:
 			pass
 
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 13fe9df..0fb81b2 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -14,151 +14,150 @@
 
 
 class TestSupplier(unittest.TestCase):
-    def test_get_supplier_group_details(self):
-        doc = frappe.new_doc("Supplier Group")
-        doc.supplier_group_name = "_Testing Supplier Group"
-        doc.payment_terms = "_Test Payment Term Template 3"
-        doc.accounts = []
-        test_account_details = {
-            "company": "_Test Company",
-            "account": "Creditors - _TC",
-        }
-        doc.append("accounts", test_account_details)
-        doc.save()
-        s_doc = frappe.new_doc("Supplier")
-        s_doc.supplier_name = "Testing Supplier"
-        s_doc.supplier_group = "_Testing Supplier Group"
-        s_doc.payment_terms = ""
-        s_doc.accounts = []
-        s_doc.insert()
-        s_doc.get_supplier_group_details()
-        self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
-        self.assertEqual(s_doc.accounts[0].company, "_Test Company")
-        self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
-        s_doc.delete()
-        doc.delete()
+	def test_get_supplier_group_details(self):
+		doc = frappe.new_doc("Supplier Group")
+		doc.supplier_group_name = "_Testing Supplier Group"
+		doc.payment_terms = "_Test Payment Term Template 3"
+		doc.accounts = []
+		test_account_details = {
+			"company": "_Test Company",
+			"account": "Creditors - _TC",
+		}
+		doc.append("accounts", test_account_details)
+		doc.save()
+		s_doc = frappe.new_doc("Supplier")
+		s_doc.supplier_name = "Testing Supplier"
+		s_doc.supplier_group = "_Testing Supplier Group"
+		s_doc.payment_terms = ""
+		s_doc.accounts = []
+		s_doc.insert()
+		s_doc.get_supplier_group_details()
+		self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
+		self.assertEqual(s_doc.accounts[0].company, "_Test Company")
+		self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
+		s_doc.delete()
+		doc.delete()
 
-    def test_supplier_default_payment_terms(self):
-        # Payment Term based on Days after invoice date
-        frappe.db.set_value(
-            "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
+	def test_supplier_default_payment_terms(self):
+		# Payment Term based on Days after invoice date
+		frappe.db.set_value(
+			"Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
 
-        due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2016-02-21")
+		due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2016-02-21")
 
-        due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2017-02-21")
+		due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2017-02-21")
 
-        # Payment Term based on last day of month
-        frappe.db.set_value(
-            "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
+		# Payment Term based on last day of month
+		frappe.db.set_value(
+			"Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
 
-        due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2016-02-29")
+		due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2016-02-29")
 
-        due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2017-02-28")
+		due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2017-02-28")
 
-        frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
+		frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
 
-        # Set credit limit for the supplier group instead of supplier and evaluate the due date
-        frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
+		# Set credit limit for the supplier group instead of supplier and evaluate the due date
+		frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
 
-        due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2016-02-21")
+		due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2016-02-21")
 
-        # Payment terms for Supplier Group instead of supplier and evaluate the due date
-        frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
+		# Payment terms for Supplier Group instead of supplier and evaluate the due date
+		frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
 
-        # Leap year
-        due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2016-02-29")
-        # # Non Leap year
-        due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
-        self.assertEqual(due_date, "2017-02-28")
+		# Leap year
+		due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2016-02-29")
+		# # Non Leap year
+		due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
+		self.assertEqual(due_date, "2017-02-28")
 
-        # Supplier with no default Payment Terms Template
-        frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
-        frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
+		# Supplier with no default Payment Terms Template
+		frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
+		frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
 
-        due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
-        self.assertEqual(due_date, "2016-01-22")
-        # # Non Leap year
-        due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
-        self.assertEqual(due_date, "2017-01-22")
+		due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
+		self.assertEqual(due_date, "2016-01-22")
+		# # Non Leap year
+		due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
+		self.assertEqual(due_date, "2017-01-22")
 
-    def test_supplier_disabled(self):
-        make_test_records("Item")
+	def test_supplier_disabled(self):
+		make_test_records("Item")
 
-        frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
+		frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
 
-        from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+		from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
 
-        po = create_purchase_order(do_not_save=True)
+		po = create_purchase_order(do_not_save=True)
 
-        self.assertRaises(PartyDisabled, po.save)
+		self.assertRaises(PartyDisabled, po.save)
 
-        frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
+		frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
 
-        po.save()
+		po.save()
 
-    def test_supplier_country(self):
-        # Test that country field exists in Supplier DocType
-        supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
-        self.assertTrue('country' in supplier.as_dict())
+	def test_supplier_country(self):
+		# Test that country field exists in Supplier DocType
+		supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
+		self.assertTrue('country' in supplier.as_dict())
 
-        # Test if test supplier field record is 'Greece'
-        self.assertEqual(supplier.country, "Greece")
+		# Test if test supplier field record is 'Greece'
+		self.assertEqual(supplier.country, "Greece")
 
-        # Test update Supplier instance country value
-        supplier = frappe.get_doc('Supplier', '_Test Supplier')
-        supplier.country = 'Greece'
-        supplier.save()
-        self.assertEqual(supplier.country, "Greece")
+		# Test update Supplier instance country value
+		supplier = frappe.get_doc('Supplier', '_Test Supplier')
+		supplier.country = 'Greece'
+		supplier.save()
+		self.assertEqual(supplier.country, "Greece")
 
-    def test_party_details_tax_category(self):
-        from erpnext.accounts.party import get_party_details
+	def test_party_details_tax_category(self):
+		from erpnext.accounts.party import get_party_details
 
-        frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
+		frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
 
-        # Tax Category without Address
-        details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
-        self.assertEqual(details.tax_category, "_Test Tax Category 1")
+		# Tax Category without Address
+		details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
+		self.assertEqual(details.tax_category, "_Test Tax Category 1")
 
-        address = frappe.get_doc(dict(
-            doctype='Address',
-            address_title='_Test Address With Tax Category',
-            tax_category='_Test Tax Category 2',
-            address_type='Billing',
-            address_line1='Station Road',
-            city='_Test City',
-            country='India',
-            links=[dict(
-                link_doctype='Supplier',
-                link_name='_Test Supplier With Tax Category'
-            )]
-        )).insert()
+		address = frappe.get_doc(dict(
+			doctype='Address',
+			address_title='_Test Address With Tax Category',
+			tax_category='_Test Tax Category 2',
+			address_type='Billing',
+			address_line1='Station Road',
+			city='_Test City',
+			country='India',
+			links=[dict(
+				link_doctype='Supplier',
+				link_name='_Test Supplier With Tax Category'
+			)]
+		)).insert()
 
-        # Tax Category with Address
-        details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
-        self.assertEqual(details.tax_category, "_Test Tax Category 2")
+		# Tax Category with Address
+		details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
+		self.assertEqual(details.tax_category, "_Test Tax Category 2")
 
-        # Rollback
-        address.delete()
+		# Rollback
+		address.delete()
 
 def create_supplier(**args):
-    args = frappe._dict(args)
+	args = frappe._dict(args)
 
-    try:
-        doc = frappe.get_doc({
-            "doctype": "Supplier",
-            "supplier_name": args.supplier_name,
-            "supplier_group": args.supplier_group or "Services",
-            "supplier_type": args.supplier_type or "Company",
-            "tax_withholding_category": args.tax_withholding_category
-        }).insert()
+	if frappe.db.exists("Supplier", args.supplier_name):
+		return frappe.get_doc("Supplier", args.supplier_name)
 
-        return doc
+	doc = frappe.get_doc({
+		"doctype": "Supplier",
+		"supplier_name": args.supplier_name,
+		"supplier_group": args.supplier_group or "Services",
+		"supplier_type": args.supplier_type or "Company",
+		"tax_withholding_category": args.tax_withholding_category
+	}).insert()
 
-    except frappe.DuplicateEntryError:
-        return frappe.get_doc("Supplier", args.supplier_name)
+	return doc
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d05787f..a94af10 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1566,13 +1566,12 @@
 		tax.rate = None
 
 
-def validate_account_head(tax, doc):
-	company = frappe.get_cached_value('Account',
-		tax.account_head, 'company')
+def validate_account_head(idx, account, company):
+	account_company = frappe.get_cached_value('Account', account, 'company')
 
-	if company != doc.company:
+	if account_company != company:
 		frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
-			.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
+			.format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
 
 
 def validate_cost_center(tax, doc):
diff --git a/erpnext/controllers/employee_boarding_controller.py b/erpnext/controllers/employee_boarding_controller.py
index ae2c737..dd02ce1 100644
--- a/erpnext/controllers/employee_boarding_controller.py
+++ b/erpnext/controllers/employee_boarding_controller.py
@@ -104,11 +104,11 @@
 	def get_task_dates(self, activity, holiday_list):
 		start_date = end_date = None
 
-		if activity.begin_on:
+		if activity.begin_on is not None:
 			start_date = add_days(self.boarding_begins_on, activity.begin_on)
 			start_date = self.update_if_holiday(start_date, holiday_list)
 
-			if activity.duration:
+			if activity.duration is not None:
 				end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
 				end_date = self.update_if_holiday(end_date, holiday_list)
 
diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 8519e68..6be8c94 100644
--- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -175,7 +175,7 @@
 	def create_tax_rule(self):
 		tax_rule = frappe.get_test_records("Tax Rule")[0]
 		try:
-			frappe.get_doc(tax_rule).insert()
+			frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)
 		except (frappe.DuplicateEntryError, ConflictingTaxRule):
 			pass
 
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 54ed6f7..26bd19f 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -82,7 +82,7 @@
 				"is_private": True
 			})
 			try:
-				f.insert()
+				f.insert(ignore_if_duplicate=True)
 			except frappe.DuplicateEntryError:
 				pass
 			setattr(self, key, f.file_url)
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index a4e2157..14c86d5 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -8,10 +8,6 @@
 
 from erpnext import get_default_company, get_region
 
-TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
-SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
-TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
-TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
 SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
 	"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
 	"SE", "SI", "SK", "US"]
@@ -35,12 +31,14 @@
 	if api_key and api_url:
 		client = taxjar.Client(api_key=api_key, api_url=api_url)
 		client.set_api_config('headers', {
-				'x-api-version': '2020-08-07'
+				'x-api-version': '2022-01-24'
 			})
 		return client
 
 
 def create_transaction(doc, method):
+	TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+
 	"""Create an order transaction in TaxJar"""
 
 	if not TAXJAR_CREATE_TRANSACTIONS:
@@ -51,6 +49,7 @@
 	if not client:
 		return
 
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
 	sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
 
 	if not sales_tax:
@@ -79,6 +78,7 @@
 
 def delete_transaction(doc, method):
 	"""Delete an existing TaxJar order transaction"""
+	TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
 
 	if not TAXJAR_CREATE_TRANSACTIONS:
 		return
@@ -92,6 +92,8 @@
 
 
 def get_tax_data(doc):
+	SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
+
 	from_address = get_company_address_details(doc)
 	from_shipping_state = from_address.get("state")
 	from_country_code = frappe.db.get_value("Country", from_address.country, "code")
@@ -113,20 +115,20 @@
 		to_shipping_state = get_state_code(to_address, 'Shipping')
 
 	tax_dict = {
-		'from_country': from_country_code,
-		'from_zip': from_address.pincode,
-		'from_state': from_shipping_state,
-		'from_city': from_address.city,
-		'from_street': from_address.address_line1,
-		'to_country': to_country_code,
-		'to_zip': to_address.pincode,
-		'to_city': to_address.city,
-		'to_street': to_address.address_line1,
-		'to_state': to_shipping_state,
-		'shipping': shipping,
-		'amount': doc.net_total,
-		'plugin': 'erpnext',
-		'line_items': line_items
+		"from_country": from_country_code,
+		"from_zip": from_address.pincode,
+		"from_state": from_shipping_state,
+		"from_city": from_address.city,
+		"from_street": from_address.address_line1,
+		"to_country": to_country_code,
+		"to_zip": to_address.pincode,
+		"to_city": to_address.city,
+		"to_street": to_address.address_line1,
+		"to_state": to_shipping_state,
+		"shipping": shipping,
+		"amount": doc.net_total,
+		"plugin": "erpnext",
+		"line_items": line_items
 	}
 	return tax_dict
 
@@ -156,6 +158,9 @@
 	return tax_dict
 
 def set_sales_tax(doc, method):
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+	TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+
 	if not TAXJAR_CALCULATE_TAX:
 		return
 
@@ -206,6 +211,7 @@
 			doc.run_method("calculate_taxes_and_totals")
 
 def check_for_nexus(doc, tax_dict):
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
 	if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
 		for item in doc.get("items"):
 			item.tax_collectable = flt(0)
@@ -218,6 +224,8 @@
 
 def check_sales_tax_exemption(doc):
 	# if the party is exempt from sales tax, then set all tax account heads to zero
+	TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
+
 	sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
 		or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
 		and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index a2df26c..6e52eb9 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -142,7 +142,7 @@
 						"file_url": self.image,
 						"attached_to_doctype": "User",
 						"attached_to_name": self.user_id
-					}).insert()
+					}).insert(ignore_if_duplicate=True)
 				except frappe.DuplicateEntryError:
 					# already exists
 					pass
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 2d129c8..0fb821d 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -4,7 +4,7 @@
 import unittest
 
 import frappe
-from frappe.utils import getdate
+from frappe.utils import add_days, getdate
 
 from erpnext.hr.doctype.employee_onboarding.employee_onboarding import (
 	IncompleteTaskError,
@@ -35,6 +35,15 @@
 		# boarding status
 		self.assertEqual(onboarding.boarding_status, 'Pending')
 
+		# start and end dates
+		start_date, end_date = frappe.db.get_value('Task', onboarding.activities[0].task, ['exp_start_date', 'exp_end_date'])
+		self.assertEqual(getdate(start_date), getdate(onboarding.boarding_begins_on))
+		self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[0].duration))
+
+		start_date, end_date = frappe.db.get_value('Task', onboarding.activities[1].task, ['exp_start_date', 'exp_end_date'])
+		self.assertEqual(getdate(start_date), add_days(onboarding.boarding_begins_on, onboarding.activities[0].duration))
+		self.assertEqual(getdate(end_date), add_days(start_date, onboarding.activities[1].duration))
+
 		# complete the task
 		project = frappe.get_doc('Project', onboarding.project)
 		for task in frappe.get_all('Task', dict(project=project.name)):
@@ -57,10 +66,7 @@
 		self.assertEqual(employee.employee_name, 'Test Researcher')
 
 	def tearDown(self):
-		for entry in frappe.get_all('Employee Onboarding'):
-			doc = frappe.get_doc('Employee Onboarding', entry.name)
-			doc.cancel()
-			doc.delete()
+		frappe.db.rollback()
 
 
 def get_job_applicant():
@@ -87,23 +93,31 @@
 def create_employee_onboarding():
 	applicant = get_job_applicant()
 	job_offer = get_job_offer(applicant.name)
-	holiday_list = make_holiday_list()
+
+	holiday_list = make_holiday_list('_Test Employee Boarding')
+	holiday_list = frappe.get_doc('Holiday List', holiday_list)
+	holiday_list.holidays = []
+	holiday_list.save()
 
 	onboarding = frappe.new_doc('Employee Onboarding')
 	onboarding.job_applicant = applicant.name
 	onboarding.job_offer = job_offer.name
 	onboarding.date_of_joining = onboarding.boarding_begins_on = getdate()
 	onboarding.company = '_Test Company'
-	onboarding.holiday_list = holiday_list
+	onboarding.holiday_list = holiday_list.name
 	onboarding.designation = 'Researcher'
 	onboarding.append('activities', {
 		'activity_name': 'Assign ID Card',
 		'role': 'HR User',
-		'required_for_employee_creation': 1
+		'required_for_employee_creation': 1,
+		'begin_on': 0,
+		'duration': 1
 	})
 	onboarding.append('activities', {
 		'activity_name': 'Assign a laptop',
-		'role': 'HR User'
+		'role': 'HR User',
+		'begin_on': 1,
+		'duration': 1
 	})
 	onboarding.status = 'Pending'
 	onboarding.insert()
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py
index 30e19f1..59fb2fd 100644
--- a/erpnext/hr/doctype/exit_interview/exit_interview.py
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.py
@@ -128,4 +128,4 @@
 		message += _('{0} due to missing email information for employee(s): {1}').format(
 			frappe.bold('Sending Failed'), ', '.join(email_failure))
 
-	frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
\ No newline at end of file
+	frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
index acd50f2..abb2887 100644
--- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
@@ -82,7 +82,7 @@
 			"vehicle_value": flt(500000)
 		})
 	try:
-		vehicle.insert()
+		vehicle.insert(ignore_if_duplicate=True)
 	except frappe.DuplicateEntryError:
 		pass
 	return license_plate
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index 7811d56..50926d7 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -14,11 +14,15 @@
   "applicant",
   "section_break_7",
   "disbursement_date",
+  "clearance_date",
   "column_break_8",
   "disbursed_amount",
   "accounting_dimensions_section",
   "cost_center",
-  "customer_details_section",
+  "accounting_details",
+  "disbursement_account",
+  "column_break_16",
+  "loan_account",
   "bank_account",
   "disbursement_references_section",
   "reference_date",
@@ -107,11 +111,6 @@
    "label": "Disbursement Details"
   },
   {
-   "fieldname": "customer_details_section",
-   "fieldtype": "Section Break",
-   "label": "Customer Details"
-  },
-  {
    "fetch_from": "against_loan.applicant_type",
    "fieldname": "applicant_type",
    "fieldtype": "Select",
@@ -149,15 +148,48 @@
    "fieldname": "reference_number",
    "fieldtype": "Data",
    "label": "Reference Number"
+  },
+  {
+   "fieldname": "clearance_date",
+   "fieldtype": "Date",
+   "label": "Clearance Date",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "accounting_details",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fetch_from": "against_loan.disbursement_account",
+   "fieldname": "disbursement_account",
+   "fieldtype": "Link",
+   "label": "Disbursement Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_16",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "against_loan.loan_account",
+   "fieldname": "loan_account",
+   "fieldtype": "Link",
+   "label": "Loan Account",
+   "options": "Account",
+   "read_only": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-04-19 18:09:32.175355",
+ "modified": "2022-02-17 18:23:44.157598",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan Disbursement",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -194,5 +226,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index df3aadf..54a03b9 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -42,9 +42,6 @@
 		if not self.posting_date:
 			self.posting_date = self.disbursement_date or nowdate()
 
-		if not self.bank_account and self.applicant_type == "Customer":
-			self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account")
-
 	def validate_disbursal_amount(self):
 		possible_disbursal_amount = get_disbursal_amount(self.against_loan)
 
@@ -117,12 +114,11 @@
 
 	def make_gl_entries(self, cancel=0, adv_adj=0):
 		gle_map = []
-		loan_details = frappe.get_doc("Loan", self.against_loan)
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.loan_account,
-				"against": loan_details.disbursement_account,
+				"account": self.loan_account,
+				"against": self.disbursement_account,
 				"debit": self.disbursed_amount,
 				"debit_in_account_currency": self.disbursed_amount,
 				"against_voucher_type": "Loan",
@@ -137,8 +133,8 @@
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.disbursement_account,
-				"against": loan_details.loan_account,
+				"account": self.disbursement_account,
+				"against": self.loan_account,
 				"credit": self.disbursed_amount,
 				"credit_in_account_currency": self.disbursed_amount,
 				"against_voucher_type": "Loan",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
index 93ef217..480e010 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json
@@ -1,7 +1,7 @@
 {
  "actions": [],
  "autoname": "LM-REP-.####",
- "creation": "2019-09-03 14:44:39.977266",
+ "creation": "2022-01-25 10:30:02.767941",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
@@ -13,6 +13,7 @@
   "column_break_3",
   "company",
   "posting_date",
+  "clearance_date",
   "rate_of_interest",
   "payroll_payable_account",
   "is_term_loan",
@@ -37,7 +38,12 @@
   "total_penalty_paid",
   "total_interest_paid",
   "repayment_details",
-  "amended_from"
+  "amended_from",
+  "accounting_details_section",
+  "payment_account",
+  "penalty_income_account",
+  "column_break_36",
+  "loan_account"
  ],
  "fields": [
   {
@@ -260,12 +266,52 @@
    "fieldname": "repay_from_salary",
    "fieldtype": "Check",
    "label": "Repay From Salary"
+  },
+  {
+   "fieldname": "clearance_date",
+   "fieldtype": "Date",
+   "label": "Clearance Date",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "accounting_details_section",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fetch_from": "against_loan.payment_account",
+   "fieldname": "payment_account",
+   "fieldtype": "Link",
+   "label": "Repayment Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_36",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "against_loan.loan_account",
+   "fieldname": "loan_account",
+   "fieldtype": "Link",
+   "label": "Loan Account",
+   "options": "Account",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "against_loan.penalty_income_account",
+   "fieldname": "penalty_income_account",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "label": "Penalty Income Account",
+   "options": "Account"
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-01-06 01:51:06.707782",
+ "modified": "2022-02-18 19:10:07.742298",
  "modified_by": "Administrator",
  "module": "Loan Management",
  "name": "Loan Repayment",
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index f3ed611..67c2b1e 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -310,7 +310,6 @@
 
 	def make_gl_entries(self, cancel=0, adv_adj=0):
 		gle_map = []
-		loan_details = frappe.get_doc("Loan", self.against_loan)
 
 		if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
 			remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount,
@@ -323,13 +322,13 @@
 		if self.repay_from_salary:
 			payment_account = self.payroll_payable_account
 		else:
-			payment_account = loan_details.payment_account
+			payment_account = self.payment_account
 
 		if self.total_penalty_paid:
 			gle_map.append(
 				self.get_gl_dict({
-					"account": loan_details.loan_account,
-					"against": loan_details.payment_account,
+					"account": self.loan_account,
+					"against": payment_account,
 					"debit": self.total_penalty_paid,
 					"debit_in_account_currency": self.total_penalty_paid,
 					"against_voucher_type": "Loan",
@@ -344,8 +343,8 @@
 
 			gle_map.append(
 				self.get_gl_dict({
-					"account": loan_details.penalty_income_account,
-					"against": loan_details.loan_account,
+					"account": self.penalty_income_account,
+					"against": self.loan_account,
 					"credit": self.total_penalty_paid,
 					"credit_in_account_currency": self.total_penalty_paid,
 					"against_voucher_type": "Loan",
@@ -359,8 +358,7 @@
 		gle_map.append(
 			self.get_gl_dict({
 				"account": payment_account,
-				"against": loan_details.loan_account + ", " + loan_details.interest_income_account
-						+ ", " + loan_details.penalty_income_account,
+				"against": self.loan_account + ", " + self.penalty_income_account,
 				"debit": self.amount_paid,
 				"debit_in_account_currency": self.amount_paid,
 				"against_voucher_type": "Loan",
@@ -368,16 +366,16 @@
 				"remarks": remarks,
 				"cost_center": self.cost_center,
 				"posting_date": getdate(self.posting_date),
-				"party_type": loan_details.applicant_type if self.repay_from_salary else '',
-				"party": loan_details.applicant if self.repay_from_salary else ''
+				"party_type": self.applicant_type if self.repay_from_salary else '',
+				"party": self.applicant if self.repay_from_salary else ''
 			})
 		)
 
 		gle_map.append(
 			self.get_gl_dict({
-				"account": loan_details.loan_account,
-				"party_type": loan_details.applicant_type,
-				"party": loan_details.applicant,
+				"account": self.loan_account,
+				"party_type": self.applicant_type,
+				"party": self.applicant,
 				"against": payment_account,
 				"credit": self.amount_paid,
 				"credit_in_account_currency": self.amount_paid,
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
index 9f51ded..e436fdc 100644
--- a/erpnext/manufacturing/report/test_reports.py
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -55,10 +55,11 @@
 	def test_execute_all_manufacturing_reports(self):
 		"""Test that all script report in manufacturing modules are executable with supported filters"""
 		for report, filter in REPORT_FILTER_TEST_CASES:
-			execute_script_report(
-				report_name=report,
-				module="Manufacturing",
-				filters=filter,
-				default_filters=DEFAULT_FILTERS,
-				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
-			)
+			with self.subTest(report=report):
+				execute_script_report(
+					report_name=report,
+					module="Manufacturing",
+					filters=filter,
+					default_filters=DEFAULT_FILTERS,
+					optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+				)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 52c29b2..7560f2f 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -353,4 +353,5 @@
 erpnext.patches.v13_0.update_exchange_rate_settings
 erpnext.patches.v14_0.delete_amazon_mws_doctype
 erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
+erpnext.patches.v13_0.update_accounts_in_loan_docs
 erpnext.patches.v14_0.update_batch_valuation_flag
diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
new file mode 100644
index 0000000..440f912
--- /dev/null
+++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py
@@ -0,0 +1,37 @@
+import frappe
+
+
+def execute():
+	ld = frappe.qb.DocType("Loan Disbursement").as_("ld")
+	lr = frappe.qb.DocType("Loan Repayment").as_("lr")
+	loan = frappe.qb.DocType("Loan")
+
+	frappe.qb.update(
+		ld
+	).inner_join(
+		loan
+	).on(
+		loan.name == ld.against_loan
+	).set(
+		ld.disbursement_account, loan.disbursement_account
+	).set(
+		ld.loan_account, loan.loan_account
+	).where(
+		ld.docstatus < 2
+	).run()
+
+	frappe.qb.update(
+		lr
+	).inner_join(
+		loan
+	).on(
+		loan.name == lr.against_loan
+	).set(
+		lr.payment_account, loan.payment_account
+	).set(
+		lr.loan_account, loan.loan_account
+	).set(
+		lr.penalty_income_account, loan.penalty_income_account
+	).where(
+		lr.docstatus < 2
+	).run()
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f727ff4..d2a3998 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -1268,7 +1268,7 @@
 			for i, earning in enumerate(self.earnings):
 				if earning.salary_component == salary_component:
 					self.earnings[i].amount = wages_amount
-				self.gross_pay += self.earnings[i].amount
+				self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount"))
 		self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
 
 	def compute_year_to_date(self):
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index daa0f89..6a5debf 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -1019,13 +1019,13 @@
 	frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None)
 	frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None)
 
-def make_holiday_list():
+def make_holiday_list(holiday_list_name=None):
 	fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
-	holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
+	holiday_list = frappe.db.exists("Holiday List", holiday_list_name or "Salary Slip Test Holiday List")
 	if not holiday_list:
 		holiday_list = frappe.get_doc({
 			"doctype": "Holiday List",
-			"holiday_list_name": "Salary Slip Test Holiday List",
+			"holiday_list_name": holiday_list_name or "Salary Slip Test Holiday List",
 			"from_date": fiscal_year[1],
 			"to_date": fiscal_year[2],
 			"weekly_off": "Sunday"
diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
index b30d983..c3be146 100644
--- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py
+++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py
@@ -21,7 +21,7 @@
 					{'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'},
 				],
 				'no_of_columns': 3
-			}).insert()
+			}).insert(ignore_if_duplicate=True)
 		except frappe.DuplicateEntryError:
 			pass
 
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index ca73393..214a1be 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -182,6 +182,12 @@
 				onchange: () => this.update_options(),
 			},
 			{
+				fieldtype: "Check",
+				label: "Loan Repayment",
+				fieldname: "loan_repayment",
+				onchange: () => this.update_options(),
+			},
+			{
 				fieldname: "column_break_5",
 				fieldtype: "Column Break",
 			},
@@ -191,7 +197,6 @@
 				fieldname: "sales_invoice",
 				onchange: () => this.update_options(),
 			},
-
 			{
 				fieldtype: "Check",
 				label: "Purchase Invoice",
@@ -199,6 +204,12 @@
 				onchange: () => this.update_options(),
 			},
 			{
+				fieldtype: "Check",
+				label: "Show Only Exact Amount",
+				fieldname: "exact_match",
+				onchange: () => this.update_options(),
+			},
+			{
 				fieldname: "column_break_5",
 				fieldtype: "Column Break",
 			},
@@ -210,8 +221,8 @@
 			},
 			{
 				fieldtype: "Check",
-				label: "Show Only Exact Amount",
-				fieldname: "exact_match",
+				label: "Loan Disbursement",
+				fieldname: "loan_disbursement",
 				onchange: () => this.update_options(),
 			},
 			{
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 933ced0..00373a6 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -525,6 +525,7 @@
 
 		item.weight_per_unit = 0;
 		item.weight_uom = '';
+		item.conversion_factor = 0;
 
 		if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
 			update_stock = cint(me.frm.doc.update_stock);
@@ -2284,13 +2285,17 @@
 	}
 
 	coupon_code() {
-		var me = this;
-		frappe.run_serially([
-			() => this.frm.doc.ignore_pricing_rule=1,
-			() => me.ignore_pricing_rule(),
-			() => this.frm.doc.ignore_pricing_rule=0,
-			() => me.apply_pricing_rule()
-		]);
+		if (this.frm.doc.coupon_code || this.frm._last_coupon_code) {
+			// reset pricing rules if coupon code is set or is unset
+			const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule;
+			return frappe.run_serially([
+				() => this.frm.doc.ignore_pricing_rule=1,
+				() => this.frm.trigger('ignore_pricing_rule'),
+				() => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule,
+				() => this.frm.trigger('apply_pricing_rule'),
+				() => this.frm._last_coupon_code = this.frm.doc.coupon_code
+			]);
+		}
 	}
 };
 
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index e746ce9..83b69ae 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -78,11 +78,11 @@
 			slide.get_input("company_name").on("change", function () {
 				var parts = slide.get_input("company_name").val().split(" ");
 				var abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join("");
-				slide.get_field("company_abbr").set_value(abbr.slice(0, 5).toUpperCase());
+				slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase());
 			}).val(frappe.boot.sysdefaults.company_name || "").trigger("change");
 
 			slide.get_input("company_abbr").on("change", function () {
-				if (slide.get_input("company_abbr").val().length > 5) {
+				if (slide.get_input("company_abbr").val().length > 10) {
 					frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters"));
 					slide.get_field("company_abbr").set_value("");
 				}
@@ -96,7 +96,7 @@
 			if (!this.values.company_abbr) {
 				return false;
 			}
-			if (this.values.company_abbr.length > 5) {
+			if (this.values.company_abbr.length > 10) {
 				return false;
 			}
 			return true;
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 4b645b9..666043b 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -338,14 +338,14 @@
 
 	.btn-add-to-wishlist {
 		svg use {
-			stroke: #F47A7A;
+			--icon-stroke: #F47A7A;
 		}
 	}
 
 	.btn-view-in-wishlist {
 		svg use {
 			fill: #F47A7A;
-			stroke: none;
+			--icon-stroke: none;
 		}
 	}
 
@@ -1022,7 +1022,7 @@
 
 .not-wished {
 	cursor: pointer;
-	stroke: #F47A7A !important;
+	--icon-stroke: #F47A7A !important;
 
 	&:hover {
 		fill: #F47A7A;
@@ -1030,7 +1030,7 @@
 }
 
 .wished {
-	stroke: none;
+	--icon-stroke: none;
 	fill: #F47A7A !important;
 }
 
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 074bd52..e835690 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -53,10 +53,7 @@
 		hsn_code.description = d["description"]
 		hsn_code.hsn_code = d[code_field]
 		hsn_code.name = d[code_field]
-		try:
-			hsn_code.db_insert()
-		except frappe.DuplicateEntryError:
-			pass
+		hsn_code.db_insert(ignore_if_duplicate=True)
 
 def add_custom_roles_for_reports():
 	for report_name in ('GST Sales Register', 'GST Purchase Register',
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index 4d75e6e..1e9f6d7 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -170,17 +170,20 @@
     });
 
 		frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
-			if (!frm.doc.ignore_pricing_rule) {
-				if (frm.doc.coupon_code) {
-					frappe.run_serially([
-						() => frm.doc.ignore_pricing_rule=1,
-						() => frm.trigger('ignore_pricing_rule'),
-						() => frm.doc.ignore_pricing_rule=0,
-						() => frm.trigger('apply_pricing_rule'),
-						() => frm.save(),
-						() => this.update_totals_section(frm.doc)
-					]);
-				}
+			if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
+				frappe.run_serially([
+					() => frm.doc.ignore_pricing_rule=1,
+					() => frm.trigger('ignore_pricing_rule'),
+					() => frm.doc.ignore_pricing_rule=0,
+					() => frm.trigger('apply_pricing_rule'),
+					() => frm.save(),
+					() => this.update_totals_section(frm.doc)
+				]);
+			} else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
+				frappe.show_alert({
+					message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
+					indicator: "orange"
+				});
 			}
 		});
 
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 4441bb9..a4f2207 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -155,7 +155,7 @@
 		doc = frappe.new_doc(r.get("doctype"))
 		doc.update(r)
 		try:
-			doc.insert(ignore_permissions=True)
+			doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
 		except frappe.DuplicateEntryError as e:
 			# pass DuplicateEntryError and continue
 			if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index baa0302..613dd3f 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -433,14 +433,13 @@
 def make_new_batch(**args):
 	args = frappe._dict(args)
 
-	try:
+	if frappe.db.exists("Batch", args.batch_id):
+		batch = frappe.get_doc("Batch", args.batch_id)
+	else:
 		batch = frappe.get_doc({
 			"doctype": "Batch",
 			"batch_id": args.batch_id,
 			"item": args.item_code,
 		}).insert()
 
-	except frappe.DuplicateEntryError:
-		batch = frappe.get_doc("Batch", args.batch_id)
-
 	return batch
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index b9e8b3f..494fb3b 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -398,6 +398,7 @@
 
 		if merge:
 			self.validate_properties_before_merge(new_name)
+			self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
 			self.validate_duplicate_website_item_before_merge(old_name, new_name)
 
 	def after_rename(self, old_name, new_name, merge):
@@ -462,6 +463,20 @@
 			msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
 			frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
 
+	def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
+		"Block merge if both old and new items have product bundles."
+		old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
+		new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
+
+		if old_bundle and new_bundle:
+			bundle_link = get_link_to_form("Product Bundle", old_bundle)
+			old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
+
+			msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
+				bundle_link, old_name, new_name
+			)
+			frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
+
 	def validate_duplicate_website_item_before_merge(self, old_name, new_name):
 		"""
 			Block merge if both old and new items have website items against them.
@@ -479,8 +494,9 @@
 
 		old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
 		web_item_link = get_link_to_form("Website Item", old_web_item)
+		old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
 
-		msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
+		msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
 		frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
 
 	def set_last_purchase_rate(self, new_name):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index fd4df42..9491e17 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -15,6 +15,7 @@
 	get_variant,
 )
 from erpnext.stock.doctype.item.item import (
+	DataValidationError,
 	InvalidBarcode,
 	StockExistsForTemplate,
 	get_item_attribute,
@@ -388,6 +389,26 @@
 		self.assertTrue(frappe.db.get_value("Bin",
 			{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
 
+	def test_item_merging_with_product_bundle(self):
+		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
+		create_item("Test Item Bundle Item 1", is_stock_item=False)
+		create_item("Test Item Bundle Item 2", is_stock_item=False)
+		create_item("Test Item inside Bundle")
+		bundle_items = ["Test Item inside Bundle"]
+
+		# make bundles for both items
+		bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
+		make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
+
+		with self.assertRaises(DataValidationError):
+			frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
+
+		bundle1.delete()
+		frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
+
+		self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
+
 	def test_uom_conversion_factor(self):
 		if frappe.db.exists('Item', 'Test Item UOM'):
 			frappe.delete_doc('Item', 'Test Item UOM')
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index b39328f..51209ac 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -56,14 +56,13 @@
 				if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
 					frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
 
-	# Validate
-	# ---------------------
 	def validate(self):
 		super(MaterialRequest, self).validate()
 
 		self.validate_schedule_date()
 		self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
 		self.validate_uom_is_integer("uom", "qty")
+		self.validate_material_request_type()
 
 		if not self.status:
 			self.status = "Draft"
@@ -83,6 +82,12 @@
 		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 		self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
 
+	def validate_material_request_type(self):
+		""" Validate fields in accordance with selected type """
+
+		if self.material_request_type != "Customer Provided":
+			self.customer = None
+
 	def set_title(self):
 		'''Set title as comma separated list of items'''
 		if not self.title:
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 6c6513b..c5afa49 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -44,6 +44,7 @@
 
 class TestStockEntry(ERPNextTestCase):
 	def tearDown(self):
+		frappe.db.rollback()
 		frappe.set_user("Administrator")
 		frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
 
@@ -565,6 +566,7 @@
 		st1.set_stock_entry_type()
 		st1.insert()
 		st1.submit()
+		st1.cancel()
 
 		frappe.set_user("Administrator")
 		remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
@@ -689,6 +691,8 @@
 		bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
 			"is_default": 1, "docstatus": 1})
 
+		make_item_variant() # make variant of _Test Variant Item if absent
+
 		work_order = frappe.new_doc("Work Order")
 		work_order.update({
 			"company": "_Test Company",
@@ -1023,13 +1027,10 @@
 
 		# Check if FG cost is calculated based on RM total cost
 		# RM total cost = 200, FG rate = 200/4(FG qty) =  50
-		self.assertEqual(se.items[1].basic_rate, 50)
+		self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
 		self.assertEqual(se.value_difference, 0.0)
 		self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
 
-		# teardown
-		se.delete()
-
 	@change_settings("Stock Settings", {"allow_negative_stock": 0})
 	def test_future_negative_sle(self):
 		# Initialize item, batch, warehouse, opening qty
diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py
index 525af40..76c2079 100644
--- a/erpnext/stock/report/test_reports.py
+++ b/erpnext/stock/report/test_reports.py
@@ -73,10 +73,11 @@
 	def test_execute_all_stock_reports(self):
 		"""Test that all script report in stock modules are executable with supported filters"""
 		for report, filter in REPORT_FILTER_TEST_CASES:
-			execute_script_report(
-				report_name=report,
-				module="Stock",
-				filters=filter,
-				default_filters=DEFAULT_FILTERS,
-				optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
-			)
+			with self.subTest(report=report):
+				execute_script_report(
+					report_name=report,
+					module="Stock",
+					filters=filter,
+					default_filters=DEFAULT_FILTERS,
+					optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+				)
diff --git a/erpnext/stock/spec/README.md b/erpnext/stock/spec/README.md
new file mode 100644
index 0000000..f5a3501
--- /dev/null
+++ b/erpnext/stock/spec/README.md
@@ -0,0 +1,103 @@
+# Implementation notes for Stock Ledger
+
+
+## Important files
+
+- `stock/stock_ledger.py`
+- `controllers/stock_controller.py`
+- `stock/valuation.py`
+
+## What is in an Stock Ledger Entry (SLE)?
+
+Stock Ledger Entry is a single row in the Stock Ledger. It signifies some
+modification of stock for a particular Item in the specified warehouse.
+
+- `item_code`: item for which ledger entry is made
+- `warehouse`: warehouse where inventory is affected
+- `actual_qty`: change in qty
+- `qty_after_transaction`: quantity available after the transaction is processed
+- `incoming_rate`: rate at which inventory was received.
+- `is_cancelled`: if 1 then stock ledger entry is cancelled and should not be used
+for any business logic except for the code that handles cancellation.
+- `posting_date` & `posting_time`: Specify the temporal ordering of stock ledger
+  entries. Ties are broken by `creation` timestamp.
+- `voucher_type`: Many transaction can create SLE, e.g. Stock Entry, Purchase
+  Invoice
+- `voucher_no`: `name` of the transaction that created SLE
+- `voucher_detail_no`: `name` of the child table row from parent transaction
+  that created the SLE.
+- `dependant_sle_voucher_detail_no`: cross-warehouse transfers need this
+  reference in order to update dependent warehouse rates in case of change in
+  rate.
+- `recalculate_rate`: if this is checked in/out rates are recomputed on
+  transactions.
+- `valuation_rate`: current average valuation rate.
+- `stock_value`: current total stock value
+- `stock_value_difference`: stock value difference made between last and current
+  entry. This value is booked in accounting ledger.
+- `stock_queue`: if FIFO/LIFO is used this represents queue/stack maintained for
+  computing incoming rate for inventory getting consumed.
+- `batch_no`: batch no for which stock entry is made; each stock entry can only
+  affect one batch number.
+- `serial_no`: newline separated list of serial numbers that were added (if
+  actual_qty > 0) or else removed. Currently multiple serial nos can have single
+  SLE but this will likely change in future.
+
+
+## Implementation of Stock Ledger
+
+Stock Ledger Entry affects stock of combinations of (item_code, warehouse) and
+optionally batch no if specified. For simplicity, lets avoid batch no. for now.
+
+
+Stock Ledger Entry table stores stock ledger for all combinations of item_code
+and warehouse. So whenever any operations are to be performed on said
+item-warehouse combination stock ledger is filtered and sorted by posting
+datetime. A typical query that will give you individual ledger looks like this:
+
+```sql
+select *
+from `tabStock Ledger Entry` as sle
+where
+    is_cancelled = 0  --- cancelled entries don't affect ledger
+    and item_code = 'item_code' and warehouse = 'warehouse_name'
+order by timestamp(posting_date, posting_time), creation
+```
+
+New entry is just an update to the last entry which is found by looking at last
+row in the filter ledger.
+
+
+### Serial nos
+
+Serial numbers do not follow any valuation method configuration and they are
+consumed at rate they were produced unless they are grouped in which case they
+are consumed at weighted average rate.
+
+
+### Batch Nos
+
+Batches are currently NOT consumed as per batch wise valuation rate, instead
+global FIFO queue for the item is used for valuation rate.
+
+
+## Creation process of SLEs
+
+- SLE creation is usually triggered by Stock Transactions using a method
+  conventionally named `update_stock_ledger()` This might not be defined for
+  stock transaction and could be specified somewhere in inheritance hierarchy of
+  controllers.
+- This method produces SLE objects which are processed by `make_sl_entries` in
+  `stock_ledger.py` which commits the SLE to database.
+- `update_entries_after` class is used to process ONLY the inserted SLE's queue
+  and valuation.
+- The change in qty is propagated to future entries immediately. Valuation and
+  queue for future entries is processed in background using repost item
+  valuation.
+
+
+## Accounting impact
+
+- Accounting impact for stock transaction is handled by `get_gl_entries()`
+  method on controllers. Each transaction has different business logic for
+  booking the accounting impact.
diff --git a/erpnext/stock/spec/reposting.md b/erpnext/stock/spec/reposting.md
new file mode 100644
index 0000000..b0d59fe
--- /dev/null
+++ b/erpnext/stock/spec/reposting.md
@@ -0,0 +1,38 @@
+# Stock Reposting
+
+Stock "reposting" is process of re-processing Stock Ledger Entry and GL Entries
+in event of backdated stock transaction.
+
+*Backdated stock transaction*: Any stock transaction for which some
+item-warehouse combination has a future transactions.
+
+## Why is this required?
+Stock Ledger is stateful, it maintains queue, qty at any
+point in time. So if you do a backdated transaction all future values change,
+queues need to be re-evaluated etc. Watch Nabin and Rohit's conference
+presentation for explanation: https://www.youtube.com/watch?v=mw3WAnekGIM
+
+## How is this implemented?
+Whenever backdated transaction is detected, instead of
+fully processing it while submitting, the processing is queued using "Repost
+Item Valuation" doctype. Every hour a scheduled job runs and processes this
+queue (for up to maximum of 25 minutes)
+
+
+## Queue implementation
+- "Repost item valuation" (RIV) is automatically submitted from backdated transactions. (check stock_controller.py)
+- Draft and cancelled RIV are ignored.
+- Keep filter of "submitted" documents when doing anything with RIVs.
+- The default status is "Queued".
+- When background job runs, it picks the oldest pending reposts and changes the status to "In Progress" and when it finishes it
+changes to "Completed"
+- There are two more status: "Failed" when reposting failed and "Skipped" when reposting is deemed not necessary so it's skipped.
+- technical detail: Entry point for whole process is "repost_entries" function in repost_item_valuation.py
+
+
+## How to identify broken stock data:
+There are 4 major reports for checking broken stock data:
+- Incorrect balance qty after the transaction - to check if the running total of qty isn't correct.
+- Incorrect stock value report - to check incorrect value books in accounts for stock transactions
+- Incorrect serial no valuation -specific to serial nos
+- Stock ledger invariant check - combined report for checking qty, running total, queue, balance value etc
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 3275521..d7adae5 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -13,7 +13,7 @@
 		<li class="wishlist wishlist-icon hidden">
 			<a class="nav-link" href="/wishlist">
 				<svg class="icon icon-lg">
-					<use href="#icon-heart-active"></use>
+					<use href="#icon-heart"></use>
 				</svg>
 				<span class="badge badge-primary shopping-badge" id="wish-count"></span>
 			</a>
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index f345a87..b882b9d 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -3731,7 +3731,7 @@
 Edit Details,Details bearbeiten,
 Edit Profile,Profil bearbeiten,
 Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
-Email,Email,
+Email,E-Mail,
 Email Campaigns,E-Mail-Kampagnen,
 Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
 Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@@ -6487,7 +6487,7 @@
 Send Emails At,Die E-Mails senden um,
 Reminder,Erinnerung,
 Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
-email,Email,
+email,E-Mail,
 Parent Department,Elternabteilung,
 Leave Block List,Urlaubssperrenliste,
 Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
index e72bfc8..95ba8f7 100644
--- a/erpnext/www/lms/macros/hero.html
+++ b/erpnext/www/lms/macros/hero.html
@@ -11,7 +11,7 @@
 			{% if frappe.session.user == 'Guest' %}
 			<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
 			{% elif not has_access %}
-			<button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()" disabled>{{_('Enroll')}}</button>
+			<button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()">{{_('Enroll')}}</button>
 			{% endif %}
 		</p>
 	</div>
@@ -20,34 +20,35 @@
 <script type="text/javascript">
 	frappe.ready(() => {
 		btn = document.getElementById('enroll');
-		if (btn) btn.disabled = false;
 	})
 
 	function enroll() {
 		let params = frappe.utils.get_query_params()
 
 		let btn = document.getElementById('enroll');
-		btn.disbaled = true;
-		btn.innerText = __('Enrolling...')
 
 		let opts = {
 			method: 'erpnext.education.utils.enroll_in_program',
 			args: {
 				program_name: params.program
-			}
+			},
+			freeze: true,
+			freeze_message: __('Enrolling...')
 		}
 
 		frappe.call(opts).then(res => {
 			let success_dialog = new frappe.ui.Dialog({
 				title: __('Success'),
+				primary_action_label: __('View Program Content'),
+				primary_action: function() {
+					window.location.reload();
+				},
 				secondary_action: function() {
-					window.location.reload()
+					window.location.reload();
 				}
 			})
-			success_dialog.set_message(__('You have successfully enrolled for the program '));
-			success_dialog.$message.show()
 			success_dialog.show();
-			btn.disbaled = false;
+			success_dialog.set_message(__('You have successfully enrolled for the program '));
 		})
 	}
 </script>
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 3946212..09f97ba 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -62,8 +62,7 @@
 					"parent_item_group": "All Item Groups",
 					"show_in_website": 1
 				},
-				fields=["name", "parent_item_group", "is_group", "image", "route"],
-				as_dict=True
+				fields=["name", "parent_item_group", "is_group", "image", "route"]
 			)
 		else:
 			doctype = frappe.unscrub(category)
@@ -71,7 +70,7 @@
 			if frappe.get_meta(doctype, cached=True).get_field("image"):
 				fields += ["image"]
 
-			categorical_data[category] = frappe.db.get_all(doctype, fields=fields, as_dict=True)
+			categorical_data[category] = frappe.db.get_all(doctype, fields=fields)
 
 	return categorical_data