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 00000000000..e69de29bb2d 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 00000000000..fe4b0852ac1 --- /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 00000000000..bc5b5c41fdd --- /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 9b996fe8ab6..0d704762db7 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -306,7 +306,10 @@ period_closing_doctypes = [ 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 688d45a5a77..f48402e175b 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -181,8 +181,10 @@ def get_random_date(start_date, start_range, end_range): 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 39170053838..9b1a41a0b77 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -168,7 +168,7 @@ frappe.ui.form.on("Company", { 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 876b6a4ac80..3ca14e65fdd 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -12,7 +12,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact 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 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad 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 844e7865e3e..432438bcfee 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 @@ class TestTransactionDeletionRecord(FrappeTestCase): 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_company(company_name): 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 527c753d6a9..9aa02784165 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 @@ frappe.ui.form.on("Transaction Deletion Record", { 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 23e59472a6d..e03e1695e0e 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 88c4b078977..00fad5f0fa6 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 @@ class TransactionDeletionRecord(Document): 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 @@ class TransactionDeletionRecord(Document): 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:
" + 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 @@ class TransactionDeletionRecord(Document): 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}) - - def delete_child_tables(self, doctype, company_fieldname): - parent_docs_to_be_deleted = frappe.get_all( - doctype, {company_fieldname: self.company}, pluck="name" - ) + 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, 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 @@ class TransactionDeletionRecord(Document): 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 @@ def get_doctypes_to_be_ignored(): 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 08a35df2c17..285cb6dd221 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 be0be945c4e..89db63694c2 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 f154cdb2474..906660739e4 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 @@ class TransactionDeletionRecordItem(Document): from frappe.types import DF doctype_name: DF.Link - no_of_docs: DF.Data | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data