From 62d0629d6119b1c5139c1bc7f750ae611db4c5a8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 May 2013 16:19:10 +0530 Subject: [PATCH] [feature] reorder level checking through scheduler --- .../journal_voucher/test_journal_voucher.py | 117 +----------------- patches/may_2013/p04_reorder_level.py | 23 ++++ patches/patch_list.py | 1 + .../global_defaults/global_defaults.txt | 48 ++++--- startup/schedule_handlers.py | 4 + stock/doctype/bin/bin.py | 84 +------------ stock/doctype/item/item.py | 8 ++ stock/doctype/item/item.txt | 17 +-- stock/utils.py | 117 +++++++++++++++++- 9 files changed, 185 insertions(+), 234 deletions(-) create mode 100644 patches/may_2013/p04_reorder_level.py diff --git a/accounts/doctype/journal_voucher/test_journal_voucher.py b/accounts/doctype/journal_voucher/test_journal_voucher.py index 7cfeb595d8..feb1e2ca5a 100644 --- a/accounts/doctype/journal_voucher/test_journal_voucher.py +++ b/accounts/doctype/journal_voucher/test_journal_voucher.py @@ -122,119 +122,4 @@ test_records = [ "parentfield": "entries", "cost_center": "_Test Cost Center - _TC" }], -] - - - - - - -# -# -# import webnotes.model -# from webnotes.utils import nowdate, flt, add_days -# from accounts.utils import get_fiscal_year, get_balance_on -# -# company = webnotes.conn.get_default("company") -# abbr = webnotes.conn.get_value("Company", company, "abbr") -# -# data = { -# "expense_account": { -# "doctype": "Account", -# "account_name": "Test Expense", -# "parent_account": "Direct Expenses - %s" % abbr, -# "company": company, -# "debit_or_credit": "Debit", -# "is_pl_account": "Yes", -# "group_or_ledger": "Ledger" -# }, -# "supplier_account": { -# "doctype": "Account", -# "account_name": "Test Supplier", -# "parent_account": "Accounts Payable - %s" % abbr, -# "company": company, -# "debit_or_credit": "Credit", -# "is_pl_account": "No", -# "group_or_ledger": "Ledger" -# }, -# "test_cost_center": { -# "doctype": "Cost Center", -# "cost_center_name": "Test Cost Center", -# "parent_cost_center": "Root - %s" % abbr, -# "company_name": company, -# "group_or_ledger": "Ledger", -# "company_abbr": abbr -# }, -# "journal_voucher": [ -# { -# "doctype": "Journal Voucher", -# "voucher_type": "Journal Entry", -# "naming_series": "JV", -# "posting_date": nowdate(), -# "remark": "Test Journal Voucher", -# "fiscal_year": get_fiscal_year(nowdate())[0], -# "company": company -# }, -# { -# "doctype": "Journal Voucher Detail", -# "parentfield": "entries", -# "account": "Test Expense - %s" % abbr, -# "debit": 5000, -# "cost_center": "Test Cost Center - %s" % abbr, -# }, -# { -# "doctype": "Journal Voucher Detail", -# "parentfield": "entries", -# "account": "Test Supplier - %s" % abbr, -# "credit": 5000, -# }, -# ] -# } -# -# def get_name(s): -# return s + " - " + abbr -# -# class TestJournalVoucher(unittest.TestCase): -# def setUp(self): -# webnotes.conn.begin() -# -# # create a dummy account -# webnotes.model.insert([data["expense_account"]]) -# webnotes.model.insert([data["supplier_account"]]) -# webnotes.model.insert([data["test_cost_center"]]) -# -# def tearDown(self): -# webnotes.conn.rollback() -# -# def test_save_journal_voucher(self): -# expense_ac_balance = get_balance_on(get_name("Test Expense"), nowdate()) -# supplier_ac_balance = get_balance_on(get_name("Test Supplier"), nowdate()) -# -# dl = webnotes.model.insert(data["journal_voucher"]) -# dl.submit() -# dl.load_from_db() -# -# # test submitted jv -# self.assertTrue(webnotes.conn.exists("Journal Voucher", dl.doclist[0].name)) -# for d in dl.doclist[1:]: -# self.assertEquals(webnotes.conn.get_value("Journal Voucher Detail", -# d.name, "parent"), dl.doclist[0].name) -# -# # test gl entry -# gle = webnotes.conn.sql("""select account, debit, credit -# from `tabGL Entry` where voucher_no = %s order by account""", -# dl.doclist[0].name) -# -# self.assertEquals((gle[0][0], flt(gle[0][1]), flt(gle[0][2])), -# ('Test Expense - %s' % abbr, 5000.0, 0.0)) -# self.assertEquals((gle[1][0], flt(gle[1][1]), flt(gle[1][2])), -# ('Test Supplier - %s' % abbr, 0.0, 5000.0)) -# -# # check balance as on today -# self.assertEqual(get_balance_on(get_name("Test Expense"), nowdate()), -# expense_ac_balance + 5000) -# self.assertEqual(get_balance_on(get_name("Test Supplier"), nowdate()), -# supplier_ac_balance + 5000) -# -# # check previous balance -# self.assertEqual(get_balance_on(get_name("Test Expense"), add_days(nowdate(), -1)), 0) \ No newline at end of file +] \ No newline at end of file diff --git a/patches/may_2013/p04_reorder_level.py b/patches/may_2013/p04_reorder_level.py new file mode 100644 index 0000000000..8f4d669bc5 --- /dev/null +++ b/patches/may_2013/p04_reorder_level.py @@ -0,0 +1,23 @@ +# ERPNext - web based ERP (http://erpnext.com) +# Copyright (C) 2012 Web Notes Technologies Pvt Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import unicode_literals +import webnotes +def execute(): + webnotes.reload_doc("Setup", "DocType", "Global Defaults") + + if webnotes.conn.exists({"doctype": "Item", "email_notify": 1}): + webnotes.conn.set_value("Global Defaults", None, "reorder_email_notify", 1) \ No newline at end of file diff --git a/patches/patch_list.py b/patches/patch_list.py index f28751c818..89f48e5ce7 100644 --- a/patches/patch_list.py +++ b/patches/patch_list.py @@ -250,4 +250,5 @@ patch_list = [ "patches.may_2013.p01_conversion_factor_and_aii", "patches.may_2013.p02_update_valuation_rate", "patches.may_2013.p03_update_support_ticket", + "patches.may_2013.p04_reorder_level", ] \ No newline at end of file diff --git a/setup/doctype/global_defaults/global_defaults.txt b/setup/doctype/global_defaults/global_defaults.txt index 853bb57705..175ca9414b 100644 --- a/setup/doctype/global_defaults/global_defaults.txt +++ b/setup/doctype/global_defaults/global_defaults.txt @@ -1,8 +1,8 @@ [ { - "creation": "2013-04-01 15:05:24", + "creation": "2013-05-02 17:53:24", "docstatus": 0, - "modified": "2013-05-02 15:05:21", + "modified": "2013-05-22 15:57:26", "modified_by": "Administrator", "owner": "Administrator" }, @@ -27,6 +27,8 @@ "permlevel": 0 }, { + "amend": 0, + "cancel": 0, "create": 1, "doctype": "DocPerm", "name": "__common__", @@ -170,7 +172,8 @@ "fieldname": "item_naming_by", "fieldtype": "Select", "label": "Item Naming By", - "options": "Item Code\nNaming Series" + "options": "Item Code\nNaming Series", + "read_only": 0 }, { "doctype": "DocField", @@ -212,14 +215,6 @@ "label": "Allow Negative Stock", "read_only": 0 }, - { - "doctype": "DocField", - "fieldname": "default_warehouse_type", - "fieldtype": "Link", - "label": "Default Warehouse Type", - "options": "Warehouse Type", - "read_only": 0 - }, { "doctype": "DocField", "fieldname": "auto_indent", @@ -227,6 +222,21 @@ "label": "Raise Material Request when stock reaches re-order level", "read_only": 0 }, + { + "doctype": "DocField", + "fieldname": "reorder_email_notify", + "fieldtype": "Check", + "label": "Notify by Email on creation of automatic Material Request" + }, + { + "default": "Hourly", + "doctype": "DocField", + "fieldname": "reorder_level_checking_frequency", + "fieldtype": "Select", + "hidden": 1, + "label": "Reorder Level Checking Frequency", + "options": "Hourly\nDaily" + }, { "default": "1", "doctype": "DocField", @@ -235,6 +245,14 @@ "read_only": 0, "width": "50%" }, + { + "doctype": "DocField", + "fieldname": "default_warehouse_type", + "fieldtype": "Link", + "label": "Default Warehouse Type", + "options": "Warehouse Type", + "read_only": 0 + }, { "description": "Percentage you are allowed to receive or deliver more against the quantity ordered.

For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to receive 110 units

", "doctype": "DocField", @@ -274,7 +292,8 @@ "fieldtype": "Check", "label": "Auto Inventory Accounting", "no_copy": 0, - "print_hide": 1 + "print_hide": 1, + "read_only": 0 }, { "description": "Accounting entry frozen up to this date, nobody can do / modify entry except authorized person", @@ -507,11 +526,6 @@ "label": "SMS Sender Name", "read_only": 0 }, - { - "amend": 0, - "cancel": 0, - "doctype": "DocPerm" - }, { "doctype": "DocPerm" } diff --git a/startup/schedule_handlers.py b/startup/schedule_handlers.py index 0799817206..cc0d1f4fea 100644 --- a/startup/schedule_handlers.py +++ b/startup/schedule_handlers.py @@ -55,6 +55,10 @@ def execute_daily(): from setup.doctype.backup_manager.backup_manager import take_backups_daily take_backups_daily() + # check reorder level + from stock.utils import reorder_item + run_fn(reorder_item) + def execute_weekly(): from setup.doctype.backup_manager.backup_manager import take_backups_weekly take_backups_weekly() diff --git a/stock/doctype/bin/bin.py b/stock/doctype/bin/bin.py index 2d98c2634f..61baafafa2 100644 --- a/stock/doctype/bin/bin.py +++ b/stock/doctype/bin/bin.py @@ -77,10 +77,6 @@ class DocType: self.doc.save() - if (flt(args.get("actual_qty")) < 0 or flt(args.get("reserved_qty")) > 0) \ - and args.get("is_cancelled") == 'No' and args.get("is_amended")=='No': - self.reorder_item(args.get("voucher_type"), args.get("voucher_no"), args.get("company")) - def get_first_sle(self): sle = sql(""" select * from `tabStock Ledger Entry` @@ -90,82 +86,4 @@ class DocType: order by timestamp(posting_date, posting_time) asc, name asc limit 1 """, (self.doc.item_code, self.doc.warehouse), as_dict=1) - return sle and sle[0] or None - - def reorder_item(self,doc_type,doc_name, company): - """ Reorder item if stock reaches reorder level""" - if not hasattr(webnotes, "auto_indent"): - webnotes.auto_indent = webnotes.conn.get_value('Global Defaults', None, 'auto_indent') - - if webnotes.auto_indent: - #check if re-order is required - item_reorder = webnotes.conn.get("Item Reorder", - {"parent": self.doc.item_code, "warehouse": self.doc.warehouse}) - if item_reorder: - reorder_level = item_reorder.warehouse_reorder_level - reorder_qty = item_reorder.warehouse_reorder_qty - material_request_type = item_reorder.material_request_type or "Purchase" - else: - reorder_level, reorder_qty = webnotes.conn.get_value("Item", self.doc.item_code, - ["re_order_level", "re_order_qty"]) - material_request_type = "Purchase" - - if flt(reorder_qty) and flt(self.doc.projected_qty) < flt(reorder_level): - self.create_material_request(doc_type, doc_name, reorder_level, reorder_qty, - company, material_request_type) - - def create_material_request(self, doc_type, doc_name, reorder_level, reorder_qty, company, - material_request_type="Purchase"): - """ Create indent on reaching reorder level """ - defaults = webnotes.defaults.get_defaults() - item = webnotes.doc("Item", self.doc.item_code) - - mr = webnotes.bean([{ - "doctype": "Material Request", - "company": company or defaults.company, - "fiscal_year": defaults.fiscal_year, - "transaction_date": nowdate(), - "material_request_type": material_request_type, - "remark": _("This is an auto generated Material Request.") + \ - _("It was raised because the (actual + ordered + indented - reserved) quantity reaches re-order level when the following record was created") + \ - ": " + _(doc_type) + " " + doc_name - }, { - "doctype": "Material Request Item", - "parenttype": "Material Request", - "parentfield": "indent_details", - "item_code": self.doc.item_code, - "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), - "uom": self.doc.stock_uom, - "warehouse": self.doc.warehouse, - "item_name": item.item_name, - "description": item.description, - "item_group": item.item_group, - "qty": reorder_qty, - "brand": item.brand, - }]) - mr.insert() - mr.submit() - - msgprint("""Item: %s is to be re-ordered. Material Request %s raised. - It was generated from %s: %s""" % - (self.doc.item_code, mr.doc.name, doc_type, doc_name)) - - if(item.email_notify): - self.send_email_notification(doc_type, doc_name, mr) - - def send_email_notification(self, doc_type, doc_name, bean): - """ Notify user about auto creation of indent""" - - from webnotes.utils.email_lib import sendmail - email_list=[d[0] for d in sql("""select distinct r.parent from tabUserRole r, tabProfile 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="""A new Material Request has been raised for Item: %s and Warehouse: %s \ - on %s due to %s: %s. See %s: %s """ % (self.doc.item_code, self.doc.warehouse, - formatdate(), doc_type, doc_name, bean.doc.doctype, - get_url_to_form(bean.doc.doctype, bean.doc.name)) - - sendmail(email_list, subject='Auto Material Request Generation Notification', msg = msg) - + return sle and sle[0] or None \ No newline at end of file diff --git a/stock/doctype/item/item.py b/stock/doctype/item/item.py index bc438a877a..d743a98005 100644 --- a/stock/doctype/item/item.py +++ b/stock/doctype/item/item.py @@ -51,6 +51,7 @@ class DocType(DocListController): self.validate_barcode() self.check_non_asset_warehouse() self.cant_change() + self.validate_item_type_for_reorder() if self.doc.name: self.old_page_name = webnotes.conn.get_value('Item', self.doc.name, 'page_name') @@ -201,6 +202,13 @@ class DocType(DocListController): webnotes.msgprint(_("As there are existing stock transactions for this \ item, you can not change the values of 'Has Serial No', \ 'Is Stock Item' and 'Valuation Method'"), raise_exception=1) + + def validate_item_type_for_reorder(self): + if self.doc.re_order_level or len(self.doclist.get({"parentfield": "item_reorder", + "material_request_type": "Purchase"})): + if not self.doc.is_purchase_item: + webnotes.msgprint(_("""To set reorder level, item must be Purchase Item"""), + raise_exception=1) def check_if_sle_exists(self): sle = webnotes.conn.sql("""select name from `tabStock Ledger Entry` diff --git a/stock/doctype/item/item.txt b/stock/doctype/item/item.txt index c799029d95..9e0a2fb24e 100644 --- a/stock/doctype/item/item.txt +++ b/stock/doctype/item/item.txt @@ -2,7 +2,7 @@ { "creation": "2013-05-03 10:45:46", "docstatus": 0, - "modified": "2013-05-07 15:58:58", + "modified": "2013-05-22 15:48:27", "modified_by": "Administrator", "owner": "Administrator" }, @@ -363,21 +363,6 @@ "label": "Re-Order Qty", "read_only": 0 }, - { - "doctype": "DocField", - "fieldname": "column_break_31", - "fieldtype": "Column Break", - "read_only": 0 - }, - { - "depends_on": "eval:doc.is_stock_item==\"Yes\"", - "description": "Send an email to users of role \"Material Manager\" and \"Purchase Manager\" when re-order level is crossed.", - "doctype": "DocField", - "fieldname": "email_notify", - "fieldtype": "Check", - "label": "Notify by Email on Re-order", - "read_only": 0 - }, { "doctype": "DocField", "fieldname": "section_break_31", diff --git a/stock/utils.py b/stock/utils.py index a2541dc69e..5e7e53bb01 100644 --- a/stock/utils.py +++ b/stock/utils.py @@ -17,7 +17,7 @@ import webnotes from webnotes import msgprint, _ import json -from webnotes.utils import flt, cstr +from webnotes.utils import flt, cstr, nowdate, add_days, cint from webnotes.defaults import get_global_default def validate_end_of_life(item_code, end_of_life=None, verbose=1): @@ -194,4 +194,117 @@ def _get_buying_amount(voucher_type, voucher_no, item_row, item_code, warehouse, buying_amount = previous_stock_value - flt(sle.stock_value) return buying_amount - return 0.0 \ No newline at end of file + return 0.0 + + +def reorder_item(): + """ Reorder item if stock reaches reorder level""" + if not hasattr(webnotes, "auto_indent"): + webnotes.auto_indent = webnotes.conn.get_value('Global Defaults', None, 'auto_indent') + + if webnotes.auto_indent: + material_requests = {} + bin_list = webnotes.conn.sql("""select item_code, warehouse, projected_qty + from tabBin where ifnull(item_code, '') != '' and ifnull(warehouse, '') != ''""", + as_dict=True) + for bin in bin_list: + #check if re-order is required + item_reorder = webnotes.conn.get("Item Reorder", + {"parent": bin.item_code, "warehouse": bin.warehouse}) + if item_reorder: + reorder_level = item_reorder.warehouse_reorder_level + reorder_qty = item_reorder.warehouse_reorder_qty + material_request_type = item_reorder.material_request_type or "Purchase" + else: + reorder_level, reorder_qty = webnotes.conn.get_value("Item", bin.item_code, + ["re_order_level", "re_order_qty"]) + material_request_type = "Purchase" + + if reorder_level and flt(bin.projected_qty) < flt(reorder_level): + if flt(reorder_level) - flt(bin.projected_qty) > flt(reorder_qty): + reorder_qty = flt(reorder_level) - flt(bin.projected_qty) + + company = webnotes.conn.get_value("Warehouse", bin.warehouse, "company") or \ + webnotes.defaults.get_defaults()["company"] or \ + webnotes.conn.sql("""select name from tabCompany limit 1""")[0][0] + + material_requests.setdefault(material_request_type, webnotes._dict()).setdefault( + company, []).append(webnotes._dict({ + "item_code": bin.item_code, + "warehouse": bin.warehouse, + "reorder_qty": reorder_qty + }) + ) + + create_material_request(material_requests) + +def create_material_request(material_requests): + """ Create indent on reaching reorder level """ + mr_list = [] + defaults = webnotes.defaults.get_defaults() + for request_type in material_requests: + for company in material_requests[request_type]: + items = material_requests[request_type][company] + if items: + mr = [{ + "doctype": "Material Request", + "company": company, + "fiscal_year": defaults.fiscal_year, + "transaction_date": nowdate(), + "material_request_type": request_type, + "remark": _("This is an auto generated Material Request.") + \ + _("""It was raised because the (actual + ordered + indented - reserved) + quantity reaches re-order level when the following record was created""") + }] + + for d in items: + item = webnotes.doc("Item", d.item_code) + mr.append({ + "doctype": "Material Request Item", + "parenttype": "Material Request", + "parentfield": "indent_details", + "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_bean = webnotes.bean(mr) + mr_bean.insert() + mr_bean.submit() + mr_list.append(mr_bean) + + if mr_list: + if not hasattr(webnotes, "reorder_email_notify"): + webnotes.reorder_email_notify = webnotes.conn.get_value('Global Defaults', None, + 'reorder_email_notify') + + if(webnotes.reorder_email_notify): + send_email_notification(mr_list) + +def send_email_notification(mr_list): + """ Notify user about auto creation of indent""" + + from webnotes.utils.email_lib import sendmail + email_list = webnotes.conn.sql_list("""select distinct r.parent + from tabUserRole r, tabProfile 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="""

Following Material Requests has been raised automatically \ + based on item reorder level:

""" + for mr in mr_list: + msg += "

" + mr.doc.name + """

+ """ + for item in mr.doclist.get({"parentfield": "indent_details"}): + msg += "" + msg += "
Item CodeWarehouseQtyUOM
" + item.item_code + "" + item.warehouse + "" + \ + cstr(item.qty) + "" + cstr(item.uom) + "
" + + sendmail(email_list, subject='Auto Material Request Generation Notification', msg = msg) \ No newline at end of file