[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
@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
"""
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):
args = json.loads(args)
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):
item = frappe.get_doc('Item', item)
@ -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,13 +171,24 @@ 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 item.variant_based_on=='Item Attribute':
if variant.attributes:
variant.description += "\n"
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).
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.
@ -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">
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 {
cursor: pointer;
}

View File

@ -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() {

View File

@ -261,6 +261,45 @@ $.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);
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 @@ $.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);
}
}
});

View File

@ -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",

View File

@ -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:

View File

@ -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
@ -193,6 +193,41 @@ class TestItem(unittest.TestCase):
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"):
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.is_stock_item = is_stock_item or 1
item.save()

View File

@ -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