diff --git a/frappe/core/doctype/custom_field/custom_field.js b/frappe/core/doctype/custom_field/custom_field.js index 040cb10616..ec4eea4809 100644 --- a/frappe/core/doctype/custom_field/custom_field.js +++ b/frappe/core/doctype/custom_field/custom_field.js @@ -47,9 +47,18 @@ cur_frm.fields_dict['dt'].get_query = function(doc, dt, dn) { } cur_frm.cscript.fieldtype = function(doc, dt, dn) { - if(doc.fieldtype == 'Link') cur_frm.fields_dict['options_help'].disp_area.innerHTML = 'Please enter name of the document you want this field to be linked to in Options.
Eg.: Customer'; - else if(doc.fieldtype == 'Select') cur_frm.fields_dict['options_help'].disp_area.innerHTML = 'Please enter values in Options, with each option on a new line.
Eg.: Field: Country
Options:
China
India
United States

'; - else cur_frm.fields_dict['options_help'].disp_area.innerHTML = ''; + if(doc.fieldtype == 'Link') { + cur_frm.fields_dict['options_help'].disp_area.innerHTML = + __('Name of the Document Type (DocType) you want this field to be linked to. e.g. Customer'); + } else if(doc.fieldtype == 'Select') { + cur_frm.fields_dict['options_help'].disp_area.innerHTML = + __('Options for select. Each option on a new line. e.g.:
Option 1
Option 2
Option 3
'); + } else if(doc.fieldtype == 'Dynamic Link') { + cur_frm.fields_dict['options_help'].disp_area.innerHTML = + __('Fieldname which will be the DocType for this link field.'); + } else { + cur_frm.fields_dict['options_help'].disp_area.innerHTML = ''; + } } diff --git a/frappe/core/doctype/custom_field/custom_field.json b/frappe/core/doctype/custom_field/custom_field.json index 58be09b1c4..55c404c902 100644 --- a/frappe/core/doctype/custom_field/custom_field.json +++ b/frappe/core/doctype/custom_field/custom_field.json @@ -57,7 +57,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Button\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime", + "options": "Button\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime", "permlevel": 0, "reqd": 1, "search_index": 0 @@ -257,7 +257,7 @@ ], "icon": "icon-glass", "idx": 1, - "modified": "2014-05-26 03:21:02.832530", + "modified": "2014-06-20 05:54:17.225853", "modified_by": "Administrator", "module": "Core", "name": "Custom Field", diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index b68a566d98..b1296d4056 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -34,7 +34,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime", + "options": "Attach\nButton\nCheck\nCode\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime", "permlevel": 0, "reqd": 1, "search_index": 1 @@ -304,7 +304,7 @@ "in_dialog": 1, "issingle": 0, "istable": 1, - "modified": "2014-05-26 03:00:13.705058", + "modified": "2014-06-20 05:42:29.975498", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 963c7dbb93..3ba9314cc0 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -157,6 +157,7 @@ class DocType(Document): def validate_fields_for_doctype(doctype): validate_fields(frappe.get_meta(doctype).get("fields")) +# this is separate because it is also called via custom field def validate_fields(fields): def check_illegal_characters(fieldname): for c in ['.', ',', ' ', '-', '&', '%', '=', '"', "'", '*', '$', @@ -200,6 +201,13 @@ def validate_fields(fields): if d.in_list_view and d.fieldtype!="Image" and (d.fieldtype in no_value_fields): frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)) + def check_dynamic_link_options(d): + if d.fieldtype=="Dynamic Link": + doctype_pointer = filter(lambda df: df.fieldname==d.options, fields) + if not doctype_pointer or (doctype_pointer[0].fieldtype!="Link") \ + or (doctype_pointer[0].options!="DocType"): + frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'")) + for d in fields: if not d.permlevel: d.permlevel = 0 if not d.fieldname: @@ -208,6 +216,7 @@ def validate_fields(fields): check_unique_fieldname(d.fieldname) check_illegal_mandatory(d) check_link_table_options(d) + check_dynamic_link_options(d) check_hidden_and_mandatory(d) check_in_list_view(d) diff --git a/frappe/core/doctype/event/event.json b/frappe/core/doctype/event/event.json index ec5e6ca3ad..de46143f73 100644 --- a/frappe/core/doctype/event/event.json +++ b/frappe/core/doctype/event/event.json @@ -218,24 +218,26 @@ }, { "fieldname": "ref_type", - "fieldtype": "Data", + "fieldtype": "Link", "hidden": 0, "label": "Ref Type", "no_copy": 0, "oldfieldname": "ref_type", "oldfieldtype": "Data", + "options": "DocType", "permlevel": 0, - "read_only": 1, + "read_only": 0, "search_index": 0 }, { "fieldname": "ref_name", - "fieldtype": "Data", + "fieldtype": "Dynamic Link", "hidden": 0, "label": "Ref Name", "no_copy": 0, "oldfieldname": "ref_name", "oldfieldtype": "Data", + "options": "ref_type", "permlevel": 0, "read_only": 1, "search_index": 0 @@ -244,7 +246,7 @@ "icon": "icon-calendar", "idx": 1, "in_create": 1, - "modified": "2014-05-27 03:49:10.612463", + "modified": "2014-06-20 06:40:05.415405", "modified_by": "Administrator", "module": "Core", "name": "Event", diff --git a/frappe/core/doctype/todo/todo.json b/frappe/core/doctype/todo/todo.json index 1139bf07bb..ec9e27e2b2 100644 --- a/frappe/core/doctype/todo/todo.json +++ b/frappe/core/doctype/todo/todo.json @@ -91,13 +91,14 @@ { "allow_on_submit": 0, "fieldname": "reference_type", - "fieldtype": "Data", + "fieldtype": "Link", "hidden": 0, "in_filter": 0, "label": "Reference Type", "no_copy": 0, "oldfieldname": "reference_type", "oldfieldtype": "Data", + "options": "DocType", "permlevel": 0, "print_hide": 0, "report_hide": 0, @@ -107,13 +108,14 @@ { "allow_on_submit": 0, "fieldname": "reference_name", - "fieldtype": "Data", + "fieldtype": "Dynamic Link", "hidden": 0, "in_filter": 0, "label": "Reference Name", "no_copy": 0, "oldfieldname": "reference_name", "oldfieldtype": "Data", + "options": "reference_type", "permlevel": 0, "print_hide": 0, "report_hide": 0, @@ -158,7 +160,7 @@ "in_dialog": 0, "issingle": 0, "max_attachments": 0, - "modified": "2014-05-27 03:49:21.667888", + "modified": "2014-06-20 06:20:11.947183", "modified_by": "Administrator", "module": "Core", "name": "ToDo", diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 7c84c5e654..b0600a96d4 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -264,11 +264,17 @@ class BaseDocument(object): return "{}: {}".format(_(df.label), docname) invalid_links = [] - for df in self.meta.get_link_fields(): - doctype = df.options + for df in self.meta.get_link_fields() + self.meta.get("fields", + {"fieldtype":"Dynamic Link"}): - if not doctype: - frappe.throw(_("Options not set for link field {0}").format(df.fieldname)) + if df.fieldtype=="Link": + doctype = df.options + if not doctype: + frappe.throw(_("Options not set for link field {0}").format(df.fieldname)) + else: + doctype = self.get(df.options) + if not doctype: + frappe.throw(_("{0} must be set first").format(self.meta.get_label(df.options))) docname = self.get(df.fieldname) if docname: diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index 488f623cea..7f2a013d8c 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -29,6 +29,7 @@ type_map = { ,'Text': ('text', '') ,'Data': ('varchar', '255') ,'Link': ('varchar', '255') + ,'Dynamic Link':('varchar', '255') ,'Password': ('varchar', '255') ,'Select': ('varchar', '255') ,'Read Only': ('varchar', '255') diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 56c334f33a..06b30b2168 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -8,6 +8,7 @@ import frappe.model.meta import frappe.defaults from frappe.utils.file_manager import remove_all from frappe import _ +from rename_doc import dynamic_link_queries def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False): """ @@ -48,6 +49,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa # check if links exist if not force: check_if_doc_is_linked(doc) + check_if_doc_is_dynamically_linked(doc) delete_from_table(doctype, name, ignore_doctypes, doc) @@ -106,3 +108,21 @@ def check_if_doc_is_linked(doc, method="Delete"): frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype, doc.name, item.parent or item.name, item.parenttype if item.parent else link_dt), frappe.LinkExistsError) + +def check_if_doc_is_dynamically_linked(doc): + for query in dynamic_link_queries: + for df in frappe.db.sql(query, as_dict=True): + if frappe.get_meta(df.parent).issingle: + + # dynamic link in single doc + refdoc = frappe.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, df.parent), frappe.LinkExistsError) + else: + + # dynamic link in table + for name in frappe.db.sql_list("""select name from `tab{parent}` where + {options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name)): + frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype, + doc.name, df.parent, name), frappe.LinkExistsError) diff --git a/frappe/model/document.py b/frappe/model/document.py index c5e18e60f7..37265cbd7e 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -494,7 +494,7 @@ class Document(BaseDocument): val1 = cint(val1) val2 = cint(val2) elif df.fieldtype in ("Data", "Text", "Small Text", "Long Text", - "Text Editor", "Select", "Link"): + "Text Editor", "Select", "Link", "Dynamic Link"): val1 = cstr(val1) val2 = cstr(val2) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 5201158d3b..0a05809c33 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -33,6 +33,8 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F link_fields = get_link_fields(doctype) update_link_field_values(link_fields, old, new, doctype) + rename_dynamic_links(doctype, old, new) + if doctype=='DocType': rename_doctype(doctype, old, new, force) @@ -274,3 +276,27 @@ def update_parenttype_values(old, new): update `tab%s` set parenttype=%s where parenttype=%s""" % (doctype, '%s', '%s'), (new, old)) + +dynamic_link_queries = [ + """select parent, fieldname, options from tabDocField where fieldtype='Dynamic Link'""", + """select dt as parent, fieldname, options from `tabCustom Field` where fieldtype='Dynamic Link'""", +] + +def rename_dynamic_links(doctype, old, new): + for query in dynamic_link_queries: + for df in frappe.db.sql(query, as_dict=True): + + # dynamic link in single, just one value to check + if frappe.get_meta(df.parent).issingle: + refdoc = frappe.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 + field=%s and value=%s""", (new, df.fieldname, old)) + else: + # replace for each value where renamed + for to_change in frappe.db.sql_list("""select name from `tab{parent}` where + {options}=%s and {fieldname}=%s""".format(**df), (doctype, old)): + + frappe.db.sql("""update `tab{parent}` set {fieldname}=%s + where name=%s""".format(**df), (new, to_change)) diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index 88e87f3bb8..a8c999f669 100644 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -1,7 +1,7 @@ // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt -frappe.ui.form.make_control = function(opts) { +frappe.ui.form.make_control = function (opts) { var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, ""); if(frappe.ui.form[control_class_name]) { return new frappe.ui.form[control_class_name](opts); @@ -802,32 +802,36 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ this.setup_buttons(); this.setup_autocomplete(); }, + get_options: function() { + return this.df.options; + }, setup_buttons: function() { var me = this; // magnifier - search this.$input_area.find(".btn-search").on("click", function() { + var doctype = me.get_options(); + if(!doctype) return; new frappe.ui.form.LinkSelector({ - doctype: me.df.options, + doctype: doctype, target: me, txt: me.get_value() }); }); // open - if(frappe.model.can_read(me.df.options)) { - this.$input_area.find(".btn-open").on("click", function() { - var value = me.get_value(); - if(value && me.df.options) frappe.set_route("Form", me.df.options, value); - }); - } else { - this.$input_area.find(".btn-open").remove(); - } + this.$input_area.find(".btn-open").on("click", function() { + var value = me.get_value(); + if(value && me.get_options()) + frappe.set_route("Form", me.get_options(), value); + }); // new - if(frappe.model.can_create(me.df.options)) { + if(this.df.fieldtype==="Dynamic Link" || frappe.model.can_create(me.df.options)) { this.$input_area.find(".btn-new").on("click", function() { - me.frm.new_doc(me.df.options, me); + var doctype = me.get_options(); + if(!doctype) return; + me.frm.new_doc(doctype, me); }); } else { this.$input_area.find(".btn-new").remove(); @@ -854,18 +858,20 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ this.$input.autocomplete({ minLength: 0, source: function(request, response) { - if (!me.$input.cache[me.df.options]) { - me.$input.cache[me.df.options] = {}; + var doctype = me.get_options(); + if(!doctype) return; + if (!me.$input.cache[doctype]) { + me.$input.cache[doctype] = {}; } - if (me.$input.cache[me.df.options][request.term]!=null) { + if (me.$input.cache[doctype][request.term]!=null) { // immediately show from cache - response(me.$input.cache[me.df.options][request.term]); + response(me.$input.cache[doctype][request.term]); } var args = { 'txt': request.term, - 'doctype': me.df.options, + 'doctype': doctype, }; me.set_custom_query(args); @@ -876,13 +882,13 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ no_spinner: true, args: args, callback: function(r) { - if(frappe.model.can_create(me.df.options)) { + if(frappe.model.can_create(doctype)) { r.results.push({ value: " " + __("Create a new {0}", [me.df.options]) + "", make_new: true }); }; - me.$input.cache[me.df.options][request.term] = r.results; + me.$input.cache[doctype][request.term] = r.results; response(r.results); }, }); @@ -901,10 +907,13 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ select: function(event, ui) { me.autocomplete_open = false; if(ui.item.make_new) { + var doctype = me.get_options(); + if(!doctype) return; + if (me.frm) { - me.frm.new_doc(me.df.options, me); + me.frm.new_doc(doctype, me); } else { - new_doc(me.df.options); + new_doc(doctype); } return false; } @@ -968,10 +977,21 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ return; } - this.frm.script_manager.validate_link_and_fetch(this.df, this.docname, value, callback); + this.frm.script_manager.validate_link_and_fetch(this.df, this.get_options(), + this.docname, value, callback); }, }); +frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ + get_options: function() { + var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); + if(!options) { + msgprint(__("Please set {0} first", + [frappe.meta.get_docfield(this.df.parent, this.df.options, this.docname).label])); + } + return options; + }, +}); frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ make_input: function() { diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index e99158a1ab..8d03823db6 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -33,6 +33,7 @@ frappe.form.formatters = { return value ? "" : ""; }, Link: function(value, docfield, options) { + var doctype = docfield._options || docfield.options; if(options && options.for_print) return value; if(!value) @@ -40,13 +41,13 @@ frappe.form.formatters = { if(docfield && docfield.link_onclick) { return repl('%(value)s', {onclick: docfield.link_onclick.replace(/"/g, '"'), value:value}); - } else if(docfield && docfield.options) { + } else if(docfield && doctype) { return repl('%(icon)s%(label)s', { - doctype: encodeURIComponent(docfield.options), + doctype: encodeURIComponent(doctype), name: encodeURIComponent(value), label: value, icon: (options && options.no_icon) ? "" : - (' ') + (' ') }); } else { return value; @@ -119,11 +120,20 @@ frappe.form.formatters = { } frappe.form.get_formatter = function(fieldtype) { - if(!fieldtype) fieldtype = "Data"; + if(!fieldtype) + fieldtype = "Data"; return frappe.form.formatters[fieldtype.replace(/ /g, "")] || frappe.form.formatters.Data; } frappe.format = function(value, df, options, doc) { if(!df) df = {"fieldtype":"Data"}; - return frappe.form.get_formatter(df.fieldtype)(value, df, options, doc); + var fieldtype = df.fieldtype || "Data"; + + // format Dynamic Link as a Link + if(fieldtype==="Dynamic Link") { + fieldtype = "Link"; + df._options = doc ? doc[df.options] : null; + } + + return frappe.form.get_formatter(fieldtype)(value, df, options, doc); } diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index 93cd5c8663..bd17d0a519 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -80,7 +80,7 @@ frappe.ui.form.ScriptManager = Class.extend({ console.log("----- end of error message -----"); console.group && console.groupEnd(); }, - validate_link_and_fetch: function(df, docname, value, callback) { + validate_link_and_fetch: function(df, doctype, docname, value, callback) { var me = this; if(value) { @@ -94,7 +94,7 @@ frappe.ui.form.ScriptManager = Class.extend({ type: "GET", args: { 'value': value, - 'options': df.options, + 'options': doctype, 'fetch': fetch }, no_spinner: true, diff --git a/frappe/public/js/frappe/ui/filters.js b/frappe/public/js/frappe/ui/filters.js index 0e1b90752e..bb24e36412 100644 --- a/frappe/public/js/frappe/ui/filters.js +++ b/frappe/public/js/frappe/ui/filters.js @@ -255,7 +255,7 @@ frappe.ui.Filter = Class.extend({ if(df.fieldtype=='Check') { df.fieldtype='Select'; df.options='No\nYes'; - } else if(['Text','Small Text','Text Editor','Code','Tag','Comments'].indexOf(df.fieldtype)!=-1) { + } else if(['Text','Small Text','Text Editor','Code','Tag','Comments','Dynamic Link'].indexOf(df.fieldtype)!=-1) { df.fieldtype = 'Data'; } else if(df.fieldtype=='Link' && this.$w.find('.condition').val()!="=") { df.fieldtype = 'Data'; diff --git a/frappe/public/js/frappe/views/doclistview.js b/frappe/public/js/frappe/views/doclistview.js index b1829d25c0..7738606fda 100644 --- a/frappe/public/js/frappe/views/doclistview.js +++ b/frappe/public/js/frappe/views/doclistview.js @@ -87,9 +87,11 @@ frappe.views.DocListView = frappe.ui.Listing.extend({ this.make_help(); this.$page.find(".show_filters").css({"padding":"15px", "margin":"0px -15px"}); var me = this; - // this.$w.on("render-complete", function() { - // me.set_sidebar_height(); - // }); + this.$w.on("render-complete", function() { + if(me.data.length===1) { + frappe.set_route("Form", me.doctype, me.data[0].name); + } + }); }, set_sidebar_height: function() {