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