diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index f8c9c0a841..53421b71bc 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -12,19 +12,42 @@ class InvalidItemAttributeValueError(frappe.ValidationError): pass class ItemTemplateCannotHaveStock(frappe.ValidationError): pass @frappe.whitelist() -def get_variant(template, args, variant=None): - """Validates Attributes and their Values, then looks for an exactly matching Item Variant +def get_variant(template, args=None, variant=None, manufacturer=None, + manufacturer_part_no=None): + """Validates Attributes and their Values, then looks for an exactly + matching Item Variant :param item: Template Item :param args: A dictionary with "Attribute" as key and "Attribute Value" as value """ - if isinstance(args, basestring): - args = json.loads(args) + item_template = frappe.get_doc('Item', template) - if not args: - frappe.throw(_("Please specify at least one attribute in the Attributes table")) + if item_template.variant_based_on=='Manufacturer' and manufacturer: + return make_variant_based_on_manufacturer(item_template, manufacturer, + manufacturer_part_no) + else: + if isinstance(args, basestring): + args = json.loads(args) - return find_variant(template, args, variant) + if not args: + frappe.throw(_("Please specify at least one attribute in the Attributes table")) + return find_variant(template, args, variant) + +def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part_no): + '''Make and return a new variant based on manufacturer and + manufacturer part no''' + from frappe.model.naming import append_number_if_name_exists + + variant = frappe.new_doc('Item') + + copy_attributes_to_variant(template, variant) + + variant.manufacturer = manufacturer + variant.manufacturer_part_no = manufacturer_part_no + + variant.item_code = append_number_if_name_exists('Item', template.name) + + return variant def validate_item_variant_attributes(item, args=None): if isinstance(item, basestring): @@ -131,6 +154,7 @@ def create_variant(item, args): template = frappe.get_doc("Item", item) variant = frappe.new_doc("Item") + variant.variant_based_on = 'Item Attribute' variant_attributes = [] for d in template.attributes: @@ -147,17 +171,28 @@ def create_variant(item, args): def copy_attributes_to_variant(item, variant): from frappe.model import no_value_fields + + # copy non no-copy fields + + exclude_fields = ["item_code", "item_name", "show_in_website"] + + if item.variant_based_on=='Manufacturer': + # don't copy manufacturer values if based on part no + exclude_fields += ['manufacturer', 'manufacturer_part_no'] + for field in item.meta.fields: if field.fieldtype not in no_value_fields and (not field.no_copy)\ - and field.fieldname not in ("item_code", "item_name", "show_in_website"): + and field.fieldname not in exclude_fields: if variant.get(field.fieldname) != item.get(field.fieldname): variant.set(field.fieldname, item.get(field.fieldname)) variant.variant_of = item.name variant.has_variants = 0 - if variant.attributes: - variant.description += "\n" - for d in variant.attributes: - variant.description += "

" + d.attribute + ": " + cstr(d.attribute_value) + "

" + + if item.variant_based_on=='Item Attribute': + if variant.attributes: + variant.description += "\n" + for d in variant.attributes: + variant.description += "

" + d.attribute + ": " + cstr(d.attribute_value) + "

" def make_variant_item_code(template_item_code, variant): """Uses template's item code and abbreviations to make variant's item code""" diff --git a/erpnext/docs/assets/img/stock/select-mfg-for-variant.png b/erpnext/docs/assets/img/stock/select-mfg-for-variant.png new file mode 100644 index 0000000000..4da1d6ce9e Binary files /dev/null and b/erpnext/docs/assets/img/stock/select-mfg-for-variant.png differ diff --git a/erpnext/docs/assets/img/stock/set-variant-by-mfg.png b/erpnext/docs/assets/img/stock/set-variant-by-mfg.png new file mode 100644 index 0000000000..2eaa8f0fa1 Binary files /dev/null and b/erpnext/docs/assets/img/stock/set-variant-by-mfg.png differ diff --git a/erpnext/docs/user/manual/en/stock/item/item-variants.md b/erpnext/docs/user/manual/en/stock/item/item-variants.md index cdca6ed961..7514404a6d 100644 --- a/erpnext/docs/user/manual/en/stock/item/item-variants.md +++ b/erpnext/docs/user/manual/en/stock/item/item-variants.md @@ -1,15 +1,28 @@ +# Item Variants + +### What are Variants? + A Item Variant is a version of a Item, such as differing sizes or differing colours (like a _blue_ t-shirt in size _small_ rather then just a t-shirt). -Without Item variants, you would have to treat the _small, medium_ and _large_ versions of a t-shirt as three separate Items; +Without Item variants, you would have to treat the _small, medium_ and _large_ versions of a t-shirt as three separate Items; Item variants let you treat the _small, medium_ and _large_ versions of a t-shirt as variations of the one Item 't-shirt'. +### Using Variants + +Variants can be based on two things + +1. Item Attributes +1. Manufacturers + +### Variants Based on Item Attributes + To use Item Variants in ERPNext, create an Item and check 'Has Variants'. -* The Item shall then be referred to as a so called 'Template'. Such a Template is not identical to a regular 'Item' any longer. For example it (the Template) can not be used directly in any Transactions (Sales Order, Delivery Note, Purchase Invoice) itself. Only the Variants of an Item (_blue_ t-shirt in size _small)_ can be practically used in such. Therefore it would be ideal to decide whether an item 'Has Variants' or not directly when creating it. +* The Item shall then be referred to as a so called 'Template'. Such a Template is not identical to a regular 'Item' any longer. For example it (the Template) can not be used directly in any Transactions (Sales Order, Delivery Note, Purchase Invoice) itself. Only the Variants of an Item (_blue_ t-shirt in size _small)_ can be practically used in such. Therefore it would be ideal to decide whether an item 'Has Variants' or not directly when creating it. Has Variants On selecting 'Has Variants' a table shall appear. Specify the variant attributes for the Item in the table. -In case the attribute has Numeric Values, you can specify the range and increment values here. +In case the attribute has Numeric Values, you can specify the range and increment values here. Valid Attributes @@ -22,3 +35,17 @@ To create 'Item Variants' against a 'Template' select 'Make Variants' Make Variants To learn more about setting Attributes Master check [Item Attributes]({{docs_base_url}}/user/manual/en/stock/setup/item-attribute.html) + +### Variants Based on Manufacturers + +To setup variants based on Manufactueres, in your Item template, set "Variants Based On" as "Manufacturers" + +Setup Item Variant by Manufacturer + +When you make a new Variant, the system will prompt you to select a Manufacturer. You can also optionally put in a Manufacturer Part Number + +Setup Item Variant by Manufacturer + +The naming of the variant will be the name (ID) of the template Item with a number suffix. e.g. "ITEM000" will have variant "ITEM000-1" \ No newline at end of file diff --git a/erpnext/public/css/erpnext.css b/erpnext/public/css/erpnext.css index 697c078289..a51472feaa 100644 --- a/erpnext/public/css/erpnext.css +++ b/erpnext/public/css/erpnext.css @@ -327,4 +327,3 @@ body[data-route="pos"] .btn-more { body[data-route="pos"] .collapse-btn { cursor: pointer; } - diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 891f37cb28..f149baf16b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -177,11 +177,11 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( fields: [ {fieldtype:'Read Only', fieldname:'item_code', label: __('Item Code'), in_list_view:1}, - {fieldtype:'Link', fieldname:'bom', options: 'BOM', + {fieldtype:'Link', fieldname:'bom', options: 'BOM', reqd: 1, label: __('Select BOM'), in_list_view:1, get_query: function(doc) { return {filters: {item: doc.item_code}}; }}, - {fieldtype:'Float', fieldname:'pending_qty', + {fieldtype:'Float', fieldname:'pending_qty', reqd: 1, label: __('Qty'), in_list_view:1}, ], get_data: function() { diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 7068c9972b..8327ea7497 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -261,6 +261,44 @@ $.extend(erpnext.item, { make_variant: function(frm) { var fields = [] + if(frm.doc.variant_based_on==="Item Attribute") { + erpnext.item.show_modal_for_item_attribute_selection(frm); + } else { + erpnext.item.show_modal_for_manufacturers(frm); + } + }, + + show_modal_for_manufacturers: function(frm) { + var dialog = new frappe.ui.Dialog({ + fields: [ + {fieldtype:'Link', options:'Manufacturer', + reqd:1, label:'Manufacturer'}, + {fieldtype:'Data', label:'Manufacturer Part Number', + fieldname: 'manufacturer_part_no'}, + ] + }); + + dialog.set_primary_action(__('Make'), function() { + var data = dialog.get_values(); + if(!data) return; + + // call the server to make the variant + data.template = frm.doc.name; + frappe.call({ + method:"erpnext.controllers.item_variant.get_variant", + args: data, + callback: function(r) { + var doclist = frappe.model.sync(r.message); + dialog.hide(); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }) + + dialog.show(); + }, + + show_modal_for_item_attribute_selection: function(frm) { for(var i=0;i< frm.doc.attributes.length;i++){ var fieldtype, desc; var row = frm.doc.attributes[i]; @@ -371,13 +409,42 @@ $.extend(erpnext.item, { }) }); }, - toggle_attributes: function(frm) { - frm.toggle_display("attributes", frm.doc.has_variants || frm.doc.variant_of); - frm.fields_dict.attributes.grid.toggle_reqd("attribute_value", frm.doc.variant_of ? 1 : 0); - frm.fields_dict.attributes.grid.set_column_disp("attribute_value", frm.doc.variant_of ? 1 : 0); - frm.toggle_enable("attributes", !frm.doc.variant_of); - frm.fields_dict.attributes.grid.toggle_enable("attribute", !frm.doc.variant_of); - frm.fields_dict.attributes.grid.toggle_enable("attribute_value", !frm.doc.variant_of); + toggle_attributes: function(frm) { + if((frm.doc.has_variants || frm.doc.variant_of) + && frm.doc.variant_based_on==='Item Attribute') { + frm.toggle_display("attributes", true); + + var grid = frm.fields_dict.attributes.grid; + + if(frm.doc.variant_of) { + // variant + + // value column is displayed but not editable + grid.set_column_disp("attribute_value", true); + grid.toggle_enable("attribute_value", false); + + grid.toggle_enable("attribute", false); + + // can't change attributes since they are + // saved when the variant was created + frm.toggle_enable("attributes", false); + } else { + // template - values not required! + + // make the grid editable + frm.toggle_enable("attributes", true); + + // value column is hidden + grid.set_column_disp("attribute_value", false); + + // enable the grid so you can add more attributes + grid.toggle_enable("attribute", true); + } + + } else { + // nothing to do with attributes, hide it + frm.toggle_display("attributes", false); + } } }); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4a5094e34b..db327cc23f 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, "autoname": "field:item_code", @@ -1218,7 +1219,39 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "", + "default": "Item Attribute", + "depends_on": "has_variants", + "fieldname": "variant_based_on", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Variant Based On", + "length": 0, + "no_copy": 0, + "options": "Item Attribute\nManufacturer", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.has_variants && doc.variant_based_on==='Item Attribute'", "fieldname": "attributes", "fieldtype": "Table", "hidden": 1, @@ -2792,6 +2825,7 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-tag", @@ -2799,12 +2833,11 @@ "image_field": "image", "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 1, - "modified": "2017-02-20 13:26:45.446617", + "modified": "2017-03-21 21:03:10.715674", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 0f0205bae1..4d0c3ac881 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -643,7 +643,7 @@ class Item(WebsiteGenerator): .format(self.stock_uom, template_uom)) def validate_attributes(self): - if self.has_variants or self.variant_of: + if (self.has_variants or self.variant_of) and self.variant_based_on=='Item Attribute': attributes = [] if not self.attributes: frappe.throw(_("Attribute table is mandatory")) @@ -654,7 +654,7 @@ class Item(WebsiteGenerator): attributes.append(d.attribute) def validate_variant_attributes(self): - if self.variant_of: + if self.variant_of and self.variant_based_on=='Item Attribute': args = {} for d in self.attributes: if not d.attribute_value: @@ -675,7 +675,7 @@ def get_timeline_data(doctype, name): from `tabStock Ledger Entry` where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) group by posting_date''', name)) - + for date, count in items.iteritems(): timestamp = get_timestamp(date) out.update({ timestamp: count }) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 15a11186e7..2a8e4344af 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -7,7 +7,7 @@ import frappe from frappe.test_runner import make_test_records from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError, - InvalidItemAttributeValueError) + InvalidItemAttributeValueError, get_variant) from frappe.model.rename_doc import rename_doc from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -167,31 +167,66 @@ class TestItem(unittest.TestCase): variant.item_name = "_Test Numeric Variant Large 1.1m" self.assertRaises(InvalidItemAttributeValueError, variant.save) - variant = create_variant("_Test Numeric Template Item", + variant = create_variant("_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.5}) self.assertEquals(variant.item_code, "_Test Numeric Template Item-L-1.5") variant.item_code = "_Test Numeric Variant-L-1.5" variant.item_name = "_Test Numeric Variant Large 1.5m" variant.save() - - def test_item_merging(self): + + def test_item_merging(self): create_item("Test Item for Merging 1") create_item("Test Item for Merging 2") - - make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC", + + make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC", qty=1, rate=100) - make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", + make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", qty=1, rate=100) - + rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) - + self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1")) - - self.assertTrue(frappe.db.get_value("Bin", + + self.assertTrue(frappe.db.get_value("Bin", {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"})) - - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + + self.assertTrue(frappe.db.get_value("Bin", + {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + + def test_item_variant_by_manufacturer(self): + if frappe.db.exists('Item', '_Test Variant Mfg'): + frappe.delete_doc('Item', '_Test Variant Mfg') + if frappe.db.exists('Item', '_Test Variant Mfg-1'): + frappe.delete_doc('Item', '_Test Variant Mfg-1') + if frappe.db.exists('Manufacturer', 'MSG1'): + frappe.delete_doc('Manufacturer', 'MSG1') + + template = frappe.get_doc(dict( + doctype='Item', + item_code='_Test Variant Mfg', + has_variant=1, + item_group='Products', + variant_based_on='Manufacturer' + )).insert() + + manufacturer = frappe.get_doc(dict( + doctype='Manufacturer', + short_name='MSG1' + )).insert() + + variant = get_variant(template.name, manufacturer=manufacturer.name) + self.assertEquals(variant.item_code, '_Test Variant Mfg-1') + self.assertEquals(variant.description, '_Test Variant Mfg') + self.assertEquals(variant.manufacturer, 'MSG1') + variant.insert() + + variant = get_variant(template.name, manufacturer=manufacturer.name, + manufacturer_part_no='007') + self.assertEquals(variant.item_code, '_Test Variant Mfg-2') + self.assertEquals(variant.description, '_Test Variant Mfg') + self.assertEquals(variant.manufacturer, 'MSG1') + self.assertEquals(variant.manufacturer_part_no, '007') + def make_item_variant(): if not frappe.db.exists("Item", "_Test Variant Item-S"): @@ -215,6 +250,5 @@ def create_item(item_code, is_stock_item=None): item.item_name = item_code item.description = item_code item.item_group = "All Item Groups" - item.is_stock_item = is_stock_item or 1 + item.is_stock_item = is_stock_item or 1 item.save() - \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index a1f80770e2..8a0be85214 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -63,7 +63,7 @@ def get_list_context(context=None): 'no_breadcrumbs': True } -def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20): +def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None): from frappe.www.list import get_list user = frappe.session.user ignore_permissions = False