diff --git a/.travis.yml b/.travis.yml index 52cdff619b..0296f38527 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,5 +107,6 @@ install: - bench build --app frappe after_script: + - pip install coverage==4.5.4 - pip install python-coveralls - coveralls -b apps/frappe -d ../../sites/.coverage diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index c7aeaa92de..5ce853c730 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -19,11 +19,13 @@ context('Report View', () => { cy.server(); cy.route('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/desk#List/${doctype_name}/Report`); - let cell = cy.get('.dt-row-0 > .dt-cell--col-3'); + // check status column added from docstatus + cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted'); + let cell = cy.get('.dt-row-0 > .dt-cell--col-4'); // select the cell cell.dblclick(); cell.find('input[data-fieldname="enabled"]').check({force: true}); - cy.get('.dt-row-0 > .dt-cell--col-4').click(); + cy.get('.dt-row-0 > .dt-cell--col-5').click(); cy.wait('@value-update'); cy.get('@doc').then(doc => { cy.call('frappe.client.get_value', { diff --git a/frappe/__init__.py b/frappe/__init__.py index 4ab86e6b5d..01187541d3 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1537,7 +1537,7 @@ def logger(module=None, with_more_info=True): from frappe.utils.logger import get_logger return get_logger(module or 'default', with_more_info=with_more_info) -def log_error(message=None, title=None): +def log_error(message=None, title=_("Error")): '''Log error to Error Log''' # AI ALERT: @@ -1546,9 +1546,8 @@ def log_error(message=None, title=None): # this hack tries to be smart about whats a title (single line ;-)) and fixes it if message: - if '\n' not in message: - title = message - error = get_traceback() + if '\n' in title: + error, title = title, message else: error = message else: diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 1e785e12f1..1faa02ec63 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -116,7 +116,7 @@ def clear_doctype_map(doctype, name): cache_key = frappe.scrub(doctype) + '_map' frappe.cache().hdel(cache_key, name) -def build_table_count_cache(): +def build_table_count_cache(*args, **kwargs): if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: return _cache = frappe.cache() @@ -137,7 +137,7 @@ def build_table_count_cache(): return counts -def build_domain_restriced_doctype_cache(): +def build_domain_restriced_doctype_cache(*args, **kwargs): if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: return _cache = frappe.cache() @@ -148,7 +148,7 @@ def build_domain_restriced_doctype_cache(): return doctypes -def build_domain_restriced_page_cache(): +def build_domain_restriced_page_cache(*args, **kwargs): if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import: return _cache = frappe.cache() 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/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 099c279dab..2d49915f59 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -126,13 +126,15 @@ class Report(Document): safe_exec(self.report_script, None, loc) return loc['data'] - def get_data(self, filters=None, limit=None, user=None, as_dict=False): + def get_data(self, filters=None, limit=None, user=None, as_dict=False, ignore_prepared_report=False): columns = [] out = [] if self.report_type in ('Query Report', 'Script Report', 'Custom Report'): # query and script reports - data = frappe.desk.query_report.run(self.name, filters=filters, user=user) + data = frappe.desk.query_report.run(self.name, + filters=filters, user=user, ignore_prepared_report=ignore_prepared_report) + for d in data.get('columns'): if isinstance(d, dict): col = frappe._dict(d) diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 82ecdbddca..c4873ee40e 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -168,7 +168,7 @@ frappe.ui.form.on('User', { email: frm.doc.email }, callback: function(r) { - if (r.message == undefined) { + if (!Array.isArray(r.message)) { frappe.route_options = { "email_id": frm.doc.email, "awaiting_password": 1, diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index 08177ecf8a..c5d598ccd3 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -11,7 +11,7 @@ {% for j in jobs %} - {{ j.queue.split(".").slice(-1)[0] }} + {{ j.queue.split(".").slice(-1)[0] }}
{{ frappe.utils.encode_tags(j.job_name) }} diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index f1eadaaf2e..089f2a733b 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -17,6 +17,14 @@ frappe.ui.form.on("Customize Form", { }; }); + frm.set_query("default_print_format", function() { + return { + filters: { + 'print_format_type': ['!=', 'JS'] + } + } + }); + $(frm.wrapper).on("grid-row-render", function(e, grid_row) { if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") { $(grid_row.row).css({"font-weight": "bold"}); diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index a330f7e97e..d51231097e 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -114,9 +114,8 @@ frappe.ui.form.on('Dashboard Chart', { } else { // standard filters if (frm.doc.document_type) { - // allow all link and select fields as filters - frm.chart_filters = []; frappe.model.with_doctype(frm.doc.document_type, () => { + frm.chart_filters = []; frappe.get_meta(frm.doc.document_type).fields.map(df => { if (['Link', 'Select'].includes(df.fieldtype)) { let _df = copy_dict(df); @@ -131,8 +130,8 @@ frappe.ui.form.on('Dashboard Chart', { frm.chart_filters.push(_df); } - frm.trigger('render_filters_table'); }); + frm.trigger('render_filters_table'); }); } } @@ -158,7 +157,7 @@ frappe.ui.form.on('Dashboard Chart', { let filters = JSON.parse(frm.doc.filters_json || '{}'); var filters_set = false; - fields.map( f => { + fields.map(f => { if (filters[f.fieldname]) { const filter_row = $(`${f.label}${filters[f.fieldname] || ""}`); table.find('tbody').append(filter_row); diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 637904b35c..45dc5d86c7 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -73,6 +73,8 @@ class Event(Document): communication.subject = self.subject communication.content = self.description if self.description else self.subject communication.communication_date = self.starts_on + communication.sender = self.owner + communication.sender_full_name = frappe.utils.get_fullname(self.owner) communication.reference_doctype = self.doctype communication.reference_name = self.name communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else "" 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/desk/form/load.py b/frappe/desk/form/load.py index 4044a3dcfc..f24f33df07 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -11,6 +11,7 @@ from frappe.model.utils.user_settings import get_user_settings from frappe.permissions import get_doc_permissions from frappe.desk.form.document_follow import is_document_followed from frappe import _ +from six.moves.urllib.parse import quote @frappe.whitelist() def getdoc(doctype, name, user=None): @@ -101,7 +102,8 @@ def get_docinfo(doc=None, doctype=None, name=None): "energy_point_logs": get_point_logs(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), - "tags": get_tags(doc.doctype, doc.name) + "tags": get_tags(doc.doctype, doc.name), + "document_email": get_document_email(doc.doctype, doc.name) } def get_milestones(doctype, name): @@ -263,4 +265,15 @@ def get_tags(doctype, name): "document_name": name }, fields=["tag"])] - return ",".join([tag for tag in tags]) \ No newline at end of file + return ",".join(tags) + +def get_document_email(doctype, name): + email = get_automatic_email_link() + if not email: + return None + + email = email.split("@") + return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1]) + +def get_automatic_email_link(): + return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index c64d2dcb4f..3d480eb00e 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -13,7 +13,7 @@ class Leaderboard { constructor(parent) { frappe.ui.make_app_page({ parent: parent, - title: "Leaderboard", + title: __("Leaderboard"), single_column: false }); this.parent = parent; @@ -187,7 +187,7 @@ class Leaderboard { render_search_box() { this.$search_box = - $(`