[fixes] tests and moved reorder_item to separate module
This commit is contained in:
parent
c62b6a815b
commit
f850987db0
@ -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"
|
||||
],
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
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,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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user