Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry
diff --git a/erpnext/accounts/doctype/account_closing_balance/__init__.py b/erpnext/accounts/doctype/account_closing_balance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/account_closing_balance/__init__.py
diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.js b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.js
new file mode 100644
index 0000000..e355914
--- /dev/null
+++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Account Closing Balance", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json
new file mode 100644
index 0000000..8dacb96
--- /dev/null
+++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.json
@@ -0,0 +1,164 @@
+{
+ "actions": [],
+ "creation": "2023-02-21 15:20:59.586811",
+ "default_view": "List",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+  "closing_date",
+  "account",
+  "cost_center",
+  "debit",
+  "credit",
+  "account_currency",
+  "debit_in_account_currency",
+  "credit_in_account_currency",
+  "project",
+  "company",
+  "finance_book",
+  "period_closing_voucher",
+  "is_period_closing_voucher_entry"
+ ],
+ "fields": [
+  {
+   "fieldname": "closing_date",
+   "fieldtype": "Date",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "label": "Closing Date",
+   "oldfieldname": "posting_date",
+   "oldfieldtype": "Date",
+   "search_index": 1
+  },
+  {
+   "fieldname": "account",
+   "fieldtype": "Link",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Account",
+   "oldfieldname": "account",
+   "oldfieldtype": "Link",
+   "options": "Account",
+   "search_index": 1
+  },
+  {
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "label": "Cost Center",
+   "oldfieldname": "cost_center",
+   "oldfieldtype": "Link",
+   "options": "Cost Center"
+  },
+  {
+   "fieldname": "debit",
+   "fieldtype": "Currency",
+   "label": "Debit Amount",
+   "oldfieldname": "debit",
+   "oldfieldtype": "Currency",
+   "options": "Company:company:default_currency"
+  },
+  {
+   "fieldname": "credit",
+   "fieldtype": "Currency",
+   "label": "Credit Amount",
+   "oldfieldname": "credit",
+   "oldfieldtype": "Currency",
+   "options": "Company:company:default_currency"
+  },
+  {
+   "fieldname": "account_currency",
+   "fieldtype": "Link",
+   "label": "Account Currency",
+   "options": "Currency"
+  },
+  {
+   "fieldname": "debit_in_account_currency",
+   "fieldtype": "Currency",
+   "label": "Debit Amount in Account Currency",
+   "options": "account_currency"
+  },
+  {
+   "fieldname": "credit_in_account_currency",
+   "fieldtype": "Currency",
+   "label": "Credit Amount in Account Currency",
+   "options": "account_currency"
+  },
+  {
+   "fieldname": "project",
+   "fieldtype": "Link",
+   "label": "Project",
+   "options": "Project"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Company",
+   "oldfieldname": "company",
+   "oldfieldtype": "Link",
+   "options": "Company",
+   "search_index": 1
+  },
+  {
+   "fieldname": "finance_book",
+   "fieldtype": "Link",
+   "label": "Finance Book",
+   "options": "Finance Book"
+  },
+  {
+   "fieldname": "period_closing_voucher",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Period Closing Voucher",
+   "options": "Period Closing Voucher",
+   "search_index": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "is_period_closing_voucher_entry",
+   "fieldtype": "Check",
+   "label": "Is Period Closing Voucher Entry"
+  }
+ ],
+ "icon": "fa fa-list",
+ "in_create": 1,
+ "links": [],
+ "modified": "2023-03-06 08:56:36.393237",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Account Closing Balance",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User"
+  },
+  {
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts Manager"
+  },
+  {
+   "export": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Auditor"
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
new file mode 100644
index 0000000..7c84237
--- /dev/null
+++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe.utils import cint, cstr
+
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+	get_accounting_dimensions,
+)
+
+
+class AccountClosingBalance(Document):
+	pass
+
+
+def make_closing_entries(closing_entries, voucher_name):
+	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
+	)
+	combined_entries = closing_entries + previous_closing_entries
+
+	merged_entries = aggregate_with_last_account_closing_balance(
+		combined_entries, accounting_dimensions
+	)
+
+	for key, value in merged_entries.items():
+		cle = frappe.new_doc("Account Closing Balance")
+		cle.update(value)
+		cle.update(value["dimensions"])
+		cle.update(
+			{
+				"period_closing_voucher": voucher_name,
+				"closing_date": closing_date,
+			}
+		)
+		cle.submit()
+
+
+def aggregate_with_last_account_closing_balance(entries, accounting_dimensions):
+	merged_entries = {}
+	for entry in entries:
+		key, key_values = generate_key(entry, accounting_dimensions)
+		merged_entries.setdefault(
+			key,
+			{
+				"debit": 0,
+				"credit": 0,
+				"debit_in_account_currency": 0,
+				"credit_in_account_currency": 0,
+			},
+		)
+
+		merged_entries[key]["dimensions"] = key_values
+		merged_entries[key]["debit"] += entry.get("debit")
+		merged_entries[key]["credit"] += entry.get("credit")
+		merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency")
+		merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency")
+
+	return merged_entries
+
+
+def generate_key(entry, accounting_dimensions):
+	key = [
+		cstr(entry.get("account")),
+		cstr(entry.get("account_currency")),
+		cstr(entry.get("cost_center")),
+		cstr(entry.get("project")),
+		cstr(entry.get("finance_book")),
+		cint(entry.get("is_period_closing_voucher_entry")),
+	]
+
+	key_values = {
+		"company": cstr(entry.get("company")),
+		"account": cstr(entry.get("account")),
+		"account_currency": cstr(entry.get("account_currency")),
+		"cost_center": cstr(entry.get("cost_center")),
+		"project": cstr(entry.get("project")),
+		"finance_book": cstr(entry.get("finance_book")),
+		"is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")),
+	}
+	for dimension in accounting_dimensions:
+		key.append(cstr(entry.get(dimension)))
+		key_values[dimension] = cstr(entry.get(dimension))
+
+	return tuple(key), key_values
+
+
+def get_previous_closing_entries(company, closing_date, accounting_dimensions):
+	entries = []
+	last_period_closing_voucher = frappe.db.get_all(
+		"Period Closing Voucher",
+		filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
+		fields=["name"],
+		order_by="posting_date desc",
+		limit=1,
+	)
+
+	if last_period_closing_voucher:
+		account_closing_balance = frappe.qb.DocType("Account Closing Balance")
+		query = frappe.qb.from_(account_closing_balance).select(
+			account_closing_balance.company,
+			account_closing_balance.account,
+			account_closing_balance.account_currency,
+			account_closing_balance.debit,
+			account_closing_balance.credit,
+			account_closing_balance.debit_in_account_currency,
+			account_closing_balance.credit_in_account_currency,
+			account_closing_balance.cost_center,
+			account_closing_balance.project,
+			account_closing_balance.finance_book,
+			account_closing_balance.is_period_closing_voucher_entry,
+		)
+
+		for dimension in accounting_dimensions:
+			query = query.select(account_closing_balance[dimension])
+
+		query = query.where(
+			account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name
+		)
+		entries = query.run(as_dict=1)
+
+	return entries
diff --git a/erpnext/accounts/doctype/account_closing_balance/test_account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/test_account_closing_balance.py
new file mode 100644
index 0000000..fc42677
--- /dev/null
+++ b/erpnext/accounts/doctype/account_closing_balance/test_account_closing_balance.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestAccountClosingBalance(FrappeTestCase):
+	pass
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 ca98bee..831c343 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -4,12 +4,13 @@
 
 import frappe
 from frappe import _
-from frappe.utils import flt
+from frappe.query_builder.functions import Sum
+from frappe.utils import add_days, flt
 
 from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
 	get_accounting_dimensions,
 )
-from erpnext.accounts.utils import get_account_currency
+from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
 from erpnext.controllers.accounts_controller import AccountsController
 
 
@@ -20,7 +21,14 @@
 
 	def on_submit(self):
 		self.db_set("gle_processing_status", "In Progress")
-		self.make_gl_entries()
+		get_opening_entries = False
+
+		if not frappe.db.exists(
+			"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
+		):
+			get_opening_entries = True
+
+		self.make_gl_entries(get_opening_entries=get_opening_entries)
 
 	def on_cancel(self):
 		self.db_set("gle_processing_status", "In Progress")
@@ -42,6 +50,14 @@
 		else:
 			make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
 
+		self.delete_closing_entries()
+
+	def delete_closing_entries(self):
+		closing_balance = frappe.qb.DocType("Account Closing Balance")
+		frappe.qb.from_(closing_balance).delete().where(
+			closing_balance.period_closing_voucher == self.name
+		).run()
+
 	def validate_account_head(self):
 		closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
 
@@ -56,8 +72,6 @@
 			frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
 
 	def validate_posting_date(self):
-		from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
-
 		validate_fiscal_year(
 			self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
 		)
@@ -66,6 +80,8 @@
 			self.posting_date, self.fiscal_year, company=self.company
 		)[1]
 
+		self.check_if_previous_year_closed()
+
 		pce = frappe.db.sql(
 			"""select name from `tabPeriod Closing Voucher`
 			where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
@@ -78,28 +94,59 @@
 				)
 			)
 
-	def make_gl_entries(self):
+	def check_if_previous_year_closed(self):
+		last_year_closing = add_days(self.year_start_date, -1)
+
+		previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
+
+		if previous_fiscal_year and not frappe.db.exists(
+			"Period Closing Voucher",
+			{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
+		):
+			frappe.throw(_("Previous Year is not closed, please close it first"))
+
+	def make_gl_entries(self, get_opening_entries=False):
 		gl_entries = self.get_gl_entries()
+		closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
 		if gl_entries:
 			if len(gl_entries) > 5000:
-				frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
+				frappe.enqueue(
+					process_gl_entries,
+					gl_entries=gl_entries,
+					closing_entries=closing_entries,
+					voucher_name=self.name,
+					queue="long",
+				)
 				frappe.msgprint(
 					_("The GL Entries will be processed in the background, it can take a few minutes."),
 					alert=True,
 				)
 			else:
-				process_gl_entries(gl_entries)
+				process_gl_entries(gl_entries, closing_entries, voucher_name=self.name)
+
+	def get_grouped_gl_entries(self, get_opening_entries=False):
+		closing_entries = []
+		for acc in self.get_balances_based_on_dimensions(
+			group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
+		):
+			closing_entries.append(self.get_closing_entries(acc))
+
+		return closing_entries
 
 	def get_gl_entries(self):
 		gl_entries = []
 
 		# pl account
-		for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
+		for acc in self.get_balances_based_on_dimensions(
+			group_by_account=True, report_type="Profit and Loss"
+		):
 			if flt(acc.bal_in_company_currency):
 				gl_entries.append(self.get_gle_for_pl_account(acc))
 
 		# closing liability account
-		for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
+		for acc in self.get_balances_based_on_dimensions(
+			group_by_account=False, report_type="Profit and Loss"
+		):
 			if flt(acc.bal_in_company_currency):
 				gl_entries.append(self.get_gle_for_closing_account(acc))
 
@@ -108,6 +155,8 @@
 	def get_gle_for_pl_account(self, acc):
 		gl_entry = self.get_gl_dict(
 			{
+				"company": self.company,
+				"closing_date": self.posting_date,
 				"account": acc.account,
 				"cost_center": acc.cost_center,
 				"finance_book": acc.finance_book,
@@ -120,6 +169,7 @@
 				if flt(acc.bal_in_account_currency) > 0
 				else 0,
 				"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
+				"is_period_closing_voucher_entry": 1,
 			},
 			item=acc,
 		)
@@ -129,6 +179,8 @@
 	def get_gle_for_closing_account(self, acc):
 		gl_entry = self.get_gl_dict(
 			{
+				"company": self.company,
+				"closing_date": self.posting_date,
 				"account": self.closing_account_head,
 				"cost_center": acc.cost_center,
 				"finance_book": acc.finance_book,
@@ -141,12 +193,36 @@
 				if flt(acc.bal_in_account_currency) < 0
 				else 0,
 				"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
+				"is_period_closing_voucher_entry": 1,
 			},
 			item=acc,
 		)
 		self.update_default_dimensions(gl_entry, acc)
 		return gl_entry
 
+	def get_closing_entries(self, acc):
+		closing_entry = self.get_gl_dict(
+			{
+				"company": self.company,
+				"closing_date": self.posting_date,
+				"period_closing_voucher": self.name,
+				"account": acc.account,
+				"cost_center": acc.cost_center,
+				"finance_book": acc.finance_book,
+				"account_currency": acc.account_currency,
+				"debit_in_account_currency": flt(acc.debit_in_account_currency),
+				"debit": flt(acc.debit),
+				"credit_in_account_currency": flt(acc.credit_in_account_currency),
+				"credit": flt(acc.credit),
+			},
+			item=acc,
+		)
+
+		for dimension in self.accounting_dimensions:
+			closing_entry.update({dimension: acc.get(dimension)})
+
+		return closing_entry
+
 	def update_default_dimensions(self, gl_entry, acc):
 		if not self.accounting_dimensions:
 			self.accounting_dimensions = get_accounting_dimensions()
@@ -154,47 +230,88 @@
 		for dimension in self.accounting_dimensions:
 			gl_entry.update({dimension: acc.get(dimension)})
 
-	def get_pl_balances_based_on_dimensions(self, group_by_account=False):
+	def get_balances_based_on_dimensions(
+		self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
+	):
 		"""Get balance for dimension-wise pl accounts"""
 
-		dimension_fields = ["t1.cost_center", "t1.finance_book"]
+		qb_dimension_fields = ["cost_center", "finance_book", "project"]
 
 		self.accounting_dimensions = get_accounting_dimensions()
 		for dimension in self.accounting_dimensions:
-			dimension_fields.append("t1.{0}".format(dimension))
+			qb_dimension_fields.append(dimension)
 
 		if group_by_account:
-			dimension_fields.append("t1.account")
+			qb_dimension_fields.append("account")
 
-		return frappe.db.sql(
-			"""
-			select
-				t2.account_currency,
-				{dimension_fields},
-				sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
-				sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
-			from `tabGL Entry` t1, `tabAccount` t2
-			where
-				t1.is_cancelled = 0
-				and t1.account = t2.name
-				and t2.report_type = 'Profit and Loss'
-				and t2.docstatus < 2
-				and t2.company = %s
-				and t1.posting_date between %s and %s
-			group by {dimension_fields}
-		""".format(
-				dimension_fields=", ".join(dimension_fields)
-			),
-			(self.company, self.get("year_start_date"), self.posting_date),
-			as_dict=1,
+		account_filters = {
+			"company": self.company,
+			"is_group": 0,
+		}
+
+		if report_type:
+			account_filters.update({"report_type": report_type})
+
+		accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
+
+		gl_entry = frappe.qb.DocType("GL Entry")
+		query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
+
+		if not for_aggregation:
+			query = query.select(
+				(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
+					"bal_in_account_currency"
+				),
+				(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
+			)
+		else:
+			query = query.select(
+				(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
+				(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
+				(Sum(gl_entry.debit)).as_("debit"),
+				(Sum(gl_entry.credit)).as_("credit"),
+			)
+
+		for dimension in qb_dimension_fields:
+			query = query.select(gl_entry[dimension])
+
+		query = query.where(
+			(gl_entry.company == self.company)
+			& (gl_entry.is_cancelled == 0)
+			& (gl_entry.account.isin(accounts))
 		)
 
+		if get_opening_entries:
+			query = query.where(
+				gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
+				| gl_entry.is_opening
+				== "Yes"
+			)
+		else:
+			query = query.where(
+				gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
+				& gl_entry.is_opening
+				== "No"
+			)
 
-def process_gl_entries(gl_entries):
+		if for_aggregation:
+			query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
+
+		for dimension in qb_dimension_fields:
+			query = query.groupby(gl_entry[dimension])
+
+		return query.run(as_dict=1)
+
+
+def process_gl_entries(gl_entries, closing_entries, voucher_name=None):
+	from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
+		make_closing_entries,
+	)
 	from erpnext.accounts.general_ledger import make_gl_entries
 
 	try:
 		make_gl_entries(gl_entries, merge_entries=False)
+		make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
 		frappe.db.set_value(
 			"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
 		)
diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
index e9ed2e4..62ae857 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
@@ -16,16 +16,17 @@
 class TestPeriodClosingVoucher(unittest.TestCase):
 	def test_closing_entry(self):
 		frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+		frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
 
 		company = create_company()
 		cost_center = create_cost_center("Test Cost Center 1")
 
 		jv1 = make_journal_entry(
+			posting_date="2021-03-15",
 			amount=400,
 			account1="Cash - TPC",
 			account2="Sales - TPC",
 			cost_center=cost_center,
-			posting_date=now(),
 			save=False,
 		)
 		jv1.company = company
@@ -33,18 +34,18 @@
 		jv1.submit()
 
 		jv2 = make_journal_entry(
+			posting_date="2021-03-15",
 			amount=600,
 			account1="Cost of Goods Sold - TPC",
 			account2="Cash - TPC",
 			cost_center=cost_center,
-			posting_date=now(),
 			save=False,
 		)
 		jv2.company = company
 		jv2.save()
 		jv2.submit()
 
-		pcv = self.make_period_closing_voucher()
+		pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
 		surplus_account = pcv.closing_account_head
 
 		expected_gle = (
@@ -65,6 +66,7 @@
 
 	def test_cost_center_wise_posting(self):
 		frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+		frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
 
 		company = create_company()
 		surplus_account = create_account()
@@ -81,6 +83,7 @@
 			debit_to="Debtors - TPC",
 			currency="USD",
 			customer="_Test Customer USD",
+			posting_date="2021-03-15",
 		)
 		create_sales_invoice(
 			company=company,
@@ -91,9 +94,10 @@
 			debit_to="Debtors - TPC",
 			currency="USD",
 			customer="_Test Customer USD",
+			posting_date="2021-03-15",
 		)
 
-		pcv = self.make_period_closing_voucher(submit=False)
+		pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
 		pcv.save()
 		pcv.submit()
 		surplus_account = pcv.closing_account_head
@@ -128,12 +132,13 @@
 
 	def test_period_closing_with_finance_book_entries(self):
 		frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+		frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
 
 		company = create_company()
 		surplus_account = create_account()
 		cost_center = create_cost_center("Test Cost Center 1")
 
-		si = create_sales_invoice(
+		create_sales_invoice(
 			company=company,
 			income_account="Sales - TPC",
 			expense_account="Cost of Goods Sold - TPC",
@@ -142,6 +147,7 @@
 			debit_to="Debtors - TPC",
 			currency="USD",
 			customer="_Test Customer USD",
+			posting_date="2021-03-15",
 		)
 
 		jv = make_journal_entry(
@@ -149,14 +155,14 @@
 			account2="Sales - TPC",
 			amount=400,
 			cost_center=cost_center,
-			posting_date=now(),
+			posting_date="2021-03-15",
 		)
 		jv.company = company
 		jv.finance_book = create_finance_book().name
 		jv.save()
 		jv.submit()
 
-		pcv = self.make_period_closing_voucher()
+		pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
 		surplus_account = pcv.closing_account_head
 
 		expected_gle = (
@@ -177,14 +183,130 @@
 
 		self.assertSequenceEqual(pcv_gle, expected_gle)
 
-	def make_period_closing_voucher(self, submit=True):
+	def test_gl_entries_restrictions(self):
+		frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+		frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
+
+		company = create_company()
+		cost_center = create_cost_center("Test Cost Center 1")
+
+		self.make_period_closing_voucher(posting_date="2021-03-31")
+
+		jv1 = make_journal_entry(
+			posting_date="2021-03-15",
+			amount=400,
+			account1="Cash - TPC",
+			account2="Sales - TPC",
+			cost_center=cost_center,
+			save=False,
+		)
+		jv1.company = company
+		jv1.save()
+
+		self.assertRaises(frappe.ValidationError, jv1.submit)
+
+	def test_closing_balance_with_dimensions(self):
+		frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+		frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
+		frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
+
+		company = create_company()
+		cost_center1 = create_cost_center("Test Cost Center 1")
+		cost_center2 = create_cost_center("Test Cost Center 2")
+
+		jv1 = make_journal_entry(
+			posting_date="2021-03-15",
+			amount=400,
+			account1="Cash - TPC",
+			account2="Sales - TPC",
+			cost_center=cost_center1,
+			save=False,
+		)
+		jv1.company = company
+		jv1.save()
+		jv1.submit()
+
+		jv2 = make_journal_entry(
+			posting_date="2021-03-15",
+			amount=200,
+			account1="Cash - TPC",
+			account2="Sales - TPC",
+			cost_center=cost_center2,
+			save=False,
+		)
+		jv2.company = company
+		jv2.save()
+		jv2.submit()
+
+		pcv1 = self.make_period_closing_voucher(posting_date="2021-03-31")
+
+		closing_balance = frappe.db.get_value(
+			"Account Closing Balance",
+			{
+				"account": "Sales - TPC",
+				"cost_center": cost_center1,
+				"period_closing_voucher": pcv1.name,
+				"is_period_closing_voucher_entry": 0,
+			},
+			["credit", "credit_in_account_currency"],
+			as_dict=1,
+		)
+
+		self.assertEqual(closing_balance.credit, 400)
+		self.assertEqual(closing_balance.credit_in_account_currency, 400)
+
+		jv3 = make_journal_entry(
+			posting_date="2022-03-15",
+			amount=300,
+			account1="Cash - TPC",
+			account2="Sales - TPC",
+			cost_center=cost_center2,
+			save=False,
+		)
+
+		jv3.company = company
+		jv3.save()
+		jv3.submit()
+
+		pcv2 = self.make_period_closing_voucher(posting_date="2022-03-31")
+
+		cc1_closing_balance = frappe.db.get_value(
+			"Account Closing Balance",
+			{
+				"account": "Sales - TPC",
+				"cost_center": cost_center1,
+				"period_closing_voucher": pcv2.name,
+				"is_period_closing_voucher_entry": 0,
+			},
+			["credit", "credit_in_account_currency"],
+			as_dict=1,
+		)
+
+		cc2_closing_balance = frappe.db.get_value(
+			"Account Closing Balance",
+			{
+				"account": "Sales - TPC",
+				"cost_center": cost_center2,
+				"period_closing_voucher": pcv2.name,
+				"is_period_closing_voucher_entry": 0,
+			},
+			["credit", "credit_in_account_currency"],
+			as_dict=1,
+		)
+
+		self.assertEqual(cc1_closing_balance.credit, 400)
+		self.assertEqual(cc1_closing_balance.credit_in_account_currency, 400)
+		self.assertEqual(cc2_closing_balance.credit, 500)
+		self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
+
+	def make_period_closing_voucher(self, posting_date=None, submit=True):
 		surplus_account = create_account()
 		cost_center = create_cost_center("Test Cost Center 1")
 		pcv = frappe.get_doc(
 			{
 				"doctype": "Period Closing Voucher",
-				"transaction_date": today(),
-				"posting_date": today(),
+				"transaction_date": posting_date or today(),
+				"posting_date": posting_date or today(),
 				"company": "Test PCV Company",
 				"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
 				"cost_center": cost_center,
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 41fdb6a..9fff6f4 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -300,6 +300,9 @@
 
 	if gl_map:
 		check_freezing_date(gl_map[0]["posting_date"], adv_adj)
+		is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
+		if gl_map[0]["voucher_type"] != "Period Closing Voucher":
+			validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
 
 	for entry in gl_map:
 		make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -519,6 +522,9 @@
 		)
 		validate_accounting_period(gl_entries)
 		check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
+
+		is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
+		validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
 		set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
 
 		for entry in gl_entries:
@@ -566,6 +572,28 @@
 				)
 
 
+def validate_against_pcv(is_opening, posting_date, company):
+	if is_opening and frappe.db.exists(
+		"Period Closing Voucher", {"docstatus": 1, "company": company}
+	):
+		frappe.throw(
+			_("Opening Entry can not be created after Period Closing Voucher is created."),
+			title=_("Invalid Opening Entry"),
+		)
+
+	last_pcv_date = frappe.db.get_value(
+		"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
+	)
+
+	if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
+		message = _("Books have been closed till the period ending on {0}").format(
+			formatdate(last_pcv_date)
+		)
+		message += "</br >"
+		message += _("You cannot create any new accounting entries till this date.")
+		frappe.throw(message, title=_("Period Closed"))
+
+
 def set_as_cancel(voucher_type, voucher_no):
 	"""
 	Set is_cancelled=1 in all original gl entries for the voucher
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py
index 07552e3..b225aac 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.py
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py
@@ -25,6 +25,8 @@
 		company=filters.company,
 	)
 
+	filters.period_start_date = period_list[0]["year_start_date"]
+
 	currency = filters.presentation_currency or frappe.get_cached_value(
 		"Company", filters.company, "default_currency"
 	)
@@ -96,7 +98,7 @@
 	chart = get_chart_data(filters, columns, asset, liability, equity)
 
 	report_summary = get_report_summary(
-		period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, filters
+		period_list, asset, liability, equity, provisional_profit_loss, currency, filters
 	)
 
 	return columns, data, message, chart, report_summary
@@ -174,7 +176,6 @@
 	liability,
 	equity,
 	provisional_profit_loss,
-	total_credit,
 	currency,
 	filters,
 	consolidated=False,
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 8c6fe43..debe655 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -418,46 +418,47 @@
 	ignore_closing_entries=False,
 ):
 	"""Returns a dict like { "account": [gl entries], ... }"""
+	gl_entries = []
 
-	additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
-
-	accounts = frappe.db.sql_list(
-		"""select name from `tabAccount`
-		where lft >= %s and rgt <= %s and company = %s""",
-		(root_lft, root_rgt, company),
+	accounts_list = frappe.db.get_all(
+		"Account",
+		filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
+		pluck="name",
 	)
 
-	if accounts:
-		additional_conditions += " and account in ({})".format(
-			", ".join(frappe.db.escape(d) for d in accounts)
-		)
+	ignore_opening_entries = False
+	if accounts_list:
+		# For balance sheet
+		if not from_date:
+			from_date = filters["period_start_date"]
+			last_period_closing_voucher = frappe.db.get_all(
+				"Period Closing Voucher",
+				filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)},
+				fields=["posting_date", "name"],
+				order_by="posting_date desc",
+				limit=1,
+			)
+			if last_period_closing_voucher:
+				gl_entries += get_accounting_entries(
+					"Account Closing Balance",
+					from_date,
+					to_date,
+					accounts_list,
+					filters,
+					ignore_closing_entries,
+					last_period_closing_voucher[0].name,
+				)
+				from_date = add_days(last_period_closing_voucher[0].posting_date, 1)
+				ignore_opening_entries = True
 
-		gl_filters = {
-			"company": company,
-			"from_date": from_date,
-			"to_date": to_date,
-			"finance_book": cstr(filters.get("finance_book")),
-		}
-
-		if filters.get("include_default_book_entries"):
-			gl_filters["company_fb"] = frappe.get_cached_value("Company", company, "default_finance_book")
-
-		for key, value in filters.items():
-			if value:
-				gl_filters.update({key: value})
-
-		gl_entries = frappe.db.sql(
-			"""
-			select posting_date, account, debit, credit, is_opening, fiscal_year,
-				debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
-			where company=%(company)s
-			{additional_conditions}
-			and posting_date <= %(to_date)s
-			and is_cancelled = 0""".format(
-				additional_conditions=additional_conditions
-			),
-			gl_filters,
-			as_dict=True,
+		gl_entries += get_accounting_entries(
+			"GL Entry",
+			from_date,
+			to_date,
+			accounts_list,
+			filters,
+			ignore_closing_entries,
+			ignore_opening_entries=ignore_opening_entries,
 		)
 
 		if filters and filters.get("presentation_currency"):
@@ -469,34 +470,82 @@
 		return gl_entries_by_account
 
 
-def get_additional_conditions(from_date, ignore_closing_entries, filters):
-	additional_conditions = []
+def get_accounting_entries(
+	doctype,
+	from_date,
+	to_date,
+	accounts,
+	filters,
+	ignore_closing_entries,
+	period_closing_voucher=None,
+	ignore_opening_entries=False,
+):
+	gl_entry = frappe.qb.DocType(doctype)
+	query = (
+		frappe.qb.from_(gl_entry)
+		.select(
+			gl_entry.account,
+			gl_entry.debit,
+			gl_entry.credit,
+			gl_entry.debit_in_account_currency,
+			gl_entry.credit_in_account_currency,
+			gl_entry.account_currency,
+		)
+		.where(gl_entry.company == filters.company)
+	)
 
+	if doctype == "GL Entry":
+		query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
+		query = query.where(gl_entry.is_cancelled == 0)
+		query = query.where(gl_entry.posting_date <= to_date)
+
+		if ignore_opening_entries:
+			query = query.where(gl_entry.is_opening == "No")
+	else:
+		query = query.select(gl_entry.closing_date.as_("posting_date"))
+		query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
+
+	query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
+	query = query.where(gl_entry.account.isin(accounts))
+
+	entries = query.run(as_dict=True)
+
+	return entries
+
+
+def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters):
+	gl_entry = frappe.qb.DocType(doctype)
 	accounting_dimensions = get_accounting_dimensions(as_list=False)
 
 	if ignore_closing_entries:
-		additional_conditions.append("ifnull(voucher_type, '')!='Period Closing Voucher'")
+		if doctype == "GL Entry":
+			query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
+		else:
+			query = query.where(gl_entry.is_period_closing_voucher_entry == 0)
 
-	if from_date:
-		additional_conditions.append("posting_date >= %(from_date)s")
+	if from_date and doctype == "GL Entry":
+		query = query.where(gl_entry.posting_date >= from_date)
 
 	if filters:
 		if filters.get("project"):
 			if not isinstance(filters.get("project"), list):
 				filters.project = frappe.parse_json(filters.get("project"))
 
-			additional_conditions.append("project in %(project)s")
+			query = query.where(gl_entry.project.isin(filters.project))
 
 		if filters.get("cost_center"):
 			filters.cost_center = get_cost_centers_with_children(filters.cost_center)
-			additional_conditions.append("cost_center in %(cost_center)s")
+			query = query.where(gl_entry.cost_center.isin(filters.cost_center))
 
 		if filters.get("include_default_book_entries"):
-			additional_conditions.append(
-				"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
+			query = query.where(
+				(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
+				| (gl_entry.finance_book.isnull())
 			)
 		else:
-			additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
+			query = query.where(
+				(gl_entry.finance_book.isin([cstr(filters.company_fb), ""])) | (gl_entry.finance_book.isnull())
+			)
 
 	if accounting_dimensions:
 		for dimension in accounting_dimensions:
@@ -505,11 +554,10 @@
 					filters[dimension.fieldname] = get_dimension_with_children(
 						dimension.document_type, filters.get(dimension.fieldname)
 					)
-					additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
-				else:
-					additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
 
-	return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
+				query = query.where(gl_entry[dimension.fieldname].isin(filters[dimension.fieldname]))
+
+	return query
 
 
 def get_cost_centers_with_children(cost_centers):
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index bc334c7..acbf7a8 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -4,7 +4,8 @@
 
 import frappe
 from frappe import _
-from frappe.utils import cstr, flt, formatdate, getdate
+from frappe.query_builder.functions import Sum
+from frappe.utils import add_days, cstr, flt, formatdate, getdate
 
 import erpnext
 from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -137,45 +138,110 @@
 
 
 def get_rootwise_opening_balances(filters, report_type):
-	additional_conditions = ""
-	if not filters.show_unclosed_fy_pl_balances:
-		additional_conditions = (
-			" and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else ""
-		)
+	gle = []
 
-	if not flt(filters.with_period_closing_entry):
-		additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'"
-
-	if filters.cost_center:
-		lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
-		additional_conditions += """ and cost_center in (select name from `tabCost Center`
-			where lft >= %s and rgt <= %s)""" % (
-			lft,
-			rgt,
-		)
-
-	if filters.project:
-		additional_conditions += " and project = %(project)s"
-
-	if filters.get("include_default_book_entries"):
-		additional_conditions += (
-			" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
-		)
-	else:
-		additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
+	last_period_closing_voucher = frappe.db.get_all(
+		"Period Closing Voucher",
+		filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
+		fields=["posting_date", "name"],
+		order_by="posting_date desc",
+		limit=1,
+	)
 
 	accounting_dimensions = get_accounting_dimensions(as_list=False)
 
-	query_filters = {
-		"company": filters.company,
-		"from_date": filters.from_date,
-		"to_date": filters.to_date,
-		"report_type": report_type,
-		"year_start_date": filters.year_start_date,
-		"project": filters.project,
-		"finance_book": filters.finance_book,
-		"company_fb": frappe.get_cached_value("Company", filters.company, "default_finance_book"),
-	}
+	if last_period_closing_voucher:
+		gle = get_opening_balance(
+			"Account Closing Balance",
+			filters,
+			report_type,
+			accounting_dimensions,
+			period_closing_voucher=last_period_closing_voucher[0].name,
+		)
+		if getdate(last_period_closing_voucher[0].posting_date) < getdate(
+			add_days(filters.from_date, -1)
+		):
+			filters.from_date = add_days(last_period_closing_voucher, 1)
+			gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
+	else:
+		gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
+
+	opening = frappe._dict()
+	for d in gle:
+		opening.setdefault(d.account, d)
+
+	return opening
+
+
+def get_opening_balance(
+	doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None
+):
+	closing_balance = frappe.qb.DocType(doctype)
+	account = frappe.qb.DocType("Account")
+
+	opening_balance = (
+		frappe.qb.from_(closing_balance)
+		.select(
+			closing_balance.account,
+			Sum(closing_balance.debit).as_("opening_debit"),
+			Sum(closing_balance.credit).as_("opening_credit"),
+		)
+		.where(
+			(closing_balance.company == filters.company)
+			& (
+				closing_balance.account.isin(
+					frappe.qb.from_(account).select("name").where(account.report_type == report_type)
+				)
+			)
+		)
+		.groupby(closing_balance.account)
+	)
+
+	if doctype == "Account Closing Balance":
+		if period_closing_voucher:
+			opening_balance = opening_balance.where(
+				closing_balance.period_closing_voucher == period_closing_voucher
+			)
+		else:
+			opening_balance = opening_balance.where(closing_balance.closing_date < filters.from_date)
+	else:
+		opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
+
+	if not filters.show_unclosed_fy_pl_balances and report_type == "Profit and Loss":
+		opening_balance = opening_balance.where(closing_balance.closing_date >= filters.year_start_date)
+
+	if not flt(filters.with_period_closing_entry):
+		if doctype == "Account Closing Balance":
+			opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
+		else:
+			opening_balance = opening_balance.where(
+				closing_balance.voucher_type != "Period Closing Voucher"
+			)
+
+	if filters.cost_center:
+		lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
+		cost_center = frappe.qb.DocType("Cost Center")
+		opening_balance = opening_balance.where(
+			closing_balance.cost_center.in_(
+				frappe.qb.from_(cost_center)
+				.select("name")
+				.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
+			)
+		)
+
+	if filters.project:
+		opening_balance = opening_balance.where(closing_balance.project == filters.project)
+
+	if filters.get("include_default_book_entries"):
+		opening_balance = opening_balance.where(
+			(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
+			| (closing_balance.finance_book.isnull())
+		)
+	else:
+		opening_balance = opening_balance.where(
+			(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
+			| (closing_balance.finance_book.isnull())
+		)
 
 	if accounting_dimensions:
 		for dimension in accounting_dimensions:
@@ -184,35 +250,17 @@
 					filters[dimension.fieldname] = get_dimension_with_children(
 						dimension.document_type, filters.get(dimension.fieldname)
 					)
-					additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
+					opening_balance = opening_balance.where(
+						closing_balance[dimension.fieldname].isin(filters[dimension.fieldname])
+					)
 				else:
-					additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
+					opening_balance = opening_balance.where(
+						closing_balance[dimension.fieldname].isin(filters[dimension.fieldname])
+					)
 
-				query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})
+	gle = opening_balance.run(as_dict=1)
 
-	gle = frappe.db.sql(
-		"""
-		select
-			account, sum(debit) as opening_debit, sum(credit) as opening_credit
-		from `tabGL Entry`
-		where
-			company=%(company)s
-			{additional_conditions}
-			and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
-			and account in (select name from `tabAccount` where report_type=%(report_type)s)
-			and is_cancelled = 0
-		group by account""".format(
-			additional_conditions=additional_conditions
-		),
-		query_filters,
-		as_dict=True,
-	)
-
-	opening = frappe._dict()
-	for d in gle:
-		opening.setdefault(d.account, d)
-
-	return opening
+	return gle
 
 
 def calculate_values(accounts, gl_entries_by_account, opening_balances):
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 005a2f1..92906c1 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -51,13 +51,25 @@
 
 @frappe.whitelist()
 def get_fiscal_year(
-	date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
+	date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
 ):
-	return get_fiscal_years(date, fiscal_year, label, verbose, company, as_dict=as_dict)[0]
+	fiscal_years = get_fiscal_years(
+		date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
+	)
+	if boolean:
+		return fiscal_years
+	else:
+		return fiscal_years[0]
 
 
 def get_fiscal_years(
-	transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
+	transaction_date=None,
+	fiscal_year=None,
+	label="Date",
+	verbose=1,
+	company=None,
+	as_dict=False,
+	boolean=False,
 ):
 	fiscal_years = frappe.cache().hget("fiscal_years", company) or []
 
@@ -121,8 +133,12 @@
 	if company:
 		error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
 
+	if boolean:
+		return False
+
 	if verbose == 1:
 		frappe.msgprint(error_msg)
+
 	raise FiscalYearError(error_msg)
 
 
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index dbfbcc9..e8822a7 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -510,6 +510,7 @@
 	"Subcontracting Order Item",
 	"Subcontracting Receipt",
 	"Subcontracting Receipt Item",
+	"Accounts Closing Balance",
 ]
 
 # get matching queries for Bank Reconciliation
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 9e2cecd..0ef51a9 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -325,6 +325,8 @@
 erpnext.patches.v13_0.update_docs_link
 erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
 erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
+erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
+erpnext.patches.v14_0.update_closing_balances
 # below migration patches should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
 execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_for_closing_balance.py b/erpnext/patches/v14_0/create_accounting_dimensions_for_closing_balance.py
new file mode 100644
index 0000000..43ad0d7
--- /dev/null
+++ b/erpnext/patches/v14_0/create_accounting_dimensions_for_closing_balance.py
@@ -0,0 +1,31 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
+
+
+def execute():
+	accounting_dimensions = frappe.db.get_all(
+		"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
+	)
+
+	if not accounting_dimensions:
+		return
+
+	doctype = "Account Closing Balance"
+
+	for d in accounting_dimensions:
+		field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
+
+		if field:
+			continue
+
+		df = {
+			"fieldname": d.fieldname,
+			"label": d.label,
+			"fieldtype": "Link",
+			"options": d.document_type,
+			"insert_after": "accounting_dimensions_section",
+		}
+
+		create_custom_field(doctype, df, ignore_validate=True)
+
+	frappe.clear_cache(doctype=doctype)
diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py
new file mode 100644
index 0000000..40a1851
--- /dev/null
+++ b/erpnext/patches/v14_0/update_closing_balances.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+
+import frappe
+
+from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
+	make_closing_entries,
+)
+from erpnext.accounts.utils import get_fiscal_year
+
+
+def execute():
+	company_wise_order = {}
+	get_opening_entries = True
+	for pcv in frappe.db.get_all(
+		"Period Closing Voucher",
+		fields=["company", "posting_date", "name"],
+		filters={"docstatus": 1},
+		order_by="posting_date",
+	):
+
+		company_wise_order.setdefault(pcv.company, [])
+		if pcv.posting_date not in company_wise_order[pcv.company]:
+			pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name)
+			pcv_doc.year_start_date = get_fiscal_year(
+				pcv.posting_date, pcv.fiscal_year, company=pcv.company
+			)[1]
+			gl_entries = pcv_doc.get_gl_entries()
+			closing_entries = pcv_doc.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
+			make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name)
+			company_wise_order[pcv.company].append(pcv.posting_date)
+			get_opening_entries = False