commit
ab43942b35
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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."),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -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
@ -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:
|
||||
|
@ -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) { %}
|
||||
|
@ -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"]
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
|
0
erpnext/stock/doctype/item_attribute/__init__.py
Normal file
0
erpnext/stock/doctype/item_attribute/__init__.py
Normal file
87
erpnext/stock/doctype/item_attribute/item_attribute.json
Normal file
87
erpnext/stock/doctype/item_attribute/item_attribute.json
Normal 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"
|
||||
}
|
19
erpnext/stock/doctype/item_attribute/item_attribute.py
Normal file
19
erpnext/stock/doctype/item_attribute/item_attribute.py
Normal 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)
|
10
erpnext/stock/doctype/item_attribute/test_item_attribute.py
Normal file
10
erpnext/stock/doctype/item_attribute/test_item_attribute.py
Normal 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
|
22
erpnext/stock/doctype/item_attribute/test_records.json
Normal file
22
erpnext/stock/doctype/item_attribute/test_records.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
@ -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
|
0
erpnext/stock/doctype/item_variant/__init__.py
Normal file
0
erpnext/stock/doctype/item_variant/__init__.py
Normal file
72
erpnext/stock/doctype/item_variant/item_variant.json
Normal file
72
erpnext/stock/doctype/item_variant/item_variant.json
Normal 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"
|
||||
}
|
9
erpnext/stock/doctype/item_variant/item_variant.py
Normal file
9
erpnext/stock/doctype/item_variant/item_variant.py
Normal 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
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
196
erpnext/stock/reorder_item.py
Normal file
196
erpnext/stock/reorder_item.py
Normal 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)
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user