[merge] item-variants
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index 2d318c6..7a79c1a 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/page/accounts_browser/accounts_browser.py b/erpnext/accounts/page/accounts_browser/accounts_browser.py
index 15cfdd2..1a81e7b 100644
--- a/erpnext/accounts/page/accounts_browser/accounts_browser.py
+++ b/erpnext/accounts/page/accounts_browser/accounts_browser.py
@@ -10,38 +10,38 @@
@frappe.whitelist()
def get_companies():
"""get a list of companies based on permission"""
- return [d.name for d in frappe.get_list("Company", fields=["name"],
+ return [d.name for d in frappe.get_list("Company", fields=["name"],
order_by="name")]
@frappe.whitelist()
def get_children():
args = frappe.local.form_dict
ctype, company = args['ctype'], args['comp']
-
+
# root
if args['parent'] in ("Accounts", "Cost Centers"):
- acc = frappe.db.sql(""" select
+ acc = frappe.db.sql(""" select
name as value, if(group_or_ledger='Group', 1, 0) as expandable
from `tab%s`
where ifnull(parent_%s,'') = ''
- and `company` = %s and docstatus<2
+ and `company` = %s and docstatus<2
order by name""" % (ctype, ctype.lower().replace(' ','_'), '%s'),
company, as_dict=1)
- else:
+ else:
# other
- acc = frappe.db.sql("""select
+ acc = frappe.db.sql("""select
name as value, if(group_or_ledger='Group', 1, 0) as expandable
- from `tab%s`
+ from `tab%s`
where ifnull(parent_%s,'') = %s
- and docstatus<2
+ and docstatus<2
order by name""" % (ctype, ctype.lower().replace(' ','_'), '%s'),
args['parent'], as_dict=1)
-
+
if ctype == 'Account':
currency = frappe.db.sql("select default_currency from `tabCompany` where name = %s", company)[0][0]
for each in acc:
bal = get_balance_on(each.get("value"))
each["currency"] = currency
each["balance"] = flt(bal)
-
+
return acc
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 0c108a4..e9de551 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -254,7 +254,7 @@
(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()
@@ -266,7 +266,7 @@
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))
diff --git a/erpnext/buying/doctype/quality_inspection/quality_inspection.py b/erpnext/buying/doctype/quality_inspection/quality_inspection.py
index da34108..216d165 100644
--- a/erpnext/buying/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/buying/doctype/quality_inspection/quality_inspection.py
@@ -8,11 +8,15 @@
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 @@
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 @@
"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/config/stock.py b/erpnext/config/stock.py
index 04e45d4..8423a16 100644
--- a/erpnext/config/stock.py
+++ b/erpnext/config/stock.py
@@ -129,6 +129,11 @@
"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/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 14785be..d31075c 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -8,6 +8,7 @@
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 @@
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") 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
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index cbbd82c..2f98dbd 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -165,6 +165,7 @@
concat(substr(tabItem.description, 1, 40), "..."), description) as decription
from tabItem
where tabItem.docstatus < 2
+ and ifnull(tabItem.has_variants, 0)=0
and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00')
and (tabItem.`{key}` LIKE %(txt)s
or tabItem.item_name LIKE %(txt)s
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c919f5a..54b911a 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -42,7 +42,7 @@
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"
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year"
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index cb96478..34598b6 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -182,10 +182,7 @@
cur_frm.fields_dict['item'].get_query = function(doc) {
return{
- query: "erpnext.controllers.queries.item_query",
- filters:{
- 'is_manufactured_item': 'Yes'
- }
+ query: "erpnext.controllers.queries.item_query"
}
}
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index e8a8682..399e71c 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -54,7 +54,7 @@
def get_item_det(self, item_code):
item = frappe.db.sql("""select name, is_asset_item, is_purchase_item,
docstatus, description, is_sub_contracted_item, stock_uom, default_bom,
- last_purchase_rate, is_manufactured_item
+ last_purchase_rate
from `tabItem` where name=%s""", item_code, as_dict = 1)
return item
@@ -153,14 +153,19 @@
if self.is_default and self.is_active:
from frappe.model.utils import set_default
set_default(self, "item")
- frappe.db.set_value("Item", self.item, "default_bom", self.name)
+ item = frappe.get_doc("Item", self.item)
+ if item.default_bom != self.name:
+ item.default_bom = self.name
+ item.save()
else:
if not self.is_active:
frappe.db.set(self, "is_default", 0)
- frappe.db.sql("update `tabItem` set default_bom = null where name = %s and default_bom = %s",
- (self.item, self.name))
+ item = frappe.get_doc("Item", self.item)
+ if item.default_bom == self.name:
+ item.default_bom = None
+ item.save()
def clear_operations(self):
if not self.with_operations:
@@ -173,9 +178,6 @@
item = self.get_item_det(self.item)
if not item:
frappe.throw(_("Item {0} does not exist in the system or has expired").format(self.item))
- elif item[0]['is_manufactured_item'] != 'Yes' \
- and item[0]['is_sub_contracted_item'] != 'Yes':
- frappe.throw(_("Item {0} must be manufactured or sub-contracted").format(self.item))
else:
ret = frappe.db.get_value("Item", self.item, ["description", "stock_uom"])
self.description = ret[0]
@@ -199,28 +201,14 @@
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_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.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:
@@ -426,3 +414,16 @@
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_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 28ee49a..753a708 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -28,3 +28,15 @@
def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
self.assertEquals(len(get_bom_items(bom="BOM/_Test FG Item 2/001", qty=1, fetch_exploded=1)), 3)
+
+ def test_default_bom(self):
+ bom = frappe.get_doc("BOM", "BOM/_Test FG Item 2/001")
+ bom.is_active = 0
+ bom.save()
+
+ self.assertEqual(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), "")
+
+ bom.is_active = 1
+ bom.save()
+
+ self.assertTrue(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), "BOM/_Test FG Item 2/001")
diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json
index 17c28d5..26aa6d3 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 309f47c..fbca0d5 100644
--- a/erpnext/manufacturing/doctype/production_order/production_order.py
+++ b/erpnext/manufacturing/doctype/production_order/production_order.py
@@ -7,12 +7,12 @@
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 @@
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 @@
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`
@@ -185,5 +179,5 @@
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/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py
index a9975c1..96fe6e4 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 @@
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,12 +48,14 @@
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()
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/item.js b/erpnext/stock/doctype/item/item.js
index fce8dfa..b05ce25 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -9,6 +9,17 @@
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"));
+ 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]));
+ }
+
if (frappe.defaults.get_default("item_naming_by")!="Naming Series") {
cur_frm.toggle_display("naming_series", false);
} else {
@@ -25,7 +36,7 @@
if (!doc.__islocal && doc.show_in_website) {
cur_frm.set_intro(__("Published on website at: {0}",
- [repl('<a href="/%(website_route)s" target="_blank">/%(website_route)s</a>', doc.__onload)]));
+ [repl('<a href="/%(website_route)s" target="_blank">/%(website_route)s</a>', doc.__onload)]), true);
}
erpnext.item.toggle_reqd(cur_frm);
@@ -35,11 +46,32 @@
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)
@@ -49,7 +81,7 @@
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) {
@@ -59,16 +91,6 @@
cur_frm.set_value("description", doc.item_code);
}
-cur_frm.fields_dict['default_bom'].get_query = function(doc) {
- return {
- filters: {
- 'item': doc.item_code,
- 'is_active': 0
- }
- }
-}
-
-
// Expense Account
// ---------------------------------
cur_frm.fields_dict['expense_account'].get_query = function(doc) {
@@ -143,7 +165,9 @@
doc.description_html = repl('<table style="width: 100%; table-layout: fixed;">' +
'<tr><td style="width:110px"><img src="%(imgurl)s" width="100px"></td>' +
'<td>%(desc)s</td></tr>' +
- '</table>', {imgurl: frappe.utils.get_file_link(doc.image), desc:doc.description});
+ '</table>', {
+ imgurl: frappe.utils.get_file_link(doc.image),
+ desc: doc.description.replace(/\n/g, "<br>")});
refresh_field('description_html');
}
@@ -185,4 +209,4 @@
else {
msgprint(__("You may need to update: {0}", [frappe.meta.get_docfield(cur_frm.doc.doctype, "description_html").label]));
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index db39f7b..12ff900 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -1,905 +1,946 @@
{
- "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,
+ "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": "",
+ "read_only": 1
+ },
+ {
+ "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": "<a href=\"#Sales Browser/Item Group\">Add / Edit</a>",
- "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": "<a href=\"#Sales Browser/Item Group\">Add / Edit</a>",
+ "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": 0,
"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,
+ "description": "Will also apply for variants unless overrridden",
+ "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": 0,
+ "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\"",
+ "description": "Will also apply for variants",
+ "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,
+ "description": "Will also apply for variants",
+ "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": "Will also apply to variants",
+ "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-10-07 05:25:19.921651",
+ "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 b8a3190..cf46372 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -9,8 +9,11 @@
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"
@@ -23,7 +26,7 @@
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:
@@ -39,6 +42,8 @@
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()
@@ -50,6 +55,7 @@
self.validate_barcode()
self.cant_change()
self.validate_item_type_for_reorder()
+ self.validate_variants()
if not self.get("__islocal"):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@@ -61,6 +67,7 @@
invalidate_cache_for_item(self)
self.validate_name_with_item_group()
self.update_item_price()
+ self.sync_variants()
def get_context(self, context):
context["parent_groups"] = get_parent_item_groups(self.item_group) + \
@@ -114,6 +121,146 @@
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 = []
+
+ 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)
+ 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 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 []
+
+ self.variant_attributes = {}
+ variant_dict = {}
+ variant_item_codes = []
+
+ for d in self.item_variants:
+ variant_dict.setdefault(d.item_attribute, []).append(d.item_attribute_value)
+
+ all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
+
+ # sort attributes by their priority
+ attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
+
+ def add_attribute_suffixes(item_code, my_attributes, attributes):
+ attr = frappe.get_doc("Item Attribute", attributes[0])
+ for value in attr.item_attribute_values:
+ if value.attribute_value in variant_dict[attr.name]:
+ _my_attributes = copy.deepcopy(my_attributes)
+ _my_attributes.append([attr.name, value.attribute_value])
+ if len(attributes) > 1:
+ add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
+ else:
+ variant_item_codes.append(item_code + "-" + value.abbr)
+ self.variant_attributes[item_code + "-" + value.abbr] = _my_attributes
+
+ add_attribute_suffixes(self.name, [], attributes)
+
+ return variant_item_codes
+
+ def make_variant(self, item_code):
+ item = frappe.new_doc("Item")
+ item.item_code = item_code
+ self.copy_attributes_to_variant(self, item, insert=True)
+ item.insert()
+
+ def update_variant(self, item_code):
+ item = frappe.get_doc("Item", item_code)
+ item.item_code = item_code
+ self.copy_attributes_to_variant(self, item)
+ item.save()
+
+ def copy_attributes_to_variant(self, template, variant, insert=False):
+ from frappe.model import no_value_fields
+ for field in self.meta.fields:
+ if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
+ and field.fieldname != "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"
+
+ 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 += "<div style='margin-top: 4px; font-size: 80%'>" + attr[0] + ": " + attr[1] + "</div>"
+ 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'):
@@ -126,9 +273,6 @@
frappe.throw(_("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx))
def validate_item_type(self):
- if cstr(self.is_manufactured_item) == "No":
- self.is_pro_applicable = "No"
-
if self.is_pro_applicable == 'Yes' and self.is_stock_item == 'No':
frappe.throw(_("As Production Order can be made for this item, it must be a stock item."))
@@ -140,6 +284,11 @@
def check_for_active_boms(self):
+ if self.default_bom:
+ bom_item = frappe.db.get_value("BOM", self.default_bom, "item")
+ if bom_item not in (self.name, self.variant_of):
+ frappe.throw(_("Default BOM must be for this item or its template"))
+
if self.is_purchase_item != "Yes":
bom_mat = frappe.db.sql("""select distinct t1.parent
from `tabBOM Item` t1, `tabBOM` t2 where t2.name = t1.parent
@@ -149,12 +298,6 @@
if bom_mat and bom_mat[0][0]:
frappe.throw(_("Item must be a purchase item, as it is present in one or many Active BOMs"))
- if self.is_manufactured_item != "Yes":
- bom = frappe.db.sql("""select name from `tabBOM` where item = %s
- and is_active = 1""", (self.name,))
- if bom and bom[0][0]:
- frappe.throw(_("""Allow Bill of Materials should be 'Yes'. Because one or many active BOMs present for this item"""))
-
def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """
cust_code=[]
@@ -222,6 +365,8 @@
def on_trash(self):
super(Item, self).on_trash()
frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code)
+ for variant_of in frappe.get_all("Item", {"variant_of": self.name}):
+ frappe.delete_doc("Item", variant_of.name)
def before_rename(self, olddn, newdn, merge=False):
if merge:
diff --git a/erpnext/stock/doctype/item/item_list.html b/erpnext/stock/doctype/item/item_list.html
index ebc2c7f..abfc2c6 100644
--- a/erpnext/stock/doctype/item/item_list.html
+++ b/erpnext/stock/doctype/item/item_list.html
@@ -26,11 +26,11 @@
<i class="icon-shopping-cart text-muted"></i>
</span>
{% } %}
- {% if(doc.is_manufactured_item==="Yes") { %}
+ {% if(doc.default_bom==="Yes") { %}
<span style="margin-right: 8px;"
title="{%= __("Manufactured Item") %}" class="filterable"
- data-filter="is_manufactured_item,=,Yes">
- <i class="icon-wrench text-muted"></i>
+ data-filter="default_bom,=,{%= doc.default_bom %}">
+ <i class="icon-site-map text-muted"></i>
</span>
{% } %}
{% if(doc.show_in_website) { %}
diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js
index e1cd020..c6a437b 100644
--- a/erpnext/stock/doctype/item/item_list.js
+++ b/erpnext/stock/doctype/item/item_list.js
@@ -1,5 +1,5 @@
frappe.listview_settings['Item'] = {
- add_fields: ["`tabItem`.`item_name`", "`tabItem`.`stock_uom`", "`tabItem`.`item_group`", "`tabItem`.`image`",
- "`tabItem`.`is_stock_item`", "`tabItem`.`is_sales_item`", "`tabItem`.`is_purchase_item`",
- "`tabItem`.`is_manufactured_item`", "`tabItem`.`show_in_website`"]
+ add_fields: ["item_name", "stock_uom", "item_group", "image",
+ "is_stock_item", "is_sales_item", "is_purchase_item", "show_in_website",
+ "default_bom"]
};
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 56150ca..803913f 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -6,13 +6,81 @@
import frappe
from frappe.test_runner import make_test_records
+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 - _TC",
+ "qty": 1,
+ "incoming_rate": 1
+ })
+ se.insert()
+ se.submit()
+
+ item.has_variants = 1
+ self.assertRaises(ItemTemplateCannotHaveStock, item.save)
+
+ def test_variant_item_codes(self):
+ 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:
+ 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'])
+
+ 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):
- from erpnext.stock.doctype.item.item import WarehouseNotSet
item = frappe.copy_doc(test_records[0])
item.is_stock_item = "Yes"
item.default_warehouse = None
@@ -23,12 +91,12 @@
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",
"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/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index a256149..7f5271e 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",
@@ -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
@@ -57,7 +55,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",
@@ -83,11 +81,11 @@
"tax_type": "_Test Account Excise Duty - _TC"
}
],
- "stock_uom": "_Test UOM"
+ "stock_uom": "_Test UOM 1"
},
{
"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",
@@ -105,10 +103,10 @@
"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",
+ "description": "_Test Sales BOM Item 5",
"doctype": "Item",
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"has_batch_no": "No",
@@ -129,7 +127,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 +147,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 +166,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 +185,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 +205,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 +227,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",
@@ -243,9 +241,47 @@
"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",
"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_manufactured_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",
+ "has_variants": 1,
+ "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"}
+ ],
+ "item_reorder": [
+ {
+ "material_request_type": "Purchase",
+ "warehouse": "_Test Warehouse - _TC",
+ "warehouse_reorder_level": 20,
+ "warehouse_reorder_qty": 20
+ }
+ ]
}
+
]
diff --git a/erpnext/stock/doctype/item_attribute/__init__.py b/erpnext/stock/doctype/item_attribute/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/item_attribute/__init__.py
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 0000000..d41646c
--- /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 0000000..e4fd38c
--- /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 0000000..51c335c
--- /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 0000000..0208c17
--- /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": "S"},
+ {"attribute_value": "Medium", "abbr": "M"},
+ {"attribute_value": "Large", "abbr": "L"}
+ ]
+ },
+ {
+ "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 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/item_attribute_value/__init__.py
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 0000000..f6d66bc
--- /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 0000000..c3a83b7
--- /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_variant/__init__.py b/erpnext/stock/doctype/item_variant/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/item_variant/__init__.py
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 0000000..aeb445e
--- /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 0000000..7b3c15d
--- /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
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a2a156f..d06a761 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -11,7 +11,8 @@
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, get_conversion_factor
+from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
class NotUpdateStockError(frappe.ValidationError): pass
class StockOverReturnError(frappe.ValidationError): pass
@@ -42,8 +43,8 @@
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 +97,23 @@
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"""
@@ -227,7 +230,7 @@
"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
@@ -240,17 +243,16 @@
self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty))
# get incoming rate
- if not d.bom_no:
- 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)
+ 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)
# 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):
@@ -291,10 +293,8 @@
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"""
@@ -408,20 +408,21 @@
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,
@@ -434,8 +435,7 @@
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}")
@@ -478,29 +478,47 @@
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)
- # 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)
+ # 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
+ 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,
@@ -509,27 +527,7 @@
"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()
@@ -541,6 +539,7 @@
for item in item_dict.values():
item.from_warehouse = item.default_warehouse
+ item.to_warehouse = ""
return item_dict
@@ -597,8 +596,8 @@
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"]
@@ -608,6 +607,11 @@
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
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index cc89832..fd878bc 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -68,26 +68,44 @@
frappe.db.set_default("allow_negative_stock", 0)
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()
+ self._test_auto_material_request("_Test Item")
+
+ def test_auto_material_request_for_variant(self):
+ self._test_auto_material_request("_Test Variant Item-S")
+
+ def _test_auto_material_request(self, item_code):
+ item = frappe.get_doc("Item", item_code)
+
+ 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
+ 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,
+ "warehouse": warehouse}, "projected_qty") or 0
- st1 = frappe.copy_doc(test_records[0])
- st1.insert()
- st1.submit()
- st2 = frappe.copy_doc(test_records[1])
- st2.insert()
- st2.submit()
+ # 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
- reorder_item()
+ from erpnext.stock.reorder_item import reorder_item
+ mr_list = 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)
+ 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()
@@ -903,11 +921,35 @@
"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]
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"
@@ -922,21 +964,27 @@
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 or args.item_code,
+ "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_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 7fdd440..a47878a 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.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
@@ -53,7 +54,8 @@
frappe.throw(_("Actual Qty is mandatory"))
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':
@@ -69,6 +71,10 @@
{"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/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py b/erpnext/stock/doctype/stock_uom_replace_utility/stock_uom_replace_utility.py
index a14c21e..864a2c6 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 @@
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/get_item_details.py b/erpnext/stock/get_item_details.py
index 19f9724..2273018 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -40,7 +40,7 @@
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,12 @@
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"))
+
+ 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')
@@ -138,31 +142,21 @@
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,
"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 +171,24 @@
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 +197,12 @@
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 +212,10 @@
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 +254,6 @@
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()
@@ -276,8 +293,13 @@
@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/reorder_item.py b/erpnext/stock/reorder_item.py
new file mode 100644
index 0000000..3afbb4b
--- /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="""<h3>Following Material Requests has been raised automatically \
+ based on item reorder level:</h3>"""
+ for mr in mr_list:
+ msg += "<p><b><u>" + mr.name + """</u></b></p><table class='table table-bordered'><tr>
+ <th>Item Code</th><th>Warehouse</th><th>Qty</th><th>UOM</th></tr>"""
+ for item in mr.get("indent_details"):
+ msg += "<tr><td>" + item.item_code + "</td><td>" + item.warehouse + "</td><td>" + \
+ cstr(item.qty) + "</td><td>" + cstr(item.uom) + "</td></tr>"
+ msg += "</table>"
+ 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:
+---
+<pre>
+%s
+</pre>
+---
+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 c95a84b..3a78e07 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -4,23 +4,29 @@
import frappe
from frappe import _
import json
-from frappe.utils import flt, cstr, nowdate, add_days, cint
-from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
+from frappe.utils import flt, cstr, nowdate, nowtime
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:
@@ -28,6 +34,20 @@
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
@@ -180,185 +200,3 @@
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:
- _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`"""))
- 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.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:
- 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)
-
-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="""<h3>Following Material Requests has been raised automatically \
- based on item reorder level:</h3>"""
- for mr in mr_list:
- msg += "<p><b><u>" + mr.name + """</u></b></p><table class='table table-bordered'><tr>
- <th>Item Code</th><th>Warehouse</th><th>Qty</th><th>UOM</th></tr>"""
- for item in mr.get("indent_details"):
- msg += "<tr><td>" + item.item_code + "</td><td>" + item.warehouse + "</td><td>" + \
- cstr(item.qty) + "</td><td>" + cstr(item.uom) + "</td></tr>"
- msg += "</table>"
- 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:
----
-<pre>
-%s
-</pre>
----
-Regards,
-Administrator""" % ("\n\n".join(exceptions_list),)
-
- from frappe.email import sendmail_to_system_managers
- sendmail_to_system_managers(subject, content)