feat: utility to repost accounting ledgers without cancellation (#36469)

* feat: introduce doctypes for repost

* refactor: basic filters and validations

* chore: basic validations

* chore: added barebones function to generate ledger entries

* chore: repost on submit

* chore: repost in background

* chore: include payment entry and journal entry

* chore: ignore repost doc on cancel

* chore: preview method

* chore: rudimentary form of preview

* refactor: preview template

* refactor: basic background colors to differentiate old and new

* chore: remove commented code

* test: basic functionality

* chore: fix conflict

* chore: prevent repost on invoices with deferred accounting

* refactor(test): rename and test basic validations and methods

* refactor(test): test all validations

* fix(test): use proper name account name

* refactor(test): fix failing test case

* refactor(test): clear old entries

* refactor(test): simpler logic to clear old records

* refactor(test): make use of deletion flag

* refactor(test): split into multiple test cases
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 8d8cbef..35a3788 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@
 frappe.ui.form.on("Journal Entry", {
 	setup: function(frm) {
 		frm.add_fetch("bank_account", "account", "account");
-		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule'];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"];
 	},
 
 	refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 1e1b3ba..22e092c 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -96,6 +96,8 @@
 			"Payment Ledger Entry",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 		)
 		self.make_gl_entries(1)
 		self.update_advance_paid()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 33f2634..f131be2 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -9,7 +9,7 @@
 
 frappe.ui.form.on('Payment Entry', {
 	onload: function(frm) {
-		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"];
+		frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
 
 		if(frm.doc.__islocal) {
 			if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index cc42f9f..64b4d16 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -147,6 +147,8 @@
 			"Payment Ledger Entry",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 		)
 		super(PaymentEntry, self).on_cancel()
 		self.make_gl_entries(cancel=1)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 89d6207..66438a7 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -35,7 +35,7 @@
 		super.onload();
 
 		// Ignore linked advances
-		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
+		this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"];
 
 		if(!this.frm.doc.__islocal) {
 			// show credit_to in print format
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 7329cec..f334399 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1415,6 +1415,8 @@
 			"Repost Item Valuation",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 			"Payment Ledger Entry",
 			"Tax Withheld Vouchers",
 			"Serial and Batch Bundle",
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
new file mode 100644
index 0000000..2dec8f7
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html
@@ -0,0 +1,44 @@
+<style>
+	.print-format {
+		padding: 4mm;
+		font-size: 8.0pt !important;
+	}
+	.print-format td {
+		vertical-align:middle !important;
+	}
+	.old {
+	    background-color: #FFB3C0;
+	}
+	.new {
+	    background-color: #B3FFCC;
+	}
+</style>
+
+
+<table class="table table-bordered table-condensed">
+  <colgroup>
+  {% for col in gl_columns%}
+  <col style="width: 18mm;">
+  {% endfor %}
+  </colgroup>
+  <thead>
+    <tr>
+    {% for col in gl_columns%}
+    <td>{{ col.label }}</td>
+    {% endfor %}
+    </tr>
+  </thead>
+{% for gl in gl_data%}
+{% if gl["old"]%}
+<tr class="old">
+{% else %}
+<tr class="new">
+{% endif %}
+  {% for col in gl_columns %}
+  <td class="text-right">
+    {{ gl[col.fieldname] }}
+  </td>
+  {% endfor %}
+</tr>
+{% endfor %}
+</table>
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
new file mode 100644
index 0000000..3a87a38
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js
@@ -0,0 +1,50 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Repost Accounting Ledger", {
+	setup: function(frm) {
+		frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) {
+			return {
+				filters: {
+					name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']],
+				}
+			}
+		}
+
+		frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) {
+			if (doc.company) {
+				return {
+					filters: {
+						company: doc.company,
+						docstatus: 1
+					}
+				}
+			}
+		}
+	},
+
+	refresh: function(frm) {
+		frm.add_custom_button(__('Show Preview'), () => {
+			frm.call({
+				method: 'generate_preview',
+				doc: frm.doc,
+				freeze: true,
+				freeze_message: __('Generating Preview'),
+				callback: function(r) {
+					if (r && r.message) {
+						let content = r.message;
+						let opts = {
+							title: "Preview",
+							subtitle: "preview",
+							content: content,
+							print_settings: {orientation: "landscape"},
+							columns: [],
+							data: [],
+						}
+						frappe.render_grid(opts);
+					}
+				}
+			});
+		});
+	}
+});
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
new file mode 100644
index 0000000..8d56c9b
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
@@ -0,0 +1,81 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:ACC-REPOST-{#####}",
+ "creation": "2023-07-04 13:07:32.923675",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "company",
+  "column_break_vpup",
+  "delete_cancelled_entries",
+  "section_break_metl",
+  "vouchers",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Repost Accounting Ledger",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "vouchers",
+   "fieldtype": "Table",
+   "label": "Vouchers",
+   "options": "Repost Accounting Ledger Items"
+  },
+  {
+   "fieldname": "column_break_vpup",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_metl",
+   "fieldtype": "Section Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "delete_cancelled_entries",
+   "fieldtype": "Check",
+   "label": "Delete Cancelled Ledger Entries"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-07-27 15:47:58.975034",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
new file mode 100644
index 0000000..4cf2ed2
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.utils.data import comma_and
+
+
+class RepostAccountingLedger(Document):
+	def __init__(self, *args, **kwargs):
+		super(RepostAccountingLedger, self).__init__(*args, **kwargs)
+		self._allowed_types = set(
+			["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"]
+		)
+
+	def validate(self):
+		self.validate_vouchers()
+		self.validate_for_closed_fiscal_year()
+		self.validate_for_deferred_accounting()
+
+	def validate_for_deferred_accounting(self):
+		sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
+		docs_with_deferred_revenue = frappe.db.get_all(
+			"Sales Invoice Item",
+			filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
+			fields=["parent"],
+			as_list=1,
+		)
+
+		purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
+		docs_with_deferred_expense = frappe.db.get_all(
+			"Purchase Invoice Item",
+			filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
+			fields=["parent"],
+			as_list=1,
+		)
+
+		if docs_with_deferred_revenue or docs_with_deferred_expense:
+			frappe.throw(
+				_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
+					frappe.bold(
+						comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
+					)
+				)
+			)
+
+	def validate_for_closed_fiscal_year(self):
+		if self.vouchers:
+			latest_pcv = (
+				frappe.db.get_all(
+					"Period Closing Voucher",
+					filters={"company": self.company},
+					order_by="posting_date desc",
+					pluck="posting_date",
+					limit=1,
+				)
+				or None
+			)
+			if not latest_pcv:
+				return
+
+			for vtype in self._allowed_types:
+				if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]:
+					latest_voucher = frappe.db.get_all(
+						vtype,
+						filters={"name": ["in", names]},
+						pluck="posting_date",
+						order_by="posting_date desc",
+						limit=1,
+					)[0]
+					if latest_voucher and latest_pcv[0] >= latest_voucher:
+						frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year."))
+
+	def validate_vouchers(self):
+		if self.vouchers:
+			# Validate voucher types
+			voucher_types = set([x.voucher_type for x in self.vouchers])
+			if disallowed_types := voucher_types.difference(self._allowed_types):
+				frappe.throw(
+					_("{0} types are not allowed. Only {1} are.").format(
+						frappe.bold(comma_and(list(disallowed_types))),
+						frappe.bold(comma_and(list(self._allowed_types))),
+					)
+				)
+
+	def get_existing_ledger_entries(self):
+		vouchers = [x.voucher_no for x in self.vouchers]
+		gl = qb.DocType("GL Entry")
+		existing_gles = (
+			qb.from_(gl)
+			.select(gl.star)
+			.where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0))
+			.run(as_dict=True)
+		)
+		self.gles = frappe._dict({})
+
+		for gle in existing_gles:
+			self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault(
+				"existing", []
+			).append(gle.update({"old": True}))
+
+	def generate_preview_data(self):
+		self.gl_entries = []
+		self.get_existing_ledger_entries()
+		for x in self.vouchers:
+			doc = frappe.get_doc(x.voucher_type, x.voucher_no)
+			if doc.doctype in ["Payment Entry", "Journal Entry"]:
+				gle_map = doc.build_gl_map()
+			else:
+				gle_map = doc.get_gl_entries()
+
+			old_entries = self.gles.get((x.voucher_type, x.voucher_no))
+			if old_entries:
+				self.gl_entries.extend(old_entries.existing)
+			self.gl_entries.extend(gle_map)
+
+	@frappe.whitelist()
+	def generate_preview(self):
+		from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
+
+		gl_columns = []
+		gl_data = []
+
+		self.generate_preview_data()
+		if self.gl_entries:
+			filters = {"company": self.company, "include_dimensions": 1}
+			for x in get_gl_columns(filters):
+				if x["fieldname"] == "gl_entry":
+					x["fieldname"] = "name"
+				gl_columns.append(x)
+
+			gl_data = self.gl_entries
+		rendered_page = frappe.render_template(
+			"erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html",
+			{"gl_columns": gl_columns, "gl_data": gl_data},
+		)
+
+		return rendered_page
+
+	def on_submit(self):
+		job_name = "repost_accounting_ledger_" + self.name
+		frappe.enqueue(
+			method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
+			account_repost_doc=self.name,
+			is_async=True,
+			job_name=job_name,
+		)
+		frappe.msgprint(_("Repost has started in the background"))
+
+
+@frappe.whitelist()
+def start_repost(account_repost_doc=str) -> None:
+	if account_repost_doc:
+		repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
+
+		if repost_doc.docstatus == 1:
+			# Prevent repost on invoices with deferred accounting
+			repost_doc.validate_for_deferred_accounting()
+
+			for x in repost_doc.vouchers:
+				doc = frappe.get_doc(x.voucher_type, x.voucher_no)
+
+				if repost_doc.delete_cancelled_entries:
+					frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name})
+					frappe.db.delete(
+						"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
+					)
+
+				if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
+					if not repost_doc.delete_cancelled_entries:
+						doc.docstatus = 2
+						doc.make_gl_entries_on_cancel()
+
+					doc.docstatus = 1
+					doc.make_gl_entries()
+
+				elif doc.doctype in ["Payment Entry", "Journal Entry"]:
+					if not repost_doc.delete_cancelled_entries:
+						doc.make_gl_entries(1)
+					doc.make_gl_entries()
+
+				frappe.db.commit()
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
new file mode 100644
index 0000000..0e75dd2
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
@@ -0,0 +1,202 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe import qb
+from frappe.query_builder.functions import Sum
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, nowdate, today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
+from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+from erpnext.accounts.utils import get_fiscal_year
+
+
+class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
+	def setUp(self):
+		self.create_company()
+		self.create_customer()
+		self.create_item()
+
+	def teadDown(self):
+		frappe.db.rollback()
+
+	def test_01_basic_functions(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+		)
+
+		preq = frappe.get_doc(
+			make_payment_request(
+				dt=si.doctype,
+				dn=si.name,
+				payment_request_type="Inward",
+				party_type="Customer",
+				party=si.customer,
+			)
+		)
+		preq.save().submit()
+
+		# Test Validation Error
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.delete_cancelled_entries = True
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		ral.append(
+			"vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name}
+		)  # this should throw validation error
+		self.assertRaises(frappe.ValidationError, ral.save)
+		ral.vouchers.pop()
+		preq.cancel()
+		preq.delete()
+
+		pe = get_payment_entry(si.doctype, si.name)
+		pe.save().submit()
+		ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+		ral.save()
+
+		# manually set an incorrect debit amount in DB
+		gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
+		frappe.db.set_value("GL Entry", gle[0], "debit", 90)
+
+		gl = qb.DocType("GL Entry")
+		res = (
+			qb.from_(gl)
+			.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
+			.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
+			.run()
+		)
+
+		# Assert incorrect ledger balance
+		self.assertNotEqual(res[0], (si.name, 100, 100))
+
+		# Submit repost document
+		ral.save().submit()
+
+		# background jobs don't run on test cases. Manually triggering repost function.
+		start_repost(ral.name)
+
+		res = (
+			qb.from_(gl)
+			.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
+			.where((gl.voucher_no == si.name) & (gl.is_cancelled == 0))
+			.run()
+		)
+
+		# Ledger should reflect correct amount post repost
+		self.assertEqual(res[0], (si.name, 100, 100))
+
+	def test_02_deferred_accounting_valiations(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+			do_not_submit=True,
+		)
+		si.items[0].enable_deferred_revenue = True
+		si.items[0].deferred_revenue_account = self.deferred_revenue
+		si.items[0].service_start_date = nowdate()
+		si.items[0].service_end_date = add_days(nowdate(), 90)
+		si.save().submit()
+
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		self.assertRaises(frappe.ValidationError, ral.save)
+
+	@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
+	def test_04_pcv_validation(self):
+		# Clear old GL entries so PCV can be submitted.
+		gl = frappe.qb.DocType("GL Entry")
+		qb.from_(gl).delete().where(gl.company == self.company).run()
+
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+		)
+		pcv = frappe.get_doc(
+			{
+				"doctype": "Period Closing Voucher",
+				"transaction_date": today(),
+				"posting_date": today(),
+				"company": self.company,
+				"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
+				"cost_center": self.cost_center,
+				"closing_account_head": self.retained_earnings,
+				"remarks": "test",
+			}
+		)
+		pcv.save().submit()
+
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		self.assertRaises(frappe.ValidationError, ral.save)
+
+		pcv.reload()
+		pcv.cancel()
+		pcv.delete()
+
+	def test_03_deletion_flag_and_preview_function(self):
+		si = create_sales_invoice(
+			item=self.item,
+			company=self.company,
+			customer=self.customer,
+			debit_to=self.debit_to,
+			parent_cost_center=self.cost_center,
+			cost_center=self.cost_center,
+			rate=100,
+		)
+
+		pe = get_payment_entry(si.doctype, si.name)
+		pe.save().submit()
+
+		# without deletion flag set
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.delete_cancelled_entries = False
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+		ral.save()
+
+		# assert preview data is generated
+		preview = ral.generate_preview()
+		self.assertIsNotNone(preview)
+
+		ral.save().submit()
+
+		# background jobs don't run on test cases. Manually triggering repost function.
+		start_repost(ral.name)
+
+		self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
+		self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
+
+		# with deletion flag set
+		ral = frappe.new_doc("Repost Accounting Ledger")
+		ral.company = self.company
+		ral.delete_cancelled_entries = True
+		ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
+		ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
+		ral.save().submit()
+
+		start_repost(ral.name)
+		self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
+		self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
new file mode 100644
index 0000000..4a2041f
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-07-04 14:14:01.243848",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "voucher_type",
+  "voucher_no"
+ ],
+ "fields": [
+  {
+   "fieldname": "voucher_type",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Voucher Type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "voucher_no",
+   "fieldtype": "Dynamic Link",
+   "in_list_view": 1,
+   "label": "Voucher No",
+   "options": "voucher_type"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-07-04 14:15:51.165584",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Repost Accounting Ledger Items",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
new file mode 100644
index 0000000..9221f44
--- /dev/null
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RepostAccountingLedgerItems(Document):
+	pass
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index b45bc41..a4bcdb4 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -37,7 +37,7 @@
 		super.onload();
 
 		this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
-							  'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
+							  'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
 
 		if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
 			// show debit_to in print format
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index e331c0b..db12074 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -386,6 +386,8 @@
 			"Repost Item Valuation",
 			"Repost Payment Ledger",
 			"Repost Payment Ledger Items",
+			"Repost Accounting Ledger",
+			"Repost Accounting Ledger Items",
 			"Payment Ledger Entry",
 			"Serial and Batch Bundle",
 		)
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index c82164e..70bbf7e 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -4,7 +4,7 @@
 
 
 class AccountsTestMixin:
-	def create_customer(self, customer_name, currency=None):
+	def create_customer(self, customer_name="_Test Customer", currency=None):
 		if not frappe.db.exists("Customer", customer_name):
 			customer = frappe.new_doc("Customer")
 			customer.customer_name = customer_name
@@ -17,7 +17,7 @@
 		else:
 			self.customer = customer_name
 
-	def create_supplier(self, supplier_name, currency=None):
+	def create_supplier(self, supplier_name="_Test Supplier", currency=None):
 		if not frappe.db.exists("Supplier", supplier_name):
 			supplier = frappe.new_doc("Supplier")
 			supplier.supplier_name = supplier_name
@@ -31,7 +31,7 @@
 		else:
 			self.supplier = supplier_name
 
-	def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
+	def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None):
 		item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
 		self.item = item.name
 
@@ -62,19 +62,44 @@
 		self.debit_usd = "Debtors USD - " + abbr
 		self.cash = "Cash - " + abbr
 		self.creditors = "Creditors - " + abbr
+		self.retained_earnings = "Retained Earnings - " + abbr
 
-		# create bank account
-		bank_account = "HDFC - " + abbr
-		if frappe.db.exists("Account", bank_account):
-			self.bank = bank_account
-		else:
-			bank_acc = frappe.get_doc(
+		# Deferred revenue, expense and bank accounts
+		other_accounts = [
+			frappe._dict(
 				{
-					"doctype": "Account",
+					"attribute_name": "deferred_revenue",
+					"account_name": "Deferred Revenue",
+					"parent_account": "Current Liabilities - " + abbr,
+				}
+			),
+			frappe._dict(
+				{
+					"attribute_name": "deferred_expense",
+					"account_name": "Deferred Expense",
+					"parent_account": "Current Assets - " + abbr,
+				}
+			),
+			frappe._dict(
+				{
+					"attribute_name": "bank",
 					"account_name": "HDFC",
 					"parent_account": "Bank Accounts - " + abbr,
-					"company": self.company,
 				}
-			)
-			bank_acc.save()
-			self.bank = bank_acc.name
+			),
+		]
+		for acc in other_accounts:
+			acc_name = acc.account_name + " - " + abbr
+			if frappe.db.exists("Account", acc_name):
+				setattr(self, acc.attribute_name, acc_name)
+			else:
+				new_acc = frappe.get_doc(
+					{
+						"doctype": "Account",
+						"account_name": acc.account_name,
+						"parent_account": acc.parent_account,
+						"company": self.company,
+					}
+				)
+				new_acc.save()
+				setattr(self, acc.attribute_name, new_acc.name)