From 70aa8933b111815f6d35b559f67993708a65bb07 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 26 Sep 2014 18:54:01 +0530 Subject: [PATCH 01/12] [item variant] started --- .../doctype/pricing_rule/pricing_rule.json | 3 +- .../doctype/pricing_rule/test_records.json | 6 + erpnext/config/stock.py | 5 + erpnext/stock/doctype/item/item.js | 27 +- erpnext/stock/doctype/item/item.json | 1486 +++++++++-------- erpnext/stock/doctype/item/item.py | 42 + erpnext/stock/doctype/item/test_item.py | 12 +- erpnext/stock/doctype/item/test_records.json | 47 +- .../stock/doctype/item_attribute/__init__.py | 0 .../item_attribute/item_attribute.json | 87 + .../doctype/item_attribute/item_attribute.py | 19 + .../item_attribute/test_item_attribute.py | 10 + .../doctype/item_attribute/test_records.json | 22 + .../doctype/item_attribute_value/__init__.py | 0 .../item_attribute_value.json | 64 + .../item_attribute_value.py | 9 + .../test_item_attribute_value.py | 10 + .../item_attribute_value/test_records.json | 6 + .../stock/doctype/item_variant/__init__.py | 0 .../doctype/item_variant/item_variant.json | 72 + .../doctype/item_variant/item_variant.py | 9 + 21 files changed, 1196 insertions(+), 740 deletions(-) create mode 100644 erpnext/accounts/doctype/pricing_rule/test_records.json create mode 100644 erpnext/stock/doctype/item_attribute/__init__.py create mode 100644 erpnext/stock/doctype/item_attribute/item_attribute.json create mode 100644 erpnext/stock/doctype/item_attribute/item_attribute.py create mode 100644 erpnext/stock/doctype/item_attribute/test_item_attribute.py create mode 100644 erpnext/stock/doctype/item_attribute/test_records.json create mode 100644 erpnext/stock/doctype/item_attribute_value/__init__.py create mode 100644 erpnext/stock/doctype/item_attribute_value/item_attribute_value.json create mode 100644 erpnext/stock/doctype/item_attribute_value/item_attribute_value.py create mode 100644 erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py create mode 100644 erpnext/stock/doctype/item_attribute_value/test_records.json create mode 100644 erpnext/stock/doctype/item_variant/__init__.py create mode 100644 erpnext/stock/doctype/item_variant/item_variant.json create mode 100644 erpnext/stock/doctype/item_variant/item_variant.py diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 2d318c6360e..7a79c1af0b7 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -164,6 +164,7 @@ "permlevel": 0 }, { + "description": "Higher the number, higher the priority", "fieldname": "priority", "fieldtype": "Select", "label": "Priority", @@ -235,7 +236,7 @@ "icon": "icon-gift", "idx": 1, "istable": 0, - "modified": "2014-06-20 19:36:22.502381", + "modified": "2014-09-26 09:09:38.418765", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/test_records.json b/erpnext/accounts/doctype/pricing_rule/test_records.json new file mode 100644 index 00000000000..706d54cf2e4 --- /dev/null +++ b/erpnext/accounts/doctype/pricing_rule/test_records.json @@ -0,0 +1,6 @@ +[ + { + "doctype": "Pricing Rule", + "name": "_Test Pricing Rule 1" + } +] diff --git a/erpnext/config/stock.py b/erpnext/config/stock.py index 7a4345e4d22..f535a30e199 100644 --- a/erpnext/config/stock.py +++ b/erpnext/config/stock.py @@ -129,6 +129,11 @@ def get_data(): "description": _("Multiple Item prices."), "route": "Report/Item Price" }, + { + "type": "doctype", + "name": "Item Attribute", + "description": _("Attributes for Item Variants. e.g Size, Color etc."), + }, ] }, { diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index fce8dfaf5ba..9af1d904ccd 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -35,11 +35,32 @@ erpnext.item.toggle_reqd = function(frm) { frm.toggle_reqd("default_warehouse", frm.doc.is_stock_item==="Yes"); }; -frappe.ui.form.on("Item", "is_stock_item", function(frm) { - erpnext.item.toggle_reqd(frm); +frappe.ui.form.on("Item", "onload", function(frm) { + var df = frappe.meta.get_docfield("Item Variant", "item_attribute_value"); + df.on_make = function(field) { + field.$input.autocomplete({ + minLength: 0, + minChars: 0, + source: function(request, response) { + frappe.call({ + method:"frappe.client.get_list", + args:{ + doctype:"Item Attribute Value", + filters: [ + ["parent","=", field.doc.item_attribute], + ["attribute_value", "like", request.term + "%"] + ], + fields: ["attribute_value"] + }, + callback: function(r) { + response($.map(r.message, function(d) { return d.attribute_value; })); + } + }); + } + }) + } }); - cur_frm.cscript.make_dashboard = function() { cur_frm.dashboard.reset(); if(cur_frm.doc.__islocal) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index db39f7badbe..70e28974c02 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1,905 +1,941 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:item_code", - "creation": "2013-05-03 10:45:46", - "default_print_format": "Standard", - "description": "A Product or a Service that is bought, sold or kept in stock.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Master", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:item_code", + "creation": "2013-05-03 10:45:46", + "default_print_format": "Standard", + "description": "A Product or a Service that is bought, sold or kept in stock.", + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", "fields": [ { - "fieldname": "name_and_description_section", - "fieldtype": "Section Break", - "label": "Name and Description", - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "icon-flag", - "permlevel": 0, + "fieldname": "name_and_description_section", + "fieldtype": "Section Break", + "label": "Name and Description", + "no_copy": 0, + "oldfieldtype": "Section Break", + "options": "icon-flag", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "ITEM-", - "permlevel": 0, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "ITEM-", + "permlevel": 0, "read_only": 0 - }, + }, { - "description": "Item will be saved by this name in the data base.", - "fieldname": "item_code", - "fieldtype": "Data", - "in_filter": 0, - "label": "Item Code", - "no_copy": 1, - "oldfieldname": "item_code", - "oldfieldtype": "Data", - "permlevel": 0, - "read_only": 0, - "reqd": 0, + "description": "Item will be saved by this name in the data base.", + "fieldname": "item_code", + "fieldtype": "Data", + "in_filter": 0, + "label": "Item Code", + "no_copy": 1, + "oldfieldname": "item_code", + "oldfieldtype": "Data", + "permlevel": 0, + "read_only": 0, + "reqd": 0, "search_index": 0 - }, + }, { - "fieldname": "item_name", - "fieldtype": "Data", - "in_filter": 1, - "in_list_view": 1, - "label": "Item Name", - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "permlevel": 0, - "read_only": 0, - "reqd": 1, + "description": "If item is a variant of another item then description, image, pricing, taxes etc will be set from the template unless explicitly specified", + "fieldname": "variant_of", + "fieldtype": "Link", + "label": "Variant Of", + "options": "Item", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "in_filter": 1, + "in_list_view": 1, + "label": "Item Name", + "oldfieldname": "item_name", + "oldfieldtype": "Data", + "permlevel": 0, + "read_only": 0, + "reqd": 1, "search_index": 1 - }, + }, { - "description": "Add / Edit", - "fieldname": "item_group", - "fieldtype": "Link", - "in_filter": 1, - "label": "Item Group", - "oldfieldname": "item_group", - "oldfieldtype": "Link", - "options": "Item Group", - "permlevel": 0, - "read_only": 0, + "description": "Add / Edit", + "fieldname": "item_group", + "fieldtype": "Link", + "in_filter": 1, + "label": "Item Group", + "oldfieldname": "item_group", + "oldfieldtype": "Link", + "options": "Item Group", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "description": "Unit of measurement of this item (e.g. Kg, Unit, No, Pair).", - "fieldname": "stock_uom", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Unit of Measure", - "oldfieldname": "stock_uom", - "oldfieldtype": "Link", - "options": "UOM", - "permlevel": 0, - "read_only": 0, + "description": "Unit of measurement of this item (e.g. Kg, Unit, No, Pair).", + "fieldname": "stock_uom", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Unit of Measure", + "oldfieldname": "stock_uom", + "oldfieldtype": "Link", + "options": "UOM", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "fieldname": "brand", - "fieldtype": "Link", - "hidden": 0, - "label": "Brand", - "oldfieldname": "brand", - "oldfieldtype": "Link", - "options": "Brand", - "permlevel": 0, - "print_hide": 1, - "read_only": 0, + "fieldname": "brand", + "fieldtype": "Link", + "hidden": 0, + "label": "Brand", + "oldfieldname": "brand", + "oldfieldtype": "Link", + "options": "Brand", + "permlevel": 0, + "print_hide": 1, + "read_only": 0, "reqd": 0 - }, + }, { - "fieldname": "barcode", - "fieldtype": "Data", - "label": "Barcode", - "permlevel": 0, + "fieldname": "barcode", + "fieldtype": "Data", + "label": "Barcode", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "column_break0", - "fieldtype": "Column Break", - "permlevel": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "image", - "fieldtype": "Attach", - "label": "Image", - "options": "", - "permlevel": 0, + "fieldname": "image", + "fieldtype": "Attach", + "label": "Image", + "options": "", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "image_view", - "fieldtype": "Image", - "in_list_view": 1, - "label": "Image View", - "options": "image", - "permlevel": 0, + "fieldname": "image_view", + "fieldtype": "Image", + "in_list_view": 1, + "label": "Image View", + "options": "image", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "description", - "fieldtype": "Small Text", - "in_filter": 0, - "in_list_view": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "read_only": 0, - "reqd": 1, + "fieldname": "description", + "fieldtype": "Small Text", + "in_filter": 0, + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "permlevel": 0, + "read_only": 0, + "reqd": 1, "search_index": 0 - }, + }, { - "fieldname": "description_html", - "fieldtype": "Small Text", - "label": "Description HTML", - "permlevel": 0, + "fieldname": "description_html", + "fieldtype": "Small Text", + "label": "Description HTML", + "permlevel": 0, "read_only": 0 - }, + }, { - "description": "Generates HTML to include selected image in the description", - "fieldname": "add_image", - "fieldtype": "Button", - "label": "Generate Description HTML", - "permlevel": 0, + "description": "Generates HTML to include selected image in the description", + "fieldname": "add_image", + "fieldtype": "Button", + "label": "Generate Description HTML", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "inventory", - "fieldtype": "Section Break", - "label": "Inventory", - "oldfieldtype": "Section Break", - "options": "icon-truck", - "permlevel": 0, + "fieldname": "variants", + "fieldtype": "Section Break", + "label": "Variants", + "permlevel": 0, + "precision": "" + }, + { + "description": "Automatically set. If this item has variants, then it cannot be selected in sales orders etc.", + "fieldname": "has_variants", + "fieldtype": "Check", + "label": "Has Variants", + "options": "", + "permlevel": 0, + "precision": "", "read_only": 0 - }, + }, { - "default": "Yes", - "description": "Select \"Yes\" if you are maintaining stock of this item in your Inventory.", - "fieldname": "is_stock_item", - "fieldtype": "Select", - "label": "Is Stock Item", - "oldfieldname": "is_stock_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "depends_on": "has_variants", + "description": "A new variant (Item) will be created for each attribute value combination", + "fieldname": "item_variants", + "fieldtype": "Table", + "label": "Item Variants", + "options": "Item Variant", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "inventory", + "fieldtype": "Section Break", + "label": "Inventory", + "oldfieldtype": "Section Break", + "options": "icon-truck", + "permlevel": 0, + "read_only": 0 + }, + { + "default": "Yes", + "description": "Select \"Yes\" if you are maintaining stock of this item in your Inventory.", + "fieldname": "is_stock_item", + "fieldtype": "Select", + "label": "Is Stock Item", + "oldfieldname": "is_stock_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "depends_on": "", - "description": "Mandatory if Stock Item is \"Yes\". Also the default warehouse where reserved quantity is set from Sales Order.", - "fieldname": "default_warehouse", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Warehouse", - "oldfieldname": "default_warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "permlevel": 0, + "depends_on": "", + "description": "Mandatory if Stock Item is \"Yes\". Also the default warehouse where reserved quantity is set from Sales Order.", + "fieldname": "default_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Warehouse", + "oldfieldname": "default_warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "description": "Percentage variation in quantity to be allowed while receiving or delivering this item.", - "fieldname": "tolerance", - "fieldtype": "Float", - "label": "Allowance Percent", - "oldfieldname": "tolerance", - "oldfieldtype": "Currency", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "description": "Percentage variation in quantity to be allowed while receiving or delivering this item.", + "fieldname": "tolerance", + "fieldtype": "Float", + "label": "Allowance Percent", + "oldfieldname": "tolerance", + "oldfieldtype": "Currency", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "valuation_method", - "fieldtype": "Select", - "label": "Valuation Method", - "options": "\nFIFO\nMoving Average", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "valuation_method", + "fieldtype": "Select", + "label": "Valuation Method", + "options": "\nFIFO\nMoving Average", + "permlevel": 0, "read_only": 0 - }, + }, { - "default": "0.00", - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "description": "You can enter the minimum quantity of this item to be ordered.", - "fieldname": "min_order_qty", - "fieldtype": "Float", - "hidden": 0, - "label": "Minimum Order Qty", - "oldfieldname": "min_order_qty", - "oldfieldtype": "Currency", - "permlevel": 0, + "default": "0.00", + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "description": "You can enter the minimum quantity of this item to be ordered.", + "fieldname": "min_order_qty", + "fieldtype": "Float", + "hidden": 0, + "label": "Minimum Order Qty", + "oldfieldname": "min_order_qty", + "oldfieldtype": "Currency", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "column_break1", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "permlevel": 0, - "read_only": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "permlevel": 0, + "read_only": 0, "width": "50%" - }, + }, { - "default": "No", - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "description": "Select \"Yes\" if this item is used for some internal purpose in your company.", - "fieldname": "is_asset_item", - "fieldtype": "Select", - "label": "Is Fixed Asset Item", - "oldfieldname": "is_asset_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "description": "Select \"Yes\" if this item is used for some internal purpose in your company.", + "fieldname": "is_asset_item", + "fieldtype": "Select", + "label": "Is Fixed Asset Item", + "oldfieldname": "is_asset_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "default": "No", - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "has_batch_no", - "fieldtype": "Select", - "label": "Has Batch No", - "oldfieldname": "has_batch_no", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "has_batch_no", + "fieldtype": "Select", + "label": "Has Batch No", + "oldfieldname": "has_batch_no", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "default": "No", - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "description": "Selecting \"Yes\" will give a unique identity to each entity of this item which can be viewed in the Serial No master.", - "fieldname": "has_serial_no", - "fieldtype": "Select", - "in_filter": 1, - "label": "Has Serial No", - "oldfieldname": "has_serial_no", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "description": "Selecting \"Yes\" will give a unique identity to each entity of this item which can be viewed in the Serial No master.", + "fieldname": "has_serial_no", + "fieldtype": "Select", + "in_filter": 1, + "label": "Has Serial No", + "oldfieldname": "has_serial_no", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "depends_on": "eval: doc.has_serial_no===\"Yes\"", - "description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.", - "fieldname": "serial_no_series", - "fieldtype": "Data", - "label": "Serial Number Series", - "no_copy": 1, + "depends_on": "eval: doc.has_serial_no===\"Yes\"", + "description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.", + "fieldname": "serial_no_series", + "fieldtype": "Data", + "label": "Serial Number Series", + "no_copy": 1, "permlevel": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "warranty_period", - "fieldtype": "Data", - "label": "Warranty Period (in days)", - "oldfieldname": "warranty_period", - "oldfieldtype": "Data", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "warranty_period", + "fieldtype": "Data", + "label": "Warranty Period (in days)", + "oldfieldname": "warranty_period", + "oldfieldtype": "Data", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "end_of_life", - "fieldtype": "Date", - "label": "End of Life", - "oldfieldname": "end_of_life", - "oldfieldtype": "Date", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "end_of_life", + "fieldtype": "Date", + "label": "End of Life", + "oldfieldname": "end_of_life", + "oldfieldtype": "Date", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "description": "Net Weight of each Item", - "fieldname": "net_weight", - "fieldtype": "Float", - "label": "Net Weight", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "description": "Net Weight of each Item", + "fieldname": "net_weight", + "fieldtype": "Float", + "label": "Net Weight", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "weight_uom", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Weight UOM", - "options": "UOM", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "weight_uom", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Weight UOM", + "options": "UOM", + "permlevel": 0, "read_only": 0 - }, + }, { - "description": "Auto-raise Material Request if quantity goes below re-order level in a warehouse", - "fieldname": "reorder_section", - "fieldtype": "Section Break", - "label": "Re-order", - "options": "icon-rss", - "permlevel": 0, + "description": "Auto-raise Material Request if quantity goes below re-order level in a warehouse", + "fieldname": "reorder_section", + "fieldtype": "Section Break", + "label": "Re-order", + "options": "icon-rss", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "re_order_level", - "fieldtype": "Float", - "label": "Re-Order Level", - "oldfieldname": "re_order_level", - "oldfieldtype": "Currency", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "re_order_level", + "fieldtype": "Float", + "label": "Re-Order Level", + "oldfieldname": "re_order_level", + "oldfieldtype": "Currency", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "fieldname": "re_order_qty", - "fieldtype": "Float", - "label": "Re-Order Qty", - "permlevel": 0, + "depends_on": "eval:doc.is_stock_item==\"Yes\"", + "fieldname": "re_order_qty", + "fieldtype": "Float", + "label": "Re-Order Qty", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "section_break_31", - "fieldtype": "Section Break", - "permlevel": 0, + "fieldname": "section_break_31", + "fieldtype": "Section Break", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "item_reorder", - "fieldtype": "Table", - "label": "Warehouse-wise Item Reorder", - "options": "Item Reorder", - "permlevel": 0, + "fieldname": "item_reorder", + "fieldtype": "Table", + "label": "Warehouse-wise Item Reorder", + "options": "Item Reorder", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "purchase_details", - "fieldtype": "Section Break", - "label": "Purchase Details", - "oldfieldtype": "Section Break", - "options": "icon-shopping-cart", - "permlevel": 0, + "fieldname": "purchase_details", + "fieldtype": "Section Break", + "label": "Purchase Details", + "oldfieldtype": "Section Break", + "options": "icon-shopping-cart", + "permlevel": 0, "read_only": 0 - }, + }, { - "default": "Yes", - "description": "Selecting \"Yes\" will allow this item to appear in Purchase Order , Purchase Receipt.", - "fieldname": "is_purchase_item", - "fieldtype": "Select", - "label": "Is Purchase Item", - "oldfieldname": "is_purchase_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "Yes", + "description": "Selecting \"Yes\" will allow this item to appear in Purchase Order , Purchase Receipt.", + "fieldname": "is_purchase_item", + "fieldtype": "Select", + "label": "Is Purchase Item", + "oldfieldname": "is_purchase_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "fieldname": "default_supplier", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Supplier", - "options": "Supplier", + "fieldname": "default_supplier", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Supplier", + "options": "Supplier", "permlevel": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "description": "Lead Time days is number of days by which this item is expected in your warehouse. This days is fetched in Material Request when you select this item.", - "fieldname": "lead_time_days", - "fieldtype": "Int", - "label": "Lead Time Days", - "no_copy": 1, - "oldfieldname": "lead_time_days", - "oldfieldtype": "Int", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "description": "Lead Time days is number of days by which this item is expected in your warehouse. This days is fetched in Material Request when you select this item.", + "fieldname": "lead_time_days", + "fieldtype": "Int", + "label": "Lead Time Days", + "no_copy": 1, + "oldfieldname": "lead_time_days", + "oldfieldtype": "Int", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "description": "Default Purchase Account in which cost of the item will be debited.", - "fieldname": "expense_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Expense Account", - "oldfieldname": "purchase_account", - "oldfieldtype": "Link", - "options": "Account", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "description": "Default Purchase Account in which cost of the item will be debited.", + "fieldname": "expense_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Expense Account", + "oldfieldname": "purchase_account", + "oldfieldtype": "Link", + "options": "Account", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "description": "", - "fieldname": "buying_cost_center", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Buying Cost Center", - "oldfieldname": "cost_center", - "oldfieldtype": "Link", - "options": "Cost Center", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "description": "", + "fieldname": "buying_cost_center", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Buying Cost Center", + "oldfieldname": "cost_center", + "oldfieldtype": "Link", + "options": "Cost Center", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "fieldname": "last_purchase_rate", - "fieldtype": "Float", - "label": "Last Purchase Rate", - "no_copy": 1, - "oldfieldname": "last_purchase_rate", - "oldfieldtype": "Currency", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "fieldname": "last_purchase_rate", + "fieldtype": "Float", + "label": "Last Purchase Rate", + "no_copy": 1, + "oldfieldname": "last_purchase_rate", + "oldfieldtype": "Currency", + "permlevel": 0, "read_only": 1 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "fieldname": "column_break2", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "permlevel": 0, - "read_only": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "fieldname": "column_break2", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "permlevel": 0, + "read_only": 0, "width": "50%" - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "fieldname": "uom_conversion_details", - "fieldtype": "Table", - "label": "UOM Conversion Details", - "no_copy": 1, - "oldfieldname": "uom_conversion_details", - "oldfieldtype": "Table", - "options": "UOM Conversion Detail", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "fieldname": "uom_conversion_details", + "fieldtype": "Table", + "label": "UOM Conversion Details", + "no_copy": 1, + "oldfieldname": "uom_conversion_details", + "oldfieldtype": "Table", + "options": "UOM Conversion Detail", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "fieldname": "manufacturer", - "fieldtype": "Data", - "label": "Manufacturer", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "fieldname": "manufacturer", + "fieldtype": "Data", + "label": "Manufacturer", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "fieldname": "manufacturer_part_no", - "fieldtype": "Data", - "label": "Manufacturer Part Number", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "fieldname": "manufacturer_part_no", + "fieldtype": "Data", + "label": "Manufacturer Part Number", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_purchase_item==\"Yes\"", - "fieldname": "item_supplier_details", - "fieldtype": "Table", - "label": "Item Supplier Details", - "options": "Item Supplier", - "permlevel": 0, + "depends_on": "eval:doc.is_purchase_item==\"Yes\"", + "fieldname": "item_supplier_details", + "fieldtype": "Table", + "label": "Item Supplier Details", + "options": "Item Supplier", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "sales_details", - "fieldtype": "Section Break", - "label": "Sales Details", - "oldfieldtype": "Section Break", - "options": "icon-tag", - "permlevel": 0, + "fieldname": "sales_details", + "fieldtype": "Section Break", + "label": "Sales Details", + "oldfieldtype": "Section Break", + "options": "icon-tag", + "permlevel": 0, "read_only": 0 - }, + }, { - "default": "Yes", - "description": "Selecting \"Yes\" will allow this item to figure in Sales Order, Delivery Note", - "fieldname": "is_sales_item", - "fieldtype": "Select", - "in_filter": 1, - "label": "Is Sales Item", - "oldfieldname": "is_sales_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "Yes", + "description": "Selecting \"Yes\" will allow this item to figure in Sales Order, Delivery Note", + "fieldname": "is_sales_item", + "fieldtype": "Select", + "in_filter": 1, + "label": "Is Sales Item", + "oldfieldname": "is_sales_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "default": "No", - "depends_on": "eval:doc.is_sales_item==\"Yes\"", - "description": "Select \"Yes\" if this item represents some work like training, designing, consulting etc.", - "fieldname": "is_service_item", - "fieldtype": "Select", - "in_filter": 1, - "label": "Is Service Item", - "oldfieldname": "is_service_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "depends_on": "eval:doc.is_sales_item==\"Yes\"", + "description": "Select \"Yes\" if this item represents some work like training, designing, consulting etc.", + "fieldname": "is_service_item", + "fieldtype": "Select", + "in_filter": 1, + "label": "Is Service Item", + "oldfieldname": "is_service_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "depends_on": "eval:doc.is_sales_item==\"Yes\"", - "fieldname": "max_discount", - "fieldtype": "Float", - "label": "Max Discount (%)", - "oldfieldname": "max_discount", - "oldfieldtype": "Currency", - "permlevel": 0, + "depends_on": "eval:doc.is_sales_item==\"Yes\"", + "fieldname": "max_discount", + "fieldtype": "Float", + "label": "Max Discount (%)", + "oldfieldname": "max_discount", + "oldfieldtype": "Currency", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_sales_item==\"Yes\"", - "fieldname": "income_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Income Account", - "options": "Account", - "permlevel": 0, + "depends_on": "eval:doc.is_sales_item==\"Yes\"", + "fieldname": "income_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Income Account", + "options": "Account", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_sales_item==\"Yes\"", - "fieldname": "selling_cost_center", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Selling Cost Center", - "options": "Cost Center", - "permlevel": 0, + "depends_on": "eval:doc.is_sales_item==\"Yes\"", + "fieldname": "selling_cost_center", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Selling Cost Center", + "options": "Cost Center", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "eval:doc.is_sales_item==\"Yes\"", - "fieldname": "column_break3", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "permlevel": 0, - "read_only": 0, + "depends_on": "eval:doc.is_sales_item==\"Yes\"", + "fieldname": "column_break3", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "permlevel": 0, + "read_only": 0, "width": "50%" - }, + }, { - "depends_on": "eval:doc.is_sales_item==\"Yes\"", - "description": "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes", - "fieldname": "item_customer_details", - "fieldtype": "Table", - "label": "Customer Codes", - "options": "Item Customer Detail", - "permlevel": 0, + "depends_on": "eval:doc.is_sales_item==\"Yes\"", + "description": "For the convenience of customers, these codes can be used in print formats like Invoices and Delivery Notes", + "fieldname": "item_customer_details", + "fieldtype": "Table", + "label": "Customer Codes", + "options": "Item Customer Detail", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "item_tax_section_break", - "fieldtype": "Section Break", - "label": "Item Tax", - "oldfieldtype": "Section Break", - "options": "icon-money", - "permlevel": 0, + "fieldname": "item_tax_section_break", + "fieldtype": "Section Break", + "label": "Item Tax", + "oldfieldtype": "Section Break", + "options": "icon-money", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "item_tax", - "fieldtype": "Table", - "label": "Item Tax1", - "oldfieldname": "item_tax", - "oldfieldtype": "Table", - "options": "Item Tax", - "permlevel": 0, + "fieldname": "item_tax", + "fieldtype": "Table", + "label": "Item Tax1", + "oldfieldname": "item_tax", + "oldfieldtype": "Table", + "options": "Item Tax", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "inspection_criteria", - "fieldtype": "Section Break", - "label": "Inspection Criteria", - "oldfieldtype": "Section Break", - "options": "icon-search", - "permlevel": 0, + "fieldname": "inspection_criteria", + "fieldtype": "Section Break", + "label": "Inspection Criteria", + "oldfieldtype": "Section Break", + "options": "icon-search", + "permlevel": 0, "read_only": 0 - }, + }, { - "default": "No", - "fieldname": "inspection_required", - "fieldtype": "Select", - "label": "Inspection Required", - "no_copy": 0, - "oldfieldname": "inspection_required", - "oldfieldtype": "Select", - "options": "\nYes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "fieldname": "inspection_required", + "fieldtype": "Select", + "label": "Inspection Required", + "no_copy": 0, + "oldfieldname": "inspection_required", + "oldfieldtype": "Select", + "options": "\nYes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "depends_on": "eval:doc.inspection_required==\"Yes\"", - "description": "Quality Inspection Parameters", - "fieldname": "item_specification_details", - "fieldtype": "Table", - "label": "Item Quality Inspection Parameter", - "oldfieldname": "item_specification_details", - "oldfieldtype": "Table", - "options": "Item Quality Inspection Parameter", - "permlevel": 0, + "depends_on": "eval:doc.inspection_required==\"Yes\"", + "description": "Quality Inspection Parameters", + "fieldname": "item_specification_details", + "fieldtype": "Table", + "label": "Item Quality Inspection Parameter", + "oldfieldname": "item_specification_details", + "oldfieldtype": "Table", + "options": "Item Quality Inspection Parameter", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "manufacturing", - "fieldtype": "Section Break", - "label": "Manufacturing", - "oldfieldtype": "Section Break", - "options": "icon-cogs", - "permlevel": 0, + "fieldname": "manufacturing", + "fieldtype": "Section Break", + "label": "Manufacturing", + "oldfieldtype": "Section Break", + "options": "icon-cogs", + "permlevel": 0, "read_only": 0 - }, + }, { - "default": "No", - "description": "Selecting \"Yes\" will allow you to create Bill of Material showing raw material and operational costs incurred to manufacture this item.", - "fieldname": "is_manufactured_item", - "fieldtype": "Select", - "label": "Allow Bill of Materials", - "oldfieldname": "is_manufactured_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "description": "Selecting \"Yes\" will allow you to create Bill of Material showing raw material and operational costs incurred to manufacture this item.", + "fieldname": "is_manufactured_item", + "fieldtype": "Select", + "label": "Allow Bill of Materials", + "oldfieldname": "is_manufactured_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "depends_on": "eval:doc.is_manufactured_item==\"Yes\"", - "fieldname": "default_bom", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default BOM", - "no_copy": 1, - "oldfieldname": "default_bom", - "oldfieldtype": "Link", - "options": "BOM", - "permlevel": 0, + "depends_on": "eval:doc.is_manufactured_item==\"Yes\"", + "fieldname": "default_bom", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default BOM", + "no_copy": 1, + "oldfieldname": "default_bom", + "oldfieldtype": "Link", + "options": "BOM", + "permlevel": 0, "read_only": 1 - }, + }, { - "default": "No", - "depends_on": "eval:doc.is_manufactured_item==\"Yes\"", - "description": "Selecting \"Yes\" will allow you to make a Production Order for this item.", - "fieldname": "is_pro_applicable", - "fieldtype": "Select", - "label": "Allow Production Order", - "oldfieldname": "is_pro_applicable", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "depends_on": "eval:doc.is_manufactured_item==\"Yes\"", + "description": "Selecting \"Yes\" will allow you to make a Production Order for this item.", + "fieldname": "is_pro_applicable", + "fieldtype": "Select", + "label": "Allow Production Order", + "oldfieldname": "is_pro_applicable", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "default": "No", - "description": "Select \"Yes\" if you supply raw materials to your supplier to manufacture this item.", - "fieldname": "is_sub_contracted_item", - "fieldtype": "Select", - "label": "Is Sub Contracted Item", - "oldfieldname": "is_sub_contracted_item", - "oldfieldtype": "Select", - "options": "Yes\nNo", - "permlevel": 0, - "read_only": 0, + "default": "No", + "description": "Select \"Yes\" if you supply raw materials to your supplier to manufacture this item.", + "fieldname": "is_sub_contracted_item", + "fieldtype": "Select", + "label": "Is Sub Contracted Item", + "oldfieldname": "is_sub_contracted_item", + "oldfieldtype": "Select", + "options": "Yes\nNo", + "permlevel": 0, + "read_only": 0, "reqd": 1 - }, + }, { - "fieldname": "customer_code", - "fieldtype": "Data", - "hidden": 1, - "in_filter": 1, - "label": "Customer Code", - "no_copy": 1, - "permlevel": 0, - "print_hide": 1, + "fieldname": "customer_code", + "fieldtype": "Data", + "hidden": 1, + "in_filter": 1, + "label": "Customer Code", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, "read_only": 0 - }, + }, { - "fieldname": "website_section", - "fieldtype": "Section Break", - "label": "Website", - "options": "icon-globe", - "permlevel": 0, + "fieldname": "website_section", + "fieldtype": "Section Break", + "label": "Website", + "options": "icon-globe", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "show_in_website", - "fieldtype": "Check", - "label": "Show in Website", - "permlevel": 0, + "fieldname": "show_in_website", + "fieldtype": "Check", + "label": "Show in Website", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "description": "website page link", - "fieldname": "page_name", - "fieldtype": "Data", - "label": "Page Name", - "no_copy": 1, - "permlevel": 0, + "depends_on": "show_in_website", + "description": "website page link", + "fieldname": "page_name", + "fieldtype": "Data", + "label": "Page Name", + "no_copy": 1, + "permlevel": 0, "read_only": 1 - }, + }, { - "depends_on": "show_in_website", - "description": "Products will be sorted by weight-age in default searches. More the weight-age, higher the product will appear in the list.", - "fieldname": "weightage", - "fieldtype": "Int", - "label": "Weightage", - "permlevel": 0, - "read_only": 0, + "depends_on": "show_in_website", + "description": "Products will be sorted by weight-age in default searches. More the weight-age, higher the product will appear in the list.", + "fieldname": "weightage", + "fieldtype": "Int", + "label": "Weightage", + "permlevel": 0, + "read_only": 0, "search_index": 1 - }, + }, { - "depends_on": "show_in_website", - "description": "Show a slideshow at the top of the page", - "fieldname": "slideshow", - "fieldtype": "Link", - "label": "Slideshow", - "options": "Website Slideshow", - "permlevel": 0, + "depends_on": "show_in_website", + "description": "Show a slideshow at the top of the page", + "fieldname": "slideshow", + "fieldtype": "Link", + "label": "Slideshow", + "options": "Website Slideshow", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "description": "Item Image (if not slideshow)", - "fieldname": "website_image", - "fieldtype": "Select", - "label": "Image", - "options": "attach_files:", - "permlevel": 0, + "depends_on": "show_in_website", + "description": "Item Image (if not slideshow)", + "fieldname": "website_image", + "fieldtype": "Select", + "label": "Image", + "options": "attach_files:", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "cb72", - "fieldtype": "Column Break", - "permlevel": 0, + "fieldname": "cb72", + "fieldtype": "Column Break", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.", - "fieldname": "website_warehouse", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Website Warehouse", - "options": "Warehouse", - "permlevel": 0, + "depends_on": "show_in_website", + "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.", + "fieldname": "website_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Website Warehouse", + "options": "Warehouse", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "description": "List this Item in multiple groups on the website.", - "fieldname": "website_item_groups", - "fieldtype": "Table", - "label": "Website Item Groups", - "options": "Website Item Group", - "permlevel": 0, + "depends_on": "show_in_website", + "description": "List this Item in multiple groups on the website.", + "fieldname": "website_item_groups", + "fieldtype": "Table", + "label": "Website Item Groups", + "options": "Website Item Group", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "fieldname": "sb72", - "fieldtype": "Section Break", - "permlevel": 0, + "depends_on": "show_in_website", + "fieldname": "sb72", + "fieldtype": "Section Break", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "fieldname": "copy_from_item_group", - "fieldtype": "Button", - "label": "Copy From Item Group", - "permlevel": 0, + "depends_on": "show_in_website", + "fieldname": "copy_from_item_group", + "fieldtype": "Button", + "label": "Copy From Item Group", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "fieldname": "item_website_specifications", - "fieldtype": "Table", - "label": "Item Website Specifications", - "options": "Item Website Specification", - "permlevel": 0, + "depends_on": "show_in_website", + "fieldname": "item_website_specifications", + "fieldtype": "Table", + "label": "Item Website Specifications", + "options": "Item Website Specification", + "permlevel": 0, "read_only": 0 - }, + }, { - "depends_on": "show_in_website", - "fieldname": "web_long_description", - "fieldtype": "Text Editor", - "label": "Website Description", - "permlevel": 0, + "depends_on": "show_in_website", + "fieldname": "web_long_description", + "fieldtype": "Text Editor", + "label": "Website Description", + "permlevel": 0, "read_only": 0 - }, + }, { - "fieldname": "parent_website_route", - "fieldtype": "Read Only", - "ignore_user_permissions": 1, - "label": "Parent Website Route", - "no_copy": 1, - "options": "", + "fieldname": "parent_website_route", + "fieldtype": "Read Only", + "ignore_user_permissions": 1, + "label": "Parent Website Route", + "no_copy": 1, + "options": "", "permlevel": 0 } - ], - "icon": "icon-tag", - "idx": 1, - "max_attachments": 1, - "modified": "2014-08-19 06:41:28.565607", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item", - "owner": "Administrator", + ], + "icon": "icon-tag", + "idx": 1, + "max_attachments": 1, + "modified": "2014-09-26 06:04:48.683214", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item", + "owner": "Administrator", "permissions": [ { - "create": 1, - "delete": 1, - "email": 1, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Material Master Manager", - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "import": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Material Master Manager", + "submit": 0, "write": 1 - }, + }, { - "amend": 0, - "create": 0, - "delete": 0, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Material Manager", - "submit": 0, + "amend": 0, + "create": 0, + "delete": 0, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Material Manager", + "submit": 0, "write": 0 - }, + }, { - "amend": 0, - "apply_user_permissions": 1, - "create": 0, - "delete": 0, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Material User", - "submit": 0, + "amend": 0, + "apply_user_permissions": 1, + "create": 0, + "delete": 0, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Material User", + "submit": 0, "write": 0 - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Sales User" - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Purchase User" - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Maintenance User" - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Accounts User" - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Manufacturing User" } - ], + ], "search_fields": "item_name,description,item_group,customer_code" -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index b8a31909ceb..4fcf937e60c 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -11,6 +11,7 @@ from frappe.website.render import clear_cache from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow class WarehouseNotSet(frappe.ValidationError): pass +class DuplicateVariant(frappe.ValidationError): pass class Item(WebsiteGenerator): page_title_field = "item_name" @@ -50,6 +51,7 @@ class Item(WebsiteGenerator): self.validate_barcode() self.cant_change() self.validate_item_type_for_reorder() + self.validate_variants_are_unique() if not self.get("__islocal"): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -61,6 +63,7 @@ class Item(WebsiteGenerator): invalidate_cache_for_item(self) self.validate_name_with_item_group() self.update_item_price() + self.make_variants() def get_context(self, context): context["parent_groups"] = get_parent_item_groups(self.item_group) + \ @@ -114,6 +117,45 @@ class Item(WebsiteGenerator): 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.")) + def validate_variants_are_unique(self): + variants = [] + for d in self.item_variants: + key = (d.item_attribute, d.item_attribute_value) + if key in variants: + frappe.throw(_("{0} {1} is entered more than once in Item Variants table").format(d.item_attribute, + d.item_attribute_value), DuplicateVariant) + variants.append(key) + + def make_variants(self): + 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]) + + # delete missing variants + existing_variants = [d.name for d in frappe.get_list("Item", + filters={"variant_of":self.name}, ignore_permissions=True)] + + 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 + + # for item_code in variant_item_codes: + # for + + 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 56150cabace..b2714e90acf 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,13 +6,23 @@ import unittest import frappe from frappe.test_runner import make_test_records +from erpnext.stock.doctype.item.item import WarehouseNotSet, DuplicateVariant test_ignore = ["BOM"] test_dependencies = ["Warehouse"] class TestItem(unittest.TestCase): + def test_duplicate_variant(self): + item = frappe.copy_doc(test_records[11]) + item.append("item_variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"}) + self.assertRaises(DuplicateVariant, item.insert) + + def test_item_creation(self): + frappe.delete_doc("Item", test_records[11].get("item_code")) + item = frappe.copy_doc(test_records[11]) + item.insert() + def test_default_warehouse(self): - from erpnext.stock.doctype.item.item import WarehouseNotSet item = frappe.copy_doc(test_records[0]) item.is_stock_item = "Yes" item.default_warehouse = None diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index a2561495715..fdae118b1a8 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -1,7 +1,7 @@ [ { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test Item", + "description": "_Test Item 1", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -57,7 +57,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test Item Home Desktop 100", + "description": "_Test Item Home Desktop 100 3", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -87,7 +87,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test Item Home Desktop 200", + "description": "_Test Item Home Desktop 200 4", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -108,7 +108,7 @@ "stock_uom": "_Test UOM" }, { - "description": "_Test Sales BOM Item", + "description": "_Test Sales BOM Item 5", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -129,7 +129,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test FG Item", + "description": "_Test FG Item 6", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -149,7 +149,7 @@ "stock_uom": "_Test UOM" }, { - "description": "_Test Non Stock Item", + "description": "_Test Non Stock Item 7", "doctype": "Item", "has_batch_no": "No", "has_serial_no": "No", @@ -168,7 +168,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test Serialized Item", + "description": "_Test Serialized Item 8", "doctype": "Item", "has_batch_no": "No", "has_serial_no": "Yes", @@ -187,7 +187,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test Serialized Item", + "description": "_Test Serialized Item 9", "doctype": "Item", "has_batch_no": "No", "has_serial_no": "Yes", @@ -207,7 +207,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test Item Home Desktop Manufactured", + "description": "_Test Item Home Desktop Manufactured 10", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -229,7 +229,7 @@ }, { "default_warehouse": "_Test Warehouse - _TC", - "description": "_Test FG Item 2", + "description": "_Test FG Item 2 11", "doctype": "Item", "expense_account": "_Test Account Cost for Goods Sold - _TC", "has_batch_no": "No", @@ -247,5 +247,32 @@ "item_group": "_Test Item Group Desktops", "item_name": "_Test FG Item 2", "stock_uom": "_Test UOM" + }, + { + "default_warehouse": "_Test Warehouse - _TC", + "description": "_Test Variant Item 12", + "doctype": "Item", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "has_batch_no": "No", + "has_serial_no": "No", + "income_account": "Sales - _TC", + "inspection_required": "No", + "is_asset_item": "No", + "is_pro_applicable": "Yes", + "is_purchase_item": "Yes", + "is_sales_item": "Yes", + "is_service_item": "No", + "is_stock_item": "Yes", + "is_sub_contracted_item": "Yes", + "item_code": "_Test Variant Item", + "item_group": "_Test Item Group Desktops", + "item_name": "_Test Variant Item", + "stock_uom": "_Test UOM", + "item_variants": [ + {"item_attribute": "Test Size", "item_attribute_value": "Small"}, + {"item_attribute": "Test Size", "item_attribute_value": "Medium"}, + {"item_attribute": "Test Size", "item_attribute_value": "Large"} + ] } + ] diff --git a/erpnext/stock/doctype/item_attribute/__init__.py b/erpnext/stock/doctype/item_attribute/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.json b/erpnext/stock/doctype/item_attribute/item_attribute.json new file mode 100644 index 00000000000..d41646c5649 --- /dev/null +++ b/erpnext/stock/doctype/item_attribute/item_attribute.json @@ -0,0 +1,87 @@ +{ + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "field:attribute_name", + "creation": "2014-09-26 03:49:54.899170", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "attribute_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Attribute Name", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0 + }, + { + "default": "1", + "description": "Lower the number, higher the priority in the Item Code suffix that will be created for this Item Attribute for the Item Variant", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "item_attribute_values", + "fieldtype": "Table", + "label": "Item Attribute Values", + "options": "Item Attribute Value", + "permlevel": 0, + "precision": "" + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-edit", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "modified": "2014-09-26 06:08:28.729519", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Attribute", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 0, + "export": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 1, + "role": "Material Master Manager", + "set_user_permissions": 0, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py new file mode 100644 index 00000000000..e4fd38cce6b --- /dev/null +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -0,0 +1,19 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _ + +class ItemAttribute(Document): + def validate(self): + values, abbrs = [], [] + for d in self.item_attribute_values: + if d.attribute_value in values: + frappe.throw(_("{0} must appear only once").format(d.attribute_value)) + values.append(d.attribute_value) + + if d.abbr in abbrs: + frappe.throw(_("{0} must appear only once").format(d.abbr)) + abbrs.append(d.abbr) diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py new file mode 100644 index 00000000000..51c335ce266 --- /dev/null +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -0,0 +1,10 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and Contributors +# See license.txt + +import frappe +import unittest + +test_records = frappe.get_test_records('Item Attribute') + +class TestItemAttribute(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/item_attribute/test_records.json b/erpnext/stock/doctype/item_attribute/test_records.json new file mode 100644 index 00000000000..f4184750419 --- /dev/null +++ b/erpnext/stock/doctype/item_attribute/test_records.json @@ -0,0 +1,22 @@ +[ + { + "doctype": "Item Attribute", + "attribute_name": "Test Size", + "priority": 1, + "item_attribute_values": [ + {"attribute_value": "Small", "abbr": "SM"}, + {"attribute_value": "Medium", "abbr": "MD"}, + {"attribute_value": "Large", "abbr": "LG"} + ] + }, + { + "doctype": "Item Attribute", + "attribute_name": "Test Colour", + "priority": 2, + "item_attribute_values": [ + {"attribute_value": "Red", "abbr": "R"}, + {"attribute_value": "Green", "abbr": "G"}, + {"attribute_value": "Blue", "abbr": "B"} + ] + } +] diff --git a/erpnext/stock/doctype/item_attribute_value/__init__.py b/erpnext/stock/doctype/item_attribute_value/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/item_attribute_value/item_attribute_value.json b/erpnext/stock/doctype/item_attribute_value/item_attribute_value.json new file mode 100644 index 00000000000..f6d66bcd897 --- /dev/null +++ b/erpnext/stock/doctype/item_attribute_value/item_attribute_value.json @@ -0,0 +1,64 @@ +{ + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "", + "creation": "2014-09-26 03:52:31.161255", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "attribute_value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Attribute Value", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "description": "This will be appended to the Item Code of the variant. For example, if your abbreviation is \"SM\", and the item code is \"T-SHIRT\", the item code of the variant will be \"T-SHIRT-SM\"", + "fieldname": "abbr", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Abbreviation", + "permlevel": 0, + "precision": "", + "reqd": 1, + "search_index": 1, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-edit", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2014-09-26 06:17:47.136386", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Attribute Value", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item_attribute_value/item_attribute_value.py b/erpnext/stock/doctype/item_attribute_value/item_attribute_value.py new file mode 100644 index 00000000000..c3a83b7100e --- /dev/null +++ b/erpnext/stock/doctype/item_attribute_value/item_attribute_value.py @@ -0,0 +1,9 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ItemAttributeValue(Document): + pass diff --git a/erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py b/erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py new file mode 100644 index 00000000000..86b89080411 --- /dev/null +++ b/erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py @@ -0,0 +1,10 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and Contributors +# See license.txt + +import frappe +import unittest + +test_records = frappe.get_test_records('Item Attribute Value') + +class TestItemAttributeValue(unittest.TestCase): + pass diff --git a/erpnext/stock/doctype/item_attribute_value/test_records.json b/erpnext/stock/doctype/item_attribute_value/test_records.json new file mode 100644 index 00000000000..dcfdf23fa9c --- /dev/null +++ b/erpnext/stock/doctype/item_attribute_value/test_records.json @@ -0,0 +1,6 @@ +[ + { + "doctype": "Item Attribute Value", + "name": "_Test Item Attribute Value 1" + } +] diff --git a/erpnext/stock/doctype/item_variant/__init__.py b/erpnext/stock/doctype/item_variant/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/item_variant/item_variant.json b/erpnext/stock/doctype/item_variant/item_variant.json new file mode 100644 index 00000000000..aeb445e2f92 --- /dev/null +++ b/erpnext/stock/doctype/item_variant/item_variant.json @@ -0,0 +1,72 @@ +{ + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "", + "creation": "2014-09-26 03:54:04.370259", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Other", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "item_attribute", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Item Attribute", + "no_copy": 0, + "options": "Item Attribute", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0 + }, + { + "allow_on_submit": 0, + "fieldname": "item_attribute_value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Item Attribute Value", + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2014-09-26 06:24:14.248364", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Variant", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/stock/doctype/item_variant/item_variant.py b/erpnext/stock/doctype/item_variant/item_variant.py new file mode 100644 index 00000000000..7b3c15d275b --- /dev/null +++ b/erpnext/stock/doctype/item_variant/item_variant.py @@ -0,0 +1,9 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ItemVariant(Document): + pass From ef276045c435f662aa11b39f86232cf648ab9528 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 30 Sep 2014 17:41:33 +0530 Subject: [PATCH 02/12] [item variants] make variants, bom updates --- .../page/accounts_browser/accounts_browser.py | 20 +-- erpnext/controllers/queries.py | 1 + erpnext/manufacturing/doctype/bom/bom.js | 5 +- erpnext/manufacturing/doctype/bom/bom.py | 27 ++--- erpnext/manufacturing/doctype/bom/test_bom.py | 12 ++ erpnext/stock/doctype/item/item.js | 12 +- erpnext/stock/doctype/item/item.json | 6 +- erpnext/stock/doctype/item/item.py | 114 +++++++++++++----- erpnext/stock/doctype/item/item_list.html | 6 +- erpnext/stock/doctype/item/item_list.js | 6 +- erpnext/stock/doctype/item/test_item.py | 21 +++- erpnext/stock/doctype/item/test_records.json | 2 + .../doctype/item_attribute/test_records.json | 6 +- 13 files changed, 155 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/page/accounts_browser/accounts_browser.py b/erpnext/accounts/page/accounts_browser/accounts_browser.py index 15cfdd2acc5..1a81e7b56ed 100644 --- a/erpnext/accounts/page/accounts_browser/accounts_browser.py +++ b/erpnext/accounts/page/accounts_browser/accounts_browser.py @@ -10,38 +10,38 @@ from erpnext.accounts.utils import get_balance_on @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 cbbd82cc01c..2f98dbd258d 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -165,6 +165,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): 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 cb96478735f..34598b662bd 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -182,10 +182,7 @@ erpnext.bom.calculate_total = function(doc) { 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 75c4ffbd921..1185403e00d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -54,7 +54,7 @@ class BOM(Document): 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 @@ class BOM(Document): 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 @@ class BOM(Document): 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 @@ class BOM(Document): 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 28ee49acbc2..753a708a768 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -28,3 +28,15 @@ class TestBOM(unittest.TestCase): 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 9af1d904ccd..06aae12c75f 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -80,16 +80,6 @@ cur_frm.cscript.item_code = function(doc) { 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 @@ cur_frm.cscript.image = function() { 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 70e28974c02..0a74e41a728 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 4fcf937e60c..565d24e44b3 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -63,7 +63,7 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 @@ class Item(WebsiteGenerator): 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 ebc2c7f345d..abfc2c654ca 100644 --- a/erpnext/stock/doctype/item/item_list.html +++ b/erpnext/stock/doctype/item/item_list.html @@ -26,11 +26,11 @@ {% } %} - {% if(doc.is_manufactured_item==="Yes") { %} + {% if(doc.default_bom==="Yes") { %} - + data-filter="default_bom,=,{%= doc.default_bom %}"> + {% } %} {% if(doc.show_in_website) { %} diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js index e1cd020b5ff..c6a437b1f1b 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 b2714e90acf..c0405487f56 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -17,6 +17,25 @@ class TestItem(unittest.TestCase): 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 @@ class TestItem(unittest.TestCase): 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 fdae118b1a8..4085d988e92 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 f4184750419..0208c176d32 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"} ] }, { From 1a7637459c306521863fdc196b7b24ce24286e50 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 3 Oct 2014 16:36:43 +0530 Subject: [PATCH 03/12] [item-variants] added more validations, tests #2224 --- erpnext/manufacturing/doctype/bom/bom.py | 6 ++ erpnext/stock/doctype/item/item.js | 12 +++- erpnext/stock/doctype/item/item.json | 6 +- erpnext/stock/doctype/item/item.py | 39 +++++++++--- erpnext/stock/doctype/item/test_item.py | 57 +++++++++++++++--- .../stock/doctype/stock_entry/stock_entry.py | 33 ++++++----- .../stock_ledger_entry/stock_ledger_entry.py | 8 ++- erpnext/stock/get_item_details.py | 59 ++++++++++++------- 8 files changed, 162 insertions(+), 58 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 1185403e00d..8328fc7c89d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -207,6 +207,12 @@ class BOM(Document): def validate_bom_no(self, item, bom_no, idx): """Validate BOM No of sub-contracted items""" + bom = frappe.get_doc("BOM", bom_no) + if not bom.is_active: + frappe.throw(_("BOM {0} must be active").format(bom_no)) + if not bom.docstatus!=1: + frappe.throw(_("BOM {0} must be submitted").format(bom_no)) + bom = frappe.db.sql("""select name from `tabBOM` where name = %s and item = %s and is_active=1 and docstatus=1""", (bom_no, item), as_dict =1) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 06aae12c75f..e274690f39d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -9,6 +9,14 @@ cur_frm.cscript.refresh = function(doc) { cur_frm.cscript.make_dashboard(); + 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")); + } + 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])); + } + if (frappe.defaults.get_default("item_naming_by")!="Naming Series") { cur_frm.toggle_display("naming_series", false); } else { @@ -154,7 +162,9 @@ cur_frm.cscript.add_image = function(doc, dt, dn) { doc.description_html = repl('' + '' + '' + - '
%(desc)s
', {imgurl: frappe.utils.get_file_link(doc.image), desc:doc.description}); + '', { + imgurl: frappe.utils.get_file_link(doc.image), + desc: doc.description.replace(/\n/g, "
")}); refresh_field('description_html'); } diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 0a74e41a728..c1a1fe92dea 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -42,13 +42,15 @@ "search_index": 0 }, { + "depends_on": "variant_of", "description": "If item is a variant of another item then description, image, pricing, taxes etc will be set from the template unless explicitly specified", "fieldname": "variant_of", "fieldtype": "Link", "label": "Variant Of", "options": "Item", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "fieldname": "item_name", @@ -860,7 +862,7 @@ "icon": "icon-tag", "idx": 1, "max_attachments": 1, - "modified": "2014-09-30 14:34:50.101879", + "modified": "2014-10-03 04:58:39.278047", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 565d24e44b3..c7d9375043e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -9,9 +9,11 @@ from frappe.website.website_generator import WebsiteGenerator 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.doctype.website_slideshow.website_slideshow import get_slideshow +import copy class WarehouseNotSet(frappe.ValidationError): pass class DuplicateVariant(frappe.ValidationError): pass +class ItemTemplateCannotHaveStock(frappe.ValidationError): pass class Item(WebsiteGenerator): page_title_field = "item_name" @@ -24,7 +26,7 @@ class Item(WebsiteGenerator): self.get("__onload").sle_exists = self.check_if_sle_exists() def autoname(self): - if frappe.db.get_default("item_naming_by")=="Naming Series": + if frappe.db.get_default("item_naming_by")=="Naming Series" and not self.variant_of: from frappe.model.naming import make_autoname self.item_code = make_autoname(self.naming_series+'.#####') elif not self.item_code: @@ -51,7 +53,7 @@ class Item(WebsiteGenerator): self.validate_barcode() self.cant_change() self.validate_item_type_for_reorder() - self.validate_variants_are_unique() + self.validate_variants() if not self.get("__islocal"): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -117,6 +119,18 @@ class Item(WebsiteGenerator): 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.")) + def validate_variants(self): + self.validate_variants_are_unique() + self.validate_stock_for_template_must_be_zero() + + def validate_stock_for_template_must_be_zero(self): + if self.has_variants: + stock_in = frappe.db.sql_list("""select warehouse from tabBin + where item_code=%s and ifnull(actual_qty, 0) > 0""", self.name) + if stock_in: + frappe.throw(_("Item Template cannot have stock and varaiants. Please remove stock from warehouses {0}").format(", ".join(stock_in)), + ItemTemplateCannotHaveStock) + def validate_variants_are_unique(self): if not self.has_variants: self.item_variants = [] @@ -167,6 +181,7 @@ class Item(WebsiteGenerator): if not self.item_variants: return [] + self.variant_attributes = {} variant_dict = {} variant_item_codes = [] @@ -178,31 +193,34 @@ class Item(WebsiteGenerator): # 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, 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, 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) + 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) + self.copy_attributes_to_variant(item, item_code, 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) + self.copy_attributes_to_variant(item, item_code) item.save() - def copy_attributes_to_variant(self, variant, insert=False): + def copy_attributes_to_variant(self, variant, item_code, 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): @@ -210,6 +228,11 @@ class Item(WebsiteGenerator): variant.set(field.fieldname, self.get(field.fieldname)) variant.__dirty = True + variant.description += "\n" + for attr in self.variant_attributes[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.has_variants = 0 variant.show_in_website = 0 diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index c0405487f56..2807ca8aab7 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,21 +6,47 @@ import unittest import frappe from frappe.test_runner import make_test_records -from erpnext.stock.doctype.item.item import WarehouseNotSet, DuplicateVariant +from erpnext.stock.doctype.item.item import WarehouseNotSet, DuplicateVariant, ItemTemplateCannotHaveStock test_ignore = ["BOM"] test_dependencies = ["Warehouse"] class TestItem(unittest.TestCase): + def get_item(self, idx): + item_code = test_records[idx].get("item_code") + if not frappe.db.exists("Item", item_code): + item = frappe.copy_doc(test_records[idx]) + item.insert() + else: + item = frappe.get_doc("Item", item_code) + + return item + def test_duplicate_variant(self): item = frappe.copy_doc(test_records[11]) item.append("item_variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"}) self.assertRaises(DuplicateVariant, item.insert) + def test_template_cannot_have_stock(self): + item = self.get_item(10) + + se = frappe.new_doc("Stock Entry") + se.purpose = "Material Receipt" + se.append("mtn_details", { + "item_code": item.name, + "t_warehouse": "Stores - WP", + "qty": 1, + "incoming_rate": 1 + }) + se.insert() + se.submit() + + item.has_variants = 1 + self.assertRaises(ItemTemplateCannotHaveStock, item.save) + 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() + item = self.get_item(11) + 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: @@ -36,10 +62,23 @@ class TestItem(unittest.TestCase): '_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]) - item.insert() + self.assertEqual(item.variant_attributes['_Test Variant Item-L-R'], [['Test Size', 'Large'], ['Test Colour', 'Red']]) + self.assertEqual(item.variant_attributes['_Test Variant Item-S-G'], [['Test Size', 'Small'], ['Test Colour', 'Green']]) + + # check stock entry cannot be made + def test_stock_entry_cannot_be_made_for_template(self): + item = self.get_item(11) + + se = frappe.new_doc("Stock Entry") + se.purpose = "Material Receipt" + se.append("mtn_details", { + "item_code": item.name, + "t_warehouse": "Stores - WP", + "qty": 1, + "incoming_rate": 1 + }) + se.insert() + self.assertRaises(ItemTemplateCannotHaveStock, se.submit) def test_default_warehouse(self): item = frappe.copy_doc(test_records[0]) @@ -57,7 +96,7 @@ class TestItem(unittest.TestCase): "income_account": "Sales - _TC", "expense_account": "_Test Account Cost for Goods Sold - _TC", "cost_center": "_Test Cost Center 2 - _TC", - "qty": 1.0, + "qty": 0.0, "price_list_rate": 100.0, "base_price_list_rate": 0.0, "discount_percentage": 0.0, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2a156f037c..a2d8c269bd0 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 +from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center class NotUpdateStockError(frappe.ValidationError): pass class StockOverReturnError(frappe.ValidationError): pass @@ -42,8 +42,8 @@ class StockEntry(StockController): pro_obj = self.production_order and \ frappe.get_doc('Production Order', self.production_order) or None - self.set_transfer_qty() self.validate_item() + self.set_transfer_qty() self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "transfer_qty") self.validate_warehouse(pro_obj) @@ -96,21 +96,23 @@ class StockEntry(StockController): for item in self.get("mtn_details"): if item.item_code not in stock_items: frappe.throw(_("{0} is not a stock Item").format(item.item_code)) - if not item.stock_uom: - item.stock_uom = frappe.db.get_value("Item", item.item_code, "stock_uom") - if not item.uom: - item.uom = item.stock_uom - if not item.conversion_factor: - item.conversion_factor = 1 + + item_details = self.get_item_details(frappe._dict({"item_code": item.item_code, + "company": self.company, "project_name": self.project_name})) + + for f in ("uom", "stock_uom", "description", "item_name", "expense_account", + "cost_center", "conversion_factor"): + item.set(f, item_details.get(f)) + if not item.transfer_qty: item.transfer_qty = item.qty * item.conversion_factor + if (self.purpose in ("Material Transfer", "Sales Return", "Purchase Return") and not item.serial_no and item.item_code in serialized_items): frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), frappe.MandatoryError) - def validate_warehouse(self, pro_obj): """perform various (sometimes conditional) validations on warehouse""" @@ -408,20 +410,21 @@ class StockEntry(StockController): def get_item_details(self, args): item = frappe.db.sql("""select stock_uom, description, item_name, - expense_account, buying_cost_center from `tabItem` + expense_account, buying_cost_center, item_group from `tabItem` where name = %s and (ifnull(end_of_life,'0000-00-00')='0000-00-00' or end_of_life > now())""", (args.get('item_code')), as_dict = 1) if not item: frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code"))) + item = item[0] ret = { - 'uom' : item and item[0]['stock_uom'] or '', - 'stock_uom' : item and item[0]['stock_uom'] or '', - 'description' : item and item[0]['description'] or '', - 'item_name' : item and item[0]['item_name'] or '', + 'uom' : item.stock_uom, + 'stock_uom' : item.stock_uom, + 'description' : item.description, + 'item_name' : item.item_name, 'expense_account' : args.get("expense_account") \ or frappe.db.get_value("Company", args.get("company"), "stock_adjustment_account"), - 'cost_center' : item and item[0]['buying_cost_center'] or args.get("cost_center"), + 'cost_center' : get_default_cost_center(args, item), 'qty' : 0, 'transfer_qty' : 0, 'conversion_factor' : 1, diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 0e92b5d4710..1f7f9e51a36 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import flt, getdate, add_days, formatdate from frappe.model.document import Document from datetime import date +from erpnext.stock.doctype.item.item import ItemTemplateCannotHaveStock class StockFreezeError(frappe.ValidationError): pass @@ -50,7 +51,8 @@ class StockLedgerEntry(Document): frappe.throw(_("{0} is required").format(self.meta.get_label(k))) def validate_item(self): - item_det = frappe.db.sql("""select name, has_batch_no, docstatus, is_stock_item + item_det = frappe.db.sql("""select name, has_batch_no, docstatus, + is_stock_item, has_variants from tabItem where name=%s""", self.item_code, as_dict=True)[0] if item_det.is_stock_item != 'Yes': @@ -66,6 +68,10 @@ class StockLedgerEntry(Document): {"item": self.item_code, "name": self.batch_no}): frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, self.item_code)) + if item_det.has_variants: + frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), + ItemTemplateCannotHaveStock) + if not self.stock_uom: self.stock_uom = item_det.stock_uom diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 19f97247adf..fd728e6f4fb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -40,7 +40,7 @@ def get_item_details(args): validate_item_details(args, item) - out = get_basic_details(args, item_doc) + out = get_basic_details(args, item) get_party_item_code(args, item_doc, out) @@ -129,8 +129,9 @@ def validate_item_details(args, item): if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != "Yes": throw(_("Item {0} must be a Sub-contracted Item").format(item.name)) -def get_basic_details(args, item_doc): - item = item_doc +def get_basic_details(args, item): + if not item: + item = frappe.get_doc("Item", args.get("item_code")) from frappe.defaults import get_user_default_as_list user_default_warehouse_list = get_user_default_as_list('warehouse') @@ -143,26 +144,17 @@ def get_basic_details(args, item_doc): "item_name": item.item_name, "description": item.description_html or item.description, "warehouse": user_default_warehouse or args.warehouse or item.default_warehouse, - "income_account": (item.income_account - or args.income_account - or frappe.db.get_value("Item Group", item.item_group, "default_income_account") - or frappe.db.get_value("Company", args.company, "default_income_account")), - "expense_account": (item.expense_account - or args.expense_account - or frappe.db.get_value("Item Group", item.item_group, "default_expense_account") - or frappe.db.get_value("Company", args.company, "default_expense_account")), - "cost_center": (frappe.db.get_value("Project", args.project_name, "cost_center") - or (item.selling_cost_center if args.transaction_type == "selling" else item.buying_cost_center) - or frappe.db.get_value("Item Group", item.item_group, "default_cost_center") - or frappe.db.get_value("Company", args.company, "cost_center")), + "income_account": get_default_income_account(args, item), + "expense_account": get_default_expense_account(args, item), + "cost_center": get_default_cost_center(args, item), "batch_no": None, "item_tax_rate": json.dumps(dict(([d.tax_type, d.tax_rate] for d in - item_doc.get("item_tax")))), + item.get("item_tax")))), "uom": item.stock_uom, "min_order_qty": flt(item.min_order_qty) if args.parenttype == "Material Request" else "", "conversion_factor": 1.0, - "qty": 1.0, - "stock_qty": 1.0, + "qty": 0.0, + "stock_qty": 0.0, "price_list_rate": 0.0, "base_price_list_rate": 0.0, "rate": 0.0, @@ -177,6 +169,24 @@ def get_basic_details(args, item_doc): return out +def get_default_income_account(args, item): + return (item.income_account + or args.income_account + or frappe.db.get_value("Item Group", item.item_group, "default_income_account") + or frappe.db.get_value("Company", args.company, "default_income_account")) + +def get_default_expense_account(args, item): + return (item.expense_account + or args.expense_account + or frappe.db.get_value("Item Group", item.item_group, "default_expense_account") + or frappe.db.get_value("Company", args.company, "default_expense_account")) + +def get_default_cost_center(args, item): + return (frappe.db.get_value("Project", args.project_name, "cost_center") + or (item.selling_cost_center if args.transaction_type == "selling" else item.buying_cost_center) + or frappe.db.get_value("Item Group", item.item_group, "default_cost_center") + or frappe.db.get_value("Company", args.company, "cost_center")) + def get_price_list_rate(args, item_doc, out): meta = frappe.get_meta(args.parenttype) @@ -185,10 +195,12 @@ def get_price_list_rate(args, item_doc, out): validate_conversion_rate(args, meta) - price_list_rate = frappe.db.get_value("Item Price", - {"price_list": args.price_list, "item_code": args.item_code}, "price_list_rate") + price_list_rate = get_price_list_rate_for(args, item_doc.name) + if not price_list_rate and item_doc.variant_of: + price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) - if not price_list_rate: return {} + if not price_list_rate: + return {} out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ / flt(args.conversion_rate) @@ -198,6 +210,10 @@ def get_price_list_rate(args, item_doc, out): out.update(get_last_purchase_details(item_doc.name, args.parent, args.conversion_rate)) +def get_price_list_rate_for(args, item_code): + return frappe.db.get_value("Item Price", + {"price_list": args.price_list, "item_code": item_code}, "price_list_rate") + def validate_price_list(args): if args.get("price_list"): if not frappe.db.get_value("Price List", @@ -236,7 +252,6 @@ def get_party_item_code(args, item_doc, out): item_supplier = item_doc.get("item_supplier_details", {"supplier": args.supplier}) out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None - def get_pos_settings_item_details(company, args, pos_settings=None): res = frappe._dict() From baef96b5a2b1d4e4f8f46f50b8e6095d0d9fd57c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 7 Oct 2014 12:34:19 +0530 Subject: [PATCH 04/12] [item-variants] allow production order of variant #2224 --- .../doctype/pricing_rule/test_records.json | 6 - erpnext/manufacturing/doctype/bom/bom.py | 30 ++-- .../doctype/bom/test_records.json | 158 +++++++++++------- .../production_order/production_order.py | 14 +- erpnext/stock/doctype/item/test_records.json | 1 + .../stock/doctype/stock_entry/stock_entry.py | 48 ++---- .../doctype/stock_entry/test_stock_entry.py | 23 +++ 7 files changed, 154 insertions(+), 126 deletions(-) delete mode 100644 erpnext/accounts/doctype/pricing_rule/test_records.json diff --git a/erpnext/accounts/doctype/pricing_rule/test_records.json b/erpnext/accounts/doctype/pricing_rule/test_records.json deleted file mode 100644 index 706d54cf2e4..00000000000 --- a/erpnext/accounts/doctype/pricing_rule/test_records.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "doctype": "Pricing Rule", - "name": "_Test Pricing Rule 1" - } -] diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8328fc7c89d..7112678e6f5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -197,27 +197,14 @@ class BOM(Document): 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)) - if m.bom: - self.validate_bom_no(m.item_code, m.bom_no, m.idx) + if m.bom_no: + validate_bom_no(m.item_code, m.bom_no) if flt(m.qty) <= 0: frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx)) self.check_if_item_repeated(m.item_code, m.operation_no, check_list) - def validate_bom_no(self, item, bom_no, idx): - """Validate BOM No of sub-contracted items""" - bom = frappe.get_doc("BOM", bom_no) - if not bom.is_active: - frappe.throw(_("BOM {0} must be active").format(bom_no)) - if not bom.docstatus!=1: - frappe.throw(_("BOM {0} must be submitted").format(bom_no)) - - bom = frappe.db.sql("""select name from `tabBOM` where name = %s and item = %s - and is_active=1 and docstatus=1""", - (bom_no, item), as_dict =1) - if not bom: - frappe.throw(_("BOM {0} for Item {1} in row {2} is inactive or not submitted").format(bom_no, item, idx)) def check_if_item_repeated(self, item, op, check_list): if [cstr(item), cstr(op)] in check_list: @@ -425,3 +412,16 @@ def get_bom_items(bom, qty=1, fetch_exploded=1): items = get_bom_items_as_dict(bom, qty, fetch_exploded).values() items.sort(lambda a, b: a.item_code > b.item_code and 1 or -1) return items + +def validate_bom_no(item, bom_no): + """Validate BOM No of sub-contracted items""" + bom = frappe.get_doc("BOM", bom_no) + if not bom.is_active: + frappe.throw(_("BOM {0} must be active").format(bom_no)) + if not bom.docstatus!=1: + if not getattr(frappe.flags, "in_test", False): + frappe.throw(_("BOM {0} must be submitted").format(bom_no)) + if item and not (bom.item == item or \ + bom.item == frappe.db.get_value("Item", item, "variant_of")): + frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) + diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json index 17c28d5d84b..26aa6d3b16c 100644 --- a/erpnext/manufacturing/doctype/bom/test_records.json +++ b/erpnext/manufacturing/doctype/bom/test_records.json @@ -2,98 +2,128 @@ { "bom_materials": [ { - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Serialized Item With Series", - "parentfield": "bom_materials", - "qty": 1.0, - "rate": 5000.0, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Serialized Item With Series", + "parentfield": "bom_materials", + "qty": 1.0, + "rate": 5000.0, "stock_uom": "_Test UOM" - }, + }, { - "amount": 2000.0, - "doctype": "BOM Item", - "item_code": "_Test Item 2", - "parentfield": "bom_materials", - "qty": 2.0, - "rate": 1000.0, + "amount": 2000.0, + "doctype": "BOM Item", + "item_code": "_Test Item 2", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 1000.0, "stock_uom": "_Test UOM" } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "item": "_Test Item Home Desktop Manufactured", + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test Item Home Desktop Manufactured", "quantity": 1.0 - }, + }, { "bom_materials": [ { - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Item", - "parentfield": "bom_materials", - "qty": 1.0, - "rate": 5000.0, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "bom_materials", + "qty": 1.0, + "rate": 5000.0, "stock_uom": "_Test UOM" - }, + }, { - "amount": 2000.0, - "doctype": "BOM Item", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "bom_materials", - "qty": 2.0, - "rate": 1000.0, + "amount": 2000.0, + "doctype": "BOM Item", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 1000.0, "stock_uom": "_Test UOM" } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "item": "_Test FG Item", + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test FG Item", "quantity": 1.0 }, { "bom_operations": [ { - "operation_no": "1", - "opn_description": "_Test", - "workstation": "_Test Workstation 1", + "operation_no": "1", + "opn_description": "_Test", + "workstation": "_Test Workstation 1", "time_in_min": 60, "operating_cost": 100 } - ], + ], "bom_materials": [ { "operation_no": 1, - "amount": 5000.0, - "doctype": "BOM Item", - "item_code": "_Test Item", - "parentfield": "bom_materials", - "qty": 1.0, - "rate": 5000.0, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "bom_materials", + "qty": 1.0, + "rate": 5000.0, "stock_uom": "_Test UOM" - }, + }, { "operation_no": 1, - "amount": 2000.0, - "bom_no": "BOM/_Test Item Home Desktop Manufactured/001", - "doctype": "BOM Item", - "item_code": "_Test Item Home Desktop Manufactured", - "parentfield": "bom_materials", - "qty": 2.0, - "rate": 1000.0, + "amount": 2000.0, + "bom_no": "BOM/_Test Item Home Desktop Manufactured/001", + "doctype": "BOM Item", + "item_code": "_Test Item Home Desktop Manufactured", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 1000.0, "stock_uom": "_Test UOM" } - ], - "docstatus": 1, - "doctype": "BOM", - "is_active": 1, - "is_default": 1, - "item": "_Test FG Item 2", + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test FG Item 2", + "quantity": 1.0, + "with_operations": 1 + }, + { + "bom_operations": [ + { + "operation_no": "1", + "opn_description": "_Test", + "workstation": "_Test Workstation 1", + "time_in_min": 60, + "operating_cost": 140 + } + ], + "bom_materials": [ + { + "operation_no": 1, + "amount": 5000.0, + "doctype": "BOM Item", + "item_code": "_Test Item", + "parentfield": "bom_materials", + "qty": 2.0, + "rate": 3000.0, + "stock_uom": "_Test UOM" + } + ], + "docstatus": 1, + "doctype": "BOM", + "is_active": 1, + "is_default": 1, + "item": "_Test Variant Item", "quantity": 1.0, "with_operations": 1 } -] \ No newline at end of file +] diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index 309f47c388c..f6af357c37a 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -7,12 +7,12 @@ import frappe from frappe.utils import flt, nowdate from frappe import _ from frappe.model.document import Document +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class ProductionOrder(Document): - def validate(self): if self.docstatus == 0: self.status = "Draft" @@ -21,7 +21,9 @@ class ProductionOrder(Document): validate_status(self.status, ["Draft", "Submitted", "Stopped", "In Process", "Completed", "Cancelled"]) - self.validate_bom_no() + if self.bom_no: + validate_bom_no(self.production_item, self.bom_no) + self.validate_sales_order() self.validate_warehouse() self.set_fixed_cost() @@ -29,14 +31,6 @@ class ProductionOrder(Document): from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) - def validate_bom_no(self): - if self.bom_no: - bom = frappe.db.sql("""select name from `tabBOM` where name=%s and docstatus=1 - and is_active=1 and item=%s""" - , (self.bom_no, self.production_item), as_dict =1) - if not bom: - frappe.throw(_("BOM {0} is not active or not submitted").format(self.bom_no)) - def validate_sales_order(self): if self.sales_order: so = frappe.db.sql("""select name, delivery_date from `tabSales Order` diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 4085d988e92..01c4e4f643f 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -264,6 +264,7 @@ "is_sales_item": "Yes", "is_service_item": "No", "is_stock_item": "Yes", + "is_manufactured_item": "Yes", "is_sub_contracted_item": "Yes", "item_code": "_Test Variant Item", "item_group": "_Test Item Group Desktops", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2d8c269bd0..028fff9fc50 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -12,6 +12,7 @@ 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.manufacturing.doctype.bom.bom import validate_bom_no class NotUpdateStockError(frappe.ValidationError): pass class StockOverReturnError(frappe.ValidationError): pass @@ -293,10 +294,8 @@ class StockEntry(StockController): def validate_bom(self): for d in self.get('mtn_details'): - if d.bom_no and not frappe.db.sql("""select name from `tabBOM` - where item = %s and name = %s and docstatus = 1 and is_active = 1""", - (d.item_code, d.bom_no)): - frappe.throw(_("BOM {0} is not submitted or inactive BOM for Item {1}").format(d.bom_no, d.item_code)) + if d.bom_no: + validate_bom_no(d.item_code, d.bom_no) def validate_finished_goods(self): """validation: finished good quantity should be same as manufacturing quantity""" @@ -497,13 +496,20 @@ class StockEntry(StockController): # add raw materials to Stock Entry Detail table self.add_to_stock_entry_detail(item_dict) - # add finished good item to Stock Entry Detail table -- along with bom_no - if self.production_order and self.purpose == "Manufacture": - item = frappe.db.get_value("Item", pro_obj.production_item, ["item_name", - "description", "stock_uom", "expense_account", "buying_cost_center"], as_dict=1) + if self.bom_no: + if self.production_order: + item_code = pro_obj.production_item + to_warehouse = pro_obj.fg_warehouse + else: + item_code = frappe.db.get_value("BOM", self.bom_no, "item") + to_warehouse = "" + + item = frappe.db.get_value("Item", item_code, ["item_name", + "description", "stock_uom", "expense_account", "buying_cost_center", "name"], as_dict=1) + self.add_to_stock_entry_detail({ - cstr(pro_obj.production_item): { - "to_warehouse": pro_obj.fg_warehouse, + item.name: { + "to_warehouse": to_warehouse, "from_warehouse": "", "qty": self.fg_completed_qty, "item_name": item.item_name, @@ -512,27 +518,7 @@ class StockEntry(StockController): "expense_account": item.expense_account, "cost_center": item.buying_cost_center, } - }, bom_no=pro_obj.bom_no) - - elif self.purpose in ["Material Receipt", "Repack"]: - if self.purpose=="Material Receipt": - self.from_warehouse = "" - - item = frappe.db.sql("""select name, item_name, description, - stock_uom, expense_account, buying_cost_center from `tabItem` - where name=(select item from tabBOM where name=%s)""", - self.bom_no, as_dict=1) - self.add_to_stock_entry_detail({ - item[0]["name"] : { - "qty": self.fg_completed_qty, - "item_name": item[0].item_name, - "description": item[0]["description"], - "stock_uom": item[0]["stock_uom"], - "from_warehouse": "", - "expense_account": item[0].expense_account, - "cost_center": item[0].buying_cost_center, - } - }, bom_no=self.bom_no) + }, bom_no = self.bom_no) self.get_stock_and_rate() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c2dcdc10ca8..03eb9fe253b 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -858,6 +858,29 @@ class TestStockEntry(unittest.TestCase): fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0] self.assertEqual(fg_rate, 100.00) + def test_variant_production_order(self): + bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", + "is_default": 1, "docstatus": 1}) + + production_order = frappe.new_doc("Production Order") + production_order.update({ + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test Variant Item-S", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "Nos", + "wip_warehouse": "_Test Warehouse - _TC" + }) + production_order.insert() + production_order.submit() + + from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry + + stock_entry = frappe.get_doc(make_stock_entry(production_order.name, "Manufacture", 1)) + stock_entry.insert() + self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.mtn_details]) + def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) se.get("mtn_details")[0].item_code = item_code or "_Test Serialized Item With Series" From 724f9e57e3c19d362e195774f735a8f48ac12622 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 7 Oct 2014 15:29:58 +0530 Subject: [PATCH 05/12] [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 da341086d7f..216d1657d04 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 e8f35f1347b..85c14494181 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 e274690f39d..b05ce2579c7 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 c1a1fe92dea..12ff900667e 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 c7d9375043e..cf463726963 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 2807ca8aab7..803913f944f 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 01c4e4f643f..531f1a72f64 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 028fff9fc50..fb4493a37cd 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 03eb9fe253b..6d8dc8be129 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 fd728e6f4fb..2273018525b 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 2b4e3682d6f..0a4be40db21 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""" From db08041f0586fb6f0ab8cd4f044798989a74f2ad Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 7 Oct 2014 15:39:35 +0530 Subject: [PATCH 06/12] [fix] remove test for item attribute value --- .../item_attribute_value/test_item_attribute_value.py | 10 ---------- .../doctype/item_attribute_value/test_records.json | 6 ------ 2 files changed, 16 deletions(-) delete mode 100644 erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py delete mode 100644 erpnext/stock/doctype/item_attribute_value/test_records.json diff --git a/erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py b/erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py deleted file mode 100644 index 86b89080411..00000000000 --- a/erpnext/stock/doctype/item_attribute_value/test_item_attribute_value.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and Contributors -# See license.txt - -import frappe -import unittest - -test_records = frappe.get_test_records('Item Attribute Value') - -class TestItemAttributeValue(unittest.TestCase): - pass diff --git a/erpnext/stock/doctype/item_attribute_value/test_records.json b/erpnext/stock/doctype/item_attribute_value/test_records.json deleted file mode 100644 index dcfdf23fa9c..00000000000 --- a/erpnext/stock/doctype/item_attribute_value/test_records.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "doctype": "Item Attribute Value", - "name": "_Test Item Attribute Value 1" - } -] From b31e90c7a57031654a722cf9e92bc38b154c7c4c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 7 Oct 2014 16:56:46 +0530 Subject: [PATCH 07/12] [tests] fixes to stock entry --- .../production_order/test_production_order.py | 2 +- erpnext/stock/doctype/item/test_records.json | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 39 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index a9975c1e6f3..8a0dbe1e65a 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -54,4 +54,4 @@ class TestProductionOrder(unittest.TestCase): self.assertRaises(StockOverProductionError, s.submit) -test_records = frappe.get_test_records('Production Order') \ No newline at end of file +test_records = frappe.get_test_records('Production Order') diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 531f1a72f64..eb0d7c9daa8 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -83,7 +83,7 @@ "tax_type": "_Test Account Excise Duty - _TC" } ], - "stock_uom": "_Test UOM" + "stock_uom": "_Test UOM 1" }, { "default_warehouse": "_Test Warehouse - _TC", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fb4493a37cd..5562e5a141d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -479,23 +479,34 @@ class StockEntry(StockController): self.production_order = None if self.bom_no: - if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", - "Subcontract"]: - if self.production_order and self.purpose == "Material Transfer": - item_dict = self.get_pending_raw_materials(pro_obj) + if self.purpose in ("Material Issue", "Material Transfer", "Manufacture", + "Repack", "Subcontract"): + + if self.production_order: + # production: stores -> wip + if self.purpose == "Material Transfer": + item_dict = self.get_pending_raw_materials(pro_obj) + for item in item_dict.values(): + item["to_warehouse"] = pro_obj.wip_warehouse + + # production: wip -> finished goods + elif self.purpose == "Manufacture": + item_dict = self.get_bom_raw_materials(self.fg_completed_qty) + for item in item_dict.values(): + item["from_warehouse"] = pro_obj.wip_warehouse + + else: + frappe.throw(_("Stock Entry against Production Order must be for 'Material Transfer' or 'Manufacture'")) else: if not self.fg_completed_qty: frappe.throw(_("Manufacturing Quantity is mandatory")) item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - for item in item_dict.values(): - if pro_obj: - item["from_warehouse"] = pro_obj.wip_warehouse - item["to_warehouse"] = "" # add raw materials to Stock Entry Detail table self.add_to_stock_entry_detail(item_dict) - if self.bom_no: + # add finished goods item + if self.purpose in ("Manufacture", "Repack"): if self.production_order: item_code = pro_obj.production_item to_warehouse = pro_obj.fg_warehouse @@ -529,6 +540,7 @@ class StockEntry(StockController): for item in item_dict.values(): item.from_warehouse = item.default_warehouse + item.to_warehouse = "" return item_dict @@ -585,8 +597,8 @@ class StockEntry(StockController): for d in item_dict: se_child = self.append('mtn_details') - se_child.s_warehouse = item_dict[d].get("from_warehouse", self.from_warehouse) - se_child.t_warehouse = item_dict[d].get("to_warehouse", self.to_warehouse) + se_child.s_warehouse = item_dict[d].get("from_warehouse") + se_child.t_warehouse = item_dict[d].get("to_warehouse") se_child.item_code = cstr(d) se_child.item_name = item_dict[d]["item_name"] se_child.description = item_dict[d]["description"] @@ -596,6 +608,11 @@ class StockEntry(StockController): se_child.expense_account = item_dict[d]["expense_account"] or expense_account se_child.cost_center = item_dict[d]["cost_center"] or cost_center + if se_child.s_warehouse==None: + se_child.s_warehouse = self.from_warehouse + if se_child.t_warehouse==None: + se_child.t_warehouse = self.to_warehouse + # in stock uom se_child.transfer_qty = flt(item_dict[d]["qty"]) se_child.conversion_factor = 1.00 From c62b6a815b80a509c85ffeac3aee83dbbf3d43ee Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 7 Oct 2014 17:15:30 +0530 Subject: [PATCH 08/12] [tests] fixes to stock entry --- erpnext/controllers/buying_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 85c14494181..b279d940512 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -195,7 +195,7 @@ class BuyingController(StockController): self.round_floats_in(item) - item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") + item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 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 From f850987db05f7f162022f50254bab65673771e3d Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 8 Oct 2014 12:03:19 +0530 Subject: [PATCH 09/12] [fixes] tests and moved reorder_item to separate module --- erpnext/hooks.py | 2 +- .../production_order/test_production_order.py | 12 +- erpnext/stock/doctype/item/test_records.json | 6 +- .../stock/doctype/stock_entry/stock_entry.py | 6 +- .../doctype/stock_entry/test_stock_entry.py | 82 +++---- .../stock_uom_replace_utility.py | 2 +- erpnext/stock/reorder_item.py | 196 +++++++++++++++ erpnext/stock/utils.py | 229 +++--------------- 8 files changed, 272 insertions(+), 263 deletions(-) create mode 100644 erpnext/stock/reorder_item.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index f9ac629922e..6ef3c43b711 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -42,7 +42,7 @@ doc_events = { scheduler_events = { "daily": [ "erpnext.controllers.recurring_document.create_recurring_documents", - "erpnext.stock.utils.reorder_item", + "erpnext.stock.reorder_item.reorder_item", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets" ], diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 8a0dbe1e65a..96fe6e4dbcb 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -20,8 +20,10 @@ class TestProductionOrder(unittest.TestCase): pro_doc.submit() # add raw materials to stores - test_stock_entry.make_stock_entry("_Test Item", None, "Stores - _TC", 100, 100) - test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "Stores - _TC", 100, 100) + test_stock_entry.make_stock_entry(item_code="_Test Item", + target="Stores - _TC", qty=100, incoming_rate=100) + test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="Stores - _TC", qty=100, incoming_rate=100) # from stores to wip s = frappe.get_doc(make_stock_entry(pro_doc.name, "Material Transfer", 4)) @@ -46,8 +48,10 @@ class TestProductionOrder(unittest.TestCase): from erpnext.manufacturing.doctype.production_order.production_order import StockOverProductionError pro_doc = self.test_planned_qty() - test_stock_entry.make_stock_entry("_Test Item", None, "_Test Warehouse - _TC", 100, 100) - test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "_Test Warehouse - _TC", 100, 100) + test_stock_entry.make_stock_entry(item_code="_Test Item", + target="_Test Warehouse - _TC", qty=100, incoming_rate=100) + test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", qty=100, incoming_rate=100) s = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture", 7)) s.insert() diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index eb0d7c9daa8..7f5271e7b69 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -9,7 +9,7 @@ "income_account": "Sales - _TC", "inspection_required": "No", "is_asset_item": "No", - "is_pro_applicable": "Yes", + "is_pro_applicable": "No", "is_purchase_item": "Yes", "is_sales_item": "Yes", "is_service_item": "No", @@ -20,9 +20,7 @@ "item_name": "_Test Item", "item_reorder": [ { - "doctype": "Item Reorder", "material_request_type": "Purchase", - "parentfield": "item_reorder", "warehouse": "_Test Warehouse - _TC", "warehouse_reorder_level": 20, "warehouse_reorder_qty": 20 @@ -105,7 +103,7 @@ "item_code": "_Test Item Home Desktop 200", "item_group": "_Test Item Group Desktops", "item_name": "_Test Item Home Desktop 200", - "stock_uom": "_Test UOM" + "stock_uom": "_Test UOM 1" }, { "description": "_Test Sales BOM Item 5", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5562e5a141d..bf7b2ebd77c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -230,7 +230,7 @@ class StockEntry(StockController): "posting_date": self.posting_date, "posting_time": self.posting_time, "qty": d.s_warehouse and -1*d.transfer_qty or d.transfer_qty, - "serial_no": d.serial_no + "serial_no": d.serial_no, }) # get actual stock at source warehouse @@ -243,7 +243,7 @@ class StockEntry(StockController): self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty)) # get incoming rate - if not d.bom_no: + if not d.t_warehouse: if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force: incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d)) if incoming_rate > 0: @@ -253,7 +253,7 @@ class StockEntry(StockController): raw_material_cost += flt(d.amount) # set incoming rate for fg item - if self.purpose in ["Manufacture", "Repack"]: + if self.purpose in ("Manufacture", "Repack"): number_of_fg_items = len([t.t_warehouse for t in self.get("mtn_details") if t.t_warehouse]) for d in self.get("mtn_details"): if d.bom_no or (d.t_warehouse and number_of_fg_items == 1): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6d8dc8be129..95e4a894a1e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -10,7 +10,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_per from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError class TestStockEntry(unittest.TestCase): - def tearDown(self): frappe.set_user("Administrator") set_perpetual_inventory(0) @@ -18,60 +17,34 @@ class TestStockEntry(unittest.TestCase): frappe.db.set_default("company", self.old_default_company) def test_auto_material_request(self): - frappe.db.sql("""delete from `tabMaterial Request Item`""") - frappe.db.sql("""delete from `tabMaterial Request`""") - self._clear_stock_account_balance() - - frappe.db.set_value("Stock Settings", None, "auto_indent", 1) - - st1 = frappe.copy_doc(test_records[0]) - st1.insert() - st1.submit() - st2 = frappe.copy_doc(test_records[1]) - st2.insert() - st2.submit() - - from erpnext.stock.utils import reorder_item - reorder_item() - - 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) + self._test_auto_material_request("_Test Item") def test_auto_material_request_for_variant(self): - item_code = "_Test Variant Item-S" + self._test_auto_material_request("_Test Variant Item-S") + + def _test_auto_material_request(self, item_code): item = frappe.get_doc("Item", item_code) - template = frappe.get_doc("Item", item.variant_of) + + if item.variant_of: + template = frappe.get_doc("Item", item.variant_of) + else: + template = item 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() + make_stock_entry(item_code=item_code, target="_Test Warehouse 1 - _TC", qty=1) 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 + from erpnext.stock.reorder_item import reorder_item mr_list = reorder_item() frappe.db.set_value("Stock Settings", None, "auto_indent", 0) @@ -897,6 +870,7 @@ class TestStockEntry(unittest.TestCase): "total_fixed_cost": 1000 }) stock_entry.get_items() + fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test FG Item 2"][0] self.assertEqual(fg_rate, 1200.00) fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0] @@ -939,21 +913,27 @@ def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se.submit() return se -def make_stock_entry(item, source, target, qty, incoming_rate=None): +def make_stock_entry(**args): s = frappe.new_doc("Stock Entry") - if source and target: - s.purpose = "Material Transfer" - elif source: - s.purpose = "Material Issue" - else: - s.purpose = "Material Receipt" - s.company = "_Test Company" + args = frappe._dict(args) + if args.posting_date: + s.posting_date = args.posting_date + if args.posting_time: + s.posting_time = args.posting_time + if not args.purpose: + if args.source and args.target: + s.purpose = "Material Transfer" + elif args.source: + s.purpose = "Material Issue" + else: + s.purpose = "Material Receipt" + s.company = args.company or "_Test Company" s.append("mtn_details", { - "item_code": item, - "s_warehouse": source, - "t_warehouse": target, - "qty": qty, - "incoming_rate": incoming_rate, + "item_code": args.item, + "s_warehouse": args.from_warehouse or args.source, + "t_warehouse": args.to_warehouse or args.target, + "qty": args.qty, + "incoming_rate": args.incoming_rate, "conversion_factor": 1.0 }) s.insert() diff --git a/erpnext/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py b/erpnext/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py index a14c21e865d..864a2c66957 100644 --- a/erpnext/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py +++ b/erpnext/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py @@ -33,7 +33,7 @@ class StockUOMReplaceUtility(Document): item_doc.stock_uom = self.new_stock_uom item_doc.save() - frappe.msgprint(_("Stock UOM updatd for Item {0}").format(self.item_code)) + frappe.msgprint(_("Stock UOM updated for Item {0}").format(self.item_code)) def update_bin(self): # update bin diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py new file mode 100644 index 00000000000..3afbb4b6dc0 --- /dev/null +++ b/erpnext/stock/reorder_item.py @@ -0,0 +1,196 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.utils import flt, cstr, nowdate, add_days, cint +from erpnext.accounts.utils import get_fiscal_year, FiscalYearError + +def reorder_item(): + """ Reorder item if stock reaches reorder level""" + # if initial setup not completed, return + if not frappe.db.sql("select name from `tabFiscal Year` limit 1"): + return + + if getattr(frappe.local, "auto_indent", None) is None: + frappe.local.auto_indent = cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')) + + if frappe.local.auto_indent: + return _reorder_item() + +def _reorder_item(): + 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`""")) + default_company = (frappe.defaults.get_defaults().get("company") or + frappe.db.sql("""select name from tabCompany limit 1""")[0][0]) + + def add_to_material_request(item_code, warehouse, reorder_level, reorder_qty, material_request_type): + if warehouse not in item_warehouse_projected_qty[item_code]: + # likely a disabled warehouse or a warehouse where BIN does not exist + return + + reorder_level = flt(reorder_level) + reorder_qty = flt(reorder_qty) + projected_qty = item_warehouse_projected_qty[item_code][warehouse] + + if reorder_level and projected_qty < reorder_level: + deficiency = reorder_level - projected_qty + if deficiency > reorder_qty: + reorder_qty = deficiency + + company = warehouse_company.get(warehouse) or default_company + + material_requests[material_request_type].setdefault(company, []).append({ + "item_code": item_code, + "warehouse": warehouse, + "reorder_qty": reorder_qty + }) + + 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, + d.warehouse_reorder_qty, d.material_request_type) + + else: + # raise for default warehouse + add_to_material_request(item_code, item.default_warehouse, item.re_order_level, item.re_order_qty, "Purchase") + + if material_requests: + return create_material_request(material_requests) + +def get_item_warehouse_projected_qty(): + item_warehouse_projected_qty = {} + + for item_code, warehouse, projected_qty in frappe.db.sql("""select item_code, warehouse, projected_qty + from tabBin where ifnull(item_code, '') != '' and ifnull(warehouse, '') != '' + and exists (select name from `tabItem` + where `tabItem`.name = `tabBin`.item_code and + is_stock_item='Yes' and (is_purchase_item='Yes' or is_sub_contracted_item='Yes') and + (ifnull(end_of_life, '0000-00-00')='0000-00-00' or end_of_life > %s)) + and exists (select name from `tabWarehouse` + where `tabWarehouse`.name = `tabBin`.warehouse + and ifnull(disabled, 0)=0)""", nowdate()): + + item_warehouse_projected_qty.setdefault(item_code, {})[warehouse] = flt(projected_qty) + + return item_warehouse_projected_qty + +def create_material_request(material_requests): + """ Create indent on reaching reorder level """ + mr_list = [] + defaults = frappe.defaults.get_defaults() + exceptions_list = [] + + def _log_exception(): + if frappe.local.message_log: + exceptions_list.extend(frappe.local.message_log) + frappe.local.message_log = [] + else: + exceptions_list.append(frappe.get_traceback()) + + try: + current_fiscal_year = get_fiscal_year(nowdate())[0] or defaults.fiscal_year + + except FiscalYearError: + _log_exception() + notify_errors(exceptions_list) + return + + for request_type in material_requests: + for company in material_requests[request_type]: + try: + items = material_requests[request_type][company] + if not items: + continue + + mr = frappe.new_doc("Material Request") + mr.update({ + "company": company, + "fiscal_year": current_fiscal_year, + "transaction_date": nowdate(), + "material_request_type": request_type + }) + + for d in items: + d = frappe._dict(d) + item = frappe.get_doc("Item", d.item_code) + mr.append("indent_details", { + "doctype": "Material Request Item", + "item_code": d.item_code, + "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), + "uom": item.stock_uom, + "warehouse": d.warehouse, + "item_name": item.item_name, + "description": item.description, + "item_group": item.item_group, + "qty": d.reorder_qty, + "brand": item.brand, + }) + + mr.insert() + mr.submit() + mr_list.append(mr) + + except: + _log_exception() + + if mr_list: + if getattr(frappe.local, "reorder_email_notify", None) is None: + frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, + 'reorder_email_notify')) + + if(frappe.local.reorder_email_notify): + send_email_notification(mr_list) + + if exceptions_list: + notify_errors(exceptions_list) + + return mr_list + +def send_email_notification(mr_list): + """ Notify user about auto creation of indent""" + + email_list = frappe.db.sql_list("""select distinct r.parent + from tabUserRole r, tabUser p + where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 + and r.role in ('Purchase Manager','Material Manager') + and p.name not in ('Administrator', 'All', 'Guest')""") + + msg="""

Following Material Requests has been raised automatically \ + based on item reorder level:

""" + for mr in mr_list: + msg += "

" + mr.name + """

+ """ + for item in mr.get("indent_details"): + msg += "" + msg += "
Item CodeWarehouseQtyUOM
" + item.item_code + "" + item.warehouse + "" + \ + cstr(item.qty) + "" + cstr(item.uom) + "
" + frappe.sendmail(recipients=email_list, subject='Auto Material Request Generation Notification', msg = msg) + +def notify_errors(exceptions_list): + subject = "[Important] [ERPNext] Auto Reorder Errors" + content = """Dear System Manager, + +An error occured for certain Items while creating Material Requests based on Re-order level. + +Please rectify these issues: +--- +
+%s
+
+--- +Regards, +Administrator""" % ("\n\n".join(exceptions_list),) + + from frappe.email import sendmail_to_system_managers + sendmail_to_system_managers(subject, content) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 0a4be40db21..092f58056f7 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -4,24 +4,30 @@ import frappe from frappe import _ import json -from frappe.utils import flt, cstr, nowdate, add_days, cint +from frappe.utils import flt, cstr, nowdate, nowtime from frappe.defaults import get_global_default -from erpnext.accounts.utils import get_fiscal_year, FiscalYearError class InvalidWarehouseCompany(frappe.ValidationError): pass -def get_stock_balance_on(warehouse, posting_date=None): +def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): if not posting_date: posting_date = nowdate() + values, condition = [posting_date], "" + + if warehouse: + values.append(warehouse) + condition += " AND warehouse = %s" + + if item_code: + values.append(item_code) + condition.append(" AND item_code = %s") + stock_ledger_entries = frappe.db.sql(""" - SELECT - item_code, stock_value - FROM - `tabStock Ledger Entry` - WHERE - warehouse=%s AND posting_date <= %s + SELECT item_code, stock_value + FROM `tabStock Ledger Entry` + WHERE posting_date <= %s {0} ORDER BY timestamp(posting_date, posting_time) DESC, name DESC - """, (warehouse, posting_date), as_dict=1) + """.format(condition), values, as_dict=1) sle_map = {} for sle in stock_ledger_entries: @@ -29,6 +35,20 @@ def get_stock_balance_on(warehouse, posting_date=None): return sum(sle_map.values()) +def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None): + if not posting_date: posting_date = nowdate() + if not posting_time: posting_time = nowtime() + last_entry = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry` + where item_code=%s and warehouse=%s + and timestamp(posting_date, posting_time) < timestamp(%s, %s) + order by timestamp(posting_date, posting_time) limit 1""", + (item_code, warehouse, posting_date, posting_time)) + + if last_entry: + return last_entry[0][0] + else: + return 0.0 + def get_latest_stock_balance(): bin_map = {} for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value @@ -181,192 +201,3 @@ def get_buying_amount(item_code, item_qty, voucher_type, voucher_no, item_row, s return 0.0 - -def reorder_item(): - """ Reorder item if stock reaches reorder level""" - # if initial setup not completed, return - if not frappe.db.sql("select name from `tabFiscal Year` limit 1"): - return - - if getattr(frappe.local, "auto_indent", None) is None: - frappe.local.auto_indent = cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')) - - if frappe.local.auto_indent: - return _reorder_item() - -def _reorder_item(): - 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`""")) - default_company = (frappe.defaults.get_defaults().get("company") or - frappe.db.sql("""select name from tabCompany limit 1""")[0][0]) - - def add_to_material_request(item_code, warehouse, reorder_level, reorder_qty, material_request_type): - if warehouse not in item_warehouse_projected_qty[item_code]: - # likely a disabled warehouse or a warehouse where BIN does not exist - return - - reorder_level = flt(reorder_level) - reorder_qty = flt(reorder_qty) - projected_qty = item_warehouse_projected_qty[item_code][warehouse] - - if reorder_level and projected_qty < reorder_level: - deficiency = reorder_level - projected_qty - if deficiency > reorder_qty: - reorder_qty = deficiency - - company = warehouse_company.get(warehouse) or default_company - - material_requests[material_request_type].setdefault(company, []).append({ - "item_code": item_code, - "warehouse": warehouse, - "reorder_qty": reorder_qty - }) - - 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, - d.warehouse_reorder_qty, d.material_request_type) - - else: - # raise for default warehouse - add_to_material_request(item_code, item.default_warehouse, item.re_order_level, item.re_order_qty, "Purchase") - - if material_requests: - return create_material_request(material_requests) - -def get_item_warehouse_projected_qty(): - item_warehouse_projected_qty = {} - - for item_code, warehouse, projected_qty in frappe.db.sql("""select item_code, warehouse, projected_qty - from tabBin where ifnull(item_code, '') != '' and ifnull(warehouse, '') != '' - and exists (select name from `tabItem` - where `tabItem`.name = `tabBin`.item_code and - is_stock_item='Yes' and (is_purchase_item='Yes' or is_sub_contracted_item='Yes') and - (ifnull(end_of_life, '0000-00-00')='0000-00-00' or end_of_life > %s)) - and exists (select name from `tabWarehouse` - where `tabWarehouse`.name = `tabBin`.warehouse - and ifnull(disabled, 0)=0)""", nowdate()): - - item_warehouse_projected_qty.setdefault(item_code, {})[warehouse] = flt(projected_qty) - - return item_warehouse_projected_qty - -def create_material_request(material_requests): - """ Create indent on reaching reorder level """ - mr_list = [] - defaults = frappe.defaults.get_defaults() - exceptions_list = [] - - def _log_exception(): - if frappe.local.message_log: - exceptions_list.extend(frappe.local.message_log) - frappe.local.message_log = [] - else: - exceptions_list.append(frappe.get_traceback()) - - try: - current_fiscal_year = get_fiscal_year(nowdate())[0] or defaults.fiscal_year - - except FiscalYearError: - _log_exception() - notify_errors(exceptions_list) - return - - for request_type in material_requests: - for company in material_requests[request_type]: - try: - items = material_requests[request_type][company] - if not items: - continue - - mr = frappe.new_doc("Material Request") - mr.update({ - "company": company, - "fiscal_year": current_fiscal_year, - "transaction_date": nowdate(), - "material_request_type": request_type - }) - - for d in items: - d = frappe._dict(d) - item = frappe.get_doc("Item", d.item_code) - mr.append("indent_details", { - "doctype": "Material Request Item", - "item_code": d.item_code, - "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), - "uom": item.stock_uom, - "warehouse": d.warehouse, - "item_name": item.item_name, - "description": item.description, - "item_group": item.item_group, - "qty": d.reorder_qty, - "brand": item.brand, - }) - - mr.insert() - mr.submit() - mr_list.append(mr) - - except: - _log_exception() - - if mr_list: - if getattr(frappe.local, "reorder_email_notify", None) is None: - frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, - 'reorder_email_notify')) - - if(frappe.local.reorder_email_notify): - send_email_notification(mr_list) - - if exceptions_list: - notify_errors(exceptions_list) - - return mr_list - -def send_email_notification(mr_list): - """ Notify user about auto creation of indent""" - - email_list = frappe.db.sql_list("""select distinct r.parent - from tabUserRole r, tabUser p - where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 - and r.role in ('Purchase Manager','Material Manager') - and p.name not in ('Administrator', 'All', 'Guest')""") - - msg="""

Following Material Requests has been raised automatically \ - based on item reorder level:

""" - for mr in mr_list: - msg += "

" + mr.name + """

- """ - for item in mr.get("indent_details"): - msg += "" - msg += "
Item CodeWarehouseQtyUOM
" + item.item_code + "" + item.warehouse + "" + \ - cstr(item.qty) + "" + cstr(item.uom) + "
" - frappe.sendmail(recipients=email_list, subject='Auto Material Request Generation Notification', msg = msg) - -def notify_errors(exceptions_list): - subject = "[Important] [ERPNext] Error(s) while creating Material Requests based on Re-order Levels" - content = """Dear System Manager, - -An error occured for certain Items while creating Material Requests based on Re-order level. - -Please rectify these issues: ---- -
-%s
-
---- -Regards, -Administrator""" % ("\n\n".join(exceptions_list),) - - from frappe.email import sendmail_to_system_managers - sendmail_to_system_managers(subject, content) From c2d2857540f8472ca141b9dd7098d1fc5eb0de79 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 8 Oct 2014 12:19:55 +0530 Subject: [PATCH 10/12] [fixes] tests and moved reorder_item to separate module --- erpnext/accounts/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index cf57a61fe54..24781a3ac55 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -251,7 +251,7 @@ def fix_total_debit_credit(): (d.diff, d.voucher_type, d.voucher_no)) def get_stock_and_account_difference(account_list=None, posting_date=None): - from erpnext.stock.utils import get_stock_balance_on + from erpnext.stock.utils import get_stock_value_on if not posting_date: posting_date = nowdate() @@ -263,7 +263,7 @@ def get_stock_and_account_difference(account_list=None, posting_date=None): for account, warehouse in account_warehouse.items(): account_balance = get_balance_on(account, posting_date) - stock_value = get_stock_balance_on(warehouse, posting_date) + stock_value = get_stock_value_on(warehouse, posting_date) if abs(flt(stock_value) - flt(account_balance)) > 0.005: difference.setdefault(account, flt(stock_value) - flt(account_balance)) From 7472a1147c7ade45f06864d40ecd8ff8e3e794f4 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 8 Oct 2014 12:41:06 +0530 Subject: [PATCH 11/12] [tests] fixes --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 95e4a894a1e..f23f8112017 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -33,7 +33,7 @@ class TestStockEntry(unittest.TestCase): warehouse = "_Test Warehouse - _TC" # stock entry reqd for auto-reorder - make_stock_entry(item_code=item_code, target="_Test Warehouse 1 - _TC", qty=1) + make_stock_entry(item_code=item_code, target="_Test Warehouse 1 - _TC", qty=1, incoming_rate=1) frappe.db.set_value("Stock Settings", None, "auto_indent", 1) projected_qty = frappe.db.get_value("Bin", {"item_code": item_code, @@ -929,7 +929,7 @@ def make_stock_entry(**args): s.purpose = "Material Receipt" s.company = args.company or "_Test Company" s.append("mtn_details", { - "item_code": args.item, + "item_code": args.item or args.item_code, "s_warehouse": args.from_warehouse or args.source, "t_warehouse": args.to_warehouse or args.target, "qty": args.qty, From ebc6016ffb169a23204caf1ac24287ce3446cac2 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 8 Oct 2014 13:54:19 +0530 Subject: [PATCH 12/12] [tests] fixes --- .../doctype/production_order/production_order.py | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index f6af357c37a..fbca0d59098 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -179,5 +179,5 @@ def make_stock_entry(production_order_id, purpose, qty=None): stock_entry.from_warehouse = production_order.wip_warehouse stock_entry.to_warehouse = production_order.fg_warehouse - stock_entry.run_method("get_items") + stock_entry.get_items() return stock_entry.as_dict() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index bf7b2ebd77c..d06a7610757 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -243,14 +243,13 @@ class StockEntry(StockController): self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty)) # get incoming rate + if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force: + incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d)) + if incoming_rate > 0: + d.incoming_rate = incoming_rate + d.amount = flt(d.transfer_qty) * flt(d.incoming_rate) if not d.t_warehouse: - if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force: - incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d)) - if incoming_rate > 0: - d.incoming_rate = incoming_rate - d.amount = flt(d.transfer_qty) * flt(d.incoming_rate) - if not d.t_warehouse: - raw_material_cost += flt(d.amount) + raw_material_cost += flt(d.amount) # set incoming rate for fg item if self.purpose in ("Manufacture", "Repack"):