From 724f9e57e3c19d362e195774f735a8f48ac12622 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 7 Oct 2014 15:29:58 +0530 Subject: [PATCH] [item-variants] get table values from template if not set in variant #2224 --- .../quality_inspection/quality_inspection.py | 28 +++++++----- erpnext/controllers/buying_controller.py | 6 +-- erpnext/stock/doctype/item/item.js | 7 ++- erpnext/stock/doctype/item/item.json | 7 ++- erpnext/stock/doctype/item/item.py | 40 +++++++++++++---- erpnext/stock/doctype/item/test_item.py | 2 +- erpnext/stock/doctype/item/test_records.json | 8 ++++ .../stock/doctype/stock_entry/stock_entry.py | 5 +-- .../doctype/stock_entry/test_stock_entry.py | 44 +++++++++++++++++++ erpnext/stock/get_item_details.py | 11 ++++- erpnext/stock/utils.py | 15 +++++-- 11 files changed, 136 insertions(+), 37 deletions(-) diff --git a/erpnext/buying/doctype/quality_inspection/quality_inspection.py b/erpnext/buying/doctype/quality_inspection/quality_inspection.py index da341086d7..216d1657d0 100644 --- a/erpnext/buying/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/buying/doctype/quality_inspection/quality_inspection.py @@ -8,11 +8,15 @@ import frappe from frappe.model.document import Document class QualityInspection(Document): - def get_item_specification_details(self): self.set('qa_specification_details', []) - specification = frappe.db.sql("select specification, value from `tabItem Quality Inspection Parameter` \ - where parent = '%s' order by idx" % (self.item_code)) + variant_of = frappe.db.get_query("Item", self.item_code, "variant_of") + if variant_of: + specification = frappe.db.sql("select specification, value from `tabItem Quality Inspection Parameter` \ + where parent in (%s, %s) order by idx", (self.item_code, variant_of)) + else: + specification = frappe.db.sql("select specification, value from `tabItem Quality Inspection Parameter` \ + where parent = %s order by idx", self.item_code) for d in specification: child = self.append('qa_specification_details', {}) child.specification = d[0] @@ -21,18 +25,18 @@ class QualityInspection(Document): def on_submit(self): if self.purchase_receipt_no: - frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2 - set t1.qa_no = %s, t2.modified = %s - where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""", - (self.name, self.modified, self.purchase_receipt_no, + frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2 + set t1.qa_no = %s, t2.modified = %s + where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""", + (self.name, self.modified, self.purchase_receipt_no, self.item_code)) - + def on_cancel(self): if self.purchase_receipt_no: - frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2 + frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2 set t1.qa_no = '', t2.modified = %s - where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""", + where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""", (self.modified, self.purchase_receipt_no, self.item_code)) @@ -45,6 +49,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): "start": start, "page_len": page_len }) - return frappe.db.sql("""select item_code from `tab%(from)s` + return frappe.db.sql("""select item_code from `tab%(from)s` where parent='%(parent)s' and docstatus < 2 and item_code like '%%%(txt)s%%' %(mcond)s - order by item_code limit %(start)s, %(page_len)s""" % filters) \ No newline at end of file + order by item_code limit %(start)s, %(page_len)s""" % filters) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e8f35f1347..85c1449418 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -8,6 +8,7 @@ from frappe.utils import flt, rounded from erpnext.setup.utils import get_company_currency from erpnext.accounts.party import get_party_details +from erpnext.stock.get_item_details import get_conversion_factor from erpnext.controllers.stock_controller import StockController @@ -194,9 +195,8 @@ class BuyingController(StockController): self.round_floats_in(item) - item.conversion_factor = item.conversion_factor or flt(frappe.db.get_value( - "UOM Conversion Detail", {"parent": item.item_code, "uom": item.uom}, - "conversion_factor")) or 1 + item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") + qty_in_stock_uom = flt(item.qty * item.conversion_factor) rm_supp_cost = flt(item.rm_supp_cost) if self.doctype=="Purchase Receipt" else 0.0 diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index e274690f39..b05ce2579c 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -12,6 +12,9 @@ cur_frm.cscript.refresh = function(doc) { cur_frm.set_intro(); if (cur_frm.doc.has_variants) { cur_frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set")); + cur_frm.add_custom_button(__("Show Variants"), function() { + frappe.set_route("List", "Item", {"variant_of": cur_frm.doc.name}); + }, "icon-list", "btn-default"); } if (cur_frm.doc.variant_of) { cur_frm.set_intro(__("This Item is a Variant of {0} (Template). Attributes will be copied over from the template unless 'No Copy' is set", [cur_frm.doc.variant_of])); @@ -33,7 +36,7 @@ cur_frm.cscript.refresh = function(doc) { if (!doc.__islocal && doc.show_in_website) { cur_frm.set_intro(__("Published on website at: {0}", - [repl('/%(website_route)s', doc.__onload)])); + [repl('/%(website_route)s', doc.__onload)]), true); } erpnext.item.toggle_reqd(cur_frm); @@ -78,7 +81,7 @@ cur_frm.cscript.make_dashboard = function() { cur_frm.cscript.edit_prices_button = function() { cur_frm.add_custom_button(__("Add / Edit Prices"), function() { frappe.set_route("Report", "Item Price", {"item_code": cur_frm.doc.name}); - }, "icon-money"); + }, "icon-money", "btn-default"); } cur_frm.cscript.item_code = function(doc) { diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index c1a1fe92de..12ff900667 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -390,6 +390,7 @@ "read_only": 0 }, { + "description": "Will also apply for variants unless overrridden", "fieldname": "item_reorder", "fieldtype": "Table", "label": "Warehouse-wise Item Reorder", @@ -487,6 +488,7 @@ }, { "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "description": "Will also apply for variants", "fieldname": "uom_conversion_details", "fieldtype": "Table", "label": "UOM Conversion Details", @@ -619,6 +621,7 @@ "read_only": 0 }, { + "description": "Will also apply for variants", "fieldname": "item_tax", "fieldtype": "Table", "label": "Item Tax1", @@ -652,7 +655,7 @@ }, { "depends_on": "eval:doc.inspection_required==\"Yes\"", - "description": "Quality Inspection Parameters", + "description": "Will also apply to variants", "fieldname": "item_specification_details", "fieldtype": "Table", "label": "Item Quality Inspection Parameter", @@ -862,7 +865,7 @@ "icon": "icon-tag", "idx": 1, "max_attachments": 1, - "modified": "2014-10-03 04:58:39.278047", + "modified": "2014-10-07 05:25:19.921651", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c7d9375043..cf46372696 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -42,6 +42,8 @@ class Item(WebsiteGenerator): if self.image and not self.website_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_stock_uom_with_bin() self.add_default_uom_in_conversion_factor_table() @@ -211,32 +213,54 @@ class Item(WebsiteGenerator): def make_variant(self, item_code): item = frappe.new_doc("Item") - self.copy_attributes_to_variant(item, item_code, insert=True) 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) - self.copy_attributes_to_variant(item, item_code) + item.item_code = item_code + self.copy_attributes_to_variant(self, item) item.save() - def copy_attributes_to_variant(self, variant, item_code, insert=False): + 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): - if variant.get(field.fieldname) != self.get(field.fieldname): - variant.set(field.fieldname, self.get(field.fieldname)) + if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\ + and field.fieldname != "item_code": + if variant.get(field.fieldname) != template.get(field.fieldname): + variant.set(field.fieldname, template.get(field.fieldname)) variant.__dirty = True variant.description += "\n" - for attr in self.variant_attributes[item_code]: + + if not getattr(template, "variant_attributes", None): + template.get_variant_item_codes() + + for attr in template.variant_attributes[variant.item_code]: variant.description += "\n" + attr[0] + ": " + attr[1] if variant.description_html: variant.description_html += "
" + attr[0] + ": " + attr[1] + "
" - variant.variant_of = self.name + variant.variant_of = template.name variant.has_variants = 0 variant.show_in_website = 0 + def update_template_tables(self): + template = frappe.get_doc("Item", self.variant_of) + + # add item taxes from template + for d in template.get("item_tax"): + self.append("item_tax", {"tax_type": d.tax_type, "tax_rate": d.tax_rate}) + + # copy re-order table if empty + if not self.get("item_reorder"): + for d in template.get("item_reorder"): + n = {} + for k in ("warehouse", "warehouse_reorder_level", + "warehouse_reorder_qty", "material_request_type"): + n[k] = d.get(k) + self.append("item_reorder", n) + def validate_conversion_factor(self): check_list = [] for d in self.get('uom_conversion_details'): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 2807ca8aab..803913f944 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -34,7 +34,7 @@ class TestItem(unittest.TestCase): se.purpose = "Material Receipt" se.append("mtn_details", { "item_code": item.name, - "t_warehouse": "Stores - WP", + "t_warehouse": "Stores - _TC", "qty": 1, "incoming_rate": 1 }) diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 01c4e4f643..531f1a72f6 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -275,6 +275,14 @@ {"item_attribute": "Test Size", "item_attribute_value": "Small"}, {"item_attribute": "Test Size", "item_attribute_value": "Medium"}, {"item_attribute": "Test Size", "item_attribute_value": "Large"} + ], + "item_reorder": [ + { + "material_request_type": "Purchase", + "warehouse": "_Test Warehouse - _TC", + "warehouse_reorder_level": 20, + "warehouse_reorder_qty": 20 + } ] } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 028fff9fc5..fb4493a37c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -11,7 +11,7 @@ from frappe import _ from erpnext.stock.utils import get_incoming_rate from erpnext.stock.stock_ledger import get_previous_sle from erpnext.controllers.queries import get_match_cond -from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center +from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center, get_conversion_factor from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class NotUpdateStockError(frappe.ValidationError): pass @@ -436,8 +436,7 @@ class StockEntry(StockController): return ret def get_uom_details(self, args): - conversion_factor = frappe.db.get_value("UOM Conversion Detail", {"parent": args.get("item_code"), - "uom": args.get("uom")}, "conversion_factor") + conversion_factor = get_conversion_factor(args.get("item_code"), args.get("uom")).get("conversion_factor") if not conversion_factor: frappe.msgprint(_("UOM coversion factor required for UOM: {0} in Item: {1}") diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 03eb9fe253..6d8dc8be12 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -37,8 +37,52 @@ class TestStockEntry(unittest.TestCase): mr_name = frappe.db.sql("""select parent from `tabMaterial Request Item` where item_code='_Test Item'""") + frappe.db.set_value("Stock Settings", None, "auto_indent", 0) + self.assertTrue(mr_name) + def test_auto_material_request_for_variant(self): + item_code = "_Test Variant Item-S" + item = frappe.get_doc("Item", item_code) + template = frappe.get_doc("Item", item.variant_of) + + warehouse = "_Test Warehouse - _TC" + + # stock entry reqd for auto-reorder + se = frappe.new_doc("Stock Entry") + se.purpose = "Material Receipt" + se.company = "_Test Company" + se.append("mtn_details", { + "item_code": item_code, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 1, + "incoming_rate": 1 + }) + se.insert() + se.submit() + + frappe.db.set_value("Stock Settings", None, "auto_indent", 1) + projected_qty = frappe.db.get_value("Bin", {"item_code": item_code, + "warehouse": warehouse}, "projected_qty") or 0 + + + # update re-level qty so that it is more than projected_qty + if projected_qty > template.item_reorder[0].warehouse_reorder_level: + template.item_reorder[0].warehouse_reorder_level += projected_qty + template.save() + + from erpnext.stock.utils import reorder_item + mr_list = reorder_item() + + frappe.db.set_value("Stock Settings", None, "auto_indent", 0) + + items = [] + for mr in mr_list: + for d in mr.indent_details: + items.append(d.item_code) + + self.assertTrue(item_code in items) + def test_material_receipt_gl_entry(self): self._clear_stock_account_balance() set_perpetual_inventory() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index fd728e6f4f..2273018525 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -133,13 +133,15 @@ def get_basic_details(args, item): if not item: item = frappe.get_doc("Item", args.get("item_code")) + if item.variant_of: + item.update_template_tables() + from frappe.defaults import get_user_default_as_list user_default_warehouse_list = get_user_default_as_list('warehouse') user_default_warehouse = user_default_warehouse_list[0] \ if len(user_default_warehouse_list)==1 else "" out = frappe._dict({ - "item_code": item.name, "item_name": item.item_name, "description": item.description_html or item.description, @@ -291,8 +293,13 @@ def get_serial_nos_by_fifo(args, item_doc): @frappe.whitelist() def get_conversion_factor(item_code, uom): + variant_of = frappe.db.get_value("Item", item_code, "variant_of") + filters = {"parent": item_code, "uom": uom} + if variant_of: + filters = {"parent": ("in", (item_code, variant_of))} + return {"conversion_factor": frappe.db.get_value("UOM Conversion Detail", - {"parent": item_code, "uom": uom}, "conversion_factor")} + filters, "conversion_factor")} @frappe.whitelist() def get_projected_qty(item_code, warehouse): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 2b4e3682d6..0a4be40db2 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -192,14 +192,15 @@ def reorder_item(): frappe.local.auto_indent = cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')) if frappe.local.auto_indent: - _reorder_item() + return _reorder_item() def _reorder_item(): - # {"Purchase": {"Company": [{"item_code": "", "warehouse": "", "reorder_qty": 0.0}]}, "Transfer": {...}} material_requests = {"Purchase": {}, "Transfer": {}} item_warehouse_projected_qty = get_item_warehouse_projected_qty() - warehouse_company = frappe._dict(frappe.db.sql("""select name, company from `tabWarehouse`""")) + + warehouse_company = frappe._dict(frappe.db.sql("""select name, company + from `tabWarehouse`""")) default_company = (frappe.defaults.get_defaults().get("company") or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]) @@ -227,6 +228,10 @@ def _reorder_item(): for item_code in item_warehouse_projected_qty: item = frappe.get_doc("Item", item_code) + + if item.variant_of and not item.get("item_reorder"): + item.update_template_tables() + if item.get("item_reorder"): for d in item.get("item_reorder"): add_to_material_request(item_code, d.warehouse, d.warehouse_reorder_level, @@ -237,7 +242,7 @@ def _reorder_item(): add_to_material_request(item_code, item.default_warehouse, item.re_order_level, item.re_order_qty, "Purchase") if material_requests: - create_material_request(material_requests) + return create_material_request(material_requests) def get_item_warehouse_projected_qty(): item_warehouse_projected_qty = {} @@ -326,6 +331,8 @@ def create_material_request(material_requests): if exceptions_list: notify_errors(exceptions_list) + return mr_list + def send_email_notification(mr_list): """ Notify user about auto creation of indent"""