Merge branch 'develop' of https://github.com/frappe/frappe into change-delete-button
This commit is contained in:
commit
45d6951d92
6 changed files with 292 additions and 30 deletions
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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 += `<li><strong>${doctype}</strong>: ${docnames}</li>`;
|
||||
}
|
||||
links_text = `<ul>${links_text}</ul>`;
|
||||
|
||||
let confirm_message = __('{0} {1} is linked with the following submitted documents: {2}',
|
||||
[(me.doc.doctype).bold(), me.doc.name, 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>`
|
||||
}]
|
||||
}, () => me.handle_save_fail(btn, on_error));
|
||||
|
||||
// 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, on_error, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
d.show();
|
||||
};
|
||||
|
||||
_cancel(btn, callback, on_error, skip_confirm) {
|
||||
const me = this;
|
||||
const cancel_doc = () => {
|
||||
frappe.validated = true;
|
||||
me.script_manager.trigger("before_cancel").then(function() {
|
||||
if(!frappe.validated) {
|
||||
me.script_manager.trigger("before_cancel").then(() => {
|
||||
if (!frappe.validated) {
|
||||
return me.handle_save_fail(btn, on_error);
|
||||
}
|
||||
|
||||
var after_cancel = function(r) {
|
||||
if(r.exc) {
|
||||
if (r.exc) {
|
||||
me.handle_save_fail(btn, on_error);
|
||||
} else {
|
||||
frappe.utils.play_sound("cancel");
|
||||
|
|
@ -670,8 +748,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, me.handle_save_fail(btn, on_error));
|
||||
}
|
||||
};
|
||||
|
||||
savetrash() {
|
||||
this.validate_form_action("Delete");
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
|||
|
||||
xmloutput_fh = None
|
||||
if junit_xml_output:
|
||||
xmloutput_fh = open(junit_xml_output, 'w')
|
||||
xmloutput_fh = open(junit_xml_output, 'wb')
|
||||
unittest_runner = xmlrunner_wrapper(xmloutput_fh)
|
||||
else:
|
||||
unittest_runner = unittest.TextTestRunner
|
||||
|
|
@ -68,11 +68,11 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
|
|||
frappe.get_attr(fn)()
|
||||
|
||||
if doctype:
|
||||
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile)
|
||||
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile, junit_xml_output=junit_xml_output)
|
||||
elif module:
|
||||
ret = run_tests_for_module(module, verbose, tests, profile)
|
||||
ret = run_tests_for_module(module, verbose, tests, profile, junit_xml_output=junit_xml_output)
|
||||
else:
|
||||
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast)
|
||||
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output)
|
||||
|
||||
if frappe.db: frappe.db.commit()
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ class TimeLoggingTestResult(unittest.TextTestResult):
|
|||
super(TimeLoggingTestResult, self).addSuccess(test)
|
||||
|
||||
|
||||
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False):
|
||||
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False, junit_xml_output=False):
|
||||
import os
|
||||
|
||||
apps = [app] if app else frappe.get_installed_apps()
|
||||
|
|
@ -130,11 +130,16 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
|
|||
_add_test(app, path, filename, verbose,
|
||||
test_suite, ui_tests)
|
||||
|
||||
if junit_xml_output:
|
||||
runner = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast)
|
||||
else:
|
||||
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast)
|
||||
|
||||
if profile:
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
out = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite)
|
||||
out = runner.run(test_suite)
|
||||
|
||||
if profile:
|
||||
pr.disable()
|
||||
|
|
@ -145,7 +150,7 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
|
|||
|
||||
return out
|
||||
|
||||
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False):
|
||||
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False, junit_xml_output=False):
|
||||
modules = []
|
||||
if not isinstance(doctypes, (list, tuple)):
|
||||
doctypes = [doctypes]
|
||||
|
|
@ -163,17 +168,17 @@ def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profil
|
|||
make_test_records(doctype, verbose=verbose, force=force)
|
||||
modules.append(importlib.import_module(test_module))
|
||||
|
||||
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile)
|
||||
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
|
||||
|
||||
def run_tests_for_module(module, verbose=False, tests=(), profile=False):
|
||||
def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_xml_output=False):
|
||||
module = importlib.import_module(module)
|
||||
if hasattr(module, "test_dependencies"):
|
||||
for doctype in module.test_dependencies:
|
||||
make_test_records(doctype, verbose=verbose)
|
||||
|
||||
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile)
|
||||
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
|
||||
|
||||
def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
||||
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
|
||||
test_suite = unittest.TestSuite()
|
||||
|
||||
if not isinstance(modules, (list, tuple)):
|
||||
|
|
@ -189,13 +194,18 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
|
|||
else:
|
||||
test_suite.addTest(module_test_cases)
|
||||
|
||||
if junit_xml_output:
|
||||
runner = unittest_runner(verbosity=1+(verbose and 1 or 0))
|
||||
else:
|
||||
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0))
|
||||
|
||||
if profile:
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
frappe.flags.tests_verbose = verbose
|
||||
|
||||
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
|
||||
out = runner.run(test_suite)
|
||||
|
||||
|
||||
if profile:
|
||||
|
|
|
|||
|
|
@ -256,6 +256,10 @@ app_license = "{app_license}"
|
|||
# "Task": "{app_name}.task.get_dashboard_data"
|
||||
# }}
|
||||
|
||||
# exempt linked doctypes from being automatically cancelled
|
||||
#
|
||||
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
|
||||
|
||||
"""
|
||||
|
||||
desktop_template = """# -*- coding: utf-8 -*-
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue