[fixes] tests and moved reorder_item to separate module
This commit is contained in:
@@ -42,7 +42,7 @@ doc_events = {
|
|||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
"daily": [
|
"daily": [
|
||||||
"erpnext.controllers.recurring_document.create_recurring_documents",
|
"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.setup.doctype.email_digest.email_digest.send",
|
||||||
"erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets"
|
"erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ class TestProductionOrder(unittest.TestCase):
|
|||||||
pro_doc.submit()
|
pro_doc.submit()
|
||||||
|
|
||||||
# add raw materials to stores
|
# add raw materials to stores
|
||||||
test_stock_entry.make_stock_entry("_Test Item", None, "Stores - _TC", 100, 100)
|
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||||
test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "Stores - _TC", 100, 100)
|
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
|
# from stores to wip
|
||||||
s = frappe.get_doc(make_stock_entry(pro_doc.name, "Material Transfer", 4))
|
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
|
from erpnext.manufacturing.doctype.production_order.production_order import StockOverProductionError
|
||||||
pro_doc = self.test_planned_qty()
|
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(item_code="_Test Item",
|
||||||
test_stock_entry.make_stock_entry("_Test Item Home Desktop 100", None, "_Test Warehouse - _TC", 100, 100)
|
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 = frappe.get_doc(make_stock_entry(pro_doc.name, "Manufacture", 7))
|
||||||
s.insert()
|
s.insert()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"income_account": "Sales - _TC",
|
"income_account": "Sales - _TC",
|
||||||
"inspection_required": "No",
|
"inspection_required": "No",
|
||||||
"is_asset_item": "No",
|
"is_asset_item": "No",
|
||||||
"is_pro_applicable": "Yes",
|
"is_pro_applicable": "No",
|
||||||
"is_purchase_item": "Yes",
|
"is_purchase_item": "Yes",
|
||||||
"is_sales_item": "Yes",
|
"is_sales_item": "Yes",
|
||||||
"is_service_item": "No",
|
"is_service_item": "No",
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
"item_name": "_Test Item",
|
"item_name": "_Test Item",
|
||||||
"item_reorder": [
|
"item_reorder": [
|
||||||
{
|
{
|
||||||
"doctype": "Item Reorder",
|
|
||||||
"material_request_type": "Purchase",
|
"material_request_type": "Purchase",
|
||||||
"parentfield": "item_reorder",
|
|
||||||
"warehouse": "_Test Warehouse - _TC",
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
"warehouse_reorder_level": 20,
|
"warehouse_reorder_level": 20,
|
||||||
"warehouse_reorder_qty": 20
|
"warehouse_reorder_qty": 20
|
||||||
@@ -105,7 +103,7 @@
|
|||||||
"item_code": "_Test Item Home Desktop 200",
|
"item_code": "_Test Item Home Desktop 200",
|
||||||
"item_group": "_Test Item Group Desktops",
|
"item_group": "_Test Item Group Desktops",
|
||||||
"item_name": "_Test Item Home Desktop 200",
|
"item_name": "_Test Item Home Desktop 200",
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM 1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "_Test Sales BOM Item 5",
|
"description": "_Test Sales BOM Item 5",
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class StockEntry(StockController):
|
|||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
"qty": d.s_warehouse and -1*d.transfer_qty or d.transfer_qty,
|
"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
|
# 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))
|
self.posting_date, self.posting_time, d.actual_qty, d.transfer_qty))
|
||||||
|
|
||||||
# get incoming rate
|
# 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:
|
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))
|
incoming_rate = flt(self.get_incoming_rate(args), self.precision("incoming_rate", d))
|
||||||
if incoming_rate > 0:
|
if incoming_rate > 0:
|
||||||
@@ -253,7 +253,7 @@ class StockEntry(StockController):
|
|||||||
raw_material_cost += flt(d.amount)
|
raw_material_cost += flt(d.amount)
|
||||||
|
|
||||||
# set incoming rate for fg item
|
# 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])
|
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"):
|
for d in self.get("mtn_details"):
|
||||||
if d.bom_no or (d.t_warehouse and number_of_fg_items == 1):
|
if d.bom_no or (d.t_warehouse and number_of_fg_items == 1):
|
||||||
|
|||||||
@@ -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
|
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
|
||||||
|
|
||||||
class TestStockEntry(unittest.TestCase):
|
class TestStockEntry(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
set_perpetual_inventory(0)
|
set_perpetual_inventory(0)
|
||||||
@@ -18,60 +17,34 @@ class TestStockEntry(unittest.TestCase):
|
|||||||
frappe.db.set_default("company", self.old_default_company)
|
frappe.db.set_default("company", self.old_default_company)
|
||||||
|
|
||||||
def test_auto_material_request(self):
|
def test_auto_material_request(self):
|
||||||
frappe.db.sql("""delete from `tabMaterial Request Item`""")
|
self._test_auto_material_request("_Test 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)
|
|
||||||
|
|
||||||
def test_auto_material_request_for_variant(self):
|
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)
|
item = frappe.get_doc("Item", item_code)
|
||||||
|
|
||||||
|
if item.variant_of:
|
||||||
template = frappe.get_doc("Item", item.variant_of)
|
template = frappe.get_doc("Item", item.variant_of)
|
||||||
|
else:
|
||||||
|
template = item
|
||||||
|
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
# stock entry reqd for auto-reorder
|
# stock entry reqd for auto-reorder
|
||||||
se = frappe.new_doc("Stock Entry")
|
make_stock_entry(item_code=item_code, target="_Test Warehouse 1 - _TC", qty=1)
|
||||||
se.purpose = "Material Receipt"
|
|
||||||
se.company = "_Test Company"
|
|
||||||
se.append("mtn_details", {
|
|
||||||
"item_code": item_code,
|
|
||||||
"t_warehouse": "_Test Warehouse - _TC",
|
|
||||||
"qty": 1,
|
|
||||||
"incoming_rate": 1
|
|
||||||
})
|
|
||||||
se.insert()
|
|
||||||
se.submit()
|
|
||||||
|
|
||||||
frappe.db.set_value("Stock Settings", None, "auto_indent", 1)
|
frappe.db.set_value("Stock Settings", None, "auto_indent", 1)
|
||||||
projected_qty = frappe.db.get_value("Bin", {"item_code": item_code,
|
projected_qty = frappe.db.get_value("Bin", {"item_code": item_code,
|
||||||
"warehouse": warehouse}, "projected_qty") or 0
|
"warehouse": warehouse}, "projected_qty") or 0
|
||||||
|
|
||||||
|
|
||||||
# update re-level qty so that it is more than projected_qty
|
# update re-level qty so that it is more than projected_qty
|
||||||
if projected_qty > template.item_reorder[0].warehouse_reorder_level:
|
if projected_qty > template.item_reorder[0].warehouse_reorder_level:
|
||||||
template.item_reorder[0].warehouse_reorder_level += projected_qty
|
template.item_reorder[0].warehouse_reorder_level += projected_qty
|
||||||
template.save()
|
template.save()
|
||||||
|
|
||||||
from erpnext.stock.utils import reorder_item
|
from erpnext.stock.reorder_item import reorder_item
|
||||||
mr_list = reorder_item()
|
mr_list = reorder_item()
|
||||||
|
|
||||||
frappe.db.set_value("Stock Settings", None, "auto_indent", 0)
|
frappe.db.set_value("Stock Settings", None, "auto_indent", 0)
|
||||||
@@ -897,6 +870,7 @@ class TestStockEntry(unittest.TestCase):
|
|||||||
"total_fixed_cost": 1000
|
"total_fixed_cost": 1000
|
||||||
})
|
})
|
||||||
stock_entry.get_items()
|
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]
|
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)
|
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]
|
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()
|
se.submit()
|
||||||
return se
|
return se
|
||||||
|
|
||||||
def make_stock_entry(item, source, target, qty, incoming_rate=None):
|
def make_stock_entry(**args):
|
||||||
s = frappe.new_doc("Stock Entry")
|
s = frappe.new_doc("Stock Entry")
|
||||||
if source and target:
|
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"
|
s.purpose = "Material Transfer"
|
||||||
elif source:
|
elif args.source:
|
||||||
s.purpose = "Material Issue"
|
s.purpose = "Material Issue"
|
||||||
else:
|
else:
|
||||||
s.purpose = "Material Receipt"
|
s.purpose = "Material Receipt"
|
||||||
s.company = "_Test Company"
|
s.company = args.company or "_Test Company"
|
||||||
s.append("mtn_details", {
|
s.append("mtn_details", {
|
||||||
"item_code": item,
|
"item_code": args.item,
|
||||||
"s_warehouse": source,
|
"s_warehouse": args.from_warehouse or args.source,
|
||||||
"t_warehouse": target,
|
"t_warehouse": args.to_warehouse or args.target,
|
||||||
"qty": qty,
|
"qty": args.qty,
|
||||||
"incoming_rate": incoming_rate,
|
"incoming_rate": args.incoming_rate,
|
||||||
"conversion_factor": 1.0
|
"conversion_factor": 1.0
|
||||||
})
|
})
|
||||||
s.insert()
|
s.insert()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class StockUOMReplaceUtility(Document):
|
|||||||
item_doc.stock_uom = self.new_stock_uom
|
item_doc.stock_uom = self.new_stock_uom
|
||||||
item_doc.save()
|
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):
|
def update_bin(self):
|
||||||
# update bin
|
# update bin
|
||||||
|
|||||||
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
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
import json
|
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 frappe.defaults import get_global_default
|
||||||
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
|
|
||||||
|
|
||||||
class InvalidWarehouseCompany(frappe.ValidationError): pass
|
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()
|
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("""
|
stock_ledger_entries = frappe.db.sql("""
|
||||||
SELECT
|
SELECT item_code, stock_value
|
||||||
item_code, stock_value
|
FROM `tabStock Ledger Entry`
|
||||||
FROM
|
WHERE posting_date <= %s {0}
|
||||||
`tabStock Ledger Entry`
|
|
||||||
WHERE
|
|
||||||
warehouse=%s AND posting_date <= %s
|
|
||||||
ORDER BY timestamp(posting_date, posting_time) DESC, name DESC
|
ORDER BY timestamp(posting_date, posting_time) DESC, name DESC
|
||||||
""", (warehouse, posting_date), as_dict=1)
|
""".format(condition), values, as_dict=1)
|
||||||
|
|
||||||
sle_map = {}
|
sle_map = {}
|
||||||
for sle in stock_ledger_entries:
|
for sle in stock_ledger_entries:
|
||||||
@@ -29,6 +35,20 @@ def get_stock_balance_on(warehouse, posting_date=None):
|
|||||||
|
|
||||||
return sum(sle_map.values())
|
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():
|
def get_latest_stock_balance():
|
||||||
bin_map = {}
|
bin_map = {}
|
||||||
for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value
|
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
|
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user