[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,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 += "<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"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

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

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

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