Merge pull request #39717 from ruthra-kumar/redesign_transaction_deletion_record
refactor: Transaction Deletion record for large volumes
This commit is contained in:
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -306,7 +306,10 @@ period_closing_doctypes = [
|
|||||||
|
|
||||||
doc_events = {
|
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): {
|
tuple(period_closing_doctypes): {
|
||||||
"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
|
"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
|
||||||
|
|||||||
@@ -181,8 +181,10 @@ def get_random_date(start_date, start_range, end_range):
|
|||||||
def create_transaction_deletion_record(company):
|
def create_transaction_deletion_record(company):
|
||||||
transaction_deletion_record = frappe.new_doc("Transaction Deletion Record")
|
transaction_deletion_record = frappe.new_doc("Transaction Deletion Record")
|
||||||
transaction_deletion_record.company = company
|
transaction_deletion_record.company = company
|
||||||
|
transaction_deletion_record.process_in_single_transaction = True
|
||||||
transaction_deletion_record.save(ignore_permissions=True)
|
transaction_deletion_record.save(ignore_permissions=True)
|
||||||
transaction_deletion_record.submit()
|
transaction_deletion_record.submit()
|
||||||
|
transaction_deletion_record.start_deletion_tasks()
|
||||||
|
|
||||||
|
|
||||||
def clear_masters():
|
def clear_masters():
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ frappe.ui.form.on("Company", {
|
|||||||
|
|
||||||
delete_company_transactions: function (frm) {
|
delete_company_transactions: function (frm) {
|
||||||
frappe.call({
|
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: {
|
args: {
|
||||||
company: frm.doc.name,
|
company: frm.doc.name,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import make_records
|
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 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 frappe.utils.nestedset import NestedSet, rebuild_tree
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.account import get_account_currency
|
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
|
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()
|
@frappe.whitelist()
|
||||||
def create_transaction_deletion_request(company):
|
def create_transaction_deletion_request(company):
|
||||||
is_deletion_job_running(company)
|
from erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record import (
|
||||||
job_id = generate_id_for_deletion_job(company)
|
is_deletion_doc_running,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_deletion_doc_running(company)
|
||||||
|
|
||||||
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
||||||
tdr.insert()
|
tdr.submit()
|
||||||
|
tdr.start_deletion_tasks()
|
||||||
|
|
||||||
frappe.enqueue(
|
frappe.msgprint(
|
||||||
"frappe.utils.background_jobs.run_doc_method",
|
_("A Transaction Deletion Document: {0} is triggered for {0}").format(
|
||||||
doctype=tdr.doctype,
|
get_link_to_form("Transaction Deletion Record", tdr.name)
|
||||||
name=tdr.name,
|
),
|
||||||
doc_method="submit",
|
frappe.bold(company),
|
||||||
job_id=job_id,
|
|
||||||
queue="long",
|
|
||||||
enqueue_after_commit=True,
|
|
||||||
)
|
)
|
||||||
frappe.msgprint(_("A Transaction Deletion Job is triggered for {0}").format(frappe.bold(company)))
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class TestTransactionDeletionRecord(FrappeTestCase):
|
|||||||
for i in range(5):
|
for i in range(5):
|
||||||
create_task("Dunder Mifflin Paper Co")
|
create_task("Dunder Mifflin Paper Co")
|
||||||
tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||||
|
tdr.reload()
|
||||||
for doctype in tdr.doctypes:
|
for doctype in tdr.doctypes:
|
||||||
if doctype.doctype_name == "Task":
|
if doctype.doctype_name == "Task":
|
||||||
self.assertEqual(doctype.no_of_docs, 5)
|
self.assertEqual(doctype.no_of_docs, 5)
|
||||||
@@ -60,7 +61,9 @@ def create_company(company_name):
|
|||||||
def create_transaction_deletion_doc(company):
|
def create_transaction_deletion_doc(company):
|
||||||
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
||||||
tdr.insert()
|
tdr.insert()
|
||||||
|
tdr.process_in_single_transaction = True
|
||||||
tdr.submit()
|
tdr.submit()
|
||||||
|
tdr.start_deletion_tasks()
|
||||||
return tdr
|
return tdr
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,24 @@ frappe.ui.form.on("Transaction Deletion Record", {
|
|||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
doctypes_to_be_ignored_array = r.message;
|
doctypes_to_be_ignored_array = r.message;
|
||||||
populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm);
|
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.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) {
|
refresh: function (frm) {
|
||||||
frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false);
|
if (frm.doc.docstatus == 1 && ["Queued", "Failed"].find((x) => x == frm.doc.status)) {
|
||||||
frm.refresh_field("doctypes_to_be_ignored");
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,21 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"company",
|
"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",
|
||||||
"doctypes_to_be_ignored",
|
"doctypes_to_be_ignored",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"status"
|
"process_in_single_transaction"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -25,14 +36,16 @@
|
|||||||
"fieldname": "doctypes",
|
"fieldname": "doctypes",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Summary",
|
"label": "Summary",
|
||||||
"options": "Transaction Deletion Record Item",
|
"no_copy": 1,
|
||||||
|
"options": "Transaction Deletion Record Details",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "doctypes_to_be_ignored",
|
"fieldname": "doctypes_to_be_ignored",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Excluded DocTypes",
|
"label": "Excluded DocTypes",
|
||||||
"options": "Transaction Deletion Record Item"
|
"options": "Transaction Deletion Record Item",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
@@ -46,18 +59,93 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Status",
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-04 20:15:59.071493",
|
"modified": "2024-03-20 14:58:15.086360",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Transaction Deletion Record",
|
"name": "Transaction Deletion Record",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -76,5 +164,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb
|
from frappe import _, qb
|
||||||
from frappe.desk.notifications import clear_notifications
|
from frappe.desk.notifications import clear_notifications
|
||||||
from frappe.model.document import Document
|
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):
|
class TransactionDeletionRecord(Document):
|
||||||
@@ -18,20 +20,42 @@ class TransactionDeletionRecord(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
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 (
|
from erpnext.setup.doctype.transaction_deletion_record_item.transaction_deletion_record_item import (
|
||||||
TransactionDeletionRecordItem,
|
TransactionDeletionRecordItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
|
clear_notifications: DF.Check
|
||||||
company: DF.Link
|
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]
|
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
|
# end: auto-generated types
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
|
super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
|
||||||
self.batch_size = 5000
|
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):
|
def validate(self):
|
||||||
frappe.only_for("System Manager")
|
frappe.only_for("System Manager")
|
||||||
@@ -48,30 +72,154 @@ class TransactionDeletionRecord(Document):
|
|||||||
title=_("Not Allowed"),
|
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):
|
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:
|
if not self.doctypes_to_be_ignored:
|
||||||
self.populate_doctypes_to_be_ignored_table()
|
self.populate_doctypes_to_be_ignored_table()
|
||||||
|
|
||||||
self.delete_bins()
|
def reset_task_flags(self):
|
||||||
self.delete_lead_addresses()
|
self.clear_notifications = 0
|
||||||
self.reset_company_values()
|
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()
|
clear_notifications()
|
||||||
self.delete_company_transactions()
|
self.db_set("clear_notifications", 1)
|
||||||
|
self.enqueue_task(task="Initialize Summary Table")
|
||||||
|
|
||||||
def populate_doctypes_to_be_ignored_table(self):
|
def populate_doctypes_to_be_ignored_table(self):
|
||||||
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
|
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
|
||||||
for doctype in doctypes_to_be_ignored_list:
|
for doctype in doctypes_to_be_ignored_list:
|
||||||
self.append("doctypes_to_be_ignored", {"doctype_name": doctype})
|
self.append("doctypes_to_be_ignored", {"doctype_name": doctype})
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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.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):
|
def delete_bins(self):
|
||||||
|
self.validate_doc_status()
|
||||||
|
if not self.delete_bin_data:
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""delete from `tabBin` where warehouse in
|
"""delete from `tabBin` where warehouse in
|
||||||
(select name from tabWarehouse where company=%s)""",
|
(select name from tabWarehouse where company=%s)""",
|
||||||
self.company,
|
self.company,
|
||||||
)
|
)
|
||||||
|
self.db_set("delete_bin_data", 1)
|
||||||
|
self.enqueue_task(task="Delete Leads and Addresses")
|
||||||
|
|
||||||
def delete_lead_addresses(self):
|
def delete_lead_addresses(self):
|
||||||
"""Delete addresses to which leads are linked"""
|
"""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 = frappe.get_all("Lead", filters={"company": self.company})
|
||||||
leads = ["'%s'" % row.get("name") for row in leads]
|
leads = ["'%s'" % row.get("name") for row in leads]
|
||||||
addresses = []
|
addresses = []
|
||||||
@@ -107,45 +255,83 @@ class TransactionDeletionRecord(Document):
|
|||||||
leads=",".join(leads)
|
leads=",".join(leads)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.db_set("delete_leads_and_addresses", 1)
|
||||||
|
self.enqueue_task(task="Reset Company Values")
|
||||||
|
|
||||||
def reset_company_values(self):
|
def reset_company_values(self):
|
||||||
|
self.validate_doc_status()
|
||||||
|
if not self.reset_company_default_values:
|
||||||
company_obj = frappe.get_doc("Company", self.company)
|
company_obj = frappe.get_doc("Company", self.company)
|
||||||
company_obj.total_monthly_sales = 0
|
company_obj.total_monthly_sales = 0
|
||||||
company_obj.sales_monthly_history = None
|
company_obj.sales_monthly_history = None
|
||||||
company_obj.save()
|
company_obj.save()
|
||||||
|
self.db_set("reset_company_default_values", 1)
|
||||||
|
self.enqueue_task(task="Clear Notifications")
|
||||||
|
|
||||||
def delete_company_transactions(self):
|
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()
|
doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list()
|
||||||
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
|
docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list)
|
||||||
|
|
||||||
tables = self.get_all_child_doctypes()
|
tables = self.get_all_child_doctypes()
|
||||||
for docfield in docfields:
|
for docfield in docfields:
|
||||||
if docfield["parent"] != self.doctype:
|
if docfield["parent"] != self.doctype:
|
||||||
no_of_docs = self.get_number_of_docs_linked_with_specified_company(
|
no_of_docs = self.get_number_of_docs_linked_with_specified_company(
|
||||||
docfield["parent"], docfield["fieldname"]
|
docfield["parent"], docfield["fieldname"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if no_of_docs > 0:
|
if no_of_docs > 0:
|
||||||
self.delete_version_log(docfield["parent"], docfield["fieldname"])
|
# 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):
|
||||||
|
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 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
|
||||||
|
)
|
||||||
|
if no_of_docs > 0:
|
||||||
reference_docs = frappe.get_all(
|
reference_docs = frappe.get_all(
|
||||||
docfield["parent"], filters={docfield["fieldname"]: self.company}
|
docfield.doctype_name, filters={docfield.docfield_name: self.company}, limit=self.batch_size
|
||||||
)
|
)
|
||||||
reference_doc_names = [r.name for r in reference_docs]
|
reference_doc_names = [r.name for r in reference_docs]
|
||||||
|
|
||||||
self.delete_communications(docfield["parent"], reference_doc_names)
|
self.delete_version_log(docfield.doctype_name, reference_doc_names)
|
||||||
self.delete_comments(docfield["parent"], reference_doc_names)
|
self.delete_communications(docfield.doctype_name, reference_doc_names)
|
||||||
self.unlink_attachments(docfield["parent"], reference_doc_names)
|
self.delete_comments(docfield.doctype_name, reference_doc_names)
|
||||||
|
self.unlink_attachments(docfield.doctype_name, reference_doc_names)
|
||||||
self.populate_doctypes_table(tables, docfield["parent"], no_of_docs)
|
self.delete_child_tables(docfield.doctype_name, reference_doc_names)
|
||||||
|
self.delete_docs_linked_with_specified_company(docfield.doctype_name, reference_doc_names)
|
||||||
self.delete_child_tables(docfield["parent"], docfield["fieldname"])
|
processed = int(docfield.no_of_docs) + len(reference_doc_names)
|
||||||
self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"])
|
frappe.db.set_value(docfield.doctype, docfield.name, "no_of_docs", processed)
|
||||||
|
else:
|
||||||
naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname")
|
# reset naming series
|
||||||
|
naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname")
|
||||||
if naming_series:
|
if naming_series:
|
||||||
if "#" in naming_series:
|
if "#" in naming_series:
|
||||||
self.update_naming_series(naming_series, docfield["parent"])
|
self.update_naming_series(naming_series, docfield.doctype_name)
|
||||||
|
frappe.db.set_value(docfield.doctype, docfield.name, "done", 1)
|
||||||
|
|
||||||
|
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):
|
def get_doctypes_to_be_ignored_list(self):
|
||||||
singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
|
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):
|
def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname):
|
||||||
return frappe.db.count(doctype, {company_fieldname: self.company})
|
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:
|
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}
|
||||||
def delete_child_tables(self, doctype, company_fieldname):
|
|
||||||
parent_docs_to_be_deleted = frappe.get_all(
|
|
||||||
doctype, {company_fieldname: self.company}, pluck="name"
|
|
||||||
)
|
)
|
||||||
|
self.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
def delete_child_tables(self, doctype, reference_doc_names):
|
||||||
child_tables = frappe.get_all(
|
child_tables = frappe.get_all(
|
||||||
"DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options"
|
"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:
|
for table in child_tables:
|
||||||
frappe.db.delete(table, {"parent": ["in", batch]})
|
frappe.db.delete(table, {"parent": ["in", reference_doc_names]})
|
||||||
|
|
||||||
def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
|
def delete_docs_linked_with_specified_company(self, doctype, reference_doc_names):
|
||||||
frappe.db.delete(doctype, {company_fieldname: self.company})
|
frappe.db.delete(doctype, {"name": ("in", reference_doc_names)})
|
||||||
|
|
||||||
def update_naming_series(self, naming_series, doctype_name):
|
def update_naming_series(self, naming_series, doctype_name):
|
||||||
if "." in naming_series:
|
if "." in naming_series:
|
||||||
@@ -213,16 +398,10 @@ class TransactionDeletionRecord(Document):
|
|||||||
|
|
||||||
frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
|
frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
|
||||||
|
|
||||||
def delete_version_log(self, doctype, company_fieldname):
|
def delete_version_log(self, doctype, docnames):
|
||||||
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")
|
versions = qb.DocType("Version")
|
||||||
for batch in create_batch(names, self.batch_size):
|
|
||||||
qb.from_(versions).delete().where(
|
qb.from_(versions).delete().where(
|
||||||
(versions.ref_doctype == doctype) & (versions.docname.isin(batch))
|
(versions.ref_doctype == doctype) & (versions.docname.isin(docnames))
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
def delete_communications(self, doctype, reference_doc_names):
|
def delete_communications(self, doctype, reference_doc_names):
|
||||||
@@ -295,3 +474,34 @@ def get_doctypes_to_be_ignored():
|
|||||||
doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
|
doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
|
||||||
|
|
||||||
return doctypes_to_be_ignored
|
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")
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.listview_settings["Transaction Deletion Record"] = {
|
frappe.listview_settings["Transaction Deletion Record"] = {
|
||||||
|
add_fields: ["status"],
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (doc.docstatus == 0) {
|
let colors = {
|
||||||
return [__("Draft"), "red"];
|
Queued: "orange",
|
||||||
} else {
|
Completed: "green",
|
||||||
return [__("Completed"), "green"];
|
Running: "blue",
|
||||||
}
|
Failed: "red",
|
||||||
|
};
|
||||||
|
let status = doc.status;
|
||||||
|
return [__(status), colors[status], "status,=," + status];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"doctype_name",
|
"doctype_name"
|
||||||
"no_of_docs"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -16,18 +15,12 @@
|
|||||||
"label": "DocType",
|
"label": "DocType",
|
||||||
"options": "DocType",
|
"options": "DocType",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "no_of_docs",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Number of Docs"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-08 23:10:46.166744",
|
"modified": "2024-02-04 10:56:27.413691",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Transaction Deletion Record Item",
|
"name": "Transaction Deletion Record Item",
|
||||||
@@ -35,5 +28,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@ class TransactionDeletionRecordItem(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
doctype_name: DF.Link
|
doctype_name: DF.Link
|
||||||
no_of_docs: DF.Data | None
|
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
Reference in New Issue
Block a user