[feature] ability to have variants based on manufacturer
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index f8c9c0a..53421b7 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -12,19 +12,42 @@
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 @@
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 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 += "<p>" + d.attribute + ": " + cstr(d.attribute_value) + "</p>"
+
+ if item.variant_based_on=='Item Attribute':
+ if variant.attributes:
+ variant.description += "\n"
+ for d in variant.attributes:
+ variant.description += "<p>" + d.attribute + ": " + cstr(d.attribute_value) + "</p>"
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 0000000..4da1d6c
--- /dev/null
+++ b/erpnext/docs/assets/img/stock/select-mfg-for-variant.png
Binary files 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 0000000..2eaa8f0
--- /dev/null
+++ b/erpnext/docs/assets/img/stock/set-variant-by-mfg.png
Binary files 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 cdca6ed..7514404 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.
<img class="screenshot" alt="Has Variants" src="{{docs_base_url}}/assets/img/stock/item-has-variants.png">
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.
<img class="screenshot" alt="Valid Attributes" src="{{docs_base_url}}/assets/img/stock/item-attributes.png">
@@ -22,3 +35,17 @@
<img class="screenshot" alt="Make Variants" src="{{docs_base_url}}/assets/img/stock/make-variant-1.png">
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"
+
+<img class='screenshot' alt='Setup Item Variant by Manufacturer'
+ src='{{docs_base_url}}/assets/img/stock/select-mfg-for-variant.png'>
+
+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
+
+<img class='screenshot' alt='Setup Item Variant by Manufacturer'
+ src='{{docs_base_url}}/assets/img/stock/set-variant-by-mfg.png'>
+
+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 697c078..a51472f 100644
--- a/erpnext/public/css/erpnext.css
+++ b/erpnext/public/css/erpnext.css
@@ -327,4 +327,3 @@
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 891f37c..f149baf 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -177,11 +177,11 @@
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 7068c99..4920721 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -261,6 +261,45 @@
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);
+ console.log(doclist);
+ 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 +410,42 @@
})
});
},
- 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 4a5094e..db327cc 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 0f0205b..4d0c3ac 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -643,7 +643,7 @@
.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 @@
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 @@
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 15a1118..2a8e434 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -7,7 +7,7 @@
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 @@
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 @@
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 a1f8077..8a0be85 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -63,7 +63,7 @@
'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