Merge pull request #35689 from marination/payments-based-dunning

feat: Payments based dunning
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 9b4db49..2ce1125 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -7,11 +7,9 @@
       - '**.css'
       - '**.md'
       - '**.html'
-  push:
-    branches: [ develop ]
-    paths-ignore:
-      - '**.js'
-      - '**.md'
+  schedule:
+    # Run everday at midnight UTC / 5:30 IST
+    - cron: "0 0 * * *"
   workflow_dispatch:
     inputs:
       user:
diff --git a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json
index 8631d3d..4883106 100644
--- a/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json
+++ b/erpnext/accounts/dashboard_chart/budget_variance/budget_variance.json
@@ -4,18 +4,19 @@
  "creation": "2020-07-17 11:25:34.593061",
  "docstatus": 0,
  "doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
  "filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
  "idx": 0,
  "is_public": 1,
  "is_standard": 1,
- "modified": "2020-07-22 12:24:49.144210",
+ "modified": "2023-07-19 13:13:13.307073",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Budget Variance",
  "number_of_groups": 0,
  "owner": "Administrator",
  "report_name": "Budget Variance Report",
+ "roles": [],
  "timeseries": 0,
  "type": "Bar",
  "use_report_chart": 1,
diff --git a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json
index 3fa995b..25caa44 100644
--- a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json
+++ b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json
@@ -4,18 +4,19 @@
  "creation": "2020-07-17 11:25:34.448572",
  "docstatus": 0,
  "doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
  "filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
  "idx": 0,
  "is_public": 1,
  "is_standard": 1,
- "modified": "2020-07-22 12:33:48.888943",
+ "modified": "2023-07-19 13:08:56.470390",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Profit and Loss",
  "number_of_groups": 0,
  "owner": "Administrator",
  "report_name": "Profit and Loss Statement",
+ "roles": [],
  "timeseries": 0,
  "type": "Bar",
  "use_report_chart": 1,
diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
index 9540084..e75af70 100644
--- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
+++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
@@ -14,10 +14,8 @@
 	pass
 
 
-def make_closing_entries(closing_entries, voucher_name):
+def make_closing_entries(closing_entries, voucher_name, company, closing_date):
 	accounting_dimensions = get_accounting_dimensions()
-	company = closing_entries[0].get("company")
-	closing_date = closing_entries[0].get("closing_date")
 
 	previous_closing_entries = get_previous_closing_entries(
 		company, closing_date, accounting_dimensions
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.js b/erpnext/accounts/doctype/accounting_period/accounting_period.js
index e3d805a..f17b6f9 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.js
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.js
@@ -20,5 +20,11 @@
 				}
 			});
 		}
+
+		frm.set_query("document_type", "closed_documents", () => {
+			return {
+				query: "erpnext.controllers.queries.get_doctypes_for_closing",
+			}
+		});
 	}
 });
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index 80c9715..d5f37a6 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -11,6 +11,10 @@
 	pass
 
 
+class ClosedAccountingPeriod(frappe.ValidationError):
+	pass
+
+
 class AccountingPeriod(Document):
 	def validate(self):
 		self.validate_overlap()
@@ -65,3 +69,42 @@
 					"closed_documents",
 					{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
 				)
+
+
+def validate_accounting_period_on_doc_save(doc, method=None):
+	if doc.doctype == "Bank Clearance":
+		return
+	elif doc.doctype == "Asset":
+		if doc.is_existing_asset:
+			return
+		else:
+			date = doc.available_for_use_date
+	elif doc.doctype == "Asset Repair":
+		date = doc.completion_date
+	else:
+		date = doc.posting_date
+
+	ap = frappe.qb.DocType("Accounting Period")
+	cd = frappe.qb.DocType("Closed Document")
+
+	accounting_period = (
+		frappe.qb.from_(ap)
+		.from_(cd)
+		.select(ap.name)
+		.where(
+			(ap.name == cd.parent)
+			& (ap.company == doc.company)
+			& (cd.closed == 1)
+			& (cd.document_type == doc.doctype)
+			& (date >= ap.start_date)
+			& (date <= ap.end_date)
+		)
+	).run(as_dict=1)
+
+	if accounting_period:
+		frappe.throw(
+			_("You cannot create a {0} within the closed Accounting Period {1}").format(
+				doc.doctype, frappe.bold(accounting_period[0]["name"])
+			),
+			ClosedAccountingPeriod,
+		)
diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
index 85025d1..41d9479 100644
--- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
@@ -6,9 +6,11 @@
 import frappe
 from frappe.utils import add_months, nowdate
 
-from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
+from erpnext.accounts.doctype.accounting_period.accounting_period import (
+	ClosedAccountingPeriod,
+	OverlapError,
+)
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.accounts.general_ledger import ClosedAccountingPeriod
 
 test_dependencies = ["Item"]
 
@@ -33,9 +35,9 @@
 		ap1.save()
 
 		doc = create_sales_invoice(
-			do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
+			do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
 		)
-		self.assertRaises(ClosedAccountingPeriod, doc.submit)
+		self.assertRaises(ClosedAccountingPeriod, doc.save)
 
 	def tearDown(self):
 		for d in frappe.get_all("Accounting Period"):
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 79f0419..7542bab 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -226,10 +226,12 @@
 		latest_lookup = {}
 		for d in latest_references:
 			d = frappe._dict(d)
-			latest_lookup.update({(d.voucher_type, d.voucher_no): d})
+			latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
 
 		for d in self.get("references"):
-			latest = latest_lookup.get((d.reference_doctype, d.reference_name))
+			latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get(
+				d.payment_term
+			)
 
 			# The reference has already been fully paid
 			if not latest:
@@ -251,6 +253,18 @@
 			if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
 				frappe.throw(fail_message.format(d.idx))
 
+			if d.payment_term and (
+				(flt(d.allocated_amount)) > 0
+				and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
+			):
+				frappe.throw(
+					_(
+						"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
+					).format(
+						d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
+					)
+				)
+
 			# Check for negative outstanding invoices as well
 			if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
 				frappe.throw(fail_message.format(d.idx))
@@ -1500,7 +1514,9 @@
 			accounting_dimensions=accounting_dimensions_filter,
 		)
 
-		outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
+		outstanding_invoices = split_invoices_based_on_payment_terms(
+			outstanding_invoices, args.get("company")
+		)
 
 		for d in outstanding_invoices:
 			d["exchange_rate"] = 1
@@ -1560,8 +1576,27 @@
 	return data
 
 
-def split_invoices_based_on_payment_terms(outstanding_invoices):
+def split_invoices_based_on_payment_terms(outstanding_invoices, company):
 	invoice_ref_based_on_payment_terms = {}
+
+	company_currency = (
+		frappe.db.get_value("Company", company, "default_currency") if company else None
+	)
+	exc_rates = frappe._dict()
+	for doctype in ["Sales Invoice", "Purchase Invoice"]:
+		invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
+		for x in frappe.db.get_all(
+			doctype,
+			filters={"name": ["in", invoices]},
+			fields=["name", "currency", "conversion_rate", "party_account_currency"],
+		):
+			exc_rates[x.name] = frappe._dict(
+				conversion_rate=x.conversion_rate,
+				currency=x.currency,
+				party_account_currency=x.party_account_currency,
+				company_currency=company_currency,
+			)
+
 	for idx, d in enumerate(outstanding_invoices):
 		if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
 			payment_term_template = frappe.db.get_value(
@@ -1578,6 +1613,14 @@
 
 					for payment_term in payment_schedule:
 						if payment_term.outstanding > 0.1:
+							doc_details = exc_rates.get(payment_term.parent, None)
+							is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
+								doc_details.party_account_currency != doc_details.company_currency
+							)
+							payment_term_outstanding = flt(payment_term.outstanding)
+							if not is_multi_currency_acc:
+								payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
+
 							invoice_ref_based_on_payment_terms.setdefault(idx, [])
 							invoice_ref_based_on_payment_terms[idx].append(
 								frappe._dict(
@@ -1589,6 +1632,7 @@
 										"posting_date": d.posting_date,
 										"invoice_amount": flt(d.invoice_amount),
 										"outstanding_amount": flt(d.outstanding_amount),
+										"payment_term_outstanding": payment_term_outstanding,
 										"payment_amount": payment_term.payment_amount,
 										"payment_term": payment_term.payment_term,
 										"account": d.account,
@@ -2372,6 +2416,7 @@
 					"due_date": doc.get("due_date"),
 					"total_amount": grand_total,
 					"outstanding_amount": outstanding_amount,
+					"payment_term_outstanding": payment_term_outstanding,
 					"payment_term": payment_term.payment_term,
 					"allocated_amount": payment_term_outstanding,
 				}
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 641f452..922722f 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -133,6 +133,8 @@
 					gl_entries=gl_entries,
 					closing_entries=closing_entries,
 					voucher_name=self.name,
+					company=self.company,
+					closing_date=self.posting_date,
 					queue="long",
 				)
 				frappe.msgprint(
@@ -140,7 +142,7 @@
 					alert=True,
 				)
 			else:
-				process_gl_entries(gl_entries, closing_entries, voucher_name=self.name)
+				process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
 
 	def get_grouped_gl_entries(self, get_opening_entries=False):
 		closing_entries = []
@@ -321,7 +323,7 @@
 		return query.run(as_dict=1)
 
 
-def process_gl_entries(gl_entries, closing_entries, voucher_name=None):
+def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
 	from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
 		make_closing_entries,
 	)
@@ -329,7 +331,7 @@
 
 	try:
 		make_gl_entries(gl_entries, merge_entries=False)
-		make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
+		make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
 		frappe.db.set_value(
 			"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
 		)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index f1dad87..e9dc5fc 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -13,14 +13,11 @@
 from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
 	get_accounting_dimensions,
 )
+from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
 from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
 from erpnext.accounts.utils import create_payment_ledger_entry
 
 
-class ClosedAccountingPeriod(frappe.ValidationError):
-	pass
-
-
 def make_gl_entries(
 	gl_map,
 	cancel=False,
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 5176c31..39917f9 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -221,7 +221,10 @@
 		)
 	else:
 		if start_date:
-			opening_balance = opening_balance.where(closing_balance.posting_date >= start_date)
+			opening_balance = opening_balance.where(
+				(closing_balance.posting_date >= start_date)
+				& (closing_balance.posting_date < filters.from_date)
+			)
 			opening_balance = opening_balance.where(closing_balance.is_opening == "No")
 		else:
 			opening_balance = opening_balance.where(
diff --git a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json
index 6452ed2..751796b 100644
--- a/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json
+++ b/erpnext/buying/dashboard_chart/purchase_order_trends/purchase_order_trends.json
@@ -5,18 +5,19 @@
  "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
  "docstatus": 0,
  "doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
  "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}",
- "idx": 0,
+ "idx": 1,
  "is_public": 1,
  "is_standard": 1,
- "modified": "2020-07-21 16:13:25.092287",
+ "modified": "2023-07-19 13:06:42.937941",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Purchase Order Trends",
  "number_of_groups": 0,
  "owner": "Administrator",
  "report_name": "Purchase Order Trends",
+ "roles": [],
  "timeseries": 0,
  "type": "Line",
  "use_report_chart": 1,
diff --git a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json
index 6f7da8e..f6b9717 100644
--- a/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json
+++ b/erpnext/buying/dashboard_chart/top_suppliers/top_suppliers.json
@@ -4,18 +4,19 @@
  "creation": "2020-07-20 21:01:02.329519",
  "docstatus": 0,
  "doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
  "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}",
  "idx": 0,
  "is_public": 1,
  "is_standard": 1,
- "modified": "2020-07-22 12:43:40.829652",
+ "modified": "2023-07-19 13:07:41.753556",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Top Suppliers",
  "number_of_groups": 0,
  "owner": "Administrator",
  "report_name": "Purchase Receipt Trends",
+ "roles": [],
  "timeseries": 0,
  "type": "Bar",
  "use_report_chart": 1,
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 3bb1128..d1dcd6a 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -824,6 +824,15 @@
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
+def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters):
+	doctypes = frappe.get_hooks("period_closing_doctypes")
+	if txt:
+		doctypes = [d for d in doctypes if txt.lower() in d.lower()]
+	return [(d,) for d in set(doctypes)]
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
 def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
 
 	item_doc = frappe.get_cached_doc("Item", filters.get("item_code"))
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index b68318a..d8b40e3 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -285,10 +285,34 @@
 	"Customer": "erpnext.controllers.queries.customer_query",
 }
 
+period_closing_doctypes = [
+	"Sales Invoice",
+	"Purchase Invoice",
+	"Journal Entry",
+	"Bank Clearance",
+	"Stock Entry",
+	"Dunning",
+	"Invoice Discounting",
+	"Payment Entry",
+	"Period Closing Voucher",
+	"Process Deferred Accounting",
+	"Asset",
+	"Asset Capitalization",
+	"Asset Repair",
+	"Delivery Note",
+	"Landed Cost Voucher",
+	"Purchase Receipt",
+	"Stock Reconciliation",
+	"Subcontracting Receipt",
+]
+
 doc_events = {
 	"*": {
 		"validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
 	},
+	tuple(period_closing_doctypes): {
+		"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
+	},
 	"Stock Entry": {
 		"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
 		"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
@@ -465,15 +489,6 @@
 
 invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
 
-period_closing_doctypes = [
-	"Sales Invoice",
-	"Purchase Invoice",
-	"Journal Entry",
-	"Bank Clearance",
-	"Asset",
-	"Stock Entry",
-]
-
 bank_reconciliation_doctypes = [
 	"Payment Entry",
 	"Journal Entry",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index a988bad..d8cc8f6 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1539,7 +1539,7 @@
 		frappe.qb.from_(table)
 		.inner_join(child)
 		.on(table.name == child.parent)
-		.select(Sum(child.required_bom_qty * IfNull(child.conversion_factor, 1.0)))
+		.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
 		.where(
 			(table.docstatus == 1)
 			& (child.item_code == item_code)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index fcfba7f..f60dbfc 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -933,6 +933,54 @@
 
 		self.assertEqual(after_qty, before_qty)
 
+	def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
+		from erpnext.stock.utils import get_or_make_bin
+
+		fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
+		bom_item = make_item(
+			properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
+		).name
+
+		if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
+			doc = frappe.get_doc("Item", bom_item)
+			doc.append("uoms", {"uom": "Nos", "conversion_factor": 25})
+			doc.save()
+
+		make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
+
+		bin_name = get_or_make_bin(bom_item, "_Test Warehouse - _TC")
+		before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
+
+		pln = create_production_plan(
+			item_code=fg_item, planned_qty=100, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1"
+		)
+
+		for row in pln.mr_items:
+			self.assertEqual(row.uom, "Nos")
+			self.assertEqual(row.quantity, 4)
+
+			reserved_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
+			self.assertEqual(reserved_qty - before_qty, 100.0)
+
+		pln.submit_material_request = 1
+		pln.make_work_order()
+
+		for work_order in frappe.get_all(
+			"Work Order",
+			fields=["name"],
+			filters={"production_plan": pln.name},
+		):
+			wo_doc = frappe.get_doc("Work Order", work_order.name)
+			wo_doc.source_warehouse = "_Test Warehouse - _TC"
+			wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
+			wo_doc.fg_warehouse = "_Test Warehouse - _TC"
+			wo_doc.submit()
+
+		reserved_qty_after_mr = flt(
+			frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")
+		)
+		self.assertEqual(reserved_qty_after_mr, before_qty)
+
 	def test_skip_available_qty_for_sub_assembly_items(self):
 		from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
 
diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py
index 2947b98..2c84281 100644
--- a/erpnext/patches/v14_0/update_closing_balances.py
+++ b/erpnext/patches/v14_0/update_closing_balances.py
@@ -69,7 +69,6 @@
 
 				entries = gl_entries + closing_entries
 
-				if entries:
-					make_closing_entries(entries, voucher_name=pcv.name)
-					i += 1
-					company_wise_order[pcv.company].append(pcv.posting_date)
+				make_closing_entries(entries, pcv.name, pcv.company, pcv.posting_date)
+				company_wise_order[pcv.company].append(pcv.posting_date)
+				i += 1
diff --git a/erpnext/patches/v15_0/remove_exotel_integration.py b/erpnext/patches/v15_0/remove_exotel_integration.py
index a37773f..9b99fc6 100644
--- a/erpnext/patches/v15_0/remove_exotel_integration.py
+++ b/erpnext/patches/v15_0/remove_exotel_integration.py
@@ -1,5 +1,3 @@
-from contextlib import suppress
-
 import click
 import frappe
 from frappe import _
@@ -13,12 +11,14 @@
 	if "exotel_integration" in frappe.get_installed_apps():
 		return
 
-	with suppress(Exception):
+	try:
 		exotel = frappe.get_doc(SETTINGS_DOCTYPE)
 		if exotel.enabled:
 			notify_existing_users()
 
 		frappe.delete_doc("DocType", SETTINGS_DOCTYPE)
+	except Exception:
+		frappe.log_error("Failed to remove Exotel Integration.")
 
 
 def notify_existing_users():
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 497f8d2..cc03eca 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -401,6 +401,10 @@
 	},
 
 	get_fiscal_year: function(date) {
+		if(!date) {
+			date = frappe.datetime.get_today();
+		}
+
 		let fiscal_year = '';
 		frappe.call({
 			method: "erpnext.accounts.utils.get_fiscal_year",
diff --git a/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json b/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json
index 914d915..2f668a8 100644
--- a/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json
+++ b/erpnext/selling/dashboard_chart/sales_order_trends/sales_order_trends.json
@@ -5,18 +5,19 @@
  "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
  "docstatus": 0,
  "doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
  "filters_json": "{\"period\":\"Monthly\",\"based_on\":\"Item\"}",
- "idx": 0,
+ "idx": 1,
  "is_public": 1,
  "is_standard": 1,
- "modified": "2020-07-22 16:24:45.726270",
+ "modified": "2023-07-19 13:09:45.341791",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order Trends",
  "number_of_groups": 0,
  "owner": "Administrator",
  "report_name": "Sales Order Trends",
+ "roles": [],
  "timeseries": 0,
  "type": "Line",
  "use_report_chart": 1,
diff --git a/erpnext/selling/dashboard_chart/top_customers/top_customers.json b/erpnext/selling/dashboard_chart/top_customers/top_customers.json
index 59a2ba3..2972980 100644
--- a/erpnext/selling/dashboard_chart/top_customers/top_customers.json
+++ b/erpnext/selling/dashboard_chart/top_customers/top_customers.json
@@ -5,18 +5,19 @@
  "custom_options": "",
  "docstatus": 0,
  "doctype": "Dashboard Chart",
- "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
+ "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
  "filters_json": "{\"period\":\"Yearly\",\"based_on\":\"Customer\"}",
  "idx": 0,
  "is_public": 1,
  "is_standard": 1,
- "modified": "2020-07-22 17:03:10.320147",
+ "modified": "2023-07-19 13:14:20.151502",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Top Customers",
  "number_of_groups": 0,
  "owner": "Administrator",
  "report_name": "Delivery Note Trends",
+ "roles": [],
  "timeseries": 0,
  "type": "Bar",
  "use_report_chart": 1,
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 45100d7..796e258 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1904,12 +1904,11 @@
 						"voucher_no": so.name,
 						"voucher_detail_no": item.name,
 					},
-					fields=["status", "reserved_qty", "delivered_qty"],
+					fields=["reserved_qty", "delivered_qty"],
 				)
 
 				for sre_detail in sre_details:
 					self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty)
-					self.assertEqual(sre_detail.status, "Delivered")
 
 	def test_delivered_item_material_request(self):
 		"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index ee247fd..00b1b20 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -118,8 +118,8 @@
 			self.title = _("{0} Request for {1}").format(_(self.material_request_type), items)[:100]
 
 	def on_submit(self):
-		self.update_requested_qty()
 		self.update_requested_qty_in_production_plan()
+		self.update_requested_qty()
 		if self.material_request_type == "Purchase":
 			self.validate_budget()
 
@@ -178,8 +178,8 @@
 				)
 
 	def on_cancel(self):
-		self.update_requested_qty()
 		self.update_requested_qty_in_production_plan()
+		self.update_requested_qty()
 
 	def get_mr_items_ordered_qty(self, mr_items):
 		mr_items_ordered_qty = {}
@@ -270,7 +270,13 @@
 				item_wh_list.append([d.item_code, d.warehouse])
 
 		for item_code, warehouse in item_wh_list:
-			update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)})
+			update_bin_qty(
+				item_code,
+				warehouse,
+				{
+					"indented_qty": get_indented_qty(item_code, warehouse),
+				},
+			)
 
 	def update_requested_qty_in_production_plan(self):
 		production_plans = []
diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html
index 5652bb1..fbc80de 100644
--- a/erpnext/templates/includes/itemised_tax_breakup.html
+++ b/erpnext/templates/includes/itemised_tax_breakup.html
@@ -15,7 +15,7 @@
 			{% for item, taxes in itemised_tax.items() %}
 				<tr>
 					<td>{{ item }}</td>
-					<td class='text-right'>
+					<td class="text-right">
 						{% if doc.get('is_return') %}
 							{{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }}
 						{% else %}
@@ -25,7 +25,7 @@
 					{% for tax_account in tax_accounts %}
 						{% set tax_details = taxes.get(tax_account) %}
 						{% if tax_details %}
-							<td class='text-right'>
+							<td class="text-right">
 								{% if tax_details.tax_rate or not tax_details.tax_amount %}
 									({{ tax_details.tax_rate }}%)
 								{% endif %}