From f741d46fdb64dffed957aca1546613ae1a41d344 Mon Sep 17 00:00:00 2001 From: vishal Date: Fri, 15 Nov 2019 16:11:50 +0530 Subject: [PATCH] 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 --- frappe/desk/form/linked_with.py | 70 +++++++++++++++-- frappe/public/js/frappe/form/form.js | 110 ++++++++++++++++++++++++--- frappe/utils/boilerplate.py | 4 + 3 files changed, 168 insertions(+), 16 deletions(-) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 734b99a003..3921378565 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -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 \ No newline at end of file + return ret diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 02aa8b78dc..3ed39f28c0 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -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 += `
  • ${doctype}: ${docnames}
  • ` + } + links_text = "" + + let confirm_message = `${me.doc.doctype} ${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: `

    ${confirm_message}

    ` + }] + }, () => 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"); diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 4a7b93751a..e1dde1f0d4 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -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 -*-