[item variants] make variants, bom updates
diff --git a/erpnext/accounts/page/accounts_browser/accounts_browser.py b/erpnext/accounts/page/accounts_browser/accounts_browser.py
index 15cfdd2..1a81e7b 100644
--- a/erpnext/accounts/page/accounts_browser/accounts_browser.py
+++ b/erpnext/accounts/page/accounts_browser/accounts_browser.py
@@ -10,38 +10,38 @@
@frappe.whitelist()
def get_companies():
"""get a list of companies based on permission"""
- return [d.name for d in frappe.get_list("Company", fields=["name"],
+ return [d.name for d in frappe.get_list("Company", fields=["name"],
order_by="name")]
@frappe.whitelist()
def get_children():
args = frappe.local.form_dict
ctype, company = args['ctype'], args['comp']
-
+
# root
if args['parent'] in ("Accounts", "Cost Centers"):
- acc = frappe.db.sql(""" select
+ acc = frappe.db.sql(""" select
name as value, if(group_or_ledger='Group', 1, 0) as expandable
from `tab%s`
where ifnull(parent_%s,'') = ''
- and `company` = %s and docstatus<2
+ and `company` = %s and docstatus<2
order by name""" % (ctype, ctype.lower().replace(' ','_'), '%s'),
company, as_dict=1)
- else:
+ else:
# other
- acc = frappe.db.sql("""select
+ acc = frappe.db.sql("""select
name as value, if(group_or_ledger='Group', 1, 0) as expandable
- from `tab%s`
+ from `tab%s`
where ifnull(parent_%s,'') = %s
- and docstatus<2
+ and docstatus<2
order by name""" % (ctype, ctype.lower().replace(' ','_'), '%s'),
args['parent'], as_dict=1)
-
+
if ctype == 'Account':
currency = frappe.db.sql("select default_currency from `tabCompany` where name = %s", company)[0][0]
for each in acc:
bal = get_balance_on(each.get("value"))
each["currency"] = currency
each["balance"] = flt(bal)
-
+
return acc
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index cbbd82c..2f98dbd 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -165,6 +165,7 @@
concat(substr(tabItem.description, 1, 40), "..."), description) as decription
from tabItem
where tabItem.docstatus < 2
+ and ifnull(tabItem.has_variants, 0)=0
and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00')
and (tabItem.`{key}` LIKE %(txt)s
or tabItem.item_name LIKE %(txt)s
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index cb96478..34598b6 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -182,10 +182,7 @@
cur_frm.fields_dict['item'].get_query = function(doc) {
return{
- query: "erpnext.controllers.queries.item_query",
- filters:{
- 'is_manufactured_item': 'Yes'
- }
+ query: "erpnext.controllers.queries.item_query"
}
}
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 75c4ffb..1185403 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -54,7 +54,7 @@
def get_item_det(self, item_code):
item = frappe.db.sql("""select name, is_asset_item, is_purchase_item,
docstatus, description, is_sub_contracted_item, stock_uom, default_bom,
- last_purchase_rate, is_manufactured_item
+ last_purchase_rate
from `tabItem` where name=%s""", item_code, as_dict = 1)
return item
@@ -149,14 +149,19 @@
if self.is_default and self.is_active:
from frappe.model.utils import set_default
set_default(self, "item")
- frappe.db.set_value("Item", self.item, "default_bom", self.name)
+ item = frappe.get_doc("Item", self.item)
+ if item.default_bom != self.name:
+ item.default_bom = self.name
+ item.save()
else:
if not self.is_active:
frappe.db.set(self, "is_default", 0)
- frappe.db.sql("update `tabItem` set default_bom = null where name = %s and default_bom = %s",
- (self.item, self.name))
+ item = frappe.get_doc("Item", self.item)
+ if item.default_bom == self.name:
+ item.default_bom = None
+ item.save()
def clear_operations(self):
if not self.with_operations:
@@ -169,9 +174,6 @@
item = self.get_item_det(self.item)
if not item:
frappe.throw(_("Item {0} does not exist in the system or has expired").format(self.item))
- elif item[0]['is_manufactured_item'] != 'Yes' \
- and item[0]['is_sub_contracted_item'] != 'Yes':
- frappe.throw(_("Item {0} must be manufactured or sub-contracted").format(self.item))
else:
ret = frappe.db.get_value("Item", self.item, ["description", "stock_uom"])
self.description = ret[0]
@@ -195,15 +197,8 @@
if self.with_operations and cstr(m.operation_no) not in self.op:
frappe.throw(_("Operation {0} not present in Operations Table").format(m.operation_no))
- item = self.get_item_det(m.item_code)
- if item[0]['is_manufactured_item'] == 'Yes':
- if not m.bom_no:
- frappe.throw(_("BOM number is required for manufactured Item {0} in row {1}").format(m.item_code, m.idx))
- else:
- self.validate_bom_no(m.item_code, m.bom_no, m.idx)
-
- elif m.bom_no:
- frappe.throw(_("BOM number not allowed for non-manufactured Item {0} in row {1}").format(m.item_code, m.idx))
+ if m.bom:
+ self.validate_bom_no(m.item_code, m.bom_no, m.idx)
if flt(m.qty) <= 0:
frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx))
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 28ee49a..753a708 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -28,3 +28,15 @@
def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
self.assertEquals(len(get_bom_items(bom="BOM/_Test FG Item 2/001", qty=1, fetch_exploded=1)), 3)
+
+ def test_default_bom(self):
+ bom = frappe.get_doc("BOM", "BOM/_Test FG Item 2/001")
+ bom.is_active = 0
+ bom.save()
+
+ self.assertEqual(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), "")
+
+ bom.is_active = 1
+ bom.save()
+
+ self.assertTrue(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), "BOM/_Test FG Item 2/001")
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 9af1d90..06aae12 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -80,16 +80,6 @@
cur_frm.set_value("description", doc.item_code);
}
-cur_frm.fields_dict['default_bom'].get_query = function(doc) {
- return {
- filters: {
- 'item': doc.item_code,
- 'is_active': 0
- }
- }
-}
-
-
// Expense Account
// ---------------------------------
cur_frm.fields_dict['expense_account'].get_query = function(doc) {
@@ -206,4 +196,4 @@
else {
msgprint(__("You may need to update: {0}", [frappe.meta.get_docfield(cur_frm.doc.doctype, "description_html").label]));
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 70e2897..0a74e41 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -312,7 +312,7 @@
"fieldname": "serial_no_series",
"fieldtype": "Data",
"label": "Serial Number Series",
- "no_copy": 1,
+ "no_copy": 0,
"permlevel": 0
},
{
@@ -431,7 +431,7 @@
"fieldname": "lead_time_days",
"fieldtype": "Int",
"label": "Lead Time Days",
- "no_copy": 1,
+ "no_copy": 0,
"oldfieldname": "lead_time_days",
"oldfieldtype": "Int",
"permlevel": 0,
@@ -860,7 +860,7 @@
"icon": "icon-tag",
"idx": 1,
"max_attachments": 1,
- "modified": "2014-09-26 06:04:48.683214",
+ "modified": "2014-09-30 14:34:50.101879",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 4fcf937..565d24e 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -63,7 +63,7 @@
invalidate_cache_for_item(self)
self.validate_name_with_item_group()
self.update_item_price()
- self.make_variants()
+ self.sync_variants()
def get_context(self, context):
context["parent_groups"] = get_parent_item_groups(self.item_group) + \
@@ -118,6 +118,12 @@
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 validate_variants_are_unique(self):
+ if not self.has_variants:
+ self.item_variants = []
+
+ if self.item_variants and self.variant_of:
+ frappe.throw(_("Item cannot be a variant of a variant"))
+
variants = []
for d in self.item_variants:
key = (d.item_attribute, d.item_attribute_value)
@@ -126,35 +132,87 @@
d.item_attribute_value), DuplicateVariant)
variants.append(key)
- def make_variants(self):
+ 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",
+ {"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):
+ if not self.item_variants:
+ return []
+
variant_dict = {}
variant_item_codes = []
for d in self.item_variants:
variant_dict.setdefault(d.item_attribute, []).append(d.item_attribute_value)
- attributes = variant_dict.keys()
- for d in frappe.get_list("Item Attribute", order_by = "priority asc", ignore_permissions=True):
- if d.name in attributes:
- attr = frappe.get_doc("Item Attribute", d.name)
- abbr = dict((d.attribute_value, d.abbr) for d in attr.item_attribute_values)
- for value in variant_dict[d.name]:
- variant_item_codes.append(self.name + "-" + abbr[value])
+ all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
- # delete missing variants
- existing_variants = [d.name for d in frappe.get_list("Item",
- filters={"variant_of":self.name}, ignore_permissions=True)]
+ # sort attributes by their priority
+ attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
- for existing_variant in existing_variants:
- if existing_variant.name not in variant_item_codes:
- frappe.delete_doc("Item", existing_variant.name)
- else:
- # update mandatory fields
- pass
+ def add_attribute_suffixes(item_code, 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]:
+ if len(attributes) > 1:
+ add_attribute_suffixes(item_code + "-" + value.abbr, attributes[1:])
+ else:
+ variant_item_codes.append(item_code + "-" + value.abbr)
- # for item_code in variant_item_codes:
- # for
+ add_attribute_suffixes(self.name, attributes)
+ return variant_item_codes
+
+ def make_variant(self, item_code):
+ item = frappe.new_doc("Item")
+ self.copy_attributes_to_variant(item, insert=True)
+ item.item_code = item_code
+ item.insert()
+
+ def update_variant(self, item_code):
+ item = frappe.get_doc("Item", item_code)
+ self.copy_attributes_to_variant(item)
+ item.save()
+
+ def copy_attributes_to_variant(self, 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):
+ if variant.get(field.fieldname) != self.get(field.fieldname):
+ variant.set(field.fieldname, self.get(field.fieldname))
+ variant.__dirty = True
+
+ variant.variant_of = self.name
+ variant.has_variants = 0
+ variant.show_in_website = 0
def validate_conversion_factor(self):
check_list = []
@@ -168,9 +226,6 @@
frappe.throw(_("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx))
def validate_item_type(self):
- if cstr(self.is_manufactured_item) == "No":
- self.is_pro_applicable = "No"
-
if self.is_pro_applicable == 'Yes' and self.is_stock_item == 'No':
frappe.throw(_("As Production Order can be made for this item, it must be a stock item."))
@@ -182,6 +237,11 @@
def check_for_active_boms(self):
+ if self.default_bom:
+ bom_item = frappe.db.get_value("BOM", self.default_bom, "item")
+ if bom_item not in (self.name, self.variant_of):
+ frappe.throw(_("Default BOM must be for this item or its template"))
+
if self.is_purchase_item != "Yes":
bom_mat = frappe.db.sql("""select distinct t1.parent
from `tabBOM Item` t1, `tabBOM` t2 where t2.name = t1.parent
@@ -191,12 +251,6 @@
if bom_mat and bom_mat[0][0]:
frappe.throw(_("Item must be a purchase item, as it is present in one or many Active BOMs"))
- if self.is_manufactured_item != "Yes":
- bom = frappe.db.sql("""select name from `tabBOM` where item = %s
- and is_active = 1""", (self.name,))
- if bom and bom[0][0]:
- frappe.throw(_("""Allow Bill of Materials should be 'Yes'. Because one or many active BOMs present for this item"""))
-
def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """
cust_code=[]
@@ -264,6 +318,8 @@
def on_trash(self):
super(Item, self).on_trash()
frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code)
+ for variant_of in frappe.get_all("Item", {"variant_of": self.name}):
+ frappe.delete_doc("Item", variant_of.name)
def before_rename(self, olddn, newdn, merge=False):
if merge:
diff --git a/erpnext/stock/doctype/item/item_list.html b/erpnext/stock/doctype/item/item_list.html
index ebc2c7f..abfc2c6 100644
--- a/erpnext/stock/doctype/item/item_list.html
+++ b/erpnext/stock/doctype/item/item_list.html
@@ -26,11 +26,11 @@
<i class="icon-shopping-cart text-muted"></i>
</span>
{% } %}
- {% if(doc.is_manufactured_item==="Yes") { %}
+ {% if(doc.default_bom==="Yes") { %}
<span style="margin-right: 8px;"
title="{%= __("Manufactured Item") %}" class="filterable"
- data-filter="is_manufactured_item,=,Yes">
- <i class="icon-wrench text-muted"></i>
+ data-filter="default_bom,=,{%= doc.default_bom %}">
+ <i class="icon-site-map text-muted"></i>
</span>
{% } %}
{% if(doc.show_in_website) { %}
diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js
index e1cd020..c6a437b 100644
--- a/erpnext/stock/doctype/item/item_list.js
+++ b/erpnext/stock/doctype/item/item_list.js
@@ -1,5 +1,5 @@
frappe.listview_settings['Item'] = {
- add_fields: ["`tabItem`.`item_name`", "`tabItem`.`stock_uom`", "`tabItem`.`item_group`", "`tabItem`.`image`",
- "`tabItem`.`is_stock_item`", "`tabItem`.`is_sales_item`", "`tabItem`.`is_purchase_item`",
- "`tabItem`.`is_manufactured_item`", "`tabItem`.`show_in_website`"]
+ add_fields: ["item_name", "stock_uom", "item_group", "image",
+ "is_stock_item", "is_sales_item", "is_purchase_item", "show_in_website",
+ "default_bom"]
};
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index b2714e9..c040548 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -17,6 +17,25 @@
item.append("item_variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"})
self.assertRaises(DuplicateVariant, item.insert)
+ def test_variant_item_codes(self):
+ frappe.delete_doc("Item", test_records[11].get("item_code"))
+ item = frappe.copy_doc(test_records[11])
+ item.insert()
+ variants = ['_Test Variant Item-S', '_Test Variant Item-M', '_Test Variant Item-L']
+ self.assertEqual(item.get_variant_item_codes(), variants)
+ for v in variants:
+ self.assertTrue(frappe.db.get_value("Item", {"variant_of": item.name, "name": v}))
+
+ item.append("item_variants", {"item_attribute": "Test Colour", "item_attribute_value": "Red"})
+ item.append("item_variants", {"item_attribute": "Test Colour", "item_attribute_value": "Blue"})
+ item.append("item_variants", {"item_attribute": "Test Colour", "item_attribute_value": "Green"})
+
+ self.assertEqual(item.get_variant_item_codes(), ['_Test Variant Item-S-R',
+ '_Test Variant Item-S-G', '_Test Variant Item-S-B',
+ '_Test Variant Item-M-R', '_Test Variant Item-M-G',
+ '_Test Variant Item-M-B', '_Test Variant Item-L-R',
+ '_Test Variant Item-L-G', '_Test Variant Item-L-B'])
+
def test_item_creation(self):
frappe.delete_doc("Item", test_records[11].get("item_code"))
item = frappe.copy_doc(test_records[11])
@@ -33,7 +52,7 @@
to_check = {
"item_code": "_Test Item",
"item_name": "_Test Item",
- "description": "_Test Item",
+ "description": "_Test Item 1",
"warehouse": "_Test Warehouse - _TC",
"income_account": "Sales - _TC",
"expense_account": "_Test Account Cost for Goods Sold - _TC",
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index fdae118..4085d98 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -243,6 +243,7 @@
"is_service_item": "No",
"is_stock_item": "Yes",
"is_sub_contracted_item": "Yes",
+ "is_manufactured_item": "Yes",
"item_code": "_Test FG Item 2",
"item_group": "_Test Item Group Desktops",
"item_name": "_Test FG Item 2",
@@ -268,6 +269,7 @@
"item_group": "_Test Item Group Desktops",
"item_name": "_Test Variant Item",
"stock_uom": "_Test UOM",
+ "has_variants": 1,
"item_variants": [
{"item_attribute": "Test Size", "item_attribute_value": "Small"},
{"item_attribute": "Test Size", "item_attribute_value": "Medium"},
diff --git a/erpnext/stock/doctype/item_attribute/test_records.json b/erpnext/stock/doctype/item_attribute/test_records.json
index f418475..0208c17 100644
--- a/erpnext/stock/doctype/item_attribute/test_records.json
+++ b/erpnext/stock/doctype/item_attribute/test_records.json
@@ -4,9 +4,9 @@
"attribute_name": "Test Size",
"priority": 1,
"item_attribute_values": [
- {"attribute_value": "Small", "abbr": "SM"},
- {"attribute_value": "Medium", "abbr": "MD"},
- {"attribute_value": "Large", "abbr": "LG"}
+ {"attribute_value": "Small", "abbr": "S"},
+ {"attribute_value": "Medium", "abbr": "M"},
+ {"attribute_value": "Large", "abbr": "L"}
]
},
{