[feature] ability to have variants based on manufacturer

This commit is contained in:
Rushabh Mehta 2017-03-21 17:48:34 +01:00
parent f340e19ea7
commit a07c43fd68
11 changed files with 244 additions and 48 deletions

View File

@ -12,20 +12,43 @@ class InvalidItemAttributeValueError(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
@frappe.whitelist() @frappe.whitelist()
def get_variant(template, args, variant=None): def get_variant(template, args=None, variant=None, manufacturer=None,
"""Validates Attributes and their Values, then looks for an exactly matching Item Variant manufacturer_part_no=None):
"""Validates Attributes and their Values, then looks for an exactly
matching Item Variant
:param item: Template Item :param item: Template Item
:param args: A dictionary with "Attribute" as key and "Attribute Value" as value :param args: A dictionary with "Attribute" as key and "Attribute Value" as value
""" """
item_template = frappe.get_doc('Item', template)
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): if isinstance(args, basestring):
args = json.loads(args) args = json.loads(args)
if not args: if not args:
frappe.throw(_("Please specify at least one attribute in the Attributes table")) frappe.throw(_("Please specify at least one attribute in the Attributes table"))
return find_variant(template, args, variant) 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): def validate_item_variant_attributes(item, args=None):
if isinstance(item, basestring): if isinstance(item, basestring):
item = frappe.get_doc('Item', item) item = frappe.get_doc('Item', item)
@ -131,6 +154,7 @@ def create_variant(item, args):
template = frappe.get_doc("Item", item) template = frappe.get_doc("Item", item)
variant = frappe.new_doc("Item") variant = frappe.new_doc("Item")
variant.variant_based_on = 'Item Attribute'
variant_attributes = [] variant_attributes = []
for d in template.attributes: for d in template.attributes:
@ -147,13 +171,24 @@ def create_variant(item, args):
def copy_attributes_to_variant(item, variant): def copy_attributes_to_variant(item, variant):
from frappe.model import no_value_fields 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: for field in item.meta.fields:
if field.fieldtype not in no_value_fields and (not field.no_copy)\ 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): if variant.get(field.fieldname) != item.get(field.fieldname):
variant.set(field.fieldname, item.get(field.fieldname)) variant.set(field.fieldname, item.get(field.fieldname))
variant.variant_of = item.name variant.variant_of = item.name
variant.has_variants = 0 variant.has_variants = 0
if item.variant_based_on=='Item Attribute':
if variant.attributes: if variant.attributes:
variant.description += "\n" variant.description += "\n"
for d in variant.attributes: for d in variant.attributes:

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,7 +1,20 @@
# 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). 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'. 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'. 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.
@ -22,3 +35,17 @@ To create 'Item Variants' against a 'Template' select 'Make Variants'
<img class="screenshot" alt="Make Variants" src="{{docs_base_url}}/assets/img/stock/make-variant-1.png"> <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) 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"

View File

@ -327,4 +327,3 @@ body[data-route="pos"] .btn-more {
body[data-route="pos"] .collapse-btn { body[data-route="pos"] .collapse-btn {
cursor: pointer; cursor: pointer;
} }

View File

@ -177,11 +177,11 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
fields: [ fields: [
{fieldtype:'Read Only', fieldname:'item_code', {fieldtype:'Read Only', fieldname:'item_code',
label: __('Item Code'), in_list_view:1}, 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) { label: __('Select BOM'), in_list_view:1, get_query: function(doc) {
return {filters: {item: doc.item_code}}; return {filters: {item: doc.item_code}};
}}, }},
{fieldtype:'Float', fieldname:'pending_qty', {fieldtype:'Float', fieldname:'pending_qty', reqd: 1,
label: __('Qty'), in_list_view:1}, label: __('Qty'), in_list_view:1},
], ],
get_data: function() { get_data: function() {

View File

@ -261,6 +261,45 @@ $.extend(erpnext.item, {
make_variant: function(frm) { make_variant: function(frm) {
var fields = [] 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++){ for(var i=0;i< frm.doc.attributes.length;i++){
var fieldtype, desc; var fieldtype, desc;
var row = frm.doc.attributes[i]; var row = frm.doc.attributes[i];
@ -371,13 +410,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); toggle_attributes: function(frm) {
frm.fields_dict.attributes.grid.toggle_enable("attribute", !frm.doc.variant_of); if((frm.doc.has_variants || frm.doc.variant_of)
frm.fields_dict.attributes.grid.toggle_enable("attribute_value", !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);
}
} }
}); });

View File

@ -1,5 +1,6 @@
{ {
"allow_copy": 0, "allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:item_code", "autoname": "field:item_code",
@ -1218,7 +1219,39 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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", "fieldname": "attributes",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 1, "hidden": 1,
@ -2792,6 +2825,7 @@
"unique": 0 "unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0, "hide_heading": 0,
"hide_toolbar": 0, "hide_toolbar": 0,
"icon": "fa fa-tag", "icon": "fa fa-tag",
@ -2799,12 +2833,11 @@
"image_field": "image", "image_field": "image",
"image_view": 0, "image_view": 0,
"in_create": 0, "in_create": 0,
"in_dialog": 0,
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 1, "max_attachments": 1,
"modified": "2017-02-20 13:26:45.446617", "modified": "2017-03-21 21:03:10.715674",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -643,7 +643,7 @@ class Item(WebsiteGenerator):
.format(self.stock_uom, template_uom)) .format(self.stock_uom, template_uom))
def validate_attributes(self): 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 = [] attributes = []
if not self.attributes: if not self.attributes:
frappe.throw(_("Attribute table is mandatory")) frappe.throw(_("Attribute table is mandatory"))
@ -654,7 +654,7 @@ class Item(WebsiteGenerator):
attributes.append(d.attribute) attributes.append(d.attribute)
def validate_variant_attributes(self): def validate_variant_attributes(self):
if self.variant_of: if self.variant_of and self.variant_based_on=='Item Attribute':
args = {} args = {}
for d in self.attributes: for d in self.attributes:
if not d.attribute_value: if not d.attribute_value:

View File

@ -7,7 +7,7 @@ import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError, from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsError,
InvalidItemAttributeValueError) InvalidItemAttributeValueError, get_variant)
from frappe.model.rename_doc import rename_doc from frappe.model.rename_doc import rename_doc
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@ -193,6 +193,41 @@ class TestItem(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Bin", self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) {"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(): def make_item_variant():
if not frappe.db.exists("Item", "_Test Variant Item-S"): if not frappe.db.exists("Item", "_Test Variant Item-S"):
variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""") variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""")
@ -217,4 +252,3 @@ def create_item(item_code, is_stock_item=None):
item.item_group = "All Item Groups" 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() item.save()

View File

@ -63,7 +63,7 @@ def get_list_context(context=None):
'no_breadcrumbs': True '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 from frappe.www.list import get_list
user = frappe.session.user user = frappe.session.user
ignore_permissions = False ignore_permissions = False