diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 8d8731e012..969a71ab7d 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -26,7 +26,7 @@ class TestDocType(unittest.TestCase): }], "permissions": [{ "role": "System Manager", - "read": 1 + "read": 1, }], "name": name }) @@ -295,3 +295,58 @@ class TestDocType(unittest.TestCase): field_1.search_index = 1 self.assertRaises(CannotIndexedError, doc.insert) + + def test_cancel_link_doctype(self): + import json + from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs + + #create doctype + link_doc = self.new_doctype('Test Linked Doctype') + link_doc.is_submittable = 1 + for data in link_doc.get('permissions'): + data.submit = 1 + data.cancel = 1 + link_doc.insert() + + doc = self.new_doctype('Test Doctype') + doc.is_submittable = 1 + field_2 = doc.append('fields', {}) + field_2.label = 'Test Linked Doctype' + field_2.fieldname = 'test_linked_doctype' + field_2.fieldtype = 'Link' + field_2.options = 'Test Linked Doctype' + for data in link_doc.get('permissions'): + data.submit = 1 + data.cancel = 1 + doc.insert() + + # create doctype data + data_link_doc = frappe.new_doc('Test Linked Doctype') + data_link_doc.some_fieldname = 'Data1' + data_link_doc.insert() + data_link_doc.save() + data_link_doc.submit() + + data_doc = frappe.new_doc('Test Doctype') + data_doc.some_fieldname = 'Data1' + data_doc.test_linked_doctype = data_link_doc.name + data_doc.insert() + data_doc.save() + data_doc.submit() + + docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name) + dump_docs = json.dumps(docs.get('docs')) + cancel_all_linked_docs(dump_docs) + data_link_doc.cancel() + data_doc.load_from_db() + self.assertEqual(data_link_doc.docstatus, 2) + self.assertEqual(data_doc.docstatus, 2) + + # delete doctype record + data_doc.delete() + data_link_doc.delete() + + # delete doctype + link_doc.delete() + doc.delete() + frappe.db.commit() \ No newline at end of file diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 734b99a003..6c679bf312 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -1,14 +1,119 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # 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): + """ + Get all nested submitted linked doctype linkinfo + + Arguments: + doctype (str) - The doctype for which get all linked doctypes + name (str) - The docname for which get all linked doctypes + + Keyword Arguments: + docs (list of dict) - (Optional) Get list of dictionary for linked doctype. + + Returns: + dict - Return list of documents and link count + """ + + if not docs: + docs = [] + + linkinfo = get_linked_doctypes(doctype) + linked_docs = get_linked_docs(doctype, name, linkinfo) + + link_count = 0 + for link_doctype, link_names in linked_docs.items(): + for link in link_names: + docinfo = link.update({"doctype": link_doctype}) + validated_doc = validate_linked_doc(docinfo) + + if not validated_doc: + 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, + "docstatus": link.docstatus, + "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): + """ + Cancel all linked doctype + + Arguments: + docs (str) - It contains all list of dictionaries of a linked documents. + """ + + docs = json.loads(docs) + for i, doc in enumerate(docs, 1): + if validate_linked_doc(doc) is True: + 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() + + +def validate_linked_doc(docinfo): + """ + Validate a document to be submitted and non-exempted from auto-cancel. + + Args: + docs (dict): The document to check for submitted and non-exempt from auto-cancel + + Returns: + bool: True if linked document passes all validations, else False + """ + + # skip non-submittable doctypes since they don't need to be cancelled + if not frappe.get_meta(docinfo.get('doctype')).is_submittable: + return False + + # skip draft or cancelled documents + if docinfo.get('docstatus') != 1: + return False + + # skip other doctypes since they don't need to be cancelled + auto_cancel_exempt_doctypes = get_exempted_doctypes() + if docinfo.get('doctype') in auto_cancel_exempt_doctypes: + return False + + return True + + +def get_exempted_doctypes(): + """ Get list of doctypes exempted from being auto-cancelled """ + + auto_cancel_exempt_doctypes = [] + for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'): + auto_cancel_exempt_doctypes.append(doctypes) + return auto_cancel_exempt_doctypes @frappe.whitelist() @@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F if is_single(df.doctype): continue # optimized to get both link exists and parenttype - possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype - from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True) + possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype}, + fields=['parenttype'], distinct=True) if not possible_link: continue @@ -203,4 +308,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/integrations/oauth2_logins.py b/frappe/integrations/oauth2_logins.py index a3ee98ad4e..14a6bcc417 100644 --- a/frappe/integrations/oauth2_logins.py +++ b/frappe/integrations/oauth2_logins.py @@ -31,6 +31,10 @@ def login_via_office365(code, state): def login_via_salesforce(code, state): login_via_oauth2("salesforce", code, state, decoder=decoder_compat) +@frappe.whitelist(allow_guest=True) +def login_via_fairlogin(code, state): + login_via_oauth2("fairlogin", code, state, decoder=decoder_compat) + @frappe.whitelist(allow_guest=True) def custom(code, state): """ diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 02aa8b78dc..b55c822ba6 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -648,18 +648,96 @@ frappe.ui.form.Form = class FrappeForm { } savecancel(btn, callback, on_error) { - var me = this; - + const me = this; 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, on_error); + } else { + me._cancel(btn, callback, on_error, false); + } + } + }); + } + + _cancel_all(r, btn, callback, on_error) { + 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 += `