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:
vishal 2019-11-15 16:11:50 +05:30
parent a8269cc556
commit f741d46fdb
3 changed files with 168 additions and 16 deletions

View file

@ -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

View file

@ -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");

View file

@ -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 -*-