diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a29e0badee..ef9d094d93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,9 @@ # Contributing to Frappe / ERPNext +### Update 16-Sep-14 + +Please send pull requests to branch v5.0 + ## Reporting issues We only accept issues that are bug reports or feature requests. Bugs must be isolated and reproducible problems. Please read the following guidelines before opening any issue. diff --git a/frappe/__init__.py b/frappe/__init__.py index 24dbb5030c..88bfbb7ddc 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -549,9 +549,15 @@ def import_doc(path, ignore_links=False, ignore_insert=False, insert=False): from frappe.core.page.data_import_tool import data_import_tool data_import_tool.import_doc(path, ignore_links=ignore_links, ignore_insert=ignore_insert, insert=insert) -def copy_doc(doc): +def copy_doc(doc, ignore_no_copy=True): """ No_copy fields also get copied.""" import copy + + def remove_no_copy_fields(d): + for df in d.meta.get("fields", {"no_copy": 1}): + if hasattr(d, df.fieldname): + d.set(df.fieldname, None) + if not isinstance(doc, dict): d = doc.as_dict() else: @@ -564,12 +570,18 @@ def copy_doc(doc): newdoc.creation = None newdoc.amended_from = None newdoc.amendment_date = None + if not ignore_no_copy: + remove_no_copy_fields(newdoc) + for d in newdoc.get_all_children(): d.name = None d.parent = None d.set("__islocal", 1) d.owner = None d.creation = None + if not ignore_no_copy: + remove_no_copy_fields(d) + return newdoc def compare(val1, condition, val2): diff --git a/frappe/__version__.py b/frappe/__version__.py index 111dc9172a..ecdb1cef9e 100644 --- a/frappe/__version__.py +++ b/frappe/__version__.py @@ -1 +1 @@ -__version__ = "4.3.0" +__version__ = "4.4.0" diff --git a/frappe/core/doctype/customize_form/customize_form.py b/frappe/core/doctype/customize_form/customize_form.py index 8f9f7f8a9d..04d74eb287 100644 --- a/frappe/core/doctype/customize_form/customize_form.py +++ b/frappe/core/doctype/customize_form/customize_form.py @@ -117,6 +117,11 @@ class CustomizeForm(Document): if property == "fieldtype": self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) + elif property == "allow_on_submit" and df.get(property): + frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ + .format(df.idx)) + continue + self.make_property_setter(property=property, value=df.get(property), property_type=self.docfield_properties[property], fieldname=df.fieldname) diff --git a/frappe/core/doctype/customize_form/test_customize_form.py b/frappe/core/doctype/customize_form/test_customize_form.py index 7a1e4e6b4d..db4115fb8b 100644 --- a/frappe/core/doctype/customize_form/test_customize_form.py +++ b/frappe/core/doctype/customize_form/test_customize_form.py @@ -159,3 +159,17 @@ class TestCustomizeForm(unittest.TestCase): frappe.local.test_objects["Property Setter"] = [] make_test_records_for_doctype("Property Setter") + + def test_set_allow_on_submit(self): + d = self.get_customize_form("User") + d.get("customize_form_fields", {"fieldname": "first_name"})[0].allow_on_submit = 1 + d.get("customize_form_fields", {"fieldname": "test_custom_field"})[0].allow_on_submit = 1 + d.run_method("save_customization") + + d = self.get_customize_form("User") + + # don't allow for standard fields + self.assertEquals(d.get("customize_form_fields", {"fieldname": "first_name"})[0].allow_on_submit or 0, 0) + + # allow for custom field + self.assertEquals(d.get("customize_form_fields", {"fieldname": "test_custom_field"})[0].allow_on_submit, 1) diff --git a/frappe/core/doctype/file_data/file_data.py b/frappe/core/doctype/file_data/file_data.py index ae0971d79e..dd239d3c05 100644 --- a/frappe/core/doctype/file_data/file_data.py +++ b/frappe/core/doctype/file_data/file_data.py @@ -17,7 +17,7 @@ class FileData(Document): def before_insert(self): frappe.local.rollback_observers.append(self) - def on_update(self): + def validate(self): if not getattr(self, "ignore_duplicate_entry_error", False): # check duplicate assignement n_records = frappe.db.sql("""select name from `tabFile Data` @@ -28,8 +28,7 @@ class FileData(Document): self.attached_to_name)) if len(n_records) > 0: self.duplicate_entry = n_records[0][0] - frappe.msgprint(frappe._("Same file has already been attached to the record")) - raise frappe.DuplicateEntryError + frappe.throw(frappe._("Same file has already been attached to the record"), frappe.DuplicateEntryError) def on_trash(self): if self.attached_to_name: diff --git a/frappe/core/doctype/notification_count/notification_count.py b/frappe/core/doctype/notification_count/notification_count.py index 28bd0e2ea6..fbb8ae4718 100644 --- a/frappe/core/doctype/notification_count/notification_count.py +++ b/frappe/core/doctype/notification_count/notification_count.py @@ -89,7 +89,15 @@ def clear_notifications(user=None): def delete_notification_count_for(doctype): if frappe.flags.in_import: return - frappe.db.sql("""delete from `tabNotification Count` where for_doctype = %s""", (doctype,)) + + try: + frappe.db.sql("""delete from `tabNotification Count` where for_doctype = %s""", (doctype,)) + + except MySQLdb.OperationalError, e: + if e.args[0] != 1213: + raise + + logger.error("Deadlock") def clear_doctype_notifications(doc, method=None, *args, **kwargs): if frappe.flags.in_import: diff --git a/frappe/core/doctype/todo/todo_list.html b/frappe/core/doctype/todo/todo_list.html index 18b9dea848..20692806ee 100644 --- a/frappe/core/doctype/todo/todo_list.html +++ b/frappe/core/doctype/todo/todo_list.html @@ -23,7 +23,7 @@
- {%= frappe.avatar(doc.assigned_to) %} + {%= frappe.avatar(doc.owner) %}
diff --git a/frappe/database.py b/frappe/database.py index 4f72cfdf6c..d40bf90cb8 100644 --- a/frappe/database.py +++ b/frappe/database.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import MySQLdb +from markdown2 import UnicodeWithAttrs import warnings import datetime import frappe @@ -48,6 +49,7 @@ class Database: use_unicode=True, charset='utf8') self._conn.converter[246]=float self._conn.converter[12]=get_datetime + self._conn.encoders[UnicodeWithAttrs] = self._conn.encoders[unicode] self._cursor = self._conn.cursor() if self.user != 'root': diff --git a/frappe/hooks.py b/frappe/hooks.py index d0b51a5da0..deddf426e8 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -1,9 +1,9 @@ app_name = "frappe" app_title = "Frappe Framework" app_publisher = "Web Notes Technologies Pvt. Ltd." -app_description = "Full Stack Web Application Framwork in Python" +app_description = "Full Stack Web Application Framework in Python" app_icon = "assets/frappe/images/frappe.svg" -app_version = "4.3.0" +app_version = "4.4.0" app_color = "#3498db" app_email = "support@frappe.io" diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index aa4f4983ee..2b92a99b38 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -89,5 +89,5 @@ def get_default_value(df, defaults, user_permissions, parent_doc): elif df.fieldtype == "Time": return nowtime() - elif (df.fieldtype == "Select" and df.options and df.options != "[Select]"): + elif (df.fieldtype == "Select" and df.options and df.options not in ("[Select]", "Loading...")): return df.options.split("\n")[0] diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index a0d56f31f0..eda985b30c 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -140,7 +140,7 @@ def check_if_doc_is_dynamically_linked(doc): if frappe.get_meta(df.parent).issingle: # dynamic link in single doc - refdoc = frappe.get_singles_dict(df.parent) + refdoc = frappe.db.get_singles_dict(df.parent) if refdoc.get(df.options)==doc.doctype and refdoc.get(df.fieldname)==doc.name: frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype, doc.name, df.parent, ""), frappe.LinkExistsError) diff --git a/frappe/model/document.py b/frappe/model/document.py index 0993c19008..0097d392d6 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -80,7 +80,13 @@ class Document(BaseDocument): def load_from_db(self): if not getattr(self, "_metaclass", False) and self.meta.issingle: - self.update(frappe.db.get_singles_dict(self.doctype)) + single_doc = frappe.db.get_singles_dict(self.doctype) + if not single_doc: + single_doc = frappe.new_doc(self.doctype).as_dict() + single_doc["name"] = self.doctype + del single_doc["__islocal"] + + self.update(single_doc) self.init_valid_columns() self._fix_numeric_types() diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index f3d0a961f4..a4a8b73c09 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -299,7 +299,7 @@ def rename_dynamic_links(doctype, old, new): # dynamic link in single, just one value to check if frappe.get_meta(df.parent).issingle: - refdoc = frappe.get_singles_dict(df.parent) + refdoc = frappe.db.get_singles_dict(df.parent) if refdoc.get(df.options)==doctype and refdoc.get(df.fieldname)==old: frappe.db.sql("""update tabSingles set value=%s where diff --git a/frappe/patches.txt b/frappe/patches.txt index 6e8c2fa47e..1b9d2f143c 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -54,3 +54,4 @@ execute:frappe.reload_doc('website', 'doctype', 'web_form') #2014-09-04 execute:frappe.reload_doc('website', 'doctype', 'web_form_field') #2014-09-04 frappe.patches.v4_2.refactor_website_routing frappe.patches.v4_2.set_assign_in_doc +frappe.patches.v4_3.remove_allow_on_submit_customization diff --git a/frappe/patches/v4_3/__init__.py b/frappe/patches/v4_3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v4_3/remove_allow_on_submit_customization.py b/frappe/patches/v4_3/remove_allow_on_submit_customization.py new file mode 100644 index 0000000000..0e6cd3a11e --- /dev/null +++ b/frappe/patches/v4_3/remove_allow_on_submit_customization.py @@ -0,0 +1,11 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + for d in frappe.get_list("Property Setter", fields=["name", "doc_type"], + filters={"doctype_or_field": "DocField", "property": "allow_on_submit", "value": "1"}, ignore_permissions=True): + frappe.delete_doc("Property Setter", d.name) + frappe.clear_cache(doctype=d.doc_type) diff --git a/frappe/public/build.json b/frappe/public/build.json index 6c4b1acf11..f79dd07d7b 100644 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -107,6 +107,7 @@ "public/js/frappe/ui/tags.js", "public/js/frappe/views/container.js", + "public/js/frappe/views/factory.js", "public/js/frappe/views/pageview.js", "public/js/frappe/views/doclistview.js", "public/js/frappe/views/sidebar_stats.js", diff --git a/frappe/public/js/frappe/form/footer.js b/frappe/public/js/frappe/form/footer.js index 0c06abd652..817509a4f6 100644 --- a/frappe/public/js/frappe/form/footer.js +++ b/frappe/public/js/frappe/form/footer.js @@ -55,6 +55,11 @@ frappe.ui.form.Footer = Class.extend({ }, make_tags: function() { + if (this.frm.meta.issingle) { + this.wrapper.find(".form-tags").toggle(false); + return; + } + this.frm.tags = new frappe.ui.TagEditor({ parent: this.wrapper.find(".tag-area"), frm: this.frm, @@ -86,7 +91,7 @@ frappe.ui.form.Footer = Class.extend({ this.frm.attachments.refresh(); this.frm.comments.refresh(); this.frm.assign_to.refresh(); - this.frm.tags.refresh(); + this.frm.tags && this.frm.tags.refresh(); } }, }); diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index dedc3c35af..a788ed79c7 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -74,7 +74,7 @@ frappe.ui.form.save = function(frm, action, callback, btn) { if(df.reqd && !frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) { has_errors = true; - error_fields[error_fields.length] = df.label; + error_fields[error_fields.length] = __(df.label); // scroll to field if(!me.scroll_set) { @@ -91,8 +91,8 @@ frappe.ui.form.save = function(frm, action, callback, btn) { }); if(error_fields.length) msgprint(__('Mandatory fields required in {0}', [(doc.parenttype - ? (frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label + ' (Table)') - : doc.doctype)]) + '\n' + error_fields.join('\n')); + ? (__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label) + ' ('+ __("Table") + ')') + : __(doc.doctype))]) + '\n' + error_fields.join('\n')); }); return !has_errors; diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 19fe0487de..5a63ca3dfa 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -66,7 +66,7 @@ $.extend(frappe.model, { doc[f.fieldname] = v; updated.push(f.fieldname); } else if(f.fieldtype == "Select" && f.options - && f.options!="[Select]") { + && !in_list(["[Select]", "Loading..."], f.options)) { doc[f.fieldname] = f.options.split("\n")[0]; } } diff --git a/frappe/public/js/frappe/views/container.js b/frappe/public/js/frappe/views/container.js index b391097e6a..363692a7e3 100644 --- a/frappe/public/js/frappe/views/container.js +++ b/frappe/public/js/frappe/views/container.js @@ -73,34 +73,3 @@ frappe.views.Container = Class.extend({ return this.page; } }); - -frappe.views.Factory = Class.extend({ - init: function(opts) { - $.extend(this, opts); - }, - show: function() { - var page_name = frappe.get_route_str(), - me = this; - if(frappe.pages[page_name]) { - frappe.container.change_to(frappe.pages[page_name]); - } else { - var route = frappe.get_route(); - if(route[1]) { - me.make(route); - } else { - frappe.show_not_found(route); - } - } - }, - make_page: function(double_column) { - var page_name = frappe.get_route_str(), - page = frappe.container.add_page(page_name); - - frappe.ui.make_app_page({ - parent: page, - single_column: !double_column - }); - frappe.container.change_to(page_name); - return page; - } -}) diff --git a/frappe/public/js/frappe/views/factory.js b/frappe/public/js/frappe/views/factory.js new file mode 100644 index 0000000000..73fa87ba11 --- /dev/null +++ b/frappe/public/js/frappe/views/factory.js @@ -0,0 +1,36 @@ +// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.provide('frappe.pages'); +frappe.provide('frappe.views'); + +frappe.views.Factory = Class.extend({ + init: function(opts) { + $.extend(this, opts); + }, + show: function() { + var page_name = frappe.get_route_str(), + me = this; + if(frappe.pages[page_name] && page_name.indexOf("Form/")===-1) { + frappe.container.change_to(frappe.pages[page_name]); + } else { + var route = frappe.get_route(); + if(route[1]) { + me.make(route); + } else { + frappe.show_not_found(route); + } + } + }, + make_page: function(double_column) { + var page_name = frappe.get_route_str(), + page = frappe.container.add_page(page_name); + + frappe.ui.make_app_page({ + parent: page, + single_column: !double_column + }); + frappe.container.change_to(page_name); + return page; + } +}); diff --git a/frappe/public/js/frappe/views/ganttview.js b/frappe/public/js/frappe/views/ganttview.js index f6c102db3f..03af1482d4 100644 --- a/frappe/public/js/frappe/views/ganttview.js +++ b/frappe/public/js/frappe/views/ganttview.js @@ -44,7 +44,7 @@ frappe.views.Gantt = Class.extend({ this.appframe.add_module_icon(module) this.appframe.set_views_for(this.doctype, "gantt"); - this.appframe.set_title_right("Refresh", + this.appframe.set_title_right(__("Refresh"), function() { me.refresh(); }, "icon-refresh") this.appframe.add_field({fieldtype:"Date", label:"From", diff --git a/frappe/public/js/frappe/views/query_report.js b/frappe/public/js/frappe/views/query_report.js index b994ef829b..b7a0da4978 100644 --- a/frappe/public/js/frappe/views/query_report.js +++ b/frappe/public/js/frappe/views/query_report.js @@ -50,7 +50,7 @@ frappe.views.QueryReport = Class.extend({


\ '+__('For comparative filters, start with')+' ">" or "<", e.g. >5 or >01-02-2012\
'+__('For ranges')+' ('+__('values and dates')+') use ":", \ - e.g. "5:10" (to filter values between 5 & 10)

\ + e.g. "5:10" (' + __("to filter values between 5 & 10") + ')

\ ').appendTo(this.wrapper); this.make_toolbar(); @@ -148,10 +148,7 @@ frappe.views.QueryReport = Class.extend({ var data = []; // get filtered data for (var i=0, l=me.dataView.getLength(); i 0 or os.path.exists(get_files_path(fname)): f = fname.rsplit('.', 1) if len(f) == 1: - partial, extn = f[0], None - elif len(f) == 2: - partial, extn = f - return '{partial}{suffix}{extn}'.format(partial=partial, extn="."+extn if extn else "", suffix=optional_suffix) + partial, extn = f[0], "" + else: + partial, extn = f[0], "." + f[1] + return '{partial}{suffix}{extn}'.format(partial=partial, extn=extn, suffix=optional_suffix) return fname diff --git a/frappe/website/render.py b/frappe/website/render.py index 17c9af9398..a3f01d56e6 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -171,6 +171,7 @@ def clear_cache(path=None): else: clear_sitemap() frappe.clear_cache("Guest") + frappe.cache().delete_value("_website_pages") clear_permissions() for method in frappe.get_hooks("website_clear_cache"): diff --git a/frappe/widgets/query_report.py b/frappe/widgets/query_report.py index 95b2ce9b64..e7e82f95ee 100644 --- a/frappe/widgets/query_report.py +++ b/frappe/widgets/query_report.py @@ -86,7 +86,7 @@ def run(report_name, filters=()): method_name = get_report_module_dotted_path(module, report.name) + ".execute" columns, result = frappe.get_attr(method_name)(frappe._dict(filters)) - if report.apply_user_permissions: + if report.apply_user_permissions and result: result = get_filtered_data(report.ref_doctype, columns, result) if cint(report.add_total_row) and result: @@ -138,7 +138,7 @@ def add_total_row(result, columns): def get_filtered_data(ref_doctype, columns, data): result = [] - linked_doctypes = get_linked_doctypes(columns) + linked_doctypes = get_linked_doctypes(columns, data) match_filters_per_doctype = get_user_match_filters(linked_doctypes, ref_doctype) if match_filters_per_doctype: @@ -183,7 +183,7 @@ def has_match(row, linked_doctypes, doctype_match_filters): return resultant_match -def get_linked_doctypes(columns): +def get_linked_doctypes(columns, data): linked_doctypes = {} for idx, col in enumerate(columns): @@ -197,6 +197,11 @@ def get_linked_doctypes(columns): elif col.get("fieldtype")=="Link" and col.get("options"): linked_doctypes[col["options"]] = col["fieldname"] + # remove doctype if column is empty + for doctype, key in linked_doctypes.items(): + if not any(d[key] for d in data if d): + del linked_doctypes[doctype] + return linked_doctypes def get_user_match_filters(doctypes, ref_doctype): diff --git a/setup.py b/setup.py index 877718f0a6..a99b55fc12 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages import os -version = "4.3.0" +version = "4.4.0" with open("requirements.txt", "r") as f: install_requires = f.readlines()