item variants, creation, deleation and update logic added.

logic added to copy changes in template to variants
This commit is contained in:
Neil Trini Lasrado
2015-05-22 17:00:58 +05:30
parent 333ccd212b
commit 8fb123b20e
8 changed files with 204 additions and 125 deletions

View File

@@ -30,10 +30,6 @@ frappe.ui.form.on("Item", {
frm.add_custom_button(__("Show Variants"), function() { frm.add_custom_button(__("Show Variants"), function() {
frappe.set_route("List", "Item", {"variant_of": frm.doc.name}); frappe.set_route("List", "Item", {"variant_of": frm.doc.name});
}, "icon-list", "btn-default"); }, "icon-list", "btn-default");
frm.add_custom_button(__("Manage Variants"), function() {
frappe.route_options = {"item": frm.doc.name };
new_doc("Manage Variants");
});
} }
if (frm.doc.variant_of) { if (frm.doc.variant_of) {
frm.set_intro(__("This Item is a Variant of {0} (Template). Attributes will be copied over from the template unless 'No Copy' is set", [frm.doc.variant_of]), true); frm.set_intro(__("This Item is a Variant of {0} (Template). Attributes will be copied over from the template unless 'No Copy' is set", [frm.doc.variant_of]), true);
@@ -87,6 +83,11 @@ frappe.ui.form.on("Item", {
is_stock_item: function(frm) { is_stock_item: function(frm) {
erpnext.item.toggle_reqd(frm); erpnext.item.toggle_reqd(frm);
},
manage_variants: function(frm) {
frappe.route_options = {"item": frm.doc.name };
frappe.set_route("List", "Manage Variants");
} }
}); });

View File

@@ -12,7 +12,7 @@
{ {
"fieldname": "name_and_description_section", "fieldname": "name_and_description_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Name and Description", "label": "",
"no_copy": 0, "no_copy": 0,
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "icon-flag", "options": "icon-flag",
@@ -167,16 +167,17 @@
"search_index": 0 "search_index": 0
}, },
{ {
"depends_on": "eval:!!!doc.variant_of", "depends_on": "eval:!doc.variant_of",
"fieldname": "variants_section", "fieldname": "variants_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Variants", "label": "Variant",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": ""
}, },
{ {
"default": "0", "default": "0",
"description": "Automatically set. If this item has variants, then it cannot be selected in sales orders etc.", "depends_on": "",
"description": "If this item has variants, then it cannot be selected in sales orders etc.",
"fieldname": "has_variants", "fieldname": "has_variants",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Has Variants", "label": "Has Variants",
@@ -185,6 +186,38 @@
"precision": "", "precision": "",
"read_only": 0 "read_only": 0
}, },
{
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "has_variants",
"fieldname": "manage_variants",
"fieldtype": "Button",
"label": "Manage Variants",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "section_break_20",
"fieldtype": "Section Break",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "variant_of",
"fieldname": "attributes",
"fieldtype": "Table",
"hidden": 0,
"label": "Attributes",
"no_copy": 1,
"options": "Variant Attribute",
"permlevel": 0,
"precision": "",
"read_only": 0
},
{ {
"fieldname": "inventory", "fieldname": "inventory",
"fieldtype": "Section Break", "fieldtype": "Section Break",

View File

@@ -9,7 +9,7 @@ from frappe.website.website_generator import WebsiteGenerator
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups
from frappe.website.render import clear_cache from frappe.website.render import clear_cache
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
import copy from erpnext.stock.doctype.manage_variants.manage_variants import update_variant
class WarehouseNotSet(frappe.ValidationError): pass class WarehouseNotSet(frappe.ValidationError): pass
@@ -46,9 +46,6 @@ class Item(WebsiteGenerator):
if self.image and not self.website_image: if self.image and not self.website_image:
self.website_image = self.image self.website_image = self.image
if self.variant_of:
self.copy_attributes_to_variant(frappe.get_doc("Item", self.variant_of), self)
self.check_warehouse_is_set_for_stock_item() self.check_warehouse_is_set_for_stock_item()
self.check_stock_uom_with_bin() self.check_stock_uom_with_bin()
self.add_default_uom_in_conversion_factor_table() self.add_default_uom_in_conversion_factor_table()
@@ -63,6 +60,7 @@ class Item(WebsiteGenerator):
self.validate_warehouse_for_reorder() self.validate_warehouse_for_reorder()
self.update_item_desc() self.update_item_desc()
self.synced_with_hub = 0 self.synced_with_hub = 0
self.validate_has_variants()
if not self.get("__islocal"): if not self.get("__islocal"):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@@ -74,6 +72,7 @@ class Item(WebsiteGenerator):
invalidate_cache_for_item(self) invalidate_cache_for_item(self)
self.validate_name_with_item_group() self.validate_name_with_item_group()
self.update_item_price() self.update_item_price()
self.update_variants()
def get_context(self, context): def get_context(self, context):
context["parent_groups"] = get_parent_item_groups(self.item_group) + \ context["parent_groups"] = get_parent_item_groups(self.item_group) + \
@@ -129,105 +128,6 @@ class Item(WebsiteGenerator):
if not matched: if not matched:
frappe.throw(_("Default Unit of Measure can not be changed directly because you have already made some transaction(s) with another UOM. To change default UOM, use 'UOM Replace Utility' tool under Stock module.")) frappe.throw(_("Default Unit of Measure can not be changed directly because you have already made some transaction(s) with another UOM. To change default UOM, use 'UOM Replace Utility' tool under Stock module."))
def sync_variants(self):
variant_item_codes = self.get_variant_item_codes()
# delete missing variants
existing_variants = [d.name for d in frappe.get_all("Item",
filters={"variant_of":self.name})]
updated, deleted = [], []
for existing_variant in existing_variants:
if existing_variant not in variant_item_codes:
frappe.delete_doc("Item", existing_variant)
deleted.append(existing_variant)
else:
self.update_variant(existing_variant)
updated.append(existing_variant)
inserted = []
for item_code in variant_item_codes:
if item_code not in existing_variants:
self.make_variant(item_code)
inserted.append(item_code)
if inserted:
frappe.msgprint(_("Item Variants {0} created").format(", ".join(inserted)))
if updated:
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
if deleted:
frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted)))
def get_variant_item_codes(self):
"""Get all possible suffixes for variants"""
if not self.variants:
return []
self.variant_attributes = {}
variant_dict = {}
variant_item_codes = []
for d in self.variants:
variant_dict.setdefault(d.item_attribute, []).append(d.item_attribute_value)
all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
# sort attributes by their priority
attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
def add_attribute_suffixes(item_code, my_attributes, attributes):
attr = frappe.get_doc("Item Attribute", attributes[0])
for value in attr.item_attribute_values:
if value.attribute_value in variant_dict[attr.name]:
_my_attributes = copy.deepcopy(my_attributes)
_my_attributes.append([attr.name, value.attribute_value])
if len(attributes) > 1:
add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
else:
variant_item_codes.append(item_code + "-" + value.abbr)
self.variant_attributes[item_code + "-" + value.abbr] = _my_attributes
add_attribute_suffixes(self.name, [], attributes)
return variant_item_codes
def make_variant(self, item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
self.copy_attributes_to_variant(self, item, insert=True)
item.insert()
def update_variant(self, item_code):
item = frappe.get_doc("Item", item_code)
item.item_code = item_code
self.copy_attributes_to_variant(self, item)
item.save()
def copy_attributes_to_variant(self, template, variant, insert=False):
from frappe.model import no_value_fields
for field in self.meta.fields:
if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
and field.fieldname not in ("item_code", "item_name"):
if variant.get(field.fieldname) != template.get(field.fieldname):
variant.set(field.fieldname, template.get(field.fieldname))
variant.__dirty = True
variant.description += "\n"
if not getattr(template, "variant_attributes", None):
template.get_variant_item_codes()
for attr in template.variant_attributes[variant.item_code]:
variant.description += "<p>" + attr[0] + ": " + attr[1] + "</p>"
variant.item_name = self.item_name + variant.item_code[len(self.name):]
variant.variant_of = template.name
variant.has_variants = 0
variant.show_in_website = 0
def update_template_tables(self): def update_template_tables(self):
template = frappe.get_doc("Item", self.variant_of) template = frappe.get_doc("Item", self.variant_of)
@@ -320,7 +220,8 @@ class Item(WebsiteGenerator):
vals.has_batch_no != self.has_batch_no or vals.has_batch_no != self.has_batch_no or
cstr(vals.valuation_method) != cstr(self.valuation_method)): cstr(vals.valuation_method) != cstr(self.valuation_method)):
if self.check_if_sle_exists() == "exists": if self.check_if_sle_exists() == "exists":
frappe.throw(_("As there are existing stock transactions for this item, you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'")) frappe.throw(_("As there are existing stock transactions for this item, \
you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'"))
def validate_reorder_level(self): def validate_reorder_level(self):
if cint(self.apply_warehouse_wise_reorder_level): if cint(self.apply_warehouse_wise_reorder_level):
@@ -423,6 +324,20 @@ class Item(WebsiteGenerator):
item_code = %s and docstatus < 2""",(self.description, self.name)) item_code = %s and docstatus < 2""",(self.description, self.name))
frappe.db.sql("""update `tabBOM Explosion Item` set description = %s where frappe.db.sql("""update `tabBOM Explosion Item` set description = %s where
item_code = %s and docstatus < 2""",(self.description, self.name)) item_code = %s and docstatus < 2""",(self.description, self.name))
def update_variants(self):
if self.has_variants:
updated = []
variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name })
for d in variants:
update_variant(self.item_code, d)
updated.append(d.item_code)
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
def validate_has_variants(self):
if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw("Item has variants.")
def validate_end_of_life(item_code, end_of_life=None, verbose=1): def validate_end_of_life(item_code, end_of_life=None, verbose=1):
if not end_of_life: if not end_of_life:

View File

@@ -12,7 +12,7 @@ frappe.ui.form.on("Manage Variants", {
frappe.call({ frappe.call({
method:"frappe.client.get_list", method:"frappe.client.get_list",
args:{ args:{
doctype:"Variant Attribute", doctype:"Item Attribute Value",
filters: [ filters: [
["parent","=", field.doc.attribute], ["parent","=", field.doc.attribute],
["attribute_value", "like", request.term + "%"] ["attribute_value", "like", request.term + "%"]
@@ -39,6 +39,17 @@ frappe.ui.form.on("Manage Variants", {
refresh: function(frm) { refresh: function(frm) {
frm.disable_save(); frm.disable_save();
},
item:function(frm) {
return frappe.call({
method: "get_item_details",
doc:frm.doc,
callback: function(r) {
refresh_field('attributes');
refresh_field('variants');
}
})
} }
}); });

View File

@@ -14,7 +14,8 @@
"label": "Item", "label": "Item",
"options": "Item", "options": "Item",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"reqd": 1
}, },
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -74,7 +75,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"modified": "2015-05-21 16:21:33.707125", "modified": "2015-05-27 04:43:52.051367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Manage Variants", "name": "Manage Variants",

View File

@@ -7,12 +7,17 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
import copy import copy
import json
class DuplicateAttribute(frappe.ValidationError): pass class DuplicateAttribute(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
class ManageVariants(Document): class ManageVariants(Document):
def get_item_details(self):
self.get_attributes()
self.get_variants()
def generate_combinations(self): def generate_combinations(self):
self.validate_attributes() self.validate_attributes()
self.validate_template_item() self.validate_template_item()
@@ -20,6 +25,31 @@ class ManageVariants(Document):
self.validate_attribute_values() self.validate_attribute_values()
self.validate_attributes_are_unique() self.validate_attributes_are_unique()
self.get_variant_item_codes() self.get_variant_item_codes()
def create_variants(self):
self.sync_variants()
def get_attributes(self):
attributes = {}
self.set('attributes', [])
for d in frappe.db.sql("""select attribute, attribute_value from `tabVariant Attribute` as attribute,
`tabItem` as item where attribute.parent= item.name and item.variant_of = %s""", self.item, as_dict=1):
attributes.setdefault(d.attribute, []).append(d.attribute_value)
for d in attributes:
attribute_values = set(attributes[d])
for value in attribute_values:
self.append('attributes',{"attribute": d, "attribute_value": value})
def get_variants(self):
self.set('variants', [])
variants = [d.name for d in frappe.get_all("Item",
filters={"variant_of":self.item})]
for d in variants:
variant_attributes, attributes = "", []
for attribute in frappe.db.sql("""select attribute, attribute_value from `tabVariant Attribute` where parent = %s""", d):
variant_attributes += attribute[1] + " "
attributes.append([attribute[0], attribute[1]])
self.append('variants',{"variant": d, "variant_attributes": variant_attributes, "attributes": json.dumps(attributes)})
def validate_attributes(self): def validate_attributes(self):
if not self.attributes: if not self.attributes:
@@ -61,7 +91,7 @@ class ManageVariants(Document):
def get_variant_item_codes(self): def get_variant_item_codes(self):
"""Get all possible suffixes for variants""" """Get all possible suffixes for variants"""
variant_dict = {} variant_dict = {}
variant_item_codes = [] self.set('variants', [])
for d in self.attributes: for d in self.attributes:
variant_dict.setdefault(d.attribute, []).append(d.attribute_value) variant_dict.setdefault(d.attribute, []).append(d.attribute_value)
@@ -80,12 +110,76 @@ class ManageVariants(Document):
if len(attributes) > 1: if len(attributes) > 1:
add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:]) add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
else: else:
variant_item_codes.append(item_code + "-" + value.abbr) variant_attributes = ""
for d in _my_attributes:
variant_attributes += d[1] + " "
self.append('variants', {"variant": item_code + "-" + value.abbr,
"attributes": json.dumps(_my_attributes), "variant_attributes": variant_attributes})
add_attribute_suffixes(self.item, [], attributes) add_attribute_suffixes(self.item, [], attributes)
for v in variant_item_codes: def sync_variants(self):
self.append('variants', {"variant": v}) variant_item_codes = []
for v in self.variants:
def create_variants(self): variant_item_codes.append(v.variant)
pass
existing_variants = [d.name for d in frappe.get_all("Item",
filters={"variant_of":self.item})]
inserted, updated, deleted = [], [], []
for existing_variant in existing_variants:
if existing_variant not in variant_item_codes:
frappe.delete_doc("Item", existing_variant)
deleted.append(existing_variant)
for item_code in variant_item_codes:
if item_code not in existing_variants:
make_variant(self.item, item_code, self.variants)
inserted.append(item_code)
else:
update_variant(self.item, existing_variant, self.variants)
updated.append(existing_variant)
if inserted:
frappe.msgprint(_("Item Variants {0} created").format(", ".join(inserted)))
if updated:
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
if deleted:
frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted)))
def make_variant(item, variant_code, variant_attribute):
variant = frappe.new_doc("Item")
variant.item_code = variant_code
template = frappe.get_doc("Item", item)
copy_attributes_to_variant(template, variant, variant_attribute, insert=True)
variant.insert()
def update_variant(item, variant_code, variant_attribute=None):
variant = frappe.get_doc("Item", variant_code)
template = frappe.get_doc("Item", item)
copy_attributes_to_variant(template, variant, variant_attribute, insert=True)
variant.save()
def copy_attributes_to_variant(template, variant, variant_attribute=None, insert=False):
from frappe.model import no_value_fields
for field in template.meta.fields:
if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
and field.fieldname not in ("item_code", "item_name"):
if variant.get(field.fieldname) != template.get(field.fieldname):
variant.set(field.fieldname, template.get(field.fieldname))
variant.item_name = template.item_name + variant.item_code[len(template.name):]
variant.variant_of = template.name
variant.has_variants = 0
variant.show_in_website = 0
if variant_attribute:
for d in variant_attribute:
if d.variant == variant.item_code:
variant.attributes= []
for a in json.loads(d.attributes):
variant.append('attributes', {"attribute": a[0], "attribute_value": a[1]})
if variant.attributes:
variant.description += "\n"
for d in variant.attributes:
variant.description += "<p>" + d.attribute + ": " + d.attribute_value + "</p>"

View File

@@ -8,7 +8,7 @@ from frappe import _
from frappe.utils import flt, getdate, add_days, formatdate from frappe.utils import flt, getdate, add_days, formatdate
from frappe.model.document import Document from frappe.model.document import Document
from datetime import date from datetime import date
from erpnext.stock.doctype.item.item import ItemTemplateCannotHaveStock from erpnext.stock.doctype.manage_variants.manage_variants import ItemTemplateCannotHaveStock
class StockFreezeError(frappe.ValidationError): pass class StockFreezeError(frappe.ValidationError): pass

View File

@@ -28,6 +28,30 @@
"reqd": 1, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0 "set_only_once": 0
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "variant_attributes",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Variant Attributes",
"permlevel": 0,
"precision": "",
"read_only": 1
},
{
"fieldname": "attributes",
"fieldtype": "Text",
"hidden": 1,
"label": "attributes",
"permlevel": 0,
"precision": "",
"read_only": 1
} }
], ],
"hide_heading": 0, "hide_heading": 0,
@@ -38,7 +62,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"modified": "2015-05-21 16:18:16.605271", "modified": "2015-05-28 04:58:20.495616",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Variant Item", "name": "Variant Item",