diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index f40d519b92..821c81cad3 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -239,3 +239,20 @@ def make_variant_item_code(template_item_code, template_item_name, variant): 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 393e90c052..0730df96b2 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 0000000000..6a5f36da77 --- /dev/null +++ b/erpnext/public/js/templates/item_quick_entry.html @@ -0,0 +1,3 @@ +
{{ __("Variant Attributes") }}
+
+
\ 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 0000000000..ebb92dac8f --- /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(``, { + "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('%(item)s', { + 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:') + '

', + 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 113a2efb50..3c334c46b6 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -80,7 +80,7 @@ erpnext.stock.ItemDashboard = Class.extend({ $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result); } else { var message = __(" Currently no stock available in any warehouse") - $(" "+message+"").appendTo(this.result); + $(""+message+"").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 6f3fce5ba2..656ee69020 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -74,19 +74,8 @@ frappe.ui.form.on("Item", { } 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.ui.form.on("Item", { 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 @@ $.extend(erpnext.item, { // 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 b86ad46985..bfb4c6227e 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 6a6af3d583..d6c4b7d1db 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -30,14 +30,11 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 99% rename from erpnext/crm/doctype/item/test_item.js rename to erpnext/stock/doctype/item/tests/test_item.js index c9b14ca1a9..5e3524e5b6 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 058828cc3d..5186408721 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -57,11 +57,11 @@ class Warehouse(NestedSet): 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 6cf379472c..cbaaf10762 100644 --- a/erpnext/tests/ui/tests.txt +++ b/erpnext/tests/ui/tests.txt @@ -9,7 +9,7 @@ erpnext/accounts/doctype/shipping_rule/test_shipping_rule.js 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