Merge pull request #39717 from ruthra-kumar/redesign_transaction_deletion_record

refactor: Transaction Deletion record for large volumes
diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py b/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py
diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json
new file mode 100644
index 0000000..fe4b085
--- /dev/null
+++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json
@@ -0,0 +1,58 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2024-02-04 10:53:32.307930",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "doctype_name",
+  "docfield_name",
+  "no_of_docs",
+  "done"
+ ],
+ "fields": [
+  {
+   "fieldname": "doctype_name",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "DocType",
+   "options": "DocType",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "docfield_name",
+   "fieldtype": "Data",
+   "label": "DocField",
+   "read_only": 1
+  },
+  {
+   "fieldname": "no_of_docs",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "No of Docs",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "done",
+   "fieldtype": "Check",
+   "in_list_view": 1,
+   "label": "Done",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2024-02-05 17:35:09.556054",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Transaction Deletion Record Details",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py
new file mode 100644
index 0000000..bc5b5c4
--- /dev/null
+++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TransactionDeletionRecordDetails(Document):
+	# begin: auto-generated types
+	# This code is auto-generated. Do not modify anything in this block.
+
+	from typing import TYPE_CHECKING
+
+	if TYPE_CHECKING:
+		from frappe.types import DF
+
+		docfield_name: DF.Data | None
+		doctype_name: DF.Link
+		done: DF.Check
+		no_of_docs: DF.Int
+		parent: DF.Data
+		parentfield: DF.Data
+		parenttype: DF.Data
+	# end: auto-generated types
+
+	pass
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9b996fe..0d70476 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -306,7 +306,10 @@
 
 doc_events = {
 	"*": {
-		"validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
+		"validate": [
+			"erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
+			"erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job",
+		],
 	},
 	tuple(period_closing_doctypes): {
 		"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py
index 688d45a..f48402e 100644
--- a/erpnext/setup/demo.py
+++ b/erpnext/setup/demo.py
@@ -181,8 +181,10 @@
 def create_transaction_deletion_record(company):
 	transaction_deletion_record = frappe.new_doc("Transaction Deletion Record")
 	transaction_deletion_record.company = company
+	transaction_deletion_record.process_in_single_transaction = True
 	transaction_deletion_record.save(ignore_permissions=True)
 	transaction_deletion_record.submit()
+	transaction_deletion_record.start_deletion_tasks()
 
 
 def clear_masters():
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 3917005..9b1a41a 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -168,7 +168,7 @@
 
 	delete_company_transactions: function (frm) {
 		frappe.call({
-			method: "erpnext.setup.doctype.company.company.is_deletion_job_running",
+			method: "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.is_deletion_doc_running",
 			args: {
 				company: frm.doc.name,
 			},
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 876b6a4..3ca14e6 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -12,7 +12,6 @@
 from frappe.custom.doctype.property_setter.property_setter import make_property_setter
 from frappe.desk.page.setup_wizard.setup_wizard import make_records
 from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today
-from frappe.utils.background_jobs import get_job, is_job_enqueued
 from frappe.utils.nestedset import NestedSet, rebuild_tree
 
 from erpnext.accounts.doctype.account.account import get_account_currency
@@ -901,37 +900,21 @@
 		return None
 
 
-def generate_id_for_deletion_job(company):
-	return "delete_company_transactions_" + company
-
-
-@frappe.whitelist()
-def is_deletion_job_running(company):
-	job_id = generate_id_for_deletion_job(company)
-	if is_job_enqueued(job_id):
-		job_name = get_job(job_id).get_id()  # job name will have site prefix
-		frappe.throw(
-			_("A Transaction Deletion Job: {0} is already running for {1}").format(
-				frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company)
-			)
-		)
-
-
 @frappe.whitelist()
 def create_transaction_deletion_request(company):
-	is_deletion_job_running(company)
-	job_id = generate_id_for_deletion_job(company)
+	from erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record import (
+		is_deletion_doc_running,
+	)
+
+	is_deletion_doc_running(company)
 
 	tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
-	tdr.insert()
+	tdr.submit()
+	tdr.start_deletion_tasks()
 
-	frappe.enqueue(
-		"frappe.utils.background_jobs.run_doc_method",
-		doctype=tdr.doctype,
-		name=tdr.name,
-		doc_method="submit",
-		job_id=job_id,
-		queue="long",
-		enqueue_after_commit=True,
+	frappe.msgprint(
+		_("A Transaction Deletion Document: {0} is triggered for {0}").format(
+			get_link_to_form("Transaction Deletion Record", tdr.name)
+		),
+		frappe.bold(company),
 	)
-	frappe.msgprint(_("A Transaction Deletion Job is triggered for {0}").format(frappe.bold(company)))
diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
index 844e786..432438b 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
@@ -29,6 +29,7 @@
 		for i in range(5):
 			create_task("Dunder Mifflin Paper Co")
 		tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
+		tdr.reload()
 		for doctype in tdr.doctypes:
 			if doctype.doctype_name == "Task":
 				self.assertEqual(doctype.no_of_docs, 5)
@@ -60,7 +61,9 @@
 def create_transaction_deletion_doc(company):
 	tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
 	tdr.insert()
+	tdr.process_in_single_transaction = True
 	tdr.submit()
+	tdr.start_deletion_tasks()
 	return tdr
 
 
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js
index 527c753..9aa0278 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js
@@ -10,20 +10,24 @@
 				callback: function (r) {
 					doctypes_to_be_ignored_array = r.message;
 					populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm);
-					frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
 					frm.refresh_field("doctypes_to_be_ignored");
 				},
 			});
 		}
-
-		frm.get_field("doctypes_to_be_ignored").grid.cannot_add_rows = true;
-		frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
-		frm.refresh_field("doctypes_to_be_ignored");
 	},
 
 	refresh: function (frm) {
-		frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
-		frm.refresh_field("doctypes_to_be_ignored");
+		if (frm.doc.docstatus == 1 && ["Queued", "Failed"].find((x) => x == frm.doc.status)) {
+			let execute_btn = frm.doc.status == "Queued" ? __("Start Deletion") : __("Retry");
+
+			frm.add_custom_button(execute_btn, () => {
+				// Entry point for chain of events
+				frm.call({
+					method: "start_deletion_tasks",
+					doc: frm.doc,
+				});
+			});
+		}
 	},
 });
 
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json
index 23e5947..e03e169 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json
@@ -7,10 +7,21 @@
  "engine": "InnoDB",
  "field_order": [
   "company",
+  "section_break_qpwb",
+  "status",
+  "error_log",
+  "tasks_section",
+  "delete_bin_data",
+  "delete_leads_and_addresses",
+  "reset_company_default_values",
+  "clear_notifications",
+  "initialize_doctypes_table",
+  "delete_transactions",
+  "section_break_tbej",
   "doctypes",
   "doctypes_to_be_ignored",
   "amended_from",
-  "status"
+  "process_in_single_transaction"
  ],
  "fields": [
   {
@@ -25,14 +36,16 @@
    "fieldname": "doctypes",
    "fieldtype": "Table",
    "label": "Summary",
-   "options": "Transaction Deletion Record Item",
+   "no_copy": 1,
+   "options": "Transaction Deletion Record Details",
    "read_only": 1
   },
   {
    "fieldname": "doctypes_to_be_ignored",
    "fieldtype": "Table",
    "label": "Excluded DocTypes",
-   "options": "Transaction Deletion Record Item"
+   "options": "Transaction Deletion Record Item",
+   "read_only": 1
   },
   {
    "fieldname": "amended_from",
@@ -46,18 +59,93 @@
   {
    "fieldname": "status",
    "fieldtype": "Select",
-   "hidden": 1,
    "label": "Status",
-   "options": "Draft\nCompleted"
+   "no_copy": 1,
+   "options": "Queued\nRunning\nFailed\nCompleted\nCancelled",
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_tbej",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "tasks_section",
+   "fieldtype": "Section Break",
+   "label": "Tasks"
+  },
+  {
+   "default": "0",
+   "fieldname": "delete_bin_data",
+   "fieldtype": "Check",
+   "label": "Delete Bins",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "delete_leads_and_addresses",
+   "fieldtype": "Check",
+   "label": "Delete Leads and Addresses",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "clear_notifications",
+   "fieldtype": "Check",
+   "label": "Clear Notifications",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "reset_company_default_values",
+   "fieldtype": "Check",
+   "label": "Reset Company Default Values",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "delete_transactions",
+   "fieldtype": "Check",
+   "label": "Delete Transactions",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "initialize_doctypes_table",
+   "fieldtype": "Check",
+   "label": "Initialize Summary Table",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval: doc.error_log",
+   "fieldname": "error_log",
+   "fieldtype": "Long Text",
+   "label": "Error Log"
+  },
+  {
+   "fieldname": "section_break_qpwb",
+   "fieldtype": "Section Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "process_in_single_transaction",
+   "fieldtype": "Check",
+   "label": "Process in Single Transaction"
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-08-04 20:15:59.071493",
+ "modified": "2024-03-20 14:58:15.086360",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Transaction Deletion Record",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -76,5 +164,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index 88c4b07..00fad5f 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -1,12 +1,14 @@
 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
+from collections import OrderedDict
 
 import frappe
 from frappe import _, qb
 from frappe.desk.notifications import clear_notifications
 from frappe.model.document import Document
-from frappe.utils import cint, create_batch
+from frappe.utils import cint, comma_and, create_batch, get_link_to_form
+from frappe.utils.background_jobs import get_job, is_job_enqueued
 
 
 class TransactionDeletionRecord(Document):
@@ -18,20 +20,42 @@
 	if TYPE_CHECKING:
 		from frappe.types import DF
 
+		from erpnext.accounts.doctype.transaction_deletion_record_details.transaction_deletion_record_details import (
+			TransactionDeletionRecordDetails,
+		)
 		from erpnext.setup.doctype.transaction_deletion_record_item.transaction_deletion_record_item import (
 			TransactionDeletionRecordItem,
 		)
 
 		amended_from: DF.Link | None
+		clear_notifications: DF.Check
 		company: DF.Link
-		doctypes: DF.Table[TransactionDeletionRecordItem]
+		delete_bin_data: DF.Check
+		delete_leads_and_addresses: DF.Check
+		delete_transactions: DF.Check
+		doctypes: DF.Table[TransactionDeletionRecordDetails]
 		doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem]
-		status: DF.Literal["Draft", "Completed"]
+		error_log: DF.LongText | None
+		initialize_doctypes_table: DF.Check
+		process_in_single_transaction: DF.Check
+		reset_company_default_values: DF.Check
+		status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"]
 	# end: auto-generated types
 
 	def __init__(self, *args, **kwargs):
 		super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
 		self.batch_size = 5000
+		# Tasks are listed by their execution order
+		self.task_to_internal_method_map = OrderedDict(
+			{
+				"Delete Bins": "delete_bins",
+				"Delete Leads and Addresses": "delete_lead_addresses",
+				"Reset Company Values": "reset_company_values",
+				"Clear Notifications": "delete_notifications",
+				"Initialize Summary Table": "initialize_doctypes_to_be_deleted_table",
+				"Delete Transactions": "delete_company_transactions",
+			}
+		)
 
 	def validate(self):
 		frappe.only_for("System Manager")
@@ -48,104 +72,266 @@
 					title=_("Not Allowed"),
 				)
 
+	def generate_job_name_for_task(self, task=None):
+		method = self.task_to_internal_method_map[task]
+		return f"{self.name}_{method}"
+
+	def generate_job_name_for_next_tasks(self, task=None):
+		job_names = []
+		current_task_idx = list(self.task_to_internal_method_map).index(task)
+		for idx, task in enumerate(self.task_to_internal_method_map.keys(), 0):
+			# generate job_name for next tasks
+			if idx > current_task_idx:
+				job_names.append(self.generate_job_name_for_task(task))
+		return job_names
+
+	def generate_job_name_for_all_tasks(self):
+		job_names = []
+		for task in self.task_to_internal_method_map.keys():
+			job_names.append(self.generate_job_name_for_task(task))
+		return job_names
+
 	def before_submit(self):
+		if queued_docs := frappe.db.get_all(
+			"Transaction Deletion Record",
+			filters={"company": self.company, "status": ("in", ["Running", "Queued"]), "docstatus": 1},
+			pluck="name",
+		):
+			frappe.throw(
+				_(
+					"Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1}"
+				).format(
+					comma_and([get_link_to_form("Transaction Deletion Record", x) for x in queued_docs]),
+					frappe.bold(self.company),
+				)
+			)
+
 		if not self.doctypes_to_be_ignored:
 			self.populate_doctypes_to_be_ignored_table()
 
-		self.delete_bins()
-		self.delete_lead_addresses()
-		self.reset_company_values()
-		clear_notifications()
-		self.delete_company_transactions()
+	def reset_task_flags(self):
+		self.clear_notifications = 0
+		self.delete_bin_data = 0
+		self.delete_leads_and_addresses = 0
+		self.delete_transactions = 0
+		self.initialize_doctypes_table = 0
+		self.reset_company_default_values = 0
+
+	def before_save(self):
+		self.status = ""
+		self.doctypes.clear()
+		self.reset_task_flags()
+
+	def on_submit(self):
+		self.db_set("status", "Queued")
+
+	def on_cancel(self):
+		self.db_set("status", "Cancelled")
+
+	def enqueue_task(self, task: str | None = None):
+		if task and task in self.task_to_internal_method_map:
+			# make sure that none of next tasks are already running
+			job_names = self.generate_job_name_for_next_tasks(task=task)
+			self.validate_running_task_for_doc(job_names=job_names)
+
+			# Generate Job Id to uniquely identify each task for this document
+			job_id = self.generate_job_name_for_task(task)
+
+			if self.process_in_single_transaction:
+				self.execute_task(task_to_execute=task)
+			else:
+				frappe.enqueue(
+					"frappe.utils.background_jobs.run_doc_method",
+					doctype=self.doctype,
+					name=self.name,
+					doc_method="execute_task",
+					job_id=job_id,
+					queue="long",
+					enqueue_after_commit=True,
+					task_to_execute=task,
+				)
+
+	def execute_task(self, task_to_execute: str | None = None):
+		if task_to_execute:
+			method = self.task_to_internal_method_map[task_to_execute]
+			if task := getattr(self, method, None):
+				try:
+					task()
+				except Exception as err:
+					frappe.db.rollback()
+					traceback = frappe.get_traceback(with_context=True)
+					if traceback:
+						message = "Traceback: <br>" + traceback
+						frappe.db.set_value(self.doctype, self.name, "error_log", message)
+					frappe.db.set_value(self.doctype, self.name, "status", "Failed")
+
+	def delete_notifications(self):
+		self.validate_doc_status()
+		if not self.clear_notifications:
+			clear_notifications()
+			self.db_set("clear_notifications", 1)
+		self.enqueue_task(task="Initialize Summary Table")
 
 	def populate_doctypes_to_be_ignored_table(self):
 		doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
 		for doctype in doctypes_to_be_ignored_list:
 			self.append("doctypes_to_be_ignored", {"doctype_name": doctype})
 
-	def delete_bins(self):
-		frappe.db.sql(
-			"""delete from `tabBin` where warehouse in
-				(select name from tabWarehouse where company=%s)""",
-			self.company,
-		)
+	def validate_running_task_for_doc(self, job_names: list = None):
+		# at most only one task should be runnning
+		running_tasks = []
+		for x in job_names:
+			if is_job_enqueued(x):
+				running_tasks.append(get_job(x).get_id())
 
-	def delete_lead_addresses(self):
-		"""Delete addresses to which leads are linked"""
-		leads = frappe.get_all("Lead", filters={"company": self.company})
-		leads = ["'%s'" % row.get("name") for row in leads]
-		addresses = []
-		if leads:
-			addresses = frappe.db.sql_list(
-				"""select parent from `tabDynamic Link` where link_name
-				in ({leads})""".format(
-					leads=",".join(leads)
+		if running_tasks:
+			frappe.throw(
+				_("{0} is already running for {1}").format(
+					comma_and([get_link_to_form("RQ Job", x) for x in running_tasks]), self.name
 				)
 			)
 
-			if addresses:
-				addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
-
-				frappe.db.sql(
-					"""delete from `tabAddress` where name in ({addresses}) and
-					name not in (select distinct dl1.parent from `tabDynamic Link` dl1
-					inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
-					and dl1.link_doctype<>dl2.link_doctype)""".format(
-						addresses=",".join(addresses)
-					)
+	def validate_doc_status(self):
+		if self.status != "Running":
+			frappe.throw(
+				_("{0} is not running. Cannot trigger events for this Document").format(
+					get_link_to_form("Transaction Deletion Record", self.name)
 				)
+			)
 
-				frappe.db.sql(
-					"""delete from `tabDynamic Link` where link_doctype='Lead'
-					and parenttype='Address' and link_name in ({leads})""".format(
+	@frappe.whitelist()
+	def start_deletion_tasks(self):
+		# This method is the entry point for the chain of events that follow
+		self.db_set("status", "Running")
+		self.enqueue_task(task="Delete Bins")
+
+	def delete_bins(self):
+		self.validate_doc_status()
+		if not self.delete_bin_data:
+			frappe.db.sql(
+				"""delete from `tabBin` where warehouse in
+					(select name from tabWarehouse where company=%s)""",
+				self.company,
+			)
+			self.db_set("delete_bin_data", 1)
+		self.enqueue_task(task="Delete Leads and Addresses")
+
+	def delete_lead_addresses(self):
+		"""Delete addresses to which leads are linked"""
+		self.validate_doc_status()
+		if not self.delete_leads_and_addresses:
+			leads = frappe.get_all("Lead", filters={"company": self.company})
+			leads = ["'%s'" % row.get("name") for row in leads]
+			addresses = []
+			if leads:
+				addresses = frappe.db.sql_list(
+					"""select parent from `tabDynamic Link` where link_name
+					in ({leads})""".format(
 						leads=",".join(leads)
 					)
 				)
 
-			frappe.db.sql(
-				"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
-					leads=",".join(leads)
+				if addresses:
+					addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
+
+					frappe.db.sql(
+						"""delete from `tabAddress` where name in ({addresses}) and
+						name not in (select distinct dl1.parent from `tabDynamic Link` dl1
+						inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
+						and dl1.link_doctype<>dl2.link_doctype)""".format(
+							addresses=",".join(addresses)
+						)
+					)
+
+					frappe.db.sql(
+						"""delete from `tabDynamic Link` where link_doctype='Lead'
+						and parenttype='Address' and link_name in ({leads})""".format(
+							leads=",".join(leads)
+						)
+					)
+
+				frappe.db.sql(
+					"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
+						leads=",".join(leads)
+					)
 				)
-			)
+			self.db_set("delete_leads_and_addresses", 1)
+		self.enqueue_task(task="Reset Company Values")
 
 	def reset_company_values(self):
-		company_obj = frappe.get_doc("Company", self.company)
-		company_obj.total_monthly_sales = 0
-		company_obj.sales_monthly_history = None
-		company_obj.save()
+		self.validate_doc_status()
+		if not self.reset_company_default_values:
+			company_obj = frappe.get_doc("Company", self.company)
+			company_obj.total_monthly_sales = 0
+			company_obj.sales_monthly_history = None
+			company_obj.save()
+			self.db_set("reset_company_default_values", 1)
+		self.enqueue_task(task="Clear Notifications")
+
+	def initialize_doctypes_to_be_deleted_table(self):
+		self.validate_doc_status()
+		if not self.initialize_doctypes_table:
+			doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
+			docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
+			tables = self.get_all_child_doctypes()
+			for docfield in docfields:
+				if docfield["parent"] != self.doctype:
+					no_of_docs = self.get_number_of_docs_linked_with_specified_company(
+						docfield["parent"], docfield["fieldname"]
+					)
+					if no_of_docs > 0:
+						# Initialize
+						self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0)
+			self.db_set("initialize_doctypes_table", 1)
+		self.enqueue_task(task="Delete Transactions")
 
 	def delete_company_transactions(self):
-		doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
-		docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
+		self.validate_doc_status()
+		if not self.delete_transactions:
+			doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
+			docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
 
-		tables = self.get_all_child_doctypes()
-		for docfield in docfields:
-			if docfield["parent"] != self.doctype:
-				no_of_docs = self.get_number_of_docs_linked_with_specified_company(
-					docfield["parent"], docfield["fieldname"]
-				)
-
-				if no_of_docs > 0:
-					self.delete_version_log(docfield["parent"], docfield["fieldname"])
-
-					reference_docs = frappe.get_all(
-						docfield["parent"], filters={docfield["fieldname"]: self.company}
+			tables = self.get_all_child_doctypes()
+			for docfield in self.doctypes:
+				if docfield.doctype_name != self.doctype and not docfield.done:
+					no_of_docs = self.get_number_of_docs_linked_with_specified_company(
+						docfield.doctype_name, docfield.docfield_name
 					)
-					reference_doc_names = [r.name for r in reference_docs]
+					if no_of_docs > 0:
+						reference_docs = frappe.get_all(
+							docfield.doctype_name, filters={docfield.docfield_name: self.company}, limit=self.batch_size
+						)
+						reference_doc_names = [r.name for r in reference_docs]
 
-					self.delete_communications(docfield["parent"], reference_doc_names)
-					self.delete_comments(docfield["parent"], reference_doc_names)
-					self.unlink_attachments(docfield["parent"], reference_doc_names)
+						self.delete_version_log(docfield.doctype_name, reference_doc_names)
+						self.delete_communications(docfield.doctype_name, reference_doc_names)
+						self.delete_comments(docfield.doctype_name, reference_doc_names)
+						self.unlink_attachments(docfield.doctype_name, reference_doc_names)
+						self.delete_child_tables(docfield.doctype_name, reference_doc_names)
+						self.delete_docs_linked_with_specified_company(docfield.doctype_name, reference_doc_names)
+						processed = int(docfield.no_of_docs) + len(reference_doc_names)
+						frappe.db.set_value(docfield.doctype, docfield.name, "no_of_docs", processed)
+					else:
+						# reset naming series
+						naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname")
+						if naming_series:
+							if "#" in naming_series:
+								self.update_naming_series(naming_series, docfield.doctype_name)
+						frappe.db.set_value(docfield.doctype, docfield.name, "done", 1)
 
-					self.populate_doctypes_table(tables, docfield["parent"], no_of_docs)
-
-					self.delete_child_tables(docfield["parent"], docfield["fieldname"])
-					self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"])
-
-					naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname")
-					if naming_series:
-						if "#" in naming_series:
-							self.update_naming_series(naming_series, docfield["parent"])
+			pending_doctypes = frappe.db.get_all(
+				"Transaction Deletion Record Details",
+				filters={"parent": self.name, "done": 0},
+				pluck="doctype_name",
+			)
+			if pending_doctypes:
+				# as method is enqueued after commit, calling itself will not make validate_doc_status to throw
+				# recursively call this task to delete all transactions
+				self.enqueue_task(task="Delete Transactions")
+			else:
+				self.db_set("status", "Completed")
+				self.db_set("delete_transactions", 1)
+				self.db_set("error_log", None)
 
 	def get_doctypes_to_be_ignored_list(self):
 		singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
@@ -174,25 +360,24 @@
 	def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
 		return frappe.db.count(doctype, {company_fieldname: self.company})
 
-	def populate_doctypes_table(self, tables, doctype, no_of_docs):
+	def populate_doctypes_table(self, tables, doctype, fieldname, no_of_docs):
+		self.flags.ignore_validate_update_after_submit = True
 		if doctype not in tables:
-			self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs})
+			self.append(
+				"doctypes", {"doctype_name": doctype, "docfield_name": fieldname, "no_of_docs": no_of_docs}
+			)
+		self.save(ignore_permissions=True)
 
-	def delete_child_tables(self, doctype, company_fieldname):
-		parent_docs_to_be_deleted = frappe.get_all(
-			doctype, {company_fieldname: self.company}, pluck="name"
-		)
-
+	def delete_child_tables(self, doctype, reference_doc_names):
 		child_tables = frappe.get_all(
 			"DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options"
 		)
 
-		for batch in create_batch(parent_docs_to_be_deleted, self.batch_size):
-			for table in child_tables:
-				frappe.db.delete(table, {"parent": ["in", batch]})
+		for table in child_tables:
+			frappe.db.delete(table, {"parent": ["in", reference_doc_names]})
 
-	def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
-		frappe.db.delete(doctype, {company_fieldname: self.company})
+	def delete_docs_linked_with_specified_company(self, doctype, reference_doc_names):
+		frappe.db.delete(doctype, {"name": ("in", reference_doc_names)})
 
 	def update_naming_series(self, naming_series, doctype_name):
 		if "." in naming_series:
@@ -213,17 +398,11 @@
 
 		frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
 
-	def delete_version_log(self, doctype, company_fieldname):
-		dt = qb.DocType(doctype)
-		names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1)
-		names = [x[0] for x in names]
-
-		if names:
-			versions = qb.DocType("Version")
-			for batch in create_batch(names, self.batch_size):
-				qb.from_(versions).delete().where(
-					(versions.ref_doctype == doctype) & (versions.docname.isin(batch))
-				).run()
+	def delete_version_log(self, doctype, docnames):
+		versions = qb.DocType("Version")
+		qb.from_(versions).delete().where(
+			(versions.ref_doctype == doctype) & (versions.docname.isin(docnames))
+		).run()
 
 	def delete_communications(self, doctype, reference_doc_names):
 		communications = frappe.get_all(
@@ -295,3 +474,34 @@
 	doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
 
 	return doctypes_to_be_ignored
+
+
+@frappe.whitelist()
+def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
+	if company:
+		if running_deletion_jobs := frappe.db.get_all(
+			"Transaction Deletion Record",
+			filters={"docstatus": 1, "company": company, "status": "Running"},
+		):
+			if not err_msg:
+				err_msg = ""
+			frappe.throw(
+				title=_("Deletion in Progress!"),
+				msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format(
+					get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg
+				),
+			)
+
+
+def check_for_running_deletion_job(doc, method=None):
+	# Check if DocType has 'company' field
+	df = qb.DocType("DocField")
+	if (
+		not_allowed := qb.from_(df)
+		.select(df.parent)
+		.where((df.fieldname == "company") & (df.parent == doc.doctype))
+		.run()
+	):
+		is_deletion_doc_running(
+			doc.company, _("Cannot make any transactions until the deletion job is completed")
+		)
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js
index 08a35df..285cb6d 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js
@@ -2,11 +2,15 @@
 // License: GNU General Public License v3. See license.txt
 
 frappe.listview_settings["Transaction Deletion Record"] = {
+	add_fields: ["status"],
 	get_indicator: function (doc) {
-		if (doc.docstatus == 0) {
-			return [__("Draft"), "red"];
-		} else {
-			return [__("Completed"), "green"];
-		}
+		let colors = {
+			Queued: "orange",
+			Completed: "green",
+			Running: "blue",
+			Failed: "red",
+		};
+		let status = doc.status;
+		return [__(status), colors[status], "status,=," + status];
 	},
 };
diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json
index be0be94..89db636 100644
--- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json
+++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json
@@ -5,8 +5,7 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "doctype_name",
-  "no_of_docs"
+  "doctype_name"
  ],
  "fields": [
   {
@@ -16,18 +15,12 @@
    "label": "DocType",
    "options": "DocType",
    "reqd": 1
-  },
-  {
-   "fieldname": "no_of_docs",
-   "fieldtype": "Data",
-   "in_list_view": 1,
-   "label": "Number of Docs"
   }
  ],
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-05-08 23:10:46.166744",
+ "modified": "2024-02-04 10:56:27.413691",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Transaction Deletion Record Item",
@@ -35,5 +28,6 @@
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py
index f154cdb..9066607 100644
--- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py
+++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py
@@ -16,7 +16,6 @@
 		from frappe.types import DF
 
 		doctype_name: DF.Link
-		no_of_docs: DF.Data | None
 		parent: DF.Data
 		parentfield: DF.Data
 		parenttype: DF.Data