Merge pull request #2231 from rmehta/item-variants

Item variants
This commit is contained in:
Rushabh Mehta 2014-10-21 12:38:16 +05:30
commit ab43942b35
39 changed files with 1994 additions and 1226 deletions

View File

@ -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",

View File

@ -10,38 +10,38 @@ from erpnext.accounts.utils import get_balance_on
@frappe.whitelist()
def get_companies():
"""get a list of companies based on permission"""
return [d.name for d in frappe.get_list("Company", fields=["name"],
return [d.name for d in frappe.get_list("Company", fields=["name"],
order_by="name")]
@frappe.whitelist()
def get_children():
args = frappe.local.form_dict
ctype, company = args['ctype'], args['comp']
# root
if args['parent'] in ("Accounts", "Cost Centers"):
acc = frappe.db.sql(""" select
acc = frappe.db.sql(""" select
name as value, if(group_or_ledger='Group', 1, 0) as expandable
from `tab%s`
where ifnull(parent_%s,'') = ''
and `company` = %s and docstatus<2
and `company` = %s and docstatus<2
order by name""" % (ctype, ctype.lower().replace(' ','_'), '%s'),
company, as_dict=1)
else:
else:
# other
acc = frappe.db.sql("""select
acc = frappe.db.sql("""select
name as value, if(group_or_ledger='Group', 1, 0) as expandable
from `tab%s`
from `tab%s`
where ifnull(parent_%s,'') = %s
and docstatus<2
and docstatus<2
order by name""" % (ctype, ctype.lower().replace(' ','_'), '%s'),
args['parent'], as_dict=1)
if ctype == 'Account':
currency = frappe.db.sql("select default_currency from `tabCompany` where name = %s", company)[0][0]
for each in acc:
bal = get_balance_on(each.get("value"))
each["currency"] = currency
each["balance"] = flt(bal)
return acc

View File

@ -254,7 +254,7 @@ def fix_total_debit_credit():
(d.diff, d.voucher_type, d.voucher_no))
def get_stock_and_account_difference(account_list=None, posting_date=None):
from erpnext.stock.utils import get_stock_balance_on
from erpnext.stock.utils import get_stock_value_on
if not posting_date: posting_date = nowdate()
@ -266,7 +266,7 @@ def get_stock_and_account_difference(account_list=None, posting_date=None):
for account, warehouse in account_warehouse.items():
account_balance = get_balance_on(account, posting_date)
stock_value = get_stock_balance_on(warehouse, posting_date)
stock_value = get_stock_value_on(warehouse, posting_date)
if abs(flt(stock_value) - flt(account_balance)) > 0.005:
difference.setdefault(account, flt(stock_value) - flt(account_balance))

View File

@ -8,11 +8,15 @@ import frappe
from frappe.model.document import Document
class QualityInspection(Document):
def get_item_specification_details(self):
self.set('qa_specification_details', [])
specification = frappe.db.sql("select specification, value from `tabItem Quality Inspection Parameter` \
where parent = '%s' order by idx" % (self.item_code))
variant_of = frappe.db.get_query("Item", self.item_code, "variant_of")
if variant_of:
specification = frappe.db.sql("select specification, value from `tabItem Quality Inspection Parameter` \
where parent in (%s, %s) order by idx", (self.item_code, variant_of))
else:
specification = frappe.db.sql("select specification, value from `tabItem Quality Inspection Parameter` \
where parent = %s order by idx", self.item_code)
for d in specification:
child = self.append('qa_specification_details', {})
child.specification = d[0]
@ -21,18 +25,18 @@ class QualityInspection(Document):
def on_submit(self):
if self.purchase_receipt_no:
frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2
set t1.qa_no = %s, t2.modified = %s
where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""",
(self.name, self.modified, self.purchase_receipt_no,
frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2
set t1.qa_no = %s, t2.modified = %s
where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""",
(self.name, self.modified, self.purchase_receipt_no,
self.item_code))
def on_cancel(self):
if self.purchase_receipt_no:
frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2
frappe.db.sql("""update `tabPurchase Receipt Item` t1, `tabPurchase Receipt` t2
set t1.qa_no = '', t2.modified = %s
where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""",
where t1.parent = %s and t1.item_code = %s and t1.parent = t2.name""",
(self.modified, self.purchase_receipt_no, self.item_code))
@ -45,6 +49,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
"start": start,
"page_len": page_len
})
return frappe.db.sql("""select item_code from `tab%(from)s`
return frappe.db.sql("""select item_code from `tab%(from)s`
where parent='%(parent)s' and docstatus < 2 and item_code like '%%%(txt)s%%' %(mcond)s
order by item_code limit %(start)s, %(page_len)s""" % filters)
order by item_code limit %(start)s, %(page_len)s""" % filters)

View File

@ -129,6 +129,11 @@ def get_data():
"description": _("Multiple Item prices."),
"route": "Report/Item Price"
},
{
"type": "doctype",
"name": "Item Attribute",
"description": _("Attributes for Item Variants. e.g Size, Color etc."),
},
]
},
{

View File

@ -8,6 +8,7 @@ from frappe.utils import flt, rounded
from erpnext.setup.utils import get_company_currency
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.controllers.stock_controller import StockController
@ -194,9 +195,8 @@ class BuyingController(StockController):
self.round_floats_in(item)
item.conversion_factor = item.conversion_factor or flt(frappe.db.get_value(
"UOM Conversion Detail", {"parent": item.item_code, "uom": item.uom},
"conversion_factor")) or 1
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") 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

View File

@ -165,6 +165,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
concat(substr(tabItem.description, 1, 40), "..."), description) as decription
from tabItem
where tabItem.docstatus < 2
and ifnull(tabItem.has_variants, 0)=0
and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00')
and (tabItem.`{key}` LIKE %(txt)s
or tabItem.item_name LIKE %(txt)s

View File

@ -42,7 +42,7 @@ doc_events = {
scheduler_events = {
"daily": [
"erpnext.controllers.recurring_document.create_recurring_documents",
"erpnext.stock.utils.reorder_item",
"erpnext.stock.reorder_item.reorder_item",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets"
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year"

View File

@ -182,10 +182,7 @@ erpnext.bom.calculate_total = function(doc) {
cur_frm.fields_dict['item'].get_query = function(doc) {
return{
query: "erpnext.controllers.queries.item_query",
filters:{
'is_manufactured_item': 'Yes'
}
query: "erpnext.controllers.queries.item_query"
}
}

View File

@ -54,7 +54,7 @@ class BOM(Document):
def get_item_det(self, item_code):
item = frappe.db.sql("""select name, is_asset_item, is_purchase_item,
docstatus, description, is_sub_contracted_item, stock_uom, default_bom,
last_purchase_rate, is_manufactured_item
last_purchase_rate
from `tabItem` where name=%s""", item_code, as_dict = 1)
return item
@ -149,14 +149,19 @@ class BOM(Document):
if self.is_default and self.is_active:
from frappe.model.utils import set_default
set_default(self, "item")
frappe.db.set_value("Item", self.item, "default_bom", self.name)
item = frappe.get_doc("Item", self.item)
if item.default_bom != self.name:
item.default_bom = self.name
item.save()
else:
if not self.is_active:
frappe.db.set(self, "is_default", 0)
frappe.db.sql("update `tabItem` set default_bom = null where name = %s and default_bom = %s",
(self.item, self.name))
item = frappe.get_doc("Item", self.item)
if item.default_bom == self.name:
item.default_bom = None
item.save()
def clear_operations(self):
if not self.with_operations:
@ -169,9 +174,6 @@ class BOM(Document):
item = self.get_item_det(self.item)
if not item:
frappe.throw(_("Item {0} does not exist in the system or has expired").format(self.item))
elif item[0]['is_manufactured_item'] != 'Yes' \
and item[0]['is_sub_contracted_item'] != 'Yes':
frappe.throw(_("Item {0} must be manufactured or sub-contracted").format(self.item))
else:
ret = frappe.db.get_value("Item", self.item, ["description", "stock_uom"])
self.description = ret[0]
@ -195,28 +197,14 @@ class BOM(Document):
if self.with_operations and cstr(m.operation_no) not in self.op:
frappe.throw(_("Operation {0} not present in Operations Table").format(m.operation_no))
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:
@ -424,3 +412,16 @@ def get_bom_items(bom, qty=1, fetch_exploded=1):
items = get_bom_items_as_dict(bom, qty, fetch_exploded).values()
items.sort(lambda a, b: a.item_code > b.item_code and 1 or -1)
return items
def validate_bom_no(item, bom_no):
"""Validate BOM No of sub-contracted items"""
bom = frappe.get_doc("BOM", bom_no)
if not bom.is_active:
frappe.throw(_("BOM {0} must be active").format(bom_no))
if not bom.docstatus!=1:
if not getattr(frappe.flags, "in_test", False):
frappe.throw(_("BOM {0} must be submitted").format(bom_no))
if item and not (bom.item == item or \
bom.item == frappe.db.get_value("Item", item, "variant_of")):
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))

View File

@ -28,3 +28,15 @@ class TestBOM(unittest.TestCase):
def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
self.assertEquals(len(get_bom_items(bom="BOM/_Test FG Item 2/001", qty=1, fetch_exploded=1)), 3)
def test_default_bom(self):
bom = frappe.get_doc("BOM", "BOM/_Test FG Item 2/001")
bom.is_active = 0
bom.save()
self.assertEqual(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), "")
bom.is_active = 1
bom.save()
self.assertTrue(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), "BOM/_Test FG Item 2/001")

View File

@ -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
}
]
]

View File

@ -7,12 +7,12 @@ import frappe
from frappe.utils import flt, nowdate
from frappe import _
from frappe.model.document import Document
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
class OverProductionError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
class ProductionOrder(Document):
def validate(self):
if self.docstatus == 0:
self.status = "Draft"
@ -21,7 +21,9 @@ class ProductionOrder(Document):
validate_status(self.status, ["Draft", "Submitted", "Stopped",
"In Process", "Completed", "Cancelled"])
self.validate_bom_no()
if self.bom_no:
validate_bom_no(self.production_item, self.bom_no)
self.validate_sales_order()
self.validate_warehouse()
self.set_fixed_cost()
@ -29,14 +31,6 @@ class ProductionOrder(Document):
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
def validate_bom_no(self):
if self.bom_no:
bom = frappe.db.sql("""select name from `tabBOM` where name=%s and docstatus=1
and is_active=1 and item=%s"""
, (self.bom_no, self.production_item), as_dict =1)
if not bom:
frappe.throw(_("BOM {0} is not active or not submitted").format(self.bom_no))
def validate_sales_order(self):
if self.sales_order:
so = frappe.db.sql("""select name, delivery_date from `tabSales Order`
@ -185,5 +179,5 @@ def make_stock_entry(production_order_id, purpose, qty=None):
stock_entry.from_warehouse = production_order.wip_warehouse
stock_entry.to_warehouse = production_order.fg_warehouse
stock_entry.run_method("get_items")
stock_entry.get_items()
return stock_entry.as_dict()

View File

@ -20,8 +20,10 @@ class TestProductionOrder(unittest.TestCase):
pro_doc.submit()
# add raw materials to stores
test_stock_entry.make_stock_entry("_Test Item", None, "Stores - _TC", 100, 100)
test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "Stores - _TC", 100, 100)
test_stock_entry.make_stock_entry(item_code="_Test Item",
target="Stores - _TC", qty=100, incoming_rate=100)
test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="Stores - _TC", qty=100, incoming_rate=100)
# from stores to wip
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Material Transfer", 4))
@ -46,12 +48,14 @@ class TestProductionOrder(unittest.TestCase):
from erpnext.manufacturing.doctype.production_order.production_order import StockOverProductionError
pro_doc = self.test_planned_qty()
test_stock_entry.make_stock_entry("_Test Item", None, "_Test Warehouse - _TC", 100, 100)
test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "_Test Warehouse - _TC", 100, 100)
test_stock_entry.make_stock_entry(item_code="_Test Item",
target="_Test Warehouse - _TC", qty=100, incoming_rate=100)
test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC", qty=100, incoming_rate=100)
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture", 7))
s.insert()
self.assertRaises(StockOverProductionError, s.submit)
test_records = frappe.get_test_records('Production Order')
test_records = frappe.get_test_records('Production Order')

View File

@ -9,6 +9,17 @@ cur_frm.cscript.refresh = function(doc) {
cur_frm.cscript.make_dashboard();
cur_frm.set_intro();
if (cur_frm.doc.has_variants) {
cur_frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"));
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 @@ cur_frm.cscript.refresh = function(doc) {
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 @@ erpnext.item.toggle_reqd = function(frm) {
frm.toggle_reqd("default_warehouse", frm.doc.is_stock_item==="Yes");
};
frappe.ui.form.on("Item", "is_stock_item", function(frm) {
erpnext.item.toggle_reqd(frm);
frappe.ui.form.on("Item", "onload", function(frm) {
var df = frappe.meta.get_docfield("Item Variant", "item_attribute_value");
df.on_make = function(field) {
field.$input.autocomplete({
minLength: 0,
minChars: 0,
source: function(request, response) {
frappe.call({
method:"frappe.client.get_list",
args:{
doctype:"Item Attribute Value",
filters: [
["parent","=", field.doc.item_attribute],
["attribute_value", "like", request.term + "%"]
],
fields: ["attribute_value"]
},
callback: function(r) {
response($.map(r.message, function(d) { return d.attribute_value; }));
}
});
}
})
}
});
cur_frm.cscript.make_dashboard = function() {
cur_frm.dashboard.reset();
if(cur_frm.doc.__islocal)
@ -49,7 +81,7 @@ cur_frm.cscript.make_dashboard = function() {
cur_frm.cscript.edit_prices_button = function() {
cur_frm.add_custom_button(__("Add / Edit Prices"), function() {
frappe.set_route("Report", "Item Price", {"item_code": cur_frm.doc.name});
}, "icon-money");
}, "icon-money", "btn-default");
}
cur_frm.cscript.item_code = function(doc) {
@ -59,16 +91,6 @@ cur_frm.cscript.item_code = function(doc) {
cur_frm.set_value("description", doc.item_code);
}
cur_frm.fields_dict['default_bom'].get_query = function(doc) {
return {
filters: {
'item': doc.item_code,
'is_active': 0
}
}
}
// Expense Account
// ---------------------------------
cur_frm.fields_dict['expense_account'].get_query = function(doc) {
@ -143,7 +165,9 @@ cur_frm.cscript.add_image = function(doc, dt, dn) {
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 @@ cur_frm.cscript.image = function() {
else {
msgprint(__("You may need to update: {0}", [frappe.meta.get_docfield(cur_frm.doc.doctype, "description_html").label]));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,8 +9,11 @@ from frappe.website.website_generator import WebsiteGenerator
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups
from frappe.website.render import clear_cache
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
import copy
class WarehouseNotSet(frappe.ValidationError): pass
class DuplicateVariant(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
class Item(WebsiteGenerator):
page_title_field = "item_name"
@ -23,7 +26,7 @@ class Item(WebsiteGenerator):
self.get("__onload").sle_exists = self.check_if_sle_exists()
def autoname(self):
if frappe.db.get_default("item_naming_by")=="Naming Series":
if frappe.db.get_default("item_naming_by")=="Naming Series" and not self.variant_of:
from frappe.model.naming import make_autoname
self.item_code = make_autoname(self.naming_series+'.#####')
elif not self.item_code:
@ -39,6 +42,8 @@ class Item(WebsiteGenerator):
if self.image and not self.website_image:
self.website_image = self.image
if self.variant_of:
self.copy_attributes_to_variant(frappe.get_doc("Item", self.variant_of), self)
self.check_warehouse_is_set_for_stock_item()
self.check_stock_uom_with_bin()
self.add_default_uom_in_conversion_factor_table()
@ -50,6 +55,7 @@ class Item(WebsiteGenerator):
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 @@ class Item(WebsiteGenerator):
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 @@ class Item(WebsiteGenerator):
if not matched:
frappe.throw(_("Default Unit of Measure can not be changed directly because you have already made some transaction(s) with another UOM. To change default UOM, use 'UOM Replace Utility' tool under Stock module."))
def validate_variants(self):
self.validate_variants_are_unique()
self.validate_stock_for_template_must_be_zero()
def validate_stock_for_template_must_be_zero(self):
if self.has_variants:
stock_in = frappe.db.sql_list("""select warehouse from tabBin
where item_code=%s and ifnull(actual_qty, 0) > 0""", self.name)
if stock_in:
frappe.throw(_("Item Template cannot have stock and varaiants. Please remove stock from warehouses {0}").format(", ".join(stock_in)),
ItemTemplateCannotHaveStock)
def validate_variants_are_unique(self):
if not self.has_variants:
self.item_variants = []
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 @@ class Item(WebsiteGenerator):
frappe.throw(_("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx))
def validate_item_type(self):
if cstr(self.is_manufactured_item) == "No":
self.is_pro_applicable = "No"
if self.is_pro_applicable == 'Yes' and self.is_stock_item == 'No':
frappe.throw(_("As Production Order can be made for this item, it must be a stock item."))
@ -140,6 +284,11 @@ class Item(WebsiteGenerator):
def check_for_active_boms(self):
if self.default_bom:
bom_item = frappe.db.get_value("BOM", self.default_bom, "item")
if bom_item not in (self.name, self.variant_of):
frappe.throw(_("Default BOM must be for this item or its template"))
if self.is_purchase_item != "Yes":
bom_mat = frappe.db.sql("""select distinct t1.parent
from `tabBOM Item` t1, `tabBOM` t2 where t2.name = t1.parent
@ -149,12 +298,6 @@ class Item(WebsiteGenerator):
if bom_mat and bom_mat[0][0]:
frappe.throw(_("Item must be a purchase item, as it is present in one or many Active BOMs"))
if self.is_manufactured_item != "Yes":
bom = frappe.db.sql("""select name from `tabBOM` where item = %s
and is_active = 1""", (self.name,))
if bom and bom[0][0]:
frappe.throw(_("""Allow Bill of Materials should be 'Yes'. Because one or many active BOMs present for this item"""))
def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """
cust_code=[]
@ -222,6 +365,8 @@ class Item(WebsiteGenerator):
def on_trash(self):
super(Item, self).on_trash()
frappe.db.sql("""delete from tabBin where item_code=%s""", self.item_code)
for variant_of in frappe.get_all("Item", {"variant_of": self.name}):
frappe.delete_doc("Item", variant_of.name)
def before_rename(self, olddn, newdn, merge=False):
if merge:

View File

@ -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) { %}

View File

@ -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"]
};

View File

@ -6,13 +6,81 @@ import unittest
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 @@ class TestItem(unittest.TestCase):
to_check = {
"item_code": "_Test Item",
"item_name": "_Test Item",
"description": "_Test Item",
"description": "_Test Item 1",
"warehouse": "_Test Warehouse - _TC",
"income_account": "Sales - _TC",
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"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,

View File

@ -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
}
]
}
]

View File

@ -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"
}

View File

@ -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)

View File

@ -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

View File

@ -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"}
]
}
]

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -11,7 +11,8 @@ from frappe import _
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.get_item_details import get_available_qty
from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center, 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 @@ class StockEntry(StockController):
pro_obj = self.production_order and \
frappe.get_doc('Production Order', self.production_order) or None
self.set_transfer_qty()
self.validate_item()
self.set_transfer_qty()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "transfer_qty")
self.validate_warehouse(pro_obj)
@ -96,21 +97,23 @@ class StockEntry(StockController):
for item in self.get("mtn_details"):
if item.item_code not in stock_items:
frappe.throw(_("{0} is not a stock Item").format(item.item_code))
if not item.stock_uom:
item.stock_uom = frappe.db.get_value("Item", item.item_code, "stock_uom")
if not item.uom:
item.uom = item.stock_uom
if not item.conversion_factor:
item.conversion_factor = 1
item_details = self.get_item_details(frappe._dict({"item_code": item.item_code,
"company": self.company, "project_name": self.project_name}))
for f in ("uom", "stock_uom", "description", "item_name", "expense_account",
"cost_center", "conversion_factor"):
item.set(f, item_details.get(f))
if not item.transfer_qty:
item.transfer_qty = item.qty * item.conversion_factor
if (self.purpose in ("Material Transfer", "Sales Return", "Purchase Return")
and not item.serial_no
and item.item_code in serialized_items):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
frappe.MandatoryError)
def validate_warehouse(self, pro_obj):
"""perform various (sometimes conditional) validations on warehouse"""
@ -227,7 +230,7 @@ class StockEntry(StockController):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": d.s_warehouse and -1*d.transfer_qty or d.transfer_qty,
"serial_no": d.serial_no
"serial_no": d.serial_no,
})
# get actual stock at source warehouse
@ -240,17 +243,16 @@ class StockEntry(StockController):
self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty))
# get incoming rate
if not d.bom_no:
if not 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 @@ class StockEntry(StockController):
def validate_bom(self):
for d in self.get('mtn_details'):
if d.bom_no and not frappe.db.sql("""select name from `tabBOM`
where item = %s and name = %s and docstatus = 1 and is_active = 1""",
(d.item_code, d.bom_no)):
frappe.throw(_("BOM {0} is not submitted or inactive BOM for Item {1}").format(d.bom_no, d.item_code))
if d.bom_no:
validate_bom_no(d.item_code, d.bom_no)
def validate_finished_goods(self):
"""validation: finished good quantity should be same as manufacturing quantity"""
@ -408,20 +408,21 @@ class StockEntry(StockController):
def get_item_details(self, args):
item = frappe.db.sql("""select stock_uom, description, item_name,
expense_account, buying_cost_center from `tabItem`
expense_account, buying_cost_center, item_group from `tabItem`
where name = %s and (ifnull(end_of_life,'0000-00-00')='0000-00-00' or end_of_life > now())""",
(args.get('item_code')), as_dict = 1)
if not item:
frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code")))
item = item[0]
ret = {
'uom' : item and item[0]['stock_uom'] or '',
'stock_uom' : item and item[0]['stock_uom'] or '',
'description' : item and item[0]['description'] or '',
'item_name' : item and item[0]['item_name'] or '',
'uom' : item.stock_uom,
'stock_uom' : item.stock_uom,
'description' : item.description,
'item_name' : item.item_name,
'expense_account' : args.get("expense_account") \
or frappe.db.get_value("Company", args.get("company"), "stock_adjustment_account"),
'cost_center' : item and item[0]['buying_cost_center'] or args.get("cost_center"),
'cost_center' : get_default_cost_center(args, item),
'qty' : 0,
'transfer_qty' : 0,
'conversion_factor' : 1,
@ -434,8 +435,7 @@ class StockEntry(StockController):
return ret
def get_uom_details(self, args):
conversion_factor = frappe.db.get_value("UOM Conversion Detail", {"parent": args.get("item_code"),
"uom": args.get("uom")}, "conversion_factor")
conversion_factor = get_conversion_factor(args.get("item_code"), args.get("uom")).get("conversion_factor")
if not conversion_factor:
frappe.msgprint(_("UOM coversion factor required for UOM: {0} in Item: {1}")
@ -478,29 +478,47 @@ class StockEntry(StockController):
self.production_order = None
if self.bom_no:
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
"Subcontract"]:
if self.production_order and self.purpose == "Material Transfer":
item_dict = self.get_pending_raw_materials(pro_obj)
if self.purpose in ("Material Issue", "Material Transfer", "Manufacture",
"Repack", "Subcontract"):
if self.production_order:
# production: stores -> wip
if self.purpose == "Material Transfer":
item_dict = self.get_pending_raw_materials(pro_obj)
for item in item_dict.values():
item["to_warehouse"] = pro_obj.wip_warehouse
# production: wip -> finished goods
elif self.purpose == "Manufacture":
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
for item in item_dict.values():
item["from_warehouse"] = pro_obj.wip_warehouse
else:
frappe.throw(_("Stock Entry against Production Order must be for 'Material Transfer' or 'Manufacture'"))
else:
if not self.fg_completed_qty:
frappe.throw(_("Manufacturing Quantity is mandatory"))
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
for item in item_dict.values():
if pro_obj:
item["from_warehouse"] = pro_obj.wip_warehouse
item["to_warehouse"] = ""
# add raw materials to Stock Entry Detail table
self.add_to_stock_entry_detail(item_dict)
# 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 @@ class StockEntry(StockController):
"expense_account": item.expense_account,
"cost_center": item.buying_cost_center,
}
}, bom_no=pro_obj.bom_no)
elif self.purpose in ["Material Receipt", "Repack"]:
if self.purpose=="Material Receipt":
self.from_warehouse = ""
item = frappe.db.sql("""select name, item_name, description,
stock_uom, expense_account, buying_cost_center from `tabItem`
where name=(select item from tabBOM where name=%s)""",
self.bom_no, as_dict=1)
self.add_to_stock_entry_detail({
item[0]["name"] : {
"qty": self.fg_completed_qty,
"item_name": item[0].item_name,
"description": item[0]["description"],
"stock_uom": item[0]["stock_uom"],
"from_warehouse": "",
"expense_account": item[0].expense_account,
"cost_center": item[0].buying_cost_center,
}
}, bom_no=self.bom_no)
}, bom_no = self.bom_no)
self.get_stock_and_rate()
@ -541,6 +539,7 @@ class StockEntry(StockController):
for item in item_dict.values():
item.from_warehouse = item.default_warehouse
item.to_warehouse = ""
return item_dict
@ -597,8 +596,8 @@ class StockEntry(StockController):
for d in item_dict:
se_child = self.append('mtn_details')
se_child.s_warehouse = item_dict[d].get("from_warehouse", self.from_warehouse)
se_child.t_warehouse = item_dict[d].get("to_warehouse", self.to_warehouse)
se_child.s_warehouse = item_dict[d].get("from_warehouse")
se_child.t_warehouse = item_dict[d].get("to_warehouse")
se_child.item_code = cstr(d)
se_child.item_name = item_dict[d]["item_name"]
se_child.description = item_dict[d]["description"]
@ -608,6 +607,11 @@ class StockEntry(StockController):
se_child.expense_account = item_dict[d]["expense_account"] or expense_account
se_child.cost_center = item_dict[d]["cost_center"] or cost_center
if se_child.s_warehouse==None:
se_child.s_warehouse = self.from_warehouse
if se_child.t_warehouse==None:
se_child.t_warehouse = self.to_warehouse
# in stock uom
se_child.transfer_qty = flt(item_dict[d]["qty"])
se_child.conversion_factor = 1.00

View File

@ -10,7 +10,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_per
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
set_perpetual_inventory(0)
@ -18,26 +17,44 @@ class TestStockEntry(unittest.TestCase):
frappe.db.set_default("company", self.old_default_company)
def test_auto_material_request(self):
frappe.db.sql("""delete from `tabMaterial Request Item`""")
frappe.db.sql("""delete from `tabMaterial Request`""")
self._clear_stock_account_balance()
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()
@ -853,11 +870,35 @@ class TestStockEntry(unittest.TestCase):
"total_fixed_cost": 1000
})
stock_entry.get_items()
fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test FG Item 2"][0]
self.assertEqual(fg_rate, 1200.00)
fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0]
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"
@ -872,21 +913,27 @@ def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
se.submit()
return se
def make_stock_entry(item, source, target, qty, incoming_rate=None):
def make_stock_entry(**args):
s = frappe.new_doc("Stock Entry")
if source and target:
s.purpose = "Material Transfer"
elif source:
s.purpose = "Material Issue"
else:
s.purpose = "Material Receipt"
s.company = "_Test Company"
args = frappe._dict(args)
if args.posting_date:
s.posting_date = args.posting_date
if args.posting_time:
s.posting_time = args.posting_time
if not args.purpose:
if args.source and args.target:
s.purpose = "Material Transfer"
elif args.source:
s.purpose = "Material Issue"
else:
s.purpose = "Material Receipt"
s.company = args.company or "_Test Company"
s.append("mtn_details", {
"item_code": item,
"s_warehouse": source,
"t_warehouse": target,
"qty": qty,
"incoming_rate": incoming_rate,
"item_code": args.item 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()

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.utils import flt, getdate, add_days, formatdate
from frappe.model.document import Document
from datetime import date
from erpnext.stock.doctype.item.item import ItemTemplateCannotHaveStock
class StockFreezeError(frappe.ValidationError): pass
@ -50,7 +51,8 @@ class StockLedgerEntry(Document):
frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
def validate_item(self):
item_det = frappe.db.sql("""select name, has_batch_no, docstatus, is_stock_item
item_det = frappe.db.sql("""select name, has_batch_no, docstatus,
is_stock_item, has_variants
from tabItem where name=%s""", self.item_code, as_dict=True)[0]
if item_det.is_stock_item != 'Yes':
@ -66,6 +68,10 @@ class StockLedgerEntry(Document):
{"item": self.item_code, "name": self.batch_no}):
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, self.item_code))
if item_det.has_variants:
frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code),
ItemTemplateCannotHaveStock)
if not self.stock_uom:
self.stock_uom = item_det.stock_uom

View File

@ -33,7 +33,7 @@ class StockUOMReplaceUtility(Document):
item_doc.stock_uom = self.new_stock_uom
item_doc.save()
frappe.msgprint(_("Stock UOM updatd for Item {0}").format(self.item_code))
frappe.msgprint(_("Stock UOM updated for Item {0}").format(self.item_code))
def update_bin(self):
# update bin

View File

@ -40,7 +40,7 @@ def get_item_details(args):
validate_item_details(args, item)
out = get_basic_details(args, item_doc)
out = get_basic_details(args, item)
get_party_item_code(args, item_doc, out)
@ -129,8 +129,12 @@ def validate_item_details(args, item):
if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != "Yes":
throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
def get_basic_details(args, item_doc):
item = item_doc
def get_basic_details(args, item):
if not item:
item = frappe.get_doc("Item", args.get("item_code"))
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 @@ def get_basic_details(args, item_doc):
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 @@ def get_basic_details(args, item_doc):
return out
def get_default_income_account(args, item):
return (item.income_account
or args.income_account
or frappe.db.get_value("Item Group", item.item_group, "default_income_account")
or frappe.db.get_value("Company", args.company, "default_income_account"))
def get_default_expense_account(args, item):
return (item.expense_account
or args.expense_account
or frappe.db.get_value("Item Group", item.item_group, "default_expense_account")
or frappe.db.get_value("Company", args.company, "default_expense_account"))
def get_default_cost_center(args, item):
return (frappe.db.get_value("Project", args.project_name, "cost_center")
or (item.selling_cost_center if args.transaction_type == "selling" else item.buying_cost_center)
or frappe.db.get_value("Item Group", item.item_group, "default_cost_center")
or frappe.db.get_value("Company", args.company, "cost_center"))
def get_price_list_rate(args, item_doc, out):
meta = frappe.get_meta(args.parenttype)
@ -185,10 +197,12 @@ def get_price_list_rate(args, item_doc, out):
validate_conversion_rate(args, meta)
price_list_rate = frappe.db.get_value("Item Price",
{"price_list": args.price_list, "item_code": args.item_code}, "price_list_rate")
price_list_rate = get_price_list_rate_for(args, item_doc.name)
if not price_list_rate and item_doc.variant_of:
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
if not price_list_rate: return {}
if not price_list_rate:
return {}
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \
/ flt(args.conversion_rate)
@ -198,6 +212,10 @@ def get_price_list_rate(args, item_doc, out):
out.update(get_last_purchase_details(item_doc.name,
args.parent, args.conversion_rate))
def get_price_list_rate_for(args, item_code):
return frappe.db.get_value("Item Price",
{"price_list": args.price_list, "item_code": item_code}, "price_list_rate")
def validate_price_list(args):
if args.get("price_list"):
if not frappe.db.get_value("Price List",
@ -236,7 +254,6 @@ def get_party_item_code(args, item_doc, out):
item_supplier = item_doc.get("item_supplier_details", {"supplier": args.supplier})
out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None
def get_pos_settings_item_details(company, args, pos_settings=None):
res = frappe._dict()
@ -276,8 +293,13 @@ def get_serial_nos_by_fifo(args, item_doc):
@frappe.whitelist()
def get_conversion_factor(item_code, uom):
variant_of = frappe.db.get_value("Item", item_code, "variant_of")
filters = {"parent": item_code, "uom": uom}
if variant_of:
filters = {"parent": ("in", (item_code, variant_of))}
return {"conversion_factor": frappe.db.get_value("UOM Conversion Detail",
{"parent": item_code, "uom": uom}, "conversion_factor")}
filters, "conversion_factor")}
@frappe.whitelist()
def get_projected_qty(item_code, warehouse):

View File

@ -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)

View File

@ -4,24 +4,30 @@
import frappe
from frappe import _
import json
from frappe.utils import flt, cstr, nowdate, add_days, cint
from frappe.utils import flt, cstr, nowdate, nowtime
from frappe.defaults import get_global_default
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
class InvalidWarehouseCompany(frappe.ValidationError): pass
def get_stock_balance_on(warehouse, posting_date=None):
def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
if not posting_date: posting_date = nowdate()
values, condition = [posting_date], ""
if warehouse:
values.append(warehouse)
condition += " AND warehouse = %s"
if item_code:
values.append(item_code)
condition.append(" AND item_code = %s")
stock_ledger_entries = frappe.db.sql("""
SELECT
item_code, stock_value
FROM
`tabStock Ledger Entry`
WHERE
warehouse=%s AND posting_date <= %s
SELECT item_code, stock_value
FROM `tabStock Ledger Entry`
WHERE posting_date <= %s {0}
ORDER BY timestamp(posting_date, posting_time) DESC, name DESC
""", (warehouse, posting_date), as_dict=1)
""".format(condition), values, as_dict=1)
sle_map = {}
for sle in stock_ledger_entries:
@ -29,6 +35,20 @@ def get_stock_balance_on(warehouse, posting_date=None):
return sum(sle_map.values())
def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None):
if not posting_date: posting_date = nowdate()
if not posting_time: posting_time = nowtime()
last_entry = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry`
where item_code=%s and warehouse=%s
and timestamp(posting_date, posting_time) < timestamp(%s, %s)
order by timestamp(posting_date, posting_time) limit 1""",
(item_code, warehouse, posting_date, posting_time))
if last_entry:
return last_entry[0][0]
else:
return 0.0
def get_latest_stock_balance():
bin_map = {}
for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value
@ -181,185 +201,3 @@ def get_buying_amount(item_code, item_qty, voucher_type, voucher_no, item_row, s
return 0.0
def reorder_item():
""" Reorder item if stock reaches reorder level"""
# if initial setup not completed, return
if not frappe.db.sql("select name from `tabFiscal Year` limit 1"):
return
if getattr(frappe.local, "auto_indent", None) is None:
frappe.local.auto_indent = cint(frappe.db.get_value('Stock Settings', None, 'auto_indent'))
if frappe.local.auto_indent:
_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)