[new feature] Product Configurator (via Item Quick Entry) (#11535)

* [Feature] Item Variant Creation from Quick Entry

* [minor] formatted js

* [minor] set 3 attribute per page instead of 5 in template

* [fix] fixed codecy issue

* [fix] label translation

* [minor] changed trigger event of item template

* [fix] moved item ui tests under stock

* [UI test] added test for item attribute

* [UI test] added test for creation of item variant from quick entry

* [fix] item variant ui test fixes

* [wip]

* [cleanup] item quick entry

* [remove] tests, fixtures were missing

* [refactor] use set_only_once in item
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index f40d519..821c81c 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -239,3 +239,20 @@
 	if abbreviations:
 		variant.item_code = "{0}-{1}".format(template_item_code, "-".join(abbreviations))
 		variant.item_name = "{0}-{1}".format(template_item_name, "-".join(abbreviations))
+
+@frappe.whitelist()
+def create_variant_doc_for_quick_entry(template, args):
+	variant_based_on = frappe.db.get_value("Item", template, "variant_based_on")
+	args = json.loads(args)
+	if variant_based_on == "Manufacturer":
+		variant = get_variant(template, **args)
+	else:
+		existing_variant = get_variant(template, args)
+		if existing_variant:
+			return existing_variant
+		else:
+			variant = create_variant(template, args=args)
+			variant.name = variant.item_code
+			validate_item_variant_attributes(variant, args)
+	return variant.as_dict()
+
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 393e90c..0730df9 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -32,7 +32,9 @@
         "public/js/utils/item_selector.js",
         "public/js/help_links.js",
         "public/js/schools/student_button.html",
-        "public/js/schools/assessment_result_tool.html"
+        "public/js/schools/assessment_result_tool.html",
+        "public/js/templates/item_quick_entry.html",
+        "public/js/utils/item_quick_entry.js"
     ],
     "js/item-dashboard.min.js": [
         "stock/dashboard/item_dashboard.html",
diff --git a/erpnext/public/js/templates/item_quick_entry.html b/erpnext/public/js/templates/item_quick_entry.html
new file mode 100644
index 0000000..6a5f36d
--- /dev/null
+++ b/erpnext/public/js/templates/item_quick_entry.html
@@ -0,0 +1,3 @@
+<div class="h6 uppercase" style="margin-top: 30px;">{{ __("Variant Attributes") }}</div>
+<div class="attributes hide-control">
+</div>
\ No newline at end of file
diff --git a/erpnext/public/js/utils/item_quick_entry.js b/erpnext/public/js/utils/item_quick_entry.js
new file mode 100644
index 0000000..ebb92da
--- /dev/null
+++ b/erpnext/public/js/utils/item_quick_entry.js
@@ -0,0 +1,408 @@
+frappe.provide('frappe.ui.form');
+
+frappe.ui.form.ItemQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({
+	init: function(doctype, after_insert) {
+		this._super(doctype, after_insert);
+	},
+
+	render_dialog: function() {
+		this.mandatory = this.get_variant_fields().concat(this.mandatory);
+		this.mandatory = this.mandatory.concat(this.get_attributes_fields());
+		this._super();
+		this.init_post_render_dialog_operations();
+		this.preset_fields_for_template();
+		this.dialog.$wrapper.find('.edit-full').text(__('Edit in full page for more options like assets, serial nos, batches etc.'))
+	},
+
+	init_post_render_dialog_operations: function() {
+		this.dialog.fields_dict.attribute_html.$wrapper.append(frappe.render_template("item_quick_entry"));
+		this.init_for_create_variant_trigger();
+		this.init_for_item_template_trigger();
+		// explicitly hide manufacturing fields as hidden not working.
+		this.toggle_manufacturer_fields();
+		this.dialog.get_field("item_template").df.hidden = 1;
+		this.dialog.get_field("item_template").refresh();
+	},
+
+	register_primary_action: function() {
+		var me = this;
+		this.dialog.set_primary_action(__('Save'), function() {
+			if (me.dialog.working) return;
+
+			var data = me.dialog.get_values();
+			var variant_values = {};
+
+			if (me.dialog.fields_dict.create_variant.$input.prop("checked")) {
+				variant_values = me.get_variant_doc();
+				if (!Object.keys(variant_values).length) {
+					data = null;
+				}
+				variant_values.stock_uom = me.template_doc.stock_uom;
+				variant_values.item_group = me.template_doc.item_group;
+			}
+
+			if (data) {
+				me.dialog.working = true;
+				var values = me.update_doc();
+				//patch for manufacturer type variants as extend is overwriting it.
+				if (variant_values['variant_based_on'] == "Manufacturer") {
+					values['variant_based_on'] = "Manufacturer";
+				}
+				$.extend(variant_values, values);
+				me.insert(variant_values);
+			}
+		});
+	},
+
+	insert: function(variant_values) {
+		let me = this;
+		return new Promise(resolve => {
+			frappe.call({
+				method: "frappe.client.insert",
+				args: {
+					doc: variant_values
+				},
+				callback: function(r) {
+					me.dialog.hide();
+					// delete the old doc
+					frappe.model.clear_doc(me.dialog.doc.doctype, me.dialog.doc.name);
+					me.dialog.doc = r.message;
+					if (frappe._from_link) {
+						frappe.ui.form.update_calling_link(me.dialog.doc);
+					} else {
+						if (me.after_insert) {
+							me.after_insert(me.dialog.doc);
+						} else {
+							me.open_from_if_not_list();
+						}
+					}
+				},
+				error: function() {
+					me.open_doc();
+				},
+				always: function() {
+					me.dialog.working = false;
+					resolve(me.dialog.doc);
+				},
+				freeze: true
+			});
+		});
+	},
+
+	open_doc: function() {
+		this.dialog.hide();
+		this.update_doc();
+		if (this.dialog.fields_dict.create_variant.$input.prop("checked")) {
+			var template = this.dialog.fields_dict.item_template.input.value;
+			if (template)
+				frappe.set_route("Form", this.doctype, template);
+		} else {
+			frappe.set_route('Form', this.doctype, this.doc.name);
+		}
+	},
+
+	get_variant_fields: function() {
+		var variant_fields = [{
+			fieldname: "create_variant",
+			fieldtype: "Check",
+			label: __("Create Variant")
+		},
+		{
+			fieldname: 'item_template',
+			label: __('Item Template'),
+			reqd: 0,
+			fieldtype: 'Link',
+			options: "Item",
+			get_query: function() {
+				return {
+					filters: {
+						"has_variants": 1
+					}
+				};
+			}
+		}];
+
+		return variant_fields;
+	},
+
+	get_manufacturing_fields: function() {
+		this.manufacturer_fields = [{
+			fieldtype: 'Link',
+			options: 'Manufacturer',
+			label: 'Manufacturer',
+			fieldname: "manufacturer",
+			hidden: 1,
+			reqd: 0
+		}, {
+			fieldtype: 'Data',
+			label: 'Manufacturer Part Number',
+			fieldname: 'manufacturer_part_no',
+			hidden: 1,
+			reqd: 0
+		}];
+		return this.manufacturer_fields;
+	},
+
+	get_attributes_fields: function() {
+		var attribute_fields = [{
+			fieldname: 'attribute_html',
+			fieldtype: 'HTML'
+		}]
+
+		attribute_fields = attribute_fields.concat(this.get_manufacturing_fields());
+		return attribute_fields;
+	},
+
+	init_for_create_variant_trigger: function() {
+		var me = this;
+
+		this.dialog.fields_dict.create_variant.$input.on("click", function() {
+			me.preset_fields_for_template();
+			me.init_post_template_trigger_operations(false, [], true);
+		});
+	},
+
+	preset_fields_for_template: function() {
+		var for_variant = this.dialog.get_value('create_variant');
+
+		// setup template field, seen and mandatory if variant
+		let template_field = this.dialog.get_field("item_template");
+		template_field.df.reqd = for_variant;
+		template_field.set_value('');
+		template_field.df.hidden = !for_variant;
+		template_field.refresh();
+
+		// hide properties for variant
+		['item_code', 'item_name', 'item_group', 'stock_uom'].forEach((d) => {
+			let f = this.dialog.get_field(d);
+			f.df.hidden = for_variant;
+			f.refresh();
+		});
+
+		this.dialog.get_field('attribute_html').toggle(false);
+
+		// non mandatory for variants
+		['item_code', 'stock_uom', 'item_group'].forEach((d) => {
+			let f = this.dialog.get_field(d);
+			f.df.reqd = !for_variant;
+			f.refresh();
+		});
+
+	},
+
+	init_for_item_template_trigger: function() {
+		var me = this;
+
+		me.dialog.fields_dict["item_template"].df.onchange = () => {
+			var template = me.dialog.fields_dict.item_template.input.value;
+			me.template_doc = null;
+			if (template) {
+				frappe.call({
+					method: "frappe.client.get",
+					args: {
+						doctype: "Item",
+						name: template
+					},
+					callback: function(r) {
+						me.template_doc = r.message;
+						me.is_manufacturer = false;
+
+						if (me.template_doc.variant_based_on === "Manufacturer") {
+							me.init_post_template_trigger_operations(true, [], true);
+						} else {
+
+							me.init_post_template_trigger_operations(false, me.template_doc.attributes, false);
+							me.render_attributes(me.template_doc.attributes);
+						}
+					}
+				});
+			} else {
+				me.dialog.get_field('attribute_html').toggle(false);
+				me.init_post_template_trigger_operations(false, [], true);
+			}
+		}
+	},
+
+	init_post_template_trigger_operations: function(is_manufacturer, attributes, attributes_flag) {
+		this.attributes = attributes;
+		this.attribute_values = {};
+		this.attributes_count = attributes.length;
+
+		this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty();
+		this.is_manufacturer = is_manufacturer;
+		this.toggle_manufacturer_fields();
+		this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").toggleClass("hide-control", attributes_flag);
+		this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes-header").toggleClass("hide-control", attributes_flag);
+	},
+
+	toggle_manufacturer_fields: function() {
+		var me = this;
+		$.each(this.manufacturer_fields, function(i, dialog_field) {
+			me.dialog.get_field(dialog_field.fieldname).df.hidden = !me.is_manufacturer;
+			me.dialog.get_field(dialog_field.fieldname).df.reqd = dialog_field.fieldname == 'manufacturer' ? me.is_manufacturer : false;
+			me.dialog.get_field(dialog_field.fieldname).refresh();
+		});
+	},
+
+	initiate_render_attributes: function() {
+		this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty();
+		this.render_attributes(this.attributes);
+	},
+
+	render_attributes: function(attributes) {
+		var me = this;
+
+		this.dialog.get_field('attribute_html').toggle(true);
+
+		$.each(attributes, function(index, row) {
+			var desc = "";
+			var fieldtype = "Data";
+			if (row.numeric_values) {
+				fieldtype = "Float";
+				desc = "Min Value: " + row.from_range + " , Max Value: " + row.to_range + ", in Increments of: " + row.increment;
+			}
+
+			me.init_make_control(fieldtype, row);
+			me[row.attribute].set_value(me.attribute_values[row.attribute] || "");
+			me[row.attribute].$wrapper.toggleClass("has-error", me.attribute_values[row.attribute] ? false : true);
+
+			// Set Label explicitly as make_control is not displaying label
+			$(me[row.attribute].label_area).text(__(row.attribute));
+
+			if (desc) {
+				$(repl(`<p class="help-box small text-muted hidden-xs">%(desc)s</p>`, {
+					"desc": desc
+				})).insertAfter(me[row.attribute].input_area);
+			}
+
+			if (!row.numeric_values) {
+				me.init_awesomplete_for_attribute(row);
+			} else {
+				me[row.attribute].$input.on("change", function() {
+					me.attribute_values[row.attribute] = $(this).val();
+					$(this).closest(".frappe-control").toggleClass("has-error", $(this).val() ? false : true);
+				});
+			}
+		});
+	},
+
+	init_make_control: function(fieldtype, row) {
+		this[row.attribute] = frappe.ui.form.make_control({
+			df: {
+				"fieldtype": fieldtype,
+				"label": row.attribute,
+				"fieldname": row.attribute,
+				"options": row.options || ""
+			},
+			parent: $(this.dialog.fields_dict.attribute_html.wrapper).find(".attributes"),
+			only_input: false
+		});
+		this[row.attribute].make_input();
+	},
+
+	init_awesomplete_for_attribute: function(row) {
+		var me = this;
+
+		this[row.attribute].input.awesomplete = new Awesomplete(this[row.attribute].input, {
+			minChars: 0,
+			maxItems: 99,
+			autoFirst: true,
+			list: [],
+		});
+
+		this[row.attribute].$input.on('input', function(e) {
+			frappe.call({
+				method: "frappe.client.get_list",
+				args: {
+					doctype: "Item Attribute Value",
+					filters: [
+						["parent", "=", $(e.target).attr("data-fieldname")],
+						["attribute_value", "like", e.target.value + "%"]
+					],
+					fields: ["attribute_value"]
+				},
+				callback: function(r) {
+					if (r.message) {
+						e.target.awesomplete.list = r.message.map(function(d) {
+							return d.attribute_value;
+						});
+					}
+				}
+			});
+		}).on('focus', function(e) {
+			$(e.target).val('').trigger('input');
+		}).on("awesomplete-close", function (e) {
+			me.attribute_values[$(e.target).attr("data-fieldname")] = e.target.value;
+			$(e.target).closest(".frappe-control").toggleClass("has-error", e.target.value ? false : true);
+		});
+	},
+
+	get_variant_doc: function() {
+		var me = this;
+		var variant_doc = {};
+		var attribute = this.validate_mandatory_attributes();
+
+		if (Object.keys(attribute).length) {
+			frappe.call({
+				method: "erpnext.controllers.item_variant.create_variant_doc_for_quick_entry",
+				args: {
+					"template": me.dialog.fields_dict.item_template.$input.val(),
+					args: attribute
+				},
+				async: false,
+				callback: function(r) {
+					if (Object.prototype.toString.call(r.message) == "[object Object]") {
+						variant_doc = r.message;
+					} else {
+						var msgprint_dialog = frappe.msgprint(__("Item Variant {0} already exists with same attributes", [repl('<a class="strong variant-click" data-item-code="%(item)s" \
+								>%(item)s</a>', {
+								item: r.message
+							})]));
+
+						msgprint_dialog.$wrapper.find(".variant-click").on("click", function() {
+							msgprint_dialog.hide();
+							me.dialog.hide();
+							if (frappe._from_link) {
+								frappe._from_link.set_value($(this).attr("data-item-code"));
+							} else {
+								frappe.set_route('Form', "Item", $(this).attr("data-item-code"));
+							}
+						});
+					}
+				}
+			})
+		}
+		return variant_doc;
+	},
+
+	validate_mandatory_attributes: function() {
+		var me = this;
+		var attribute = {};
+		var mandatory = [];
+
+		$.each(this.attributes, function(index, attr) {
+			var value = me.attribute_values[attr.attribute] || "";
+			if (value) {
+				attribute[attr.attribute] = attr.numeric_values ? flt(value) : value;
+			} else {
+				mandatory.push(attr.attribute);
+			}
+		})
+
+		if (mandatory.length) {
+			frappe.msgprint({
+				title: __('Missing Values Required'),
+				message: __('Following fields have missing values:') + '<br><br><ul><li>' + mandatory.join('<li>') + '</ul>',
+				indicator: 'orange'
+			});
+			return {};
+		}
+
+		if (this.is_manufacturer) {
+			$.each(this.manufacturer_fields, function(index, field) {
+				attribute[field.fieldname] = me.dialog.fields_dict[field.fieldname].input.value;
+			});
+		}
+		return attribute;
+	}
+});
\ No newline at end of file
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 113a2ef..3c334c4 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -80,7 +80,7 @@
 			$(frappe.render_template('item_dashboard_list', context)).appendTo(this.result);
 		} else {
 			var message = __(" Currently no stock available in any warehouse")
-			$("<span class='small'> <i class='fa fa-exclamation-triangle' aria-hidden='true'></i>"+message+"</span>").appendTo(this.result);
+			$("<span class='text-muted small'>"+message+"</span>").appendTo(this.result);
 		}
 	},
 	get_item_dashboard_data: function(data, max_count, show_item) {
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 6f3fce5..656ee69 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -74,19 +74,8 @@
 		}
 
 		erpnext.item.edit_prices_button(frm);
-
-		// make sensitive fields(has_serial_no, is_stock_item, valuation_method, has_batch_no)
-		// read only if any stock ledger entry exists
-		if (!frm.doc.__islocal && frm.doc.is_stock_item) {
-			frm.toggle_enable(['has_serial_no', 'is_stock_item', 'valuation_method', 'has_batch_no'],
-				(frm.doc.__onload && frm.doc.__onload.sle_exists=="exists") ? false : true);
-		}
-
 		erpnext.item.toggle_attributes(frm);
 
-		frm.toggle_enable("is_fixed_asset", (frm.doc.__islocal || (!frm.doc.is_stock_item &&
-			((frm.doc.__onload && frm.doc.__onload.asset_exists) ? false : true))));
-
 		frm.add_custom_button(__('Duplicate'), function() {
 			var new_item = frappe.model.copy_doc(frm.doc);
 			if(new_item.item_name===new_item.item_code) {
@@ -103,25 +92,18 @@
 				frappe.set_route("Form", "Item Variant Settings");
 			}, __("View"));
 		}
-
-		if(frm.doc.__onload && frm.doc.__onload.stock_exists) {
-			// Hide variants section if stock exists
-			frm.toggle_display("variants_section", 0);
-		}
 	},
 
 	validate: function(frm){
 		erpnext.item.weight_to_validate(frm);
 	},
 
-	image: function(frm) {
+	image: function() {
 		refresh_field("image_view");
 	},
 
 	is_fixed_asset: function(frm) {
-		if (frm.doc.is_fixed_asset) {
-			frm.set_value("is_stock_item", 0);
-		}
+		frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1);
 	},
 
 	page_name: frappe.utils.warn_page_name_change,
@@ -469,5 +451,6 @@
 			// nothing to do with attributes, hide it
 			frm.toggle_display("attributes", false);
 		}
+		frm.layout.refresh_sections();
 	}
 });
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index b86ad46..bfb4c62 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -138,7 +138,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -358,7 +358,7 @@
   {
    "allow_bulk_edit": 0, 
    "allow_on_submit": 0, 
-   "bold": 0, 
+   "bold": 1, 
    "collapsible": 0, 
    "columns": 0, 
    "default": "1", 
@@ -384,9 +384,9 @@
    "read_only": 0, 
    "remember_last_selected_value": 0, 
    "report_hide": 0, 
-   "reqd": 1, 
+   "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -508,7 +508,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -891,7 +891,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -1180,7 +1180,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -1307,7 +1307,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -1437,7 +1437,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -1446,7 +1446,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
-   "depends_on": "eval:doc.has_variants && doc.variant_based_on==='Item Attribute'", 
+   "depends_on": "eval:(doc.has_variants || doc.variant_of) && doc.variant_based_on==='Item Attribute'", 
    "fieldname": "attributes", 
    "fieldtype": "Table", 
    "hidden": 1, 
@@ -1469,7 +1469,7 @@
    "report_hide": 0, 
    "reqd": 0, 
    "search_index": 0, 
-   "set_only_once": 0, 
+   "set_only_once": 1, 
    "unique": 0
   }, 
   {
@@ -3360,7 +3360,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 1, 
- "modified": "2017-11-01 11:53:52.060505", 
+ "modified": "2017-11-13 15:49:13.213990", 
  "modified_by": "Administrator", 
  "module": "Stock", 
  "name": "Item", 
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 6a6af3d..d6c4b7d 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -30,14 +30,11 @@
 	def onload(self):
 		super(Item, self).onload()
 
-		self.set_onload('sle_exists', self.check_if_sle_exists())
+		self.set_onload('stock_exists', self.stock_ledger_created())
 		if self.is_fixed_asset:
 			asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
 			self.set_onload("asset_exists", True if asset else False)
 
-		if frappe.db.get_value('Stock Ledger Entry', {'item_code': self.name}):
-			self.set_onload('stock_exists', True)
-
 	def autoname(self):
 		if frappe.db.get_default("item_naming_by")=="Naming Series":
 			if self.variant_of:
@@ -60,6 +57,9 @@
 		if self.is_sales_item and not self.get('is_item_from_hub'):
 			self.publish_in_hub = 1
 
+	def before_save(self):
+		self.get_doc_before_save()
+
 	def after_insert(self):
 		'''set opening stock and item price'''
 		if self.standard_rate:
@@ -89,7 +89,6 @@
 		self.fill_customer_code()
 		self.check_item_tax()
 		self.validate_barcode()
-		self.cant_change()
 		self.validate_warehouse_for_reorder()
 		self.update_bom_item_desc()
 		self.synced_with_hub = 0
@@ -247,6 +246,14 @@
 			if not self.asset_category:
 				frappe.throw(_("Asset Category is mandatory for Fixed Asset item"))
 
+			if self.stock_ledger_created():
+				frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created."))
+
+		if not self.is_fixed_asset:
+			asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
+			if asset:
+				frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item'))
+
 	def get_context(self, context):
 		context.show_search=True
 		context.search_link = '/product_search'
@@ -456,62 +463,27 @@
 			if duplicate:
 				frappe.throw(_("Barcode {0} already used in Item {1}").format(self.barcode, duplicate[0][0]))
 
-	def cant_change(self):
-		if not self.get("__islocal"):
-			to_check = ("has_serial_no", "is_stock_item",
-				"valuation_method", "has_batch_no", "is_fixed_asset")
 
-			vals = frappe.db.get_value("Item", self.name, to_check, as_dict=True)
-			if not vals.get('valuation_method') and self.get('valuation_method'):
-				vals['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO"
-
-			if vals:
-				for key in to_check:
-					if cstr(self.get(key)) != cstr(vals.get(key)):
-						if not self.check_if_linked_document_exists(key):
-							break # no linked document, allowed
-						else:
-							frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(key))))
-
-			if vals and not self.is_fixed_asset and self.is_fixed_asset != vals.is_fixed_asset:
-				asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
-				if asset:
-					frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item'))
-
-	def check_if_linked_document_exists(self, key):
-		linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "Purchase Receipt Item",
-			"Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"]
-
-		# For "Is Stock Item", following doctypes is important
-		# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
-		if key == "is_stock_item":
-			linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item"]
-
-		for doctype in linked_doctypes:
-			if frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \
-				frappe.db.get_value("Production Order",
-					filters={"production_item": self.name, "docstatus": 1}):
-				return True
-
+	def validate_warehouse_for_reorder(self):
+		'''Validate Reorder level table for duplicate and conditional mandatory'''
+		warehouse = []
 		for d in self.get("reorder_levels"):
+			if not d.warehouse_group:
+				d.warehouse_group = d.warehouse
+			if d.get("warehouse") and d.get("warehouse") not in warehouse:
+				warehouse += [d.get("warehouse")]
+			else:
+				frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}")
+					.format(d.idx, d.warehouse), DuplicateReorderRows)
+
 			if d.warehouse_reorder_level and not d.warehouse_reorder_qty:
 				frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx))
 
-	def validate_warehouse_for_reorder(self):
-		warehouse = []
-		for i in self.get("reorder_levels"):
-			if not i.warehouse_group:
-				i.warehouse_group = i.warehouse
-			if i.get("warehouse") and i.get("warehouse") not in warehouse:
-				warehouse += [i.get("warehouse")]
-			else:
-				frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}")
-					.format(i.idx, i.warehouse), DuplicateReorderRows)
-
-	def check_if_sle_exists(self):
-		sle = frappe.db.sql("""select name from `tabStock Ledger Entry`
-			where item_code = %s""", self.name)
-		return sle and 'exists' or 'not exists'
+	def stock_ledger_created(self):
+		if not hasattr(self, '_stock_ledger_created'):
+			self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry`
+				where item_code = %s limit 1""", self.name))
+		return self._stock_ledger_created
 
 	def validate_name_with_item_group(self):
 		# causes problem with tree build
@@ -638,19 +610,19 @@
 				template_item.save()
 
 	def update_variants(self):
-			if self.flags.dont_update_variants or \
-				frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'):
-				return
-			if self.has_variants:
-				updated = []
-				variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name })
-				for d in variants:
-					variant = frappe.get_doc("Item", d)
-					copy_attributes_to_variant(self, variant)
-					variant.save()
-					updated.append(d.item_code)
-				if updated:
-					frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
+		if self.flags.dont_update_variants or \
+			frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'):
+			return
+		if self.has_variants:
+			updated = []
+			variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name })
+			for d in variants:
+				variant = frappe.get_doc("Item", d)
+				copy_attributes_to_variant(self, variant)
+				variant.save()
+				updated.append(d.item_code)
+			if updated:
+				frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
 
 	def validate_has_variants(self):
 		if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
@@ -658,10 +630,15 @@
 				frappe.throw(_("Item has variants."))
 
 	def validate_stock_exists_for_template_item(self):
-		if self.has_variants and \
-			frappe.db.get_value('Stock Ledger Entry', {'item_code': self.name}):
-			frappe.throw(_("As stock exists against an item {0}, you can not enable has variants property")
-				.format(self.name), StockExistsForTemplate)
+		if self.stock_ledger_created():
+			if (self._doc_before_save.has_variants != self.has_variants or
+				self.variant_of != self._doc_before_save.variant_of):
+				frappe.throw(_("Cannot change Variant properties after stock transction. You will have to make a new Item to do this.").format(self.name),
+					StockExistsForTemplate)
+
+			if self.has_variants or self.variant_of:
+				if not self.is_child_table_same('attributes'):
+					frappe.throw(_('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item'))
 
 	def validate_uom(self):
 		if not self.get("__islocal"):
diff --git a/erpnext/crm/doctype/item/test_item.js b/erpnext/stock/doctype/item/tests/test_item.js
similarity index 98%
rename from erpnext/crm/doctype/item/test_item.js
rename to erpnext/stock/doctype/item/tests/test_item.js
index c9b14ca..5e3524e 100644
--- a/erpnext/crm/doctype/item/test_item.js
+++ b/erpnext/stock/doctype/item/tests/test_item.js
@@ -1,3 +1,4 @@
+QUnit.module('stock');
 QUnit.test("test: item", function (assert) {
 	assert.expect(6);
 	let done = assert.async();
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 058828c..5186408 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -57,11 +57,11 @@
 
 	def check_if_sle_exists(self):
 		return frappe.db.sql("""select name from `tabStock Ledger Entry`
-			where warehouse = %s""", self.name)
+			where warehouse = %s limit 1""", self.name)
 
 	def check_if_child_exists(self):
 		return frappe.db.sql("""select name from `tabWarehouse`
-			where parent_warehouse = %s""", self.name)
+			where parent_warehouse = %s limit 1""", self.name)
 
 	def before_rename(self, old_name, new_name, merge=False):
 		super(Warehouse, self).before_rename(old_name, new_name, merge)
diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt
index 6cf3794..cbaaf10 100644
--- a/erpnext/tests/ui/tests.txt
+++ b/erpnext/tests/ui/tests.txt
@@ -9,7 +9,7 @@
 erpnext/crm/doctype/lead/test_lead.js
 erpnext/crm/doctype/opportunity/test_opportunity.js
 erpnext/setup/doctype/company/tests/test_company_production.js
-erpnext/crm/doctype/item/test_item.js
+erpnext/stock/doctype/item/tests/test_item.js
 erpnext/selling/doctype/quotation/tests/test_quotation_with_discount_on_grand_total.js
 erpnext/selling/doctype/quotation/tests/test_quotation_with_item_wise_discount.js
 erpnext/selling/doctype/quotation/tests/test_quotation_with_multi_uom.js