feat: cancel multiple documents in a single transaction
- show all linked doc in a dialog box on cancel event - following changes only fetch cancelable docs - recursively build a list of linked submitted docs - cancel all linked documents before cancelling current document - add progress bar and group documents by doctype - added exempted doctype list in hooks
This commit is contained in:
parent
a8269cc556
commit
f741d46fdb
3 changed files with 168 additions and 16 deletions
|
|
@ -2,13 +2,71 @@
|
|||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, json
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from six import string_types
|
||||
|
||||
import frappe
|
||||
import frappe.desk.form.load
|
||||
import frappe.desk.form.meta
|
||||
from frappe import _
|
||||
from frappe.model.meta import is_single
|
||||
from frappe.modules import load_doctype_module
|
||||
import frappe.desk.form.meta
|
||||
import frappe.desk.form.load
|
||||
from six import string_types
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None):
|
||||
if not docs:
|
||||
docs = []
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
CANCEL_EXEMPT_DOCTYPES = []
|
||||
for doctypes in frappe.get_hooks('cancel_exempt_doctypes'):
|
||||
CANCEL_EXEMPT_DOCTYPES.extend(doctypes)
|
||||
|
||||
for link_doctype, link_names in linked_docs.items():
|
||||
# skip non-submittable doctypes since they don't need to be cancelled
|
||||
if not frappe.get_meta(link_doctype).is_submittable:
|
||||
continue
|
||||
|
||||
# skip other doctypes since they don't need to be cancelled
|
||||
if link_doctype in CANCEL_EXEMPT_DOCTYPES:
|
||||
continue
|
||||
|
||||
for link in link_names:
|
||||
if link.docstatus != 1:
|
||||
continue
|
||||
|
||||
link_count += 1
|
||||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
"link_count": links.get("count")
|
||||
})
|
||||
|
||||
# sort linked documents by ascending number of links
|
||||
docs.sort(key=lambda doc: doc.get("link_count"))
|
||||
return {
|
||||
"docs": docs,
|
||||
"count": link_count
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_all_linked_docs(docs):
|
||||
docs = json.loads(docs)
|
||||
for i, doc in enumerate(docs, 1):
|
||||
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
|
||||
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
|
||||
linked_doc.cancel()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -203,4 +261,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
|
|||
"doctype_fieldname": df.doctype_fieldname
|
||||
}
|
||||
|
||||
return ret
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -648,19 +648,103 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
savecancel(btn, callback, on_error) {
|
||||
var me = this;
|
||||
const me = this;
|
||||
const handle_fail = () => {
|
||||
$(btn).prop('disabled', false);
|
||||
if (on_error) {
|
||||
on_error();
|
||||
}
|
||||
};
|
||||
|
||||
this.validate_form_action('Cancel');
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() {
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
|
||||
args: {
|
||||
doctype: me.doc.doctype,
|
||||
name: me.doc.name
|
||||
},
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message.count > 0) {
|
||||
me._cancel_all(r, btn, callback, handle_fail);
|
||||
} else {
|
||||
me._cancel(btn, callback, handle_fail, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_cancel_all(r, btn, callback, handle_fail) {
|
||||
const me = this;
|
||||
|
||||
// add confirmation message for cancelling all linked docs
|
||||
let links_text = "";
|
||||
let links = r.message.docs;
|
||||
const doctypes = Array.from(new Set(links.map(link => link.doctype)));
|
||||
|
||||
for (let doctype of doctypes) {
|
||||
let docnames = links
|
||||
.filter((link) => link.doctype == doctype)
|
||||
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
|
||||
.join(", ");
|
||||
links_text += `<li><strong>${doctype}</strong>: ${docnames}</li>`
|
||||
}
|
||||
links_text = "<ul>" + links_text + "</ul>"
|
||||
|
||||
let confirm_message = `<strong>${me.doc.doctype}</strong> ${me.doc.name} is linked with the following submitted documents: ${links_text}`
|
||||
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
|
||||
if (can_cancel) {
|
||||
confirm_message += `Do you want to cancel all linked documents?`;
|
||||
} else {
|
||||
confirm_message += `You do not have permissions to cancel all linked documents.`;
|
||||
}
|
||||
|
||||
// generate dialog box to cancel all linked docs
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Cancel All Documents"),
|
||||
fields: [{
|
||||
fieldtype: "HTML",
|
||||
options: `<p class="frappe-confirm-message">${confirm_message}</p>`
|
||||
}]
|
||||
}, () => handle_fail());
|
||||
|
||||
// if user can cancel all linked docs, add action to the dialog
|
||||
if (can_cancel) {
|
||||
d.set_primary_action("Cancel All", () => {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
|
||||
args: {
|
||||
docs: links
|
||||
},
|
||||
freeze: true,
|
||||
callback: (resp) => {
|
||||
if (!resp.exc) {
|
||||
me.reload_doc();
|
||||
me._cancel(btn, callback, handle_fail, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
d.show();
|
||||
};
|
||||
|
||||
_cancel(btn, callback, handle_fail, skip_confirm) {
|
||||
const me = this;
|
||||
const cancel_doc = () => {
|
||||
frappe.validated = true;
|
||||
me.script_manager.trigger("before_cancel").then(function() {
|
||||
if(!frappe.validated) {
|
||||
return me.handle_save_fail(btn, on_error);
|
||||
me.script_manager.trigger("before_cancel").then(() => {
|
||||
if (!frappe.validated) {
|
||||
handle_fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var after_cancel = function(r) {
|
||||
if(r.exc) {
|
||||
me.handle_save_fail(btn, on_error);
|
||||
const after_cancel(r) {
|
||||
if (r.exc) {
|
||||
handle_fail();
|
||||
} else {
|
||||
frappe.utils.play_sound("cancel");
|
||||
me.refresh();
|
||||
|
|
@ -670,8 +754,14 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
};
|
||||
frappe.ui.form.save(me, "cancel", after_cancel, btn);
|
||||
});
|
||||
}, () => me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
}
|
||||
|
||||
if (skip_confirm) {
|
||||
cancel_doc();
|
||||
} else {
|
||||
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, handle_fail);
|
||||
}
|
||||
};
|
||||
|
||||
savetrash() {
|
||||
this.validate_form_action("Delete");
|
||||
|
|
|
|||
|
|
@ -256,6 +256,10 @@ app_license = "{app_license}"
|
|||
# "Task": "{app_name}.task.get_dashboard_data"
|
||||
# }}
|
||||
|
||||
# exempt linked doctypes from being automatically cancelled
|
||||
#
|
||||
# cancel_exempt_doctypes = ["Auto Repeat"]
|
||||
|
||||
"""
|
||||
|
||||
desktop_template = """# -*- coding: utf-8 -*-
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue