[fixes] tests and moved reorder_item to separate module

This commit is contained in:
Rushabh Mehta 2014-10-08 12:03:19 +05:30
parent c62b6a815b
commit f850987db0
8 changed files with 272 additions and 263 deletions

View File

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

View File

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

View File

@ -9,7 +9,7 @@
"income_account": "Sales - _TC",
"inspection_required": "No",
"is_asset_item": "No",
"is_pro_applicable": "Yes",
"is_pro_applicable": "No",
"is_purchase_item": "Yes",
"is_sales_item": "Yes",
"is_service_item": "No",
@ -20,9 +20,7 @@
"item_name": "_Test Item",
"item_reorder": [
{
"doctype": "Item Reorder",
"material_request_type": "Purchase",
"parentfield": "item_reorder",
"warehouse": "_Test Warehouse - _TC",
"warehouse_reorder_level": 20,
"warehouse_reorder_qty": 20
@ -105,7 +103,7 @@
"item_code": "_Test Item Home Desktop 200",
"item_group": "_Test Item Group Desktops",
"item_name": "_Test Item Home Desktop 200",
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM 1"
},
{
"description": "_Test Sales BOM Item 5",

View File

@ -230,7 +230,7 @@ class StockEntry(StockController):
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": d.s_warehouse and -1*d.transfer_qty or d.transfer_qty,
"serial_no": d.serial_no
"serial_no": d.serial_no,
})
# get actual stock at source warehouse
@ -243,7 +243,7 @@ class StockEntry(StockController):
self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty))
# get incoming rate
if not d.bom_no:
if not d.t_warehouse:
if not flt(d.incoming_rate) or d.s_warehouse or self.purpose == "Sales Return" or force:
incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d))
if incoming_rate > 0:
@ -253,7 +253,7 @@ class StockEntry(StockController):
raw_material_cost += flt(d.amount)
# set incoming rate for fg item
if self.purpose in ["Manufacture", "Repack"]:
if self.purpose in ("Manufacture", "Repack"):
number_of_fg_items = len([t.t_warehouse for t in self.get("mtn_details") if t.t_warehouse])
for d in self.get("mtn_details"):
if d.bom_no or (d.t_warehouse and number_of_fg_items == 1):

View File

@ -10,7 +10,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_per
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
set_perpetual_inventory(0)
@ -18,60 +17,34 @@ class TestStockEntry(unittest.TestCase):
frappe.db.set_default("company", self.old_default_company)
def test_auto_material_request(self):
frappe.db.sql("""delete from `tabMaterial Request Item`""")
frappe.db.sql("""delete from `tabMaterial Request`""")
self._clear_stock_account_balance()
frappe.db.set_value("Stock Settings", None, "auto_indent", 1)
st1 = frappe.copy_doc(test_records[0])
st1.insert()
st1.submit()
st2 = frappe.copy_doc(test_records[1])
st2.insert()
st2.submit()
from erpnext.stock.utils import reorder_item
reorder_item()
mr_name = frappe.db.sql("""select parent from `tabMaterial Request Item`
where item_code='_Test Item'""")
frappe.db.set_value("Stock Settings", None, "auto_indent", 0)
self.assertTrue(mr_name)
self._test_auto_material_request("_Test Item")
def test_auto_material_request_for_variant(self):
item_code = "_Test Variant Item-S"
self._test_auto_material_request("_Test Variant Item-S")
def _test_auto_material_request(self, item_code):
item = frappe.get_doc("Item", item_code)
template = frappe.get_doc("Item", item.variant_of)
if item.variant_of:
template = frappe.get_doc("Item", item.variant_of)
else:
template = item
warehouse = "_Test Warehouse - _TC"
# stock entry reqd for auto-reorder
se = frappe.new_doc("Stock Entry")
se.purpose = "Material Receipt"
se.company = "_Test Company"
se.append("mtn_details", {
"item_code": item_code,
"t_warehouse": "_Test Warehouse - _TC",
"qty": 1,
"incoming_rate": 1
})
se.insert()
se.submit()
make_stock_entry(item_code=item_code, target="_Test Warehouse 1 - _TC", qty=1)
frappe.db.set_value("Stock Settings", None, "auto_indent", 1)
projected_qty = frappe.db.get_value("Bin", {"item_code": item_code,
"warehouse": warehouse}, "projected_qty") or 0
# update re-level qty so that it is more than projected_qty
if projected_qty > template.item_reorder[0].warehouse_reorder_level:
template.item_reorder[0].warehouse_reorder_level += projected_qty
template.save()
from erpnext.stock.utils import reorder_item
from erpnext.stock.reorder_item import reorder_item
mr_list = reorder_item()
frappe.db.set_value("Stock Settings", None, "auto_indent", 0)
@ -897,6 +870,7 @@ class TestStockEntry(unittest.TestCase):
"total_fixed_cost": 1000
})
stock_entry.get_items()
fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test FG Item 2"][0]
self.assertEqual(fg_rate, 1200.00)
fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0]
@ -939,21 +913,27 @@ def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
se.submit()
return se
def make_stock_entry(item, source, target, qty, incoming_rate=None):
def make_stock_entry(**args):
s = frappe.new_doc("Stock Entry")
if source and target:
s.purpose = "Material Transfer"
elif source:
s.purpose = "Material Issue"
else:
s.purpose = "Material Receipt"
s.company = "_Test Company"
args = frappe._dict(args)
if args.posting_date:
s.posting_date = args.posting_date
if args.posting_time:
s.posting_time = args.posting_time
if not args.purpose:
if args.source and args.target:
s.purpose = "Material Transfer"
elif args.source:
s.purpose = "Material Issue"
else:
s.purpose = "Material Receipt"
s.company = args.company or "_Test Company"
s.append("mtn_details", {
"item_code": item,
"s_warehouse": source,
"t_warehouse": target,
"qty": qty,
"incoming_rate": incoming_rate,
"item_code": args.item,
"s_warehouse": args.from_warehouse or args.source,
"t_warehouse": args.to_warehouse or args.target,
"qty": args.qty,
"incoming_rate": args.incoming_rate,
"conversion_factor": 1.0
})
s.insert()

View File

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

View File

@ -0,0 +1,196 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.utils import flt, cstr, nowdate, add_days, cint
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
def reorder_item():
""" Reorder item if stock reaches reorder level"""
# if initial setup not completed, return
if not frappe.db.sql("select name from `tabFiscal Year` limit 1"):
return
if getattr(frappe.local, "auto_indent", None) is None:
frappe.local.auto_indent = cint(frappe.db.get_value('Stock Settings', None, 'auto_indent'))
if frappe.local.auto_indent:
return _reorder_item()
def _reorder_item():
material_requests = {"Purchase": {}, "Transfer": {}}
item_warehouse_projected_qty = get_item_warehouse_projected_qty()
warehouse_company = frappe._dict(frappe.db.sql("""select name, company
from `tabWarehouse`"""))
default_company = (frappe.defaults.get_defaults().get("company") or
frappe.db.sql("""select name from tabCompany limit 1""")[0][0])
def add_to_material_request(item_code, warehouse, reorder_level, reorder_qty, material_request_type):
if warehouse not in item_warehouse_projected_qty[item_code]:
# likely a disabled warehouse or a warehouse where BIN does not exist
return
reorder_level = flt(reorder_level)
reorder_qty = flt(reorder_qty)
projected_qty = item_warehouse_projected_qty[item_code][warehouse]
if reorder_level and projected_qty < reorder_level:
deficiency = reorder_level - projected_qty
if deficiency > reorder_qty:
reorder_qty = deficiency
company = warehouse_company.get(warehouse) or default_company
material_requests[material_request_type].setdefault(company, []).append({
"item_code": item_code,
"warehouse": warehouse,
"reorder_qty": reorder_qty
})
for item_code in item_warehouse_projected_qty:
item = frappe.get_doc("Item", item_code)
if item.variant_of and not item.get("item_reorder"):
item.update_template_tables()
if item.get("item_reorder"):
for d in item.get("item_reorder"):
add_to_material_request(item_code, d.warehouse, d.warehouse_reorder_level,
d.warehouse_reorder_qty, d.material_request_type)
else:
# raise for default warehouse
add_to_material_request(item_code, item.default_warehouse, item.re_order_level, item.re_order_qty, "Purchase")
if material_requests:
return create_material_request(material_requests)
def get_item_warehouse_projected_qty():
item_warehouse_projected_qty = {}
for item_code, warehouse, projected_qty in frappe.db.sql("""select item_code, warehouse, projected_qty
from tabBin where ifnull(item_code, '') != '' and ifnull(warehouse, '') != ''
and exists (select name from `tabItem`
where `tabItem`.name = `tabBin`.item_code and
is_stock_item='Yes' and (is_purchase_item='Yes' or is_sub_contracted_item='Yes') and
(ifnull(end_of_life, '0000-00-00')='0000-00-00' or end_of_life > %s))
and exists (select name from `tabWarehouse`
where `tabWarehouse`.name = `tabBin`.warehouse
and ifnull(disabled, 0)=0)""", nowdate()):
item_warehouse_projected_qty.setdefault(item_code, {})[warehouse] = flt(projected_qty)
return item_warehouse_projected_qty
def create_material_request(material_requests):
""" Create indent on reaching reorder level """
mr_list = []
defaults = frappe.defaults.get_defaults()
exceptions_list = []
def _log_exception():
if frappe.local.message_log:
exceptions_list.extend(frappe.local.message_log)
frappe.local.message_log = []
else:
exceptions_list.append(frappe.get_traceback())
try:
current_fiscal_year = get_fiscal_year(nowdate())[0] or defaults.fiscal_year
except FiscalYearError:
_log_exception()
notify_errors(exceptions_list)
return
for request_type in material_requests:
for company in material_requests[request_type]:
try:
items = material_requests[request_type][company]
if not items:
continue
mr = frappe.new_doc("Material Request")
mr.update({
"company": company,
"fiscal_year": current_fiscal_year,
"transaction_date": nowdate(),
"material_request_type": request_type
})
for d in items:
d = frappe._dict(d)
item = frappe.get_doc("Item", d.item_code)
mr.append("indent_details", {
"doctype": "Material Request Item",
"item_code": d.item_code,
"schedule_date": add_days(nowdate(),cint(item.lead_time_days)),
"uom": item.stock_uom,
"warehouse": d.warehouse,
"item_name": item.item_name,
"description": item.description,
"item_group": item.item_group,
"qty": d.reorder_qty,
"brand": item.brand,
})
mr.insert()
mr.submit()
mr_list.append(mr)
except:
_log_exception()
if mr_list:
if getattr(frappe.local, "reorder_email_notify", None) is None:
frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None,
'reorder_email_notify'))
if(frappe.local.reorder_email_notify):
send_email_notification(mr_list)
if exceptions_list:
notify_errors(exceptions_list)
return mr_list
def send_email_notification(mr_list):
""" Notify user about auto creation of indent"""
email_list = frappe.db.sql_list("""select distinct r.parent
from tabUserRole r, tabUser p
where p.name = r.parent and p.enabled = 1 and p.docstatus < 2
and r.role in ('Purchase Manager','Material Manager')
and p.name not in ('Administrator', 'All', 'Guest')""")
msg="""<h3>Following Material Requests has been raised automatically \
based on item reorder level:</h3>"""
for mr in mr_list:
msg += "<p><b><u>" + mr.name + """</u></b></p><table class='table table-bordered'><tr>
<th>Item Code</th><th>Warehouse</th><th>Qty</th><th>UOM</th></tr>"""
for item in mr.get("indent_details"):
msg += "<tr><td>" + item.item_code + "</td><td>" + item.warehouse + "</td><td>" + \
cstr(item.qty) + "</td><td>" + cstr(item.uom) + "</td></tr>"
msg += "</table>"
frappe.sendmail(recipients=email_list, subject='Auto Material Request Generation Notification', msg = msg)
def notify_errors(exceptions_list):
subject = "[Important] [ERPNext] Auto Reorder Errors"
content = """Dear System Manager,
An error occured for certain Items while creating Material Requests based on Re-order level.
Please rectify these issues:
---
<pre>
%s
</pre>
---
Regards,
Administrator""" % ("\n\n".join(exceptions_list),)
from frappe.email import sendmail_to_system_managers
sendmail_to_system_managers(subject, content)

View File

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